博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue源码分析系列之响应式数据(三)
阅读量:6271 次
发布时间:2019-06-22

本文共 12626 字,大约阅读时间需要 42 分钟。

前言

上一节着重讲述了initData中的代码,以及数据是如何从data中到视图层的,以及data修改后如何作用于视图。这一节主要记录initComputed中的内容。

正文

前情回顾

在demo示例中,我们定义了一个计算属性。

computed:{  total(){    return this.a + this.b  }}

本章节我们继续探究这个计算属性的相关流程。

initComputed

// initComputed(vm, opts.computed)function initComputed (vm: Component, computed: Object) {  // 定义计算属性相关的watchers.  const watchers = vm._computedWatchers = Object.create(null)  // 是否是服务端渲染,这里赞不考虑。  const isSSR = isServerRendering()  for (const key in computed) {    // 获得用户定义的计算属性中的item,通常是一个方法    // 在示例程序中,仅有一个key为total的计算a+b的方法。    const userDef = computed[key]    const getter = typeof userDef === 'function' ? userDef : userDef.get    if (process.env.NODE_ENV !== 'production' && getter == null) {      warn(        `Getter is missing for computed property "${key}".`,        vm      )    }    if (!isSSR) {      // create internal watcher for the computed property.      // 为计算属性创建一个内部的watcher。      // 其中computedWatcherOptions的值为lazy,意味着这个wacther内部的value,先不用计算。      // 只有在需要的情况下才计算,这里主要是在后期页面渲染中,生成虚拟dom的时候才会计算。      // 这时候new Watcher只是走一遍watcher的构造函数,其内部value由于      // lazy为true,先设置为了undefined.同时内部的dirty = lazy;      watchers[key] = new Watcher(        vm,        getter || noop,        noop,        computedWatcherOptions // 上文定义过,值为{lazy: true}      )    }    // component-defined computed properties are already defined on the    // component prototype. We only need to define computed properties defined    // at instantiation here.    // 组件定义的属性只是定义在了组件上,这里只是把它翻译到实例中。即当前的vm对象。    if (!(key in vm)) {      // 将计算属性定义到实例中。      defineComputed(vm, key, userDef)    } else if (process.env.NODE_ENV !== 'production') {      if (key in vm.$data) {        warn(`The computed property "${key}" is already defined in data.`, vm)      } else if (vm.$options.props && key in vm.$options.props) {        warn(`The computed property "${key}" is already defined as a prop.`, vm)      }    }  }}

defineComputed

const sharedPropertyDefinition = {  enumerable: true,  configurable: true,  get: noop,  set: noop}// defineComputed(vm, key, userDef)export function defineComputed (  target: any,  key: string,  userDef: Object | Function) {  // 是否需要缓存。即非服务端渲染需要缓存。  // 由于本案例用的demo非服务端渲染,这里结果是true  const shouldCache = !isServerRendering()  if (typeof userDef === 'function') {    // userDef = total() {...}    sharedPropertyDefinition.get = shouldCache      // 根据key创建计算属性的getter      ? createComputedGetter(key)      : userDef    // 计算属性是只读的,所以设置setter为noop.    sharedPropertyDefinition.set = noop  } else {    sharedPropertyDefinition.get = userDef.get      ? shouldCache && userDef.cache !== false        ? createComputedGetter(key)        : userDef.get      : noop    sharedPropertyDefinition.set = userDef.set      ? userDef.set      : noop  }  // 计算属性是只读的,所以设置值得时候需要报错提示  if (process.env.NODE_ENV !== 'production' &&      sharedPropertyDefinition.set === noop) {    sharedPropertyDefinition.set = function () {      warn(        `Computed property "${key}" was assigned to but it has no setter.`,        this      )    }  }  // 将组件属性-》实例属性,关键的一句,设置属性描述符  Object.defineProperty(target, key, sharedPropertyDefinition)}

createComputedGetter

// 根据key创建计算属性的getter// createComputedGetter(key)function createComputedGetter (key) {  return function computedGetter () {    // 非服务端渲染的时候,在上述的initComputed中定义了vm._computedWatchers = {},并根据组件中的设定watchers[key] = new Watcher(..),这里只是根据key取出了当时new的watcher    const watcher = this._computedWatchers && this._computedWatchers[key]    if (watcher) {      // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。      // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为      // dirty = lazy = true;      // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候,      // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候      // 我们的total就已经脏了。需要根据最新的a,b计算。      if (watcher.dirty) {        // 计算watcher中的值,即value属性.        watcher.evaluate()      }      // 将依赖添加到watcher中。      if (Dep.target) {        watcher.depend()      }      // getter的结果就是返回getter中的值。      return watcher.value    }  }}

initComputed小结

继initComputed之后,所有组件中的computed都被赋值到了vm实例的属性上,并设置好了getter和setter。在非服务端渲染的情况下,getter会缓存计算结果。并在需要的时候,才计算。setter则是一个什么都不做的函数,预示着计算属性只能被get,不能被set。即只读的。

接下来的问题就是:

  1. 这个计算属性什么时候会计算,前文{lazy:true}预示着当时new Watcher得到的值是undefined。还没开始计算。
  2. 计算属性是怎么知道它本身依赖于哪些属性的。以便知道其什么时候更新。
  3. vue官方文档的缓存计算结果怎么理解。

接下来我们继续剖析后面的代码。解决这里提到的三个问题。

用来生成vnode的render函数

下次再见到这个计算属性total的时候,已是在根据el选项或者template模板中,生成的render函数,render函数上一小节也提到过。长这个样子。

(function anonymous() {    with (this) {        return _c('div', {            attrs: {                "id": "demo"            }        }, [_c('div', [_c('p', [_v("a:" + _s(a))]), _v(" "), _c('p', [_v("b: " + _s(b))]), _v(" "), _c('p', [_v("a+b: " + _s(total))]), _v(" "), _c('button', {            on: {                "click": addA            }        }, [_v("a+1")])])])    }})

这里可以结合一下我们的html,看出一些特点。

a:{

{a}}

b: {

{b}}

a+b: {

{total}}

这里使用到计算属性的主要是这一句

_v("a+b: " + _s(total))

那么对于我们来说的关键就是_s(total)。由于这个函数的with(this)中,this被设置为vm实例,所以这里就可以理解为_s(vm.total)。那么这里就会触发之前定义的sharedPropertyDefinition.get

-> initComputed()-> defineComputed()-> Object.defineProperty(target, key, sharedPropertyDefinition)

也就是createComputedGetter返回的函数中的内容,也就是:

watcher细说

const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {  // 由于初始化的时候这个dirty为true,所以会进行watcher.evaluate()的计算。  if (watcher.dirty) {    watcher.evaluate()  }  if (Dep.target) {    watcher.depend()  }  // getter的结果就是返回getter中的值。  return watcher.value}

这里我们看下watcher.evaluate的部分。

// class Watcher内部/**   * Evaluate the value of the watcher.   * This only gets called for lazy watchers.   */  evaluate () {    this.value = this.get()    this.dirty = false  }

这里this.get即得到了value的值,这就是第一个问题的答案。

1.计算属性何时会计算。
即用到的时候会计算,精确的说,就是在计算vnode的时候会用到它,从而计算它。
对于第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?则是在这个
this.get内。

// Dep相关逻辑,Dep Class用来收集依赖某个值的watcherDep.target = nullconst targetStack = []export function pushTarget (_target: Watcher) {  if (Dep.target) targetStack.push(Dep.target)  Dep.target = _target}export function popTarget () {  Dep.target = targetStack.pop()}// Watcher class 相关逻辑get () {    // 将当前的watcher推到Dep.target中    pushTarget(this)    let value    const vm = this.vm    try {      // 这里的getter实际上就是对应total的函数体,      // 而这个函数体内藏有很大的猫腻,接下来我们仔细分析这一段。      value = this.getter.call(vm, vm)    } catch (e) {      if (this.user) {        handleError(e, vm, `getter for watcher "${this.expression}"`)      } else {        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  }

当代码执行到this.getter.call,实际上执行的是计算属性的函数,也就是

total() { return this.a + this.b};当代码执行到this.a时候。就会触发上一节我们所讲的defineReactive内部的代码。

这里我们以访问this.a为例export function defineReactive (  obj: Object,  // {a:1,b:1}  key: string,  // 'a'  val: any,     // 1  customSetter?: ?Function,  shallow?: boolean) {    const dep = new Dep()  const property = Object.getOwnPropertyDescriptor(obj, key)  if (property && property.configurable === false) {    return  }  // cater for pre-defined getter/setters  const getter = property && property.get  const setter = property && property.set  let childOb = !shallow && observe(val)  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter () {      const value = getter ? getter.call(obj) : val      // this.a会触发这里的代码。首先获得value,      // 由于watcher内部this.get执行total计算属性时候,已经将      // total的watcher设置为Dep.target      if (Dep.target) {        // 所以这里开始收集依赖。        dep.depend()        if (childOb) {          childOb.dep.depend()          if (Array.isArray(value)) {            dependArray(value)          }        }      }      return value    },    set: function reactiveSetter (newVal) {      const value = getter ? getter.call(obj) : val      /* eslint-disable no-self-compare */      if (newVal === value || (newVal !== newVal && value !== value)) {        return      }      /* eslint-enable no-self-compare */      if (process.env.NODE_ENV !== 'production' && customSetter) {        customSetter()      }      if (setter) {        setter.call(obj, newVal)      } else {        val = newVal      }      childOb = !shallow && observe(newVal)      dep.notify()    }  })}

上述代码中,this.a触发了dep.depend()。我们细看这里的代码。

class Dep {  //省略代码...  depend () {    // 由于这里的Dep.target此时对应的是total的watcher。    // 而这里的this.是指定义this.a时,生成的dep。    // 所以这里是告诉total依赖于this.a    if (Dep.target) {      // 通过调用addDep.让total的watcher知道total依赖this.a      Dep.target.addDep(this)    }  }}class Watcher {  // ...省略代码  addDep (dep: Dep) {    // 此时的this是total的watcher    const id = dep.id    // 防止重复收集    if (!this.newDepIds.has(id)) {      // 将依赖的可观察对象记录。      this.newDepIds.add(id)      this.newDeps.push(dep)      // 如果这个可观察对象没有记录当前watcher,      if (!this.depIds.has(id)) {        // 则将当前的watcher加入到可观察对象中        // (方便后续a变化后,告知total)        dep.addSub(this)      }    }  }}

至此,上述的第二个问题,计算属性是怎么知道它本身依赖于哪些属性的?也有了答案。就是当生成虚拟dom的时候,用到了total,由于得到total值的watcher是脏的,需要计算一次,然后就将Dep.target的watcher设为total相关的watcher。并在watcher内执行了total函数,在函数内部,访问了this.a。this.a的getter中,通过dep.depend(),将this.a的getter上方的dep,加入到total的watcher.dep中,再通过watcher中的dep.addSub(this),将total的watcher加入到了this.a的getter上方中的dep中。至此total知道了它依赖于this.a。this.a也知道了,total需要this.a。

当计算属性的依赖变更时发生了什么

当点击页面按钮的时候,会执行我们案例中绑定的this.a += 1的代码。此时会走

this.a的setter函数。我们看看setter中所做的事情。

set: function reactiveSetter (newVal) {  const value = getter ? getter.call(obj) : val  // 如果旧值与新值相当,什么都不做。直接返回。  if (newVal === value || (newVal !== newVal && value !== value)) {    return  }  // 无关代码,pass  if (process.env.NODE_ENV !== 'production' && customSetter) {    customSetter()  }  // 有定义过setter的话通过setter设置新值  if (setter) {    setter.call(obj, newVal)  } else {    // 否则的话直接设置新值    val = newVal  }  // 考虑新值是对象的情况。  childOb = !shallow && observe(newVal)  // 通知观察了this.a的观察者。  // 这里实际上是有两个观察a的观察者  // 一个是上一篇讲的updateComponent。  // 一个是这节讲的total。  dep.notify()}

这里我们看看dep.notify干了什么

class Dep {  // **** 其他代码  notify () {    // 这里的subs其实就是上述的两个watcher。    // 分别执行watcher的update    const subs = this.subs.slice()    for (let i = 0, l = subs.length; i < l; i++) {      subs[i].update()    }  }}class Watcher{  update () {    // 第一个watcher,即关于updateComponent的。    // 会执行queueWatcher。也就是会将处理放到等待队列里    // 等待队列中,而第二个watcher由于lazy为true,    // 所以只是将watcher标记为dirty。    // 由于队列这个比较复杂,所以单开话题去讲    // 这里我们只需要知道它是一个异步的队列,最后结果就是    // 挨个执行队列中watcher的run方法。    if (this.lazy) {      this.dirty = true    } else if (this.sync) {      this.run()    } else {      queueWatcher(this)    }  }  run () {    if (this.active) {      const value = this.get()      if (        value !== this.value ||        // Deep watchers and watchers on Object/Arrays should fire even        // when the value is the same, because the value may        // have mutated.        isObject(value) ||        this.deep      ) {        // set new value        const oldValue = this.value        this.value = value        if (this.user) {          try {            this.cb.call(this.vm, value, oldValue)          } catch (e) {            handleError(e, this.vm, `callback for watcher "${this.expression}"`)          }        } else {          this.cb.call(this.vm, value, oldValue)        }      }    }  }}

当触发了依赖更新时候,第一个watcher(关于total的)会将自己的dirty标记为true,第二个则会执行run方法,在其中运行this.get导致updateComponent执行,进而再次计算vnode,这时会再次计算this.total。则会再次触发total的getter,这时候我们再复习一下之前讲过的这个computed的getter:

const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {  // watcher.dirty表示这个值是脏值,过期了。所以需要重新计算。  // new Watcher的时候,这个total的watcher中,内部的dirty已经被置为  // dirty = lazy = true;  // 那么这个值什么时候会过期,会脏呢。就是内部的依赖更新时候,  // 比如我们的total依赖于this.a,this.b,当着两个值任意一个变化时候  // 我们的total就已经脏了。需要根据最新的a,b计算。  if (watcher.dirty) {    // 计算watcher中的值,即value属性.    watcher.evaluate()  }  // 将依赖添加到watcher中。  if (Dep.target) {    watcher.depend()  }  // getter的结果就是返回getter中的值。  return watcher.value}

至此,computed中total的更新流程也结束了。

所以我们的第3个问题,vue官方文档的缓存计算结果怎么理解?也就有了答案。也就是说计算属性只有其依赖变更的时候才会去计算,依赖不更新的时候,是不会计算的。正文这一小节提到的,total的更新是由于this.a的更新导致其setter被触发,因此通知了其依赖,即total这个watcher。如果total的不依赖于this.a,则total相关的watcher的dirty就不会变为true,也就不会再次计算了。

总结

本章节我们以示例程序探究了计算属性,从initComputed中,计算属性的初始化到计算属性的变更,对着代码做了进一步的解释。整体流程可以归纳为:

initComputed定义了相关的计算属性相关的watcher,以及watcher的getter。

在第一次计算vnode的时候顺便执行了计算属性的计算逻辑,顺便收集了依赖。本例中total收集到了依赖a,b;并且a,b也被告知total观察了他们。当a,b任何一个改变时的时候,就会将total相关的watcher.dirty设置为true,下次需要更新界面时,计算属性就会被重新计算。当然,如果没有依赖于total的地方。那么total是不会计算的,例如total根本没被界面或者js代码用到,就不会计算total;如果total所有的依赖没有变更,其dirty为false,则也是无需计算的。

文章链接

转载地址:http://xqlpa.baihongyu.com/

你可能感兴趣的文章
hadoop、hbase、zookeeper集群搭建
查看>>
python中一切皆对象------类的基础(五)
查看>>
modprobe
查看>>
android中用ExpandableListView实现三级扩展列表
查看>>
%Error opening tftp://255.255.255.255/cisconet.cfg
查看>>
java读取excel、txt 文件内容,传到、显示到另一个页面的文本框里面。
查看>>
《从零开始学Swift》学习笔记(Day 51)——扩展构造函数
查看>>
python多线程队列安全
查看>>
[汇编语言学习笔记][第四章第一个程序的编写]
查看>>
android 打开各种文件(setDataAndType)转:
查看>>
补交:最最原始的第一次作业(当时没有选上课,所以不知道)
查看>>
Vue实例初始化的选项配置对象详解
查看>>
PLM产品技术的发展趋势 来源:e-works 作者:清软英泰 党伟升 罗先海 耿坤瑛
查看>>
vue part3.3 小案例ajax (axios) 及页面异步显示
查看>>
浅谈MVC3自定义分页
查看>>
.net中ashx文件有什么用?功能有那些,一般用在什么情况下?
查看>>
select、poll、epoll之间的区别总结[整理]【转】
查看>>
CSS基础知识(上)
查看>>
PHP中常见的面试题2(附答案)
查看>>
26.Azure备份服务器(下)
查看>>