2020-07-28

作者: 灵魂的统帅 | 来源:发表于2020-07-28 11:03 被阅读0次

    了解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

        本文链接:https://www.haomeiwen.com/subject/zptfrktx.html