Vue 源码笔记 - initState 相关

569 阅读4分钟

iniStatevm._init 中在 initInjectinitProvide 中间的一步初始化操作,用于处理 propsmethodsdatacomputedwatch 属性的初始化。iniState 一开始就有个 vm._watchers = [] 的初始化操作,vm._watchers 用于存储当前 vm._render 过程中生成的所有 watcher 实例,因为除了 methods 以外其他几个属性都涉及响应式数据,涉及到响应式数据就会涉及到 watcher。

对于,data 我们知道是组件在调用自身的 _render 方法时,其 render watcher 会订阅视图中依赖 data 的数据,然后在那些数据变化时,组件重新调用 _render 进而生成新的视图。那对于另外三个,它们又是怎样触发 vm 的 _render?或者说与 watcher 是怎样发生纠葛的?

initProps

从父 vm 里传入的 props 相关的数据称为 propsData,数据存在组件自身的 $options.propsData 中。组件 options 里 props 属性的值称之为 propsOptions,数据存在组件自身的 $options.props 中。initProps 的处理 propsData 过程基本可以概括为:

  1. 为 vm 初始化 _props 属性为一个空对象,vm.$options._propKeys = []
  2. 依次遍历 propsOptions 里的属性名,校验 propsData 对应属性的值,将所有校验通过的值按其属性名添加到 vm._props 中,并将属性名 push 到 vm.$options._propKeys 中,然后将之转换成响应式,最后将这个属性代理到 vm 上。

需要注意的是,对于非根组件而言,将 vm._props 的里的各个属性转换为响应式时,转换是「浅」层次的,即对于 vm._props 中对象或是数组类型的值,不会进一步 observe 它们。

_props 是响应式的也就也就意味着,_props 里数据的改变会触发订阅它的 watcher 的 update,假设这个 watcher 就是当前 vm 的 render watcher,那么 this.Aprop = 'xx' 是会触发 vm._render 的,但是这样做会得到一个禁止如此的警告:因为当父组件更新时,在子组件里手动改的 props 里的数据又会被父组件传过来的数据给覆盖。那么 「props 里数据的合法变化时是如何触发组件的更新的?」的问题就变成了「父组件更新时是如何触发子组件更新的?一定会触发子组件更新吗?」的问题。很容易定位到与之相关的是下面的代码:

// 子组件在父组件 _render 过程中调用 updateChildren 函数时,被 patchVnode 函数处理
function patchVnode(oldVnode, vnode,) {
  // ...
  let i
  let data = vnode.data

  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode);
  }
  // ...
}

const componentVNodeHooks = {
  // ...

  prepatch: function prepatch (oldVnode, vnode) {
    const options = vnode.componentOptions
    const child = vnode.componentInstance = oldVnode.componentInstance

    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },

  // ...
}

function updateChildComponent(vm, propsData, listeners, parentVnode, renderChildren) {
  // ...

  // update props
  if (propsData && vm.$options.props) {
    toggleObserving(false)
    const props = vm._props
    const propKeys = vm.$options._propKeys || []

    for (let i = 0; i < propKeys.length; i++) {
      const key = propKeys[i]
      const propOptions = vm.$options.props;

      props[key] = validateProp(key, propOptions, propsData, vm)
    }

    toggleObserving(true)
    vm.$options.propsData = propsData
  }

  // ...
}

子组件 patchVnode 中会调用 component vnode 的 prepatch hook,这个 hook 就会更新 propsData,如果 propsData 里的数据确实有变化,那 props[key] = validateProp(key, propOptions, propsData, vm) 就会触发子组件的 render watcher 的 update 从而刷新视图。初此之外,$listener$attrs 属性的变化也有这样的作用。因此也可以知道,父组件的更新不一定触发子组件的更新,除非 propData$listener$attrs 发生变化。

initComputed

initComputed 会为 computed 里所有数据各自生成一个 watcher 实例,与 render watcher 不同,为 computed 属性生成的 watcher 实例有一个 { lazy: true } 的选项,这个选项是 computed watcher 专属的,方便起见,称这样的 watcher 为 lazy watcher。lazy watcher 在实例化时不会立即求值,只有在 watcher.dirtytrue 时才会求值,而其 watcher.update 方法则是将 watcher.dirty 置为 true,求值后又会将其置回 false,假设下面的有这样的组件:

{
  data() {
    return {
      hi: 'hello'
    }
  },

  computed: {
    helloKitty() {
      return this.hi + ', kitty
    }
  },

  render() {
    return <div>{this.helloKitty}</>
  },

  mounted() {
    setTimeout(() => this.hi = 'hi', 2000)
  }
}

组件从创建到 mounted,再到 updated 中间是如何由 watcher 驱动的?

首先是 initData$data.hi 变为响应式,然后 initComputed 时,为 helloKitty 创建一个 lazy watcher,因为是 lazy 的,所以此时还不会触发 $data.higetter;最后 vm._render 调用时会访问 helloKitty,进而导致 lazy watcher 求值:

// 访问 computed 时会触发这个函数,key 为 computed 属性的 key
function computedGetter() {
  const watcher = this._computedWatchers && this._computedWatchers[key]

  if(watcher) {
    if(watcher.dirty) {
      // 求值
      watcher.value = watcher.evaluate()
    }

    if(Dep.target) {
      watcher.depend()
    }

    return watcher.value
  }
}

watcher.evaluate 中首先会更新 Dep.target 为 lazy watcher,然后因为要访问 $data.hi,所以会触发其 getter,求值完后又将其还原为更新之前的值。更新之前的值为当前组件的 render watcher,因为这个求值的过程时发生在 render watcher 的求值过程中的。所以之后 watcher.depend() 的操作又会使得 render watcher 也会去订阅 lazy watcher 求值过程中订阅的响应式数据:$data.hi。也就是说,computed 里依赖的响应式数据 $data.hi 最终会同时被依赖它的 computed 数据对应的 lazy watcher 和组件的 render watcher 同时订阅。

那么,当 $data.hi 变化时,其 setter 的触发导致 lazy watcher 和 render watcher 都会 update。首先,lazy watcher update 时其 dirty 属性会被置为 true,然后在 render watcher run 的时候也就是调用组件的 _update(当然也会导致调用 _render)时,又会访问 helloKitty,即触发上面的 computedGetter,最后更新并返回更新后的值,也就完成了视图的更新。另外,通过 computedGetter 内部可以得知,只有当 watcher.dirty 为真时,lazy watcher 才会重新求值,也就是说只要依赖的响应式数据没有发生变化,那么每次都是直接返回上一次求得的值,而不会重新 evaluate,这就是官方文档里说的计算属性是基于它们的响应式依赖进行缓存的

initWatch

options.watch 里的各个数据单独创建一个 watcher,这个 watcher(称为一般 watcher) 与 render watcher 和 lazy watcher 都不同的地方在于,最终传给 new Watcher(vm, key, handler) 的第二个参数是一个字符串(当前 vm 可以借此访问到对应响应式数据的路径),第三个参数是对应响应式数据发生变化时的回调;而另外两个 watcher 创建时第二个参数都是函数(一个是 updateComponent 一个是 computedGetter),第三个参数是一个什么也不做的 noop 函数。也就是说相对于另外两个 watcher 不需要第三个参数就可以完成它们想要做的事(求值过程包含了添加、更新依赖以及更新组件,按惯常理解,更新组件应该属于回调),而一般 watcher 则是求值和回调的过程是分开的,求值就是求值,除了包括添加和更新依赖的副作用以外,并不包括像更新组件这样的副作用,所以对应的回调就需要一个额外的参数。可以这样理解:render watcher 和为 computed 所创建的 lazy watcher 仿佛就是专门为组件的更新定制的。为 $options.watch 创建的一般 watcher 按理才是 watcher 的正确使用方式:监听某个值,值变化时,触发对应回调。

一般 watcher 的依赖添加在 parsePath 返回的函数中完成。