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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,039评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,426评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,417评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,868评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,892评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,692评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,416评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,326评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,782评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,957评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,102评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,790评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,442评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,996评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,113评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,332评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,044评论 2 355