computed的实现原理

125 阅读7分钟

computed的实现原理

function initComputed(vm) {
    let computed = vm.$options.computed;
    // 1.需要有watcher  2. 还需要通过defineProperty   3.dirty
    const watchers = vm._computedWatchers = {}; // 用来稍后存放计算属性的watcher
    for(let key in computed){
        const userDef = computed[key]; // 取出对应的值来,可能是函数或者 对象
        // 获取get方法
        const getter = typeof userDef == 'function' ? userDef : userDef.get; // watcher使用的
        watchers[key] = new Watcher(vm,getter,()=>{},{lazy:true}); // watcher很懒?

        defineComputed(vm,key,userDef)//  defineReactive();
    }
}

第一步是取出所有 定义的 computed 属性 ,给每个属性生成一个watcher,(new Watcher()的四个参数当前vm , 对应的getter方法,回调函数是watch 才会使用的,使用空函数,option 里的 {lazy:true:}可以代表这是一个 计算属性的watcher),并定义一个对象 来存储之后computed 的每一个属性的watcher ,之后是给这个计算属性进行数据的劫持

我们可能 想当然的会这样写,这样确实进行了 数据 拦截,但是 确是没有缓存的,每次使用计算属性的时候都是 需要重新执行计算的

function defineComputed(target,key,userDef){  // 这样写是没有缓存的
const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get:()=>{},
    set:()=>{}
}
if(typeof userDef == 'function'){
    sharedPropertyDefinition.get = userDef // dirty 来控制是否调用userDef
}else{
//userDef 可能是对象的写法
    sharedPropertyDefinition.get = userDef.get; // 需要加缓存
    sharedPropertyDefinition.set = userDef.set;
}
    Object.defineProperty(target,key,userDef)
}

如下,我们可以将 userDef.get 包装进 一个函数 在 在函数内部使用 dirty 来控制 是否调用 userDef(即重新计算) ,

createComputedGetter函数

function createComputedGetter(key){
    return function (){ // 此方法是我们包装的方法,每次取值会调用此方法
        const watcher = this._computedWatchers[key]; // 拿到这个属性对应watcher
        if(watcher){
            if(watcher.dirty){ // 默认肯定是脏的 会重新求值
                watcher.evaluate(); // 对当前watcher求值,即调用计算属性的get
            }
            debugger;
            if (Dep.target) { // 说明还有渲染watcher,也应该一并的收集起来
                watcher.depend();
            }
            return watcher.value; // 默认返回watcher上存的值
        }
    }

}

这个方法默认返回 watcher 上存的值,从上面我们知道计算属性 watcher的值 就是watcher.evaluate()计算出来的值

evaluate() {
    this.value = this.get();
    this.dirty = false; // 取过一次值之后 就表示成已经取过值了
}

Clas watcher 的构造函数里

this.lazy = options.lazy; // 如果watcher上有lazy属性 说明是一个计算属性
this.dirty = this.lazy; // dirty代表取值时是否执行用户提供的方法

// 原本默认会先调用一次get方法 ,进行取值 将结果保留下来
// 现在改为 发现是 计算属性 就不调用 this.get 
this.value = this.lazy ? void 0 : this.get(); // 默认会调用get方法 现在改为 发现是 计算属性 就不调用 this.get

实际上 在第一次 取值的时候 还是会通过 watcher.evaluate() 调用 this.get() 从而 调用传进来的计算函数 ,并把当前watcher置为全局Dep.target(这里有个问题,依赖属性没有搜集 计算属性的渲染 watcher,后面再说), 调用计算属性函数(也就是实例化watcher传入的getter,注意,这个getter不是计算属性劫持的createComputedGetter)的时候会对该计算属性依赖的属性进行取值,这时就把该 watcher 加入到了所依赖的属性的dep里 例如:firstName和lastName 就会吧 fullName的watcher 放进 他们的 dep中

computed:{ //内部也使用了defineProperty, 内部有一个变量 dirty
    // computed还是一个watcher,内部依赖的属性会收集这个watcher
    fullName(){
        //  this.firstName ,this.lastName 在求值时, 会记住当前计算属性的watcher
        return this.firstName + this.lastName
    }
}

现在如何判断计算属性的值重新计算,肯定是依赖的属性的值改了,但是如何在依赖的属性的值改了的时候重新调用watcher的this.get()重新计算呢 如上例,当 this.firstName 更改的时候 会调用该属性的对应的dep.notify()

dep.notify(); // 异步更新 防止频繁操作

dep.notify() 会 循环调用 其保存的watcher的update 方法

notify(){
    this.subs.forEach(watcher=>watcher.update());
}

update 方法

update() {
    if (this.lazy) { // 是计算属性
        this.dirty = true;// 页面重新渲染就可以获得最新的值了
    }else{
        // 这里不要每次都调用get方法 get方法会重新渲染页面
        queueWatcher(this); // 暂存的概念
        //this.get(); // 重新渲染
    }
}

update 方法里 会把this.dirty 设置为 true ,这就意味着下次重新取值计算属性fullName的时候,fullName对应的 watcher 就会执行watcher.evaluate() ,这时候就会重新计算

if(watcher.dirty){ // 默认肯定是脏的 会重新求值
      watcher.evaluate(); // 对当前watcher求值,即调用计算属性的get
  }

现在还有一个问题 ,在this.firstName 修改之后,fullName如果重新取值就会重新计算 可是 我们希望他自动重新计算,也就是说我们希望他被重新取值,也就是说我们希望this.firstName这个属性的dep 搜集该计算属性fullName 的渲染watcher

这时,我们可以去更改Dep

Dep.target = null; // 静态属性 就一份
let stack = []; 
export function pushTarget(watcher){
   Dep.target = watcher;// 保留watcher
   stack.push(watcher); // 有渲染watcher 其他的watcher
   console.log(stack);
}

export function popTarget(){
   stack.pop();
   Dep.target = stack[stack.length-1]; // 将变量删除掉
}

所以,在渲染 fullName 所在的组件时,肯定会有一个对应的渲染 watcher ,在渲染中,对fullName取值,就会生成对应的fullName计算属性 watcher 如上更改 Dep.target 放进栈中,在取值fullName时把fullName的watcher 进栈(渲染watcher 还在栈中) 在执行完计算之后 popTarget 就把 Dep.target 变为了渲染属性

此时我们再看 createComputedGetter 生成的 fullName 属性被拦截的 get 方法 他在 计算后 会再去看看 Dep.target是否为空,如果不为空 ,那么 说明还有渲染watcher,也应该一并的收集起来,搜集进 fullName 的计算属性 watcher 中

function createComputedGetter(key){
    return function (){ // 此方法是我们包装的方法,每次取值会调用此方法
        const watcher = this._computedWatchers[key]; // 拿到这个属性对应watcher
        if(watcher){
            if(watcher.dirty){ // 默认肯定是脏的
                watcher.evaluate(); // 对当前watcher求值
            }
            debugger;
            if (Dep.target) { // 说明还有渲染watcher,也应该一并的收集起来
                watcher.depend();
            }
            return watcher.value; // 默认返回watcher上存的值
        }
    }

}

watcher的depend方法(不是Dep的depend),我们可以看到,它会拿这个 计算属性 的 dep (是的,计算属性fullName 依赖 的属性 firstName、lastName的 Dep 会存储fullName的计算属性 watcher ,且该watcher也会搜集dep )实现原理 如下 ,

class Dep 的depend 方法 会拿到 Dep.target 也就是当前的 watcher ,调用 其addDep(this) 并把自己dep传过去


depend(){
    // 我们希望 watcher 可以存放dep 
    Dep.target.addDep(this); // 实现双向记忆的,让watcher记住dep的同时 ,让dep 也记住watcher
    // this.subs.push(Dep.target);
}

class Watcher 的addDep 会 把此 dep 存入 this.deps ,并调用 dep.addSub(this) 把这个watcher 存入 dep

addDep(dep) {
    let id = dep.id;
    if (!this.depsId.has(id)) {
        this.deps.push(dep);
        this.depsId.add(id);
        dep.addSub(this)
    }
}

这就实现了 dep 和 watcher 的双向 记忆 ,watcher 存储 dep 还进行了 去重 后续我们知道 Dep 也通过了 set 来 去重 watcher

所以 下文的 代码 就是 使用 计算属性的watcher的 this.deps 去存储fullName的 渲染 watcher 也就是 在 firstName、lastName 的dep 里存储 fullName的 渲染 watcher ,自此 整个计算属性的功能完成;

depend(){
    // 计算属性watcher 会存储 dep  dep会存储watcher

    // 通过watcher找到对应的所有dep,让所有的dep 都记住这个渲染watcher
    let i = this.deps.length;
    while(i--){
        this.deps[i].depend(); // 让dep去存储渲染watcher
    }
}

当首次渲染的时候会取值fullName 属性 ,因为 该属性在初始化阶段就创建了自己的计算属性watcher,并用key :value存储在了Vue的实例上,并劫持了(Object.defineProperty)自身属性的get,因此,在取值 fullName 的时候,就会调用 fullName的 get 方法,该方法 会拿到 这个 属性 的计算属性 watcher 并 查看 watcher.dirty (初始取值 肯定是 true) ;

如果watcher.dirty 是true 就执行 watcher.evaluate(),在这个方法中 会执行 计算 操作 ,并把 watcher的dirty 赋值为 false 把计算出来的 值 赋给 watcher的 value 属性

这样就实现了缓存

在执行 evaluate(),watcher 会 调用 get() 并把自身这个 计算属性watcher 放入 栈中 并设为 Dep.target 然后执行 计算 ,计算时肯定会对 该计算属性 fullName 的 firstName、lastName 取值,这时 会调用两个属性对应的Dep.append 把 fullName 的watcher 存入到dep中,同时也把dep 存入到 fullName 的watcher 中 然后 该watcher 出栈,Dep.target 重新置为渲染watcher

evaluate() 执行完毕

get() {
    // Dep.target = watcher
    pushTarget(this); // 当前watcher实例
    let result = this.getter.call(this.vm); // 调用exprOrFn  渲染页面 取值(执行了get方法)  render方法 with(vm){_v(msg)}
    popTarget(); //渲染完成后 将watcher删掉了
    return result //赋给 了 watcher 的 value
}

但在 evaluate() 执行完之后 还会看当前 Dep.target

if (Dep.target) { // 说明还有渲染watcher,也应该一并的收集起来
    watcher.depend();
}

如果有,就让 计算属性 watcher 存储的dep 都把 该Dep.target(也就是 渲染 watcher )存入dep 中, 同时也在 Dep.target 中存了 对应的 dep

这样firstName 改变的时候 就会执行 dep.notyify,调用 fullName 的 计算属性watcher的update 方法 ,把 dirty 设为true ,调用fullName 的 渲染watcher的update 方法,重新渲染,之然就会重新取直,由于dirty 已经重新置为true ,就会重新计算,