2020-07-28

了解Vue计算属性的实现原理


computed的作用

在vue的开发中,我们不免会使用到计算属性,使用计算属性,vue会帮我们收集所有的该计算属性所依赖的所有data属性的依赖,当data属性改变时,便会重新获取computed属性,这样我们就不用关注计算属性所依赖的data属性的改变,而手动修改computed属性,这是vue强大之处之一。那么我们不免会产生疑问,computed属性为啥能随着data属性的改变而跟着改变的?

带着这个疑问,我们来解析下vue的源码,看看它是如何实现computed的依赖收集。

整体流程

computed的依赖收集是借助vue的watcher来实现的,我们称之为computed watcher,每一个计算属性会对应一个computed watcher对象,

该watcher对象包含了getter属性和get方法,getter属性就是计算属性对应的函数,get方法是用来更新计算属性(通过调用getter属性),并会把该computed watcher添加到计算属性依赖的所有data属性的订阅器列表中,这样当任何计算属性依赖的data属性改变的时候,就会调用该computed watcher的update方法,把该watcher标记为dirty,然后更新dom的dom watcher更新dom时,会触发dirty的computed

watcher调用evaluate去计算最新的值,以便更新dom。

所以computed的实现是需要两个watcher来实现的,一个用来收集依赖,一个用来更新dom,并且两种watcher是有关联的。后续我们把更新DOM的watcher称为domWatcher,另一种叫computedWatcher


initComputed

该方法是用来初始化computed属性的,它会遍历computed属性,然后做两件事:

1、为每个计算属性生成一个computedWathcer,后续计算属性依赖的data属性会把这个computedWatcher添加到自己订阅器列表中,以此来实现依赖收集。

2、挟持每个计算属性的get和set方法,set方法没有意义,主要是get方法,后面会提到。

function initComputed (vm, computed) {

  varwatchers = vm._computedWatchers = Object.create(null);

  //遍历所有的computed属性

  for (varkey in computed) {

    varuserDef = computed[key];

    //每个计算属性对应的函数或者其get方法(computed属性可以设置get方法)

    vargetter = typeof userDef === 'function' ? userDef : userDef.get;

    // ....

    if(!isSSR) {

      //为每个计算属性生成一个Wathcer

     watchers[key] = new Watcher(

        vm,

       getter || noop,

        noop,

       computedWatcherOptions

      );

    }

  if (!(keyin vm)) {

      //defineComputed的作用就是挟持每个计算属性的get和set方法

     defineComputed(vm, key, userDef);

    } else {

      // ....

    }

  }

}

defineComputed

如上面所述,definedComputed是挟持计算属性get和set方法,当然set方法对于计算属性是没什么作用,所以这里我们重点关注get方法,我们这里只需要知道get方法是触发依赖收集的关键,并且它把两种watcher进行了关联。

function defineComputed (

  target,

  key,

  userDef

) {

  varshouldCache = !isServerRendering();

  //下面这段代码就是定义get和set方法了

  if (typeofuserDef === 'function') {

   sharedPropertyDefinition.get = shouldCache

      ?createComputedGetter(key)

      :userDef;

   sharedPropertyDefinition.set = noop;

  } else {

   sharedPropertyDefinition.get = userDef.get

      ?shouldCache && userDef.cache !== false

        ?createComputedGetter(key)

        :userDef.get

      : noop;

   sharedPropertyDefinition.set = userDef.set

      ?userDef.set

      : noop;

  }

  //...

  //这里进行挟持

 Object.defineProperty(target, key, sharedPropertyDefinition);

}

createComputedGetter

createComputedGetter有两个作用:

1、收集依赖

当domWatcher获取计算属性的时候,会触发该方法,然后computedWatcher会调用evaluate方法,最终会调用computedWatcher的get方法(下面会分析),来完成依赖的收集

2、关联两种watcher

通过第一步完成依赖收集后,computedWatcher能知道依赖的data属性的改变,从而计算出最新的计算属性值,那么它是怎么让另外一个watcher,即domWatcher知道的呢,其实就是通过调用computedWatcher.depend方法把两种watcher关联起来的,这个方法会把Dep.target(就是domWatcher)放入到计算属性依赖的所有data属性的订阅器列表中。

通过这两个作用,当计算属性依赖的data属性有改变的时候,就会调用domWatcher的update方法,它会获取计算属性的值,因此会触发computedGetter方法,使得computedWatcher调用evaluate来计算最新的值,以便domWatcher更新dom。

function createComputedGetter (key) {

  returnfunction computedGetter () {

    //取出initComputed创建的watcher

    varwatcher = this._computedWatchers && this._computedWatchers[key];

    if(watcher) {

      //这个dirty的作用一个是避免重复计算,比如我们的模板中两次引用了这个计算属性,那么我们只需要计算一次就够了,一个是当计算属性依赖的data属性改变,会把这个计算属性对应的watcher给设置为dirty=true,然后

      if(watcher.dirty) {

        //这个会计算计算属性的值,并且会调用watcher的get方法,完成依赖收集

       watcher.evaluate();

      }

      //Dep.target指向的是模板中计算属性对应节点的domWatcher

      //这个语句的意思就是把domWatcher放入到当前computedWatcher的所有依赖中,这样计算属性依赖的data值一改,

      //就会触发domWatcher的update方法,它会获取计算属性的值从而触发这个computedGetter,然后computedWatcher会通过调用evaluate方法获取最新值,

      //然后交给domWatcher更新到dom

      if(Dep.target) {

       watcher.depend(); //关联了两种watcher

      }

      returnwatcher.value

    }

  }

}

Computed Watcher

watcher是实现computed依赖的关键,它的第二个参数getter属性即是计算属性对应的方法或get方法。

var Watcher = function Watcher (

  vm,

  expOrFn,

  cb,

  options,

 isRenderWatcher

) {

  this.vm =vm;

  // ...

 // watcher的第二个参数,即是我们计算属性对应的方法或get方法,用于算出计算属性的值

  if (typeofexpOrFn === 'function') {

   this.getter = expOrFn;

  } else {

   this.getter = parsePath(expOrFn);

    if(!this.getter) {

     this.getter = function () {};

    }

  }

  //不会立即计算

  this.value= this.lazy

    ?undefined

    :this.get();

};

那么只要调用getter方法,那么它就会触发计算属性所有依赖的data的get方法,我们看下get方法

 Object.defineProperty(obj, key, {

   enumerable: true,

   configurable: true,

    get:function reactiveGetter () {

      varvalue = getter ? getter.call(obj) : val;

      //Dep.target保存的是当前正在处理的Watcher,这里其实就是computedWatcher

      if(Dep.target) {

        //这句代码其实就是将Dep.target放入到该data属性的订阅器列表当中

       dep.depend();

        //...

      }

      returnvalue

    },

    ...

})

如上所述,其实就是把Dep.taget(当前的watcher)放入到该data属性的订阅器列表当中,那么这个时候,Dep.target指向哪个Watcher呢?我们看下watcher的get方法

Watcher.prototype.get = function get () {

  //这句语句会把Dep.target执行本watcher

 pushTarget(this);

  var value;

  var vm =this.vm;

  try {

    //调用getter,会触发上述讲的get,而get方法就会把Dep.target即本watcher放入到计算属性所依赖的data属性的订阅器列表中

    //这样依赖的data属性有改变就会调用该watcher的update方法

    value =this.getter.call(vm, vm);

  } catch (e){

    //...

  } finally {

    //...

   popTarget(); //将Dep.target指回上次的watcher,这里就是计算属性对应的domWatcher

   this.cleanupDeps();

  }

  returnvalue

};

可以看到get方法开始运行时,把Dep.target指向计算属性对应的computedWatcher,然后调用watcher的getter方法,触发这个计算属性对应的data属性的get方法,就会把Dep.target指向的watcher加入到这些依赖的data的订阅器列表当中,以此完成依赖收集。

这样当我们的计算属性依赖的data属性改变的时候,就会调用订阅器的notify方法,它会遍历订阅器列表,其中就包含了该计算属性对应的computedWatcher和domWatcher,调用computedWatcher的update方法会把computedWatcher置为dirty,调用domWathcer的update方法会触发computedGetter,它会再次调用computedWatcher的evaluate计算出最新的值交给domWatcher去更新dom。

Watcher.prototype.update = function update () {

  if(this.lazy) {

    //computed专属的watcher走这里

   this.dirty = true;

  } else if(this.sync) {

    // run方法会调用get方法,get方法会重新计算计算属性的值

    //但这个时候get方法不会再收集依赖了,vue会去重

   this.run();

  } else {

   queueWatcher(this);

  }

};

Watcher.prototype.run = function run () {

  if(this.active) {

    //调用get方法,重新计算计算属性的值

    var value= this.get();

    //值改变了、Array或Object类型watch配置了deep属性为true的

    if (

      value!== this.value ||

     isObject(value) ||

     this.deep

    ) {

      varoldValue = this.value;

     this.value = value;


      if(this.user) {

        //watch监听走此处

        try {

         this.cb.call(this.vm, value, oldValue);

        }catch (e) {

         handleError(e, this.vm, ("callback for watcher \"" +(this.expression) + "\""));

        }

      } else{

        //data数据改变,会触发更新函数cb,从而更新dom

       this.cb.call(this.vm, value, oldValue);

      }

    }

  }

};

总结

遍历computed,为每个计算属性新建一个computedWatcher对象,并将该computedWatcher的getter属性赋值为计算属性对应的方法或者get方法。(大家应该知道计算属性不但可以是一个函数,还可以是一个包含get方法和set方法的对象吧)

使用Object.defineProperty挟持计算属性的get方法,当模版获取计算属性的值的时候,触发get方法,它会调用第一步创建的computedWatcher的evaluate方法,而evaluate方法就会调用watcher的get方法

computedWatcher的get方法会将Dep.target指向该computedWatcher,并调用getter方法,getter方法会触发该计算属性依赖的所有data属性的get方法,从而把Dep.target指向的computedWatcher添加到data属性的订阅器列表中。同时,computedWatcher保存了依赖的data属性的订阅器(deps属性保存)。

同时调用computedWatcher的depend方法,它会把Dep.taget指向的domWatcher放入到计算属性依赖的data属性的订阅器列表中,如此计算属性依赖的data属性改变了,就会触发domWatcher和computedWatcher的update方法,computedWatcher赋值获取计算属性的最新值,domWatcher负责更新dom。

你可能感兴趣的:(2020-07-28)