天天看点

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

中是挂在当前的组件实例上的。