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 屬性,dirtytrue 表示 Watcher 依賴的屬性發生了變化,需要重新求值。dirtyfalse 表示 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 ,如果 dirtytrue ,先手動調用 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 函數會重新執行,但因爲 namename2 依賴的屬性值並沒有發生變化,所以他們相應的函數就不會執行了。

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 的值,毫無疑問,namename2 的值肯定也會變化,使用了 namename2 的函數 updateComponent 此時也應該執行。

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

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

title 屬性收集了包含 updateComponent 函數的 WatcherfirstNamesecondName 屬性都收集了包含 computed.name() 函數的 Watcher

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

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

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

怎麼讓 firstNamesecondName 收集當前的 Watcher 呢?

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

所以我們只需在 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 ,使用計算屬性的函數也是一個 Watchercomputed 函數中使用的屬性會將這兩個 Watcher 都收集上。

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

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

windliang 前端,生活,成長

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