一文搞定數據響應式原理

關注公衆號「執鳶者」,回覆「資料」獲取500G資料(各「兵種」均有),還有專業交流羣等你一塊兒來瀟灑。(哈哈)前端

在Vue中,其中最最最核心的一個知識點就是數據響應式原理,數據響應式原理歸結起來就包含兩大部分:偵測數據變化、依賴收集,瞭解這兩個知識點就瞭解到了數據響應式原理的精華。express

image.png

1、偵測數據變化

可以幀聽到數據變化是數據響應式原理的前提,由於數據響應式正是基於監聽到數據變化後來觸發一系列的更新操做。本次介紹數據響應式原理將基於Vue2.x進行,其將數據變爲可被偵測數據時主要採用了Object.defineProperty()。

1.1 非數組對象

下面先舉一個非數組對象的例子
const obj = {
    a: {
        m: {
            n: 5
        }
    },
    b: 10
};
觀察上面的對象,能夠發現其是存在包含關係的(即一個對象中可能包含另外一個對象),那麼天然會想到經過遞歸的方式實現,在Vue中爲了保證代碼較高的可讀性,引入了三個模塊實現該邏輯:observe、Observer、defineReactive,其調用關係以下所示:

image.png

1.1.1 observe

這個函數時幀聽數據變化的入口文件,經過調用該函數一方面觸發了其幀聽對象數據變化的能力;另外一方面定義了什麼時候遞歸到最內層的終止條件。
import Observer from './Observer';

export default function (value) {
    // 若是value不是對象,什麼都不作(表示該遞歸到的是基本類型,其變化可被幀聽的)
    if (typeof value !== 'object') {
        return;
    }

    // Observer實例
    let ob;
    // __ob__是value上的屬性,其值就是對應的Observer實例(表示其已是可幀聽的狀態)
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;
    }
    else {
        // 是對象且該上屬性仍是未可以幀聽狀態的
        ob = new Observer(value);
    }

    return ob;
}

1.1.2 Observer

這個函數的目的主要有兩個:一個是將該實例掛載到該對象value的__ob__屬性上(observe上用到了該屬性,經過判斷是否有該屬性判斷是否已經屬於幀聽狀態);另外一個是遍歷該對象上的全部屬性,而後將該屬性均變爲可幀聽的(經過調用defineReactive實現)。
export default class Observer {
    constructor(value) {
        // 給實例添加__ob__屬性
        def(value, '__ob__', this, false);
        // 檢查是數組仍是對象
        if (!Array.isArray(value)) {
            // 若爲對象,則進行遍歷,將其上的屬性變爲響應式的
            this.walk(value);
        }
    }

    // 對於對象上的屬性進行遍歷,將其變爲響應式的
    walk(value) {
        for (let key in value) {
            defineReactive(value, key);
        }
    }
}

1.1.3 defineReactive

這個方法主要是將Object.defineProperty封裝到一個函數中,作這一步操做的緣由是由於Object.defineProperty設置set屬性時須要一個臨時變量來存儲變化前的值,經過封裝利用閉包的思想引入val,這樣就不須要在函數外面再設置臨時變量了。
export default function defineReactive(data, key, val) {
    if (arguments.length === 2) {
        val = data[key];
    }

    // 子元素要進行observe,至此造成了遞歸
    let childOb = observe(val);

    Object.defineProperty(data, key, {
        // 可枚舉
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log(`訪問${key}屬性`);
            return val;
        },
        // setter
        set(newValue) {
            console.log(`改變${key}的屬性爲${newValue}`);
            if (val === newValue) {
                return;
            }
            val = newValue;
            // 當設置了新值,這個新值也要被observe
            childOb = observe(newValue);
        }
    });
}

1.2 數組

Object.defineProperty不能直接監聽數組內部的變化,那麼數組內容變化應該怎麼操做呢?Vue主要採用的是改裝數組方法的方式(push、pop、shift、unshift、splice、sort、reverse),在保留其原有功能的前提下,將其新添加的項變爲響應式的。
// array.js文件
// 獲得Array的原型
const arrayPrototype = Array.prototype;

// 以Array.prototype爲原型建立arrayMethods對象,並暴露
export const arrayMethods = Object.create(arrayPrototype);

// 要被改寫的7個數組方法
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

methodsNeedChange.forEach(methodName => {
    //備份原來的方法
    const original = arrayMethods[methodName];
    // 定義新的方法
    def(arrayMethods, methodName, function () {
        // 恢復原來的功能
        const result = original.apply(this, arguments);

        // 將類數組對象轉換爲數組
        const args = [...arguments];
        // 數組不會是最外層,因此其上已經添加了Observer實例
        const ob = this.__ob__;

        // push/unshift/splice會插入新項,須要將插入的新項變成observe的
        let inserted = [];

        switch (methodName) {
            case 'push':
            case 'unshift': {
                inserted = args;
                break;
            }
            case 'splice': {
                inserted = args.slice(2);
                break;
            }
        }

        // 對於有插入項的,讓新項變爲響應的
        if (inserted.length) {
            ob.observeArray(inserted);
        }

        ob.dep.notify();

        return result;
    }, false);
});
除了改裝其原有數組方法外,Observer函數中也將增長對數組的處理邏輯。
export default class Observer {
    constructor(value) {
        // 給實例添加__ob__屬性
        def(value, '__ob__', this, false);
        // 檢查是數組仍是對象
        if (Array.isArray(value)) {
            // 改變數組的原型爲新改裝的內容
            Object.setPrototypeOf(value, arrayMethods);
            // 讓這個數組變爲observe
            this.observeArray(value);
        }
        else {
            // 若爲對象,則進行遍歷,將其上的屬性變爲響應式的
            this.walk(value);
        }
    }

    // 對於對象上的屬性進行遍歷,將其變爲響應式的
    walk(value) {
        for (let key in value) {
            defineReactive(value, key);
        }
    }

    // 數組的特殊遍歷
    observeArray(arr) {
        for (let i = 0, l = arr.length; i < l; i++) {
            // 逐項進行observe
            observe(arr[i]);
        }
    }
}

2、依賴收集

目前對象中全部的屬性已經變成可幀聽狀態,下一步就進入了依賴收集階段,其整個流程以下所示:

image.png

其實看了這張神圖後,因爲能力有限還不是很理解,通過本身的拆分,認爲能夠分紅兩個步驟去理解。
  1. getter中(Object.defineProperty中的get屬性)進行收集依賴後的狀態

image.png

  1. 緊接着就是觸發依賴,該過程是在setter中進行,當觸發依賴時所存儲在Dep中的全部Watcher均會被通知並執行,通知其關聯的組件更新,例如數據更新的位置是與Dep1所關聯的數據,則其上的Watcher一、Watcher二、WatcherN均會被通知並執行。
說了這麼多,其中最核心的內容無外乎Dep類、Watcher類、defineReactive函數中的set和get函數。

2.1 Dep類

Dep類用於管理依賴,包含依賴的添加、刪除、發送消息,是一個典型的觀察者模式。
export default class Dep {
    constructor() {
        console.log('DEP構造器');
        // 數組存儲本身的訂閱者,這是Watcher實例
        this.subs = [];
    }

    // 添加訂閱
    addSub(sub) {
        this.subs.push(sub);
    }

    // 添加依賴
    depend() {
        // Dep.target指定的全局的位置
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }

    // 通知更新
    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

2.2 Watcher類

Watcher類的實例就是依賴,在其實例化階段會做爲依賴存儲到Dep中,在對應的數據改變時會更新與該數據相關的Watcher實例,進行對應任務的執行,更新對應組件。
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('Watcher構造器');
        this.target = target;
        this.getter = parsePath(expression);
        this.callback = callback;
        this.value = this.get();
    }

    update() {
        this.run();
    }

    get() {
        // 進入依賴收集階段,讓全局的Dep.target設置爲Watcher自己,就進入依賴收集階段
        Dep.target = this;
        const obj = this.target;
        let value;

        try {
            value = this.getter(obj);
        }
        finally {
            Dep.target = null;
        }

        return value;
    }

    run() {
        this.getAndInvoke(this.callback);
    }

    getAndInvoke(cb) {
        const value = this.get();

        if (value !== this.value || typeof value === 'object') {
            const oldValue = this.value;
            this.value = value;
            cb.call(this.target, value, oldValue);
        }
    }
}

function parsePath(str) {
    const segments = str.split('.');

    return obj =>{
        for (let i = 0; i < segments.length; i++) {
            if (!obj) {
                return;
            }
            obj = obj[segments[i]];
        }

        return obj;
    };
}

2.3 defineReactive函數中的set和get函數

Object.defineProperty中的getter階段進行收集依賴,setter階段觸發依賴。
export default function defineReactive(data, key, val) {
    const dep = new Dep();
    if (arguments.length === 2) {
        val = data[key];
    }

    // 子元素要進行observe,至此造成了遞歸
    let childOb = observe(val);

    Object.defineProperty(data, key, {
        // 可枚舉
        enumerable: true,
        // 可配置
        configurable: true,
        // getter
        get() {
            console.log(`訪問${key}屬性`);
            // 若是如今處於依賴收集階段
            if (Dep.target) {
                dep.depend();
                // 其子元素存在的時候也要進行依賴收集(我的認爲主要是針對數組)
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return val;
        },
        // setter
        set(newValue) {
            console.log(`改變${key}的屬性爲${newValue}`);
            if (val === newValue) {
                return;
            }
            val = newValue;
            // 當設置了新值,這個新值也要被observe
            childOb = observe(newValue);
            // 發佈訂閱模式,通知更新
            dep.notify();
        }
    });
}

參考文獻

本文是筆者看了邵山歡老師的視頻後作的一次總結,邵老師講的真心很好,爆贊。

1.若是以爲這篇文章還不錯,來個分享、點贊吧,讓更多的人也看到數組

2.關注公衆號執鳶者,領取學習資料(前端「多兵種」資料),按期爲你推送原創深度好文閉包

相關文章
相關標籤/搜索