Vue2 剝絲抽繭 - 響應式系統

目前工作中大概有 40% 的需求是在用 Vue2 的技術棧,所謂知其然更要知其所以然,爲了更好的使用 Vue 、更快的排查問題,最近學習了源碼相關的一些知識,雖然網上總結 Vue 的很多很多了,不少自己一個,但也不多自己一個,歡迎一起討論學習,發現問題歡迎指出。

響應式系統要幹什麼

回到最簡單的代碼:

data = {
    text: 'hello, world'
}

const updateComponent = () ={
    console.log('收到', data.text);
}

updateComponent()

data.text = 'hello, liang'
// 運行結果
// 收到 hello, world

響應式系統要做的事情:某個依賴了 data 數據的函數,當所依賴的 data 數據改變的時候,該函數要重新執行。

我們期望的效果:當上邊 data.text 修改的時候, updateComponent 函數再執行一次。

爲了實現響應式系統,我們需要做兩件事情:

  1. 知道 data 中的數據被哪些函數依賴

  2. data 中的數據改變的時候去調用依賴它的函數們

爲了實現第 1 點,我們需要在執行函數的時候,將當前函數保存起來,然後在讀取數據的時候將該函數保存到當前數據中。

2 點就迎刃而解了,當修改數據的時候將保存起來的函數執行一次即可。

讀取數據修改數據的時候需要做額外的事情,我們可以通過 Object.defineProperty()  重寫對象屬性的 getset 函數。

響應式數據

我們來寫一個函數,重寫屬性的 getset 函數。

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(obj, key, val) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // 讀取用戶可能自己定義了的 get、set
    const getter = property && property.get;
    const setter = property && property.set;
    // val 沒有傳進來話進行手動賦值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            /*********************************************/
            // 1.這裏需要去保存當前在執行的函數
            /*********************************************/
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            /*********************************************/
            // 2.將依賴當前數據依賴的函數執行
            /*********************************************/
        },
    });
}

爲了調用更方便,我們把第 1 步和第 2 步的操作封裝一個 Dep  類。

export default class Dep {
    static target; //當前在執行的函數
    subs; // 依賴的函數
    constructor() {
        this.subs = []; // 保存所有需要執行的函數
    }

    addSub(sub) {
        this.subs.push(sub);
    }

    depend() {
        // 觸發 get 的時候走到這裏
        if (Dep.target) {
            // 委託給 Dep.target 去調用 addSub
            Dep.target.addDep(this);
        }
    }

    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // 靜態變量,全局唯一

我們將當前執行的函數保存到 Dep 類的 target 變量上。

保存當前正在執行的函數

爲了保存當前的函數,我們還需要寫一個 Watcher 類,將需要執行的函數傳入,保存到 Watcher 類中的 getter 屬性中,然後交由 Watcher 類負責執行。

這樣在 Dep 類中, subs 中保存的就不是當前函數了,而是持有當前函數的 Watcher 對象。

import Dep from "./dep";
export default class Watcher {
    constructor(Fn) {
        this.getter = Fn;
        this.get();
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        Dep.target = this; // 保存包裝了當前正在執行的函數的 Watcher
        let value;
        try {
           // 調用當前傳進來的函數,觸發對象屬性的 get
            value = this.getter.call();
        } catch (e) {
            throw e;
        }
        return value;
    }

    /**
     * Add a dependency to this directive.
     */
    addDep(dep) {
       // 觸發 get 後會走到這裏,收集當前依賴
        // 當前正在執行的函數的 Watcher 保存到 dep 中的 subs 中
        dep.addSub(this);
    }

    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
   // 修改對象屬性值的時候觸發 set,走到這裏
    update() {
        this.run();
    }

    /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
    run() {
        this.get();
    }
}

Watcher 的作用就是將正在執行的函數通過 Watcher 包裝後保存到 Dep.target 中,然後調用傳進來的函數,此時觸發對象屬性的 get 函數,會收集當前 Watcher

如果未來修改對象屬性的值,會觸發對象屬性的 set ,接着就會調用之前收集到的 Watcher 對象,通過 Watcher 對象的 uptate 方法,來調用最初執行的函數。

響應式數據

回到我們之前沒寫完的 defineReactive 函數,按照上邊的思路,我們來補全一下。

import Dep from "./dep";
/**
 * Define a reactive property on an Object.
 */
export function defineReactive(obj, key, val) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // 讀取用戶可能自己定義了的 get、set
    const getter = property && property.get;
    const setter = property && property.set;
    // val 沒有傳進來話進行手動賦值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }

    /*********************************************/
    const dep = new Dep(); // 持有一個 Dep 對象,用來保存所有依賴於該變量的 Watcher
    /*********************************************/

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            /*********************************************/
            // 1.這裏需要去保存當前在執行的函數
            if (Dep.target) {
                dep.depend();
            }
            /*********************************************/
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            /*********************************************/
            // 2.將依賴當前數據依賴的函數執行
            dep.notify();
            /*********************************************/
        },
    });
}

Observer 對象

我們再寫一個 Observer 方法,把對象的全部屬性都變成響應式的。

export class Observer {
    constructor(value) {
        this.walk(value);
    }

    /**
     * 遍歷對象所有的屬性,調用 defineReactive
     * 攔截對象屬性的 get 和 set 方法
     */
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]);
        }
    }
}

我們提供一個 observe 方法來負責創建 Observer 對象。

export function observe(value) {
    let ob = new Observer(value);
    return ob;
}

測試

將上邊的方法引入到文章最開頭的例子,來執行一下:

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world",
};
// 將數據變成響應式的
observe(data);

const updateComponent = () ={
    console.log("收到", data.text);
};

// 當前函數由 Watcher 進行執行
new Watcher(updateComponent);

data.text = "hello, liang";

此時就會輸出兩次了~

收到 hello, world
收到 hello, liang

說明我們的響應式系統成功了。

image-20220329092722630

先從整體理解了響應式系統的整個流程:

每個屬性有一個 subs 數組,Watcher 會持有當前執行的函數,當讀取屬性的時候觸發 get ,將當前 Watcher 保存到 subs 數組中,當屬性值修改的時候,再通過 subs 數組中的 Watcher 對象執行之前保存的函數。

當然還有億點點細節需要完善,後邊的文章會繼續,準備拉一個羣收集一下大家的反饋,準備學習 Vue 源碼的同學可以加下羣,二維碼過期了可以在後臺發送 Vue 獲取。

大家對文章有什麼問題、建議都可以反饋給我。

windliang 前端,生活,成長

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FZo7rmv6423VAjSGEK-GLg