场景

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 方法。

实现思路

主要做三件事情

惰性的响应式数据
处理 computed 的值
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 函数执行的逻辑:

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 函数的执行。

下边是输出结果:

请添加图片描述
此时我们如果修改 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 中是挂在当前的组件实例上的。

转载于:
https://vue.windliang.wang/
文章源码来源于:
https://github.com/wind-liang/vue2

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐