Vue2 剝絲抽繭 - 響應式系統之 compute

Vue2 源碼從零詳解系列文章, 還沒有看過的同學可能需要看一下之前的,vue.windliang.wang/

場景

import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
        title: "標題",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
        name2: {
            get() {
                console.log("name2我執行啦!");
                return "name2" + this.firstName + this.secondName;
            },
            set() {
                console.log("name2的我執行啦!set執行啦!");
            },
        },
    },
};
observe(options.data);
initComputed(options.data, options.computed);

Vue 中肯定少不了 computed 屬性的使用,computed 的最大的作用就是惰性求值,同時它也是響應式數據。

這篇文章主要就來實現上邊的 initComputed 方法。

實現思路

主要做三件事情

  1. 惰性的響應式數據

  2. 處理 computed 的值

  3. computed 屬性的響應式

惰性的響應式數據

回想一下我們之前的 Watcher 。

如果我們調用 new Watcher

const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);

new Watcher(options.data, options.computed.name);

在 Watcher 內部我們會立即執行一次 options.computed.name 並將返回的值保存起來。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
       if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        ...
        this.value = this.get();
    }
   /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this); // 保存包裝了當前正在執行的函數的 Watcher
        let value;
        try {
            value = this.getter.call(this.data, this.data);
        } catch (e) {
            throw e;
        } finally {
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }

爲了實現惰性求值,我們可以增加一個 lazy 屬性,構造函數里我們不去直接執行。

同時增加一個 dirty 屬性,dirty 爲 true 表示 Watcher 依賴的屬性發生了變化,需要重新求值。dirty 爲 false 表示 Watcher 依賴的屬性沒有發生變化,無需重新求值。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
        if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
        this.deps = [];
        this.newDeps = []; // 記錄新一次的依賴
        this.newDepIds = new Set();
        this.id = ++uid; // uid for batching
        this.cb = cb;
        // options
        if (options) {
            this.deep = !!options.deep;
            this.sync = !!options.sync;
           /******新增 *************************/
            this.lazy = !!options.lazy;
            /************************************/

        }
       /******新增 *************************/
        this.dirty = this.lazy;
        this.value = this.lazy ? undefined : this.get();
       /************************************/
    }

我們把 computed 的函數傳給 Watcher 的時候可以增加一個 lazy 屬性,cb 參數是爲了 watch 使用,這裏就傳一個空函數。

const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);

const noop = () => {}
const watcher = new Watcher(options.data, options.computed.name, noop, {
    lazy: true,
});
console.log(watcher.value);

此時 wacher.value 就是 undefined 了,沒有拿到值。

我們還需要在 Wacher 類中提供一個 evaluate 方法,供用戶手動執行 Watcher 所保存的 computed 函數。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
        if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        ...
        // options
        if (options) {
            this.deep = !!options.deep;
            this.sync = !!options.sync;
            this.lazy = !!options.lazy;
        }
        this.dirty = this.lazy;
        this.value = this.lazy ? undefined : this.get();
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this); // 保存包裝了當前正在執行的函數的 Watcher
        let value;
        try {
            value = this.getter.call(this.data, this.data);
        } catch (e) {
            throw e;
        } finally {
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }

    ...

    /**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    /******新增 *************************/
    evaluate() {
        this.value = this.get();
        this.dirty = false; // dirty 爲 false 表示當前值已經是最新
    }
  /**********************************/
}

輸出 value 之前我們可以先執行一次 evaluate 。

const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);

const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
    lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);

輸出結果如下:

我們解決了初始時候的惰性,但如果去修改 firstName 的值,Watcher 還會立即執行,如下所示:

const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);

const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
    lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);

console.log("修改 firstName 的值");
options.data.firstName = "wind2";
setTimeout(() => {
    console.log(watcher.value);
}); // 爲什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html

輸出如下:

因此,當觸發 Watcher 執行的時候,我們應該只將 dirty 置爲 true 而不去執行。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
        if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        ...
        this.dirty = this.lazy;
        this.value = this.lazy ? undefined : this.get();
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        pushTarget(this); // 保存包裝了當前正在執行的函數的 Watcher
        let value;
        try {
            value = this.getter.call(this.data, this.data);
        } catch (e) {
            throw e;
        } finally {
            // "touch" every property so they are all tracked as
            // dependencies for deep watching
            if (this.deep) {
                traverse(value);
            }
            popTarget();
            this.cleanupDeps();
        }
        return value;
    }
    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update() {
       /******新增 *************************/
        if (this.lazy) {
            this.dirty = true;
          /************************************/
        } else if (this.sync) {
            this.run();
        } else {
            queueWatcher(this);
        }
    }

這樣,在使用 name 值前,我們先判斷下 dirty ,如果 dirty 爲 true ,先手動調用 evaluate 方法進行求值,然後再使用。

const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);

const noop = () => {};
const watcher = new Watcher(options.data, options.computed.name, noop, {
    lazy: true,
});
console.log(watcher.value);
watcher.evaluate();
console.log(watcher.value);

console.log("修改 firstName 的值");
options.data.firstName = "wind2";
setTimeout(() => {
    if (watcher.dirty) {
        watcher.evaluate();
    }
    console.log(watcher.value);
}); // 爲什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html

處理 computed 的值

接下來就是 initComputed 的邏輯,主要就是結合上邊所講的,將傳進來的 computed 轉爲惰性的響應式數據。

export function noop(a, b, c) {}

const computedWatcherOptions = { lazy: true };

// computed properties are just getters during SSR
export function initComputed(data, computed) {
    const watchers = (data._computedWatchers = Object.create(null)); // 保存當前所有的 watcher,並且掛在 data 上供後邊使用

    for (const key in computed) {
        const userDef = computed[key];
        const getter = typeof userDef === "function" ? userDef : userDef.get; // 如果是對象就取 get 的值
        // create internal watcher for the computed property.
        watchers[key] = new Watcher(
            data,
            getter || noop,
            noop,
            computedWatcherOptions
        );

        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        defineComputed(data, key, userDef);
    }
}

上邊的 defineComputed 主要就是將 computed 函數定義爲 data 的屬性,這樣就可以像正常屬性一樣去使用 computed 。

const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop,
};
export function defineComputed(target, key, userDef) {
    // 初始化 get 和 set
    if (typeof userDef === "function") {
        sharedPropertyDefinition.get = createComputedGetter(key);
        sharedPropertyDefinition.set = noop;
    } else {
        sharedPropertyDefinition.get = userDef.get
            ? createComputedGetter(key)
            : noop;
        sharedPropertyDefinition.set = userDef.set || noop;
    }
    // 將當前屬性掛到 data 上
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

其中 createComputedGetter 中去完成我們手動更新 Watcher 值的邏輯。

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key]; // 拿到相應的 watcher
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }
            return watcher.value;
        }
    };
}

讓我們測試一下 initComputed 。

import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
        title: "標題",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
        name2: {
            get() {
                console.log("name2我執行啦!");
                return "name2" + this.firstName + this.secondName;
            },
            set() {
                console.log("name2的我執行啦!set執行啦!");
            },
        },
    },
};
observe(options.data);
initComputed(options.data, options.computed);

const updateComponent = () => {
    console.log("updateComponent執行啦!");
    console.log("我使用了 name2", options.data.name2);
    document.getElementById("root").innerText =
        options.data.name + options.data.title;
};

new Watcher(options.data, updateComponent);

分析一下 updateComponent 函數執行的邏輯:

下邊是輸出結果:

此時我們如果修改 title 的值,updateComponent 函數會重新執行,但因爲 name 和 name2 依賴的屬性值並沒有發生變化,所以他們相應的函數就不會執行了。

computed 屬性的響應式

思考下邊的場景:

import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
        title: "標題",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);
initComputed(options.data, options.computed);

const updateComponent = () => {
    console.log("updateComponent執行啦!");
    document.getElementById("root").innerText =
        options.data.name + options.data.title;
};

new Watcher(options.data, updateComponent);

setTimeout(() => {
    options.data.firstName = "wind2";
}, 1000); // 爲什麼用 setTimeout 參考 https://vue.windliang.wang/posts/Vue2%E5%89%A5%E4%B8%9D%E6%8A%BD%E8%8C%A7-%E5%93%8D%E5%BA%94%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B9%8BnextTick.html

當我們修改了 firstName 的值,毫無疑問,name 和 name2 的值肯定也會變化,使用了 name 和 name2 的函數 updateComponent 此時也應該執行。

但事實上只在第一次的時候執行了,並沒有二次觸發。

讓我們看一下當前的收集依賴圖:

title 屬性收集了包含 updateComponent 函數的 Watcher ,firstName 和 secondName 屬性都收集了包含 computed.name() 函數的 Watcher。

name 屬性是我們後邊定義的,沒有 Dep 類,什麼都沒有收集。

我們現在想要實現改變 firstName 或者 secondName 值的時候,觸發 updateComponent 函數的執行。

我們只需要讀取 name 的時候,讓 firstName 和 secondName 收集一下當前的 Watcher ,因爲讀取 name 的值是在 updateComponent 中執行的,所以當前 Watcher 就是包含了 updateComponent 函數的 Watcher 。

怎麼讓 firstName 和 secondName 收集當前的 Watcher 呢?

在 name 的 get 中,我們能拿到 computed.name() 對應的 Watcher ,而在 Watcher 實例中,我們把它所有的依賴都保存起來了,也就是這裏的 firstName 和 secondName ,如下圖:

所以我們只需在 Watcher 中提供一個 depend 方法, 遍歷所有的依賴收集當前 Watcher 。

export default class Watcher {
    constructor(data, expOrFn, cb, options) {
        this.data = data;
        if (typeof expOrFn === "function") {
            this.getter = expOrFn;
        } else {
            this.getter = parsePath(expOrFn);
        }
        this.depIds = new Set(); // 擁有 has 函數可以判斷是否存在某個 id
        this.deps = [];
        this.newDeps = []; // 記錄新一次的依賴
        this.newDepIds = new Set();
        ...
        this.dirty = this.lazy;
        this.value = this.lazy ? undefined : this.get();
    }

    /**
     * Add a dependency to this directive.
     */
    addDep(dep) {
        const id = dep.id;
        // 新的依賴已經存在的話,同樣不需要繼續保存
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
    }
    /**
     * Evaluate the value of the watcher.
     * This only gets called for lazy watchers.
     */
    evaluate() {
        this.value = this.get();
        this.dirty = false;
    }
   /******新增 *************************/
    /**
     * Depend on all deps collected by this watcher.
     */
    depend() {
        let i = this.deps.length;
        while (i--) {
            this.deps[i].depend();
        }
    }
   /************************************/
}

然後在之前定義的計算屬性的 get 中觸發收集依賴即可。

function createComputedGetter(key) {
    return function computedGetter() {
        const watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
            if (watcher.dirty) {
                watcher.evaluate();
            }
            if (Dep.target) {
                watcher.depend();
            }
            return watcher.value;
        }
    };
}

回到開頭的場景:

import { observe } from "./reactive";
import { initComputed } from "./state";
import Watcher from "./watcher";
const options = {
    data: {
        firstName: "wind",
        secondName: "liang",
        title: "標題",
    },
    computed: {
        name() {
            console.log("name我執行啦!");
            return this.firstName + this.secondName;
        },
    },
};
observe(options.data);
initComputed(options.data, options.computed);

const updateComponent = () => {
    console.log("updateComponent執行啦!");
    document.getElementById("root").innerText =
        options.data.name + options.data.title;
};

new Watcher(options.data, updateComponent);

setTimeout(() => {
    options.data.firstName = "wind2";
}, 1000);

此時修改 firstName 的值就會觸發 updateComponent 函數的執行了。

此時的依賴圖如下:

總

computed 對應的函數作爲了一個 Watcher ,使用計算屬性的函數也是一個 Watcher ,computed 函數中使用的屬性會將這兩個 Watcher 都收集上。

此外 Watcher 增加了 lazy 屬性,如果 lazy 爲 true,當觸發 Watcher 執行的時候不執行內部的函數,將函數的執行讓渡給外部管理。

需要注意的一點是,我們是將 computed 所有的 watcher 都掛在了 data 上,實際上 Vue 中是掛在當前的組件實例上的。

windliang 前端,生活,成長

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