vue源码分析系列之响应式数据(三)

前言

上一节着重讲述了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,看出一些特点。

<div id="demo">
  <div>
    <p>a:{{a}}</p>
    <p>b: {{b}}</p>
    <p>a+b: {{total}}</p>
    <button @click="addA">a+1</button>
  </div>
</div>

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

_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用来收集依赖某个值的watcher
Dep.target = null
const 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,则也是无需计算的。

文章链接

  • vue源码分析系列
  • vue源码分析系列之debug环境搭建
  • vue源码分析系列之入口文件分析
  • vue源码分析系列之响应式数据(一)
  • vue源码分析系列之响应式数据(二)

相关推荐