天天看點

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

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);
           

複制

輸出結果如下:

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420084245239

我們解決了初始時候的惰性,但如果去修改

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
           

複制

輸出如下:

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420085931210

是以,當觸發

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

函數執行的邏輯:

  • console.log("我使用了 name2", options.data.name2);

    讀取了

    name2

    的值,是以會觸發我們定義好的

    get

    ,觸發

    computed

    name2.get

    函數的執行。
  • document.getElementById("root").innerText = options.data.name + options.data.title;

    :

    讀取了

    name

    的值,會觸發

    computed

    name

    函數的執行。

    讀取了

    title

    的值,

    data.title

    會收集目前

    Watcher

    ,未來

    data.title

    改變的時候,會觸發

    updateComponent

    函數的執行。

下邊是輸出結果:

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420091914961

此時我們如果修改

title

的值,

updateComponent

函數會重新執行,但因為

name

name2

依賴的屬性值并沒有發生變化,是以他們相應的函數就不會執行了。

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420092134386

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

此時也應該執行。

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

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420093442962

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

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420094032444

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

,如下圖:

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420100828571

是以我們隻需在

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

函數的執行了。

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420101247642

此時的依賴圖如下:

Vue2剝絲抽繭-響應式系統之computed場景實作思路總

image-20220420101629653

computed

對應的函數作為了一個

Watcher

,使用計算屬性的函數也是一個

Watcher

computed

函數中使用的屬性會将這兩個

Watcher

都收集上。

此外

Watcher

增加了

lazy

屬性,如果

lazy

true

,當觸發

Watcher

執行的時候不執行内部的函數,将函數的執行讓渡給外部管理。

需要注意的一點是,我們是将

computed

所有的

watcher

都挂在了

data

上,實際上

Vue

中是挂在目前的元件執行個體上的。