组件的 update

170 阅读9分钟

在组件的生命周期中,持续最久的就是 update 相关的了吧,因为只要一个 vm 实例中有 data 那么它几乎避免不了 update,这也是像 Vue 这些框架真正体现其能出的地方,因为如果都是些静态组件的话,大可没必要引入一个额外的 js 文件来实现仅用原生 API 也能轻松完成的应用了。所以,较之 mount 生命周期的内容,这部分内容可能会更多,大体分三块:响应式原理组件 patch 的算法中与 update 相关的内容异步更新的原理

响应式原理

在探究一个东西内部的 'How' 时,往往需要先弄清楚其 'What',所以先试着阐明所谓的「响应式」到底是什么。Vue 官方文档中与响应式相关的描述:「 Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。」。可知当数据模型被修改时,视图自动更新的这种性质即为「响应式」。用 UI=f(state)UI = f(state) (注:整体看,这个 ff 不是 render,而是由 render 和其他部分共同组成的驱动这一切运行的整个系统,甚至 statestate 也属于 ff,后文中如有感觉明显缺少主语而看起来是病句的请默认把这个 ff 当主语;但细分来看,ff 可以是组成它的任何子部分。后文若觉矛盾之处,按此分别理解。)来说明就是当 statestate 变化时,ff 会自动对新的 statestate 求值生成一个新的 UIUI,而不需要使用者手动重新调用 f(state)f(state) 的性质。弄清楚这一点后,How 的问题即为下面两个问题:

  1. statestate 变化后,ff 如何得知
  2. 得知 statestate 变化后,ff 如何以合适的代价重新生成正确的 UIUI

第 1 个问题于本节解答,第 2 个问题于后两节。前者的答案体现在源码中主要涉及到 DepWatchdefineReactiveobserve 函数以及 mounted 过程中的一些初始化操作

由上面对「响应式」概念阐明,可知它本质是一种属性,而且归根结底是视图所依赖的数据的属性,即当某个数据的变化会导致依赖于这个数据的视图自动更新,那么我们便称这个数据是响应式的,否则不是响应式的。而 defineReactiveobserve 正是 Vue 中用来将数据变为响应式的方法,其核心在于 defineReactiveobserve 会为每个响应式的数据创建一个与之对应的 dep 实例,因此我们甚至可以说一个数据是不是响应式关键就看它有没有一个专属于它的 dep 实例。秉着先 What 后 How 的原则,暂且不去管被用作将数据转换成响应式的 defineReactiveobserve 内部是如何实现的,我们还是先从 Dep 说起。

Dep

稍微思考一下问题 1:在 A 将要(必然)发生变化的消息未提前走漏之前,B 何以得知 A 最终发生了变化?显然,定是或者 A 自己直接告知与 B,或者 A 经由一个与双方皆有联系的 C 间接告知与 B。在这里,一个由 Dep 创建的 dep 实例就是这样一个 C,内部实现相关的代码很少:

let uid = 0

class Dep {
  constructor() {
    // 每个 dep 实例唯一的标识
    this.id = uid++
    // 订阅当前 dep 实例的 watcher 实例数组
    this.subs = []
  }
  
  addSub(sub) {
    this.subs.push(sub)
  }
  
  removeSub(sub) {
    if(this.subs.length) {
      const index = this.subs.indexOf(sub)
      
      if(index > -1) {
        this.subs.splice(index, 1)
      }
    }
  }
  
  depend() {
    // 一个 watcher 实例的求值过程即处于激活状态,此时对 dep 实例的订阅称为「收集依赖」
    // 依赖,指 watcher 实例对响应式数据的依赖
    // Dep.target 为当前处于激活状态的 watcher 实例
    // 也可为空,为空时,标识当前阶段(组件的 hooks 期间)禁止订阅
    if(Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
  notify() {
    // 即挨个「告诉」订阅当前 dep 实例(实际是订阅某个数据)的 watcher 实例
    // 其订阅的那个数据发生了变化
    for(let i = 0, l = this.subs.length; i < l; ++i) {
      this.subs[i].update()
    }
  }
}

// 全局唯一,因为任何时候最多只能有一个 watcher 实例处于激活状态,初始为空
Dep.target = null
// 先后处于激活状态的 watcher 实例组成的栈
// 因为一个 watcher 实例的求值过程中可能会激活另一个 watcher 实例
const targetStack = []

function pushTarget(target) {
  Dep.target = target
  targetStack.push(target)
}

function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

由上可知,一个 dep 实例自身相关的数据只有其的 idsubsDep.target,其所有的活动都围绕着订阅这个 dep 背后响应式数据的 watcher 实例(们):

  • 当这个 dep 实例所对应的响应式数据需要被某个 watcher 实例订阅时,watcher 实例会被当成参数传入 pushTarget 调用
  • 然后 watcher 实例和 dep 实例之间被绑上一层关系,即通过 dep.depend() 执行 watcher.addDep(dep)dep.addSub(watcher)
  • 当然,dep 实例也可以解除通过 removeSub 方法结束这段关系
  • 最后,在与之对应的响应式数据发生变化的时候,dep 实例会调用其 notify 方法,其作用是为所有的 watcher 实例调用它们的 update 方法,也就算「将变化通知到位了」

即便暂时还不知道,watcher 实例的属性,我们也可以推知它和 dep 实例的关系是多对多的,因为:

  • 一个响应式数据可以被多个 watcher 实例依赖
  • 一个 watcher 实例也可以依赖多个响应式数据

以上便是与 dep 实例本身相关的所有内容,接下来我们来弄清楚 defineReactiveobserve 的 How。

defineReactive

defineReactive 是用来处理对象的属性的,我们直接看在源码 defineReactive$$1 的代码:

function defineReactive$$1(obj, key, value, customSetter, shalow) {
  // customSetter 好像在生产环境没啥用
  // 一上来就创建一个 dep 实例,可见一个 value 至少对应一个 dep
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  
  if(property && property.configurable === false) {
    return
  }
  
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // shallow 为 false 时且 val 也为对象时继续递归 defineReactive$$1 val 的每个属性
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      
      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
      
      if (newVal === value || (Number.isNaN(newVal) && Number.isNaN(value)) {
        return
      }

      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

可以看到 defineReactive$$1 所做的事也只不过是为一个对象里的数据属性新建一个 dep 实例,然后将这个属性变为访问器属性,即定制了这个属性的 getter 和 setter:

  • 在读取时增加了「如果当前有处于激活状态的 watcher 实例,则将之与 dep 实例关联」的逻辑
  • 在赋值时增加了「如果值真的发生变化,则调用 dep.notify()」的逻辑

就是这关键的两步使得当一个对象里属性的值发生变化时,依赖这个数据的视图即得到「通过」,然后更新。

上面代码还有 observedependArray 两个陌生的东西,dependArray 顾名思义,是将一个数组类型的值所对应的 dep 实例与当前激活状态的 watcher 实例关联。组件 options 里的 data 则正是被 observe 处理进而使其属性都变为响应式的。

observe

function observe(value, asRootData) {
  // 只处理非对象类型、非 vnode 实例的值
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  
  let ob
  
  if(Object.prototype.hasOwnProperty(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
    // 某些场景 shouldObserve 为 false
  } else if (
    shouldObserve &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    // 非 vm 实例
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  
  if(asRootData) {
    // 为 true 表示处理的是组件 options 的 data
    ob.vmCount++
  }
  
  return ob
}

class Observer {
  constructor(value) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    
    // 为 value 添加一个 __ob__ 的属性,值为 observer 实例
    Object.defineProperty(value, '__ob__', {
      value: this,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    })
    
    if (Array.isArray(value)) {
      // arrayMethods 改写了数组 push、pop、shift、unshift、splice、sort、reverse
      // 使得数组调用上述方法时会触发 dep.notify 从而具备响应式属性
      // 此外,数组的其他方法的调用不会受到影响,因为 arrayMethods 维护了原型链
      value.__proto__ = arrayMethods
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  
  walk(value) {
    const keys = Object.keys(value)
    // 为每个 key 调用 defineReactive$$1
    for(let i = 0, l = keys.length; i < l; ++i) {
      defineReactive$$1(value, keys[i])
    }
  }
  
  observeArray(value) {
    // observe 每个元素
    for(let i = 0, l = value.length; i < l; ++i) {
      observe(value[i])
    }
  }
}

oberve 处理的数据类型是数组和对象,它会为每个符合要求的数据生成一个 observer 实例,存在自身的 __ob__ 属性上,而 observer 实例生成的过程中会创建一个与那个数据相关的 dep 实例,正是这个 dep 实例使得通过数组的 pushpopshiftunshiftsplicesortreverse 行为和对象的 $set$delete 的行为能够被检测。

所以,综上看来,响应式的关键是 dep 实例,每个响应式数据至少对应一个 dep 实例。创建 dep 实例的操作包含在 defineReactivenew Observer 里,前者用于将一个数据对象的属性变为响应式,后者则用于将对象和数组本身变为响应式。可以推知,对于组件 options 里一个这样的 data() { return { a: { b: 1 } } },在 observe(data) 时,会创建两个与 data.a 对应的 dep 实例:

  1. 作为 data 的一个属性被 defineReactive$$1 函数处理而创建的可以在 data.a 的 getter/setter 里访问的 dep 实例
  2. data.a 本身作为一个对象被 observe 函数处理而创建的存储在 data.a.__ob__.dep 里的 dep 实例

最后,一个关于 dep 实例的小问题:

任一个由未添加任何 Vue 插件的 Vue 或者 VueComponent 创建的 vm 实例,在其创建过程中最少会生成几个 dep 实例呢?

Watcher

watcher 实例是响应式系统中的「响应者」,也就是被 dep 实例 notify 后去执行相应回调。其可以分为三类:

  1. render-watcher,即专门用于将组件实例对应的 vnode render 为 DOM 的 watcher,每个实例仅有一个,在组件 mounted 的过程中创建
  2. lazy-watcher,用于创建 computed 类型的响应式数据,组件实例 initComputed 过程中创建
  3. 一般 watcher,调用 vm.$watch 方法时生成的 watcher,处理组件 options 里的 watch 选项时创建的就是一般 watcher, 组件实例 initWatch 过程中创建

如你所想地,这些类别的产生由 Watcher 构造函数参数的差异导致:

class Watcher {
  constructor(vm, expOrFn, cb, options, isRenderWatcher) {
    this.vm = vm
    
    // render-watcher 与一般 watcher 实际只是用途的区别
    if(isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    
    this.cb = cb
    this.id = ++uid
    // 这个 active 属性同我说的「处于激活状态」并不是一回事
    this.active = true
    this.dirty = this.lazy // 只对 lazy-watcher 有用的属性
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = expOrFn.toString()
    
    if(typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      //parsePath 使得 watch 中的可以通过形如 a.b.c 这样的属性名监听 vm.a.b.c
      this.getter = parsePath(expOrFn) || (() => {})
    }
    
    this.value = this.lazy ? undefined : this.get()
  }
  
  // 对 watcher 实例求值,即将实例变为激活状态
  get() {
    pushTarget(this)
    
    let value
    const vm = this.vm
    
    try {
      value = this.getter.call(vm, vm)
    } catch {
      
    } finally {
      if (this.deep) {
        // 处理 options.deep 为 true 的 watch
        traverse(value)
      }
      popTarget()
      // 每次退出激活状态时更新与 deps 的绑定关系
      this.cleanupDeps()
    }
    
    return value
  }
  
  addDep(dep) {
    const id = dep.id
    
    if(!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      
      if(!this.depIds.has(id)) {
        // depIds 里不包含 id,说明 this 不在 dep.subs 里
        dep.addSub(this)
      }
    }
  }
  
  cleanupDeps() {
    // 1.更新 watcher 实例与 dep 实例的绑定关系,因为可能有的 dep 实例不需要再继续订阅了
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    // 2.将 newDeps、newDepIds 的信息存储到 deps 和 depIds 上,然后初始化前者
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  
  // 被 dep 实例 notify 后 watcher 实例调用的方法
  update() {}
}

update 方法的内容是什么,我们暂时也不需要理会,只知道它是调用了后我们就能看到响应式结果的东西就好。以 render-watcher 的创建为例:

new Watcher(
  vm,
  function updateComponent() {
    vm._update(vm.render())
  }, 
  noop,
  {
  before() {
    if(vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
})

在这个 render-watcher 实例被创建过程中:

  1. updateComponentthis.getter 引用
  2. this.get() 被执行
  3. 当前 render-watcher 被激活
  4. updateComponent 得到执行
  5. vm.render()vm._update() 得到执行

之前一篇说过 vm.render()vm._update() 意味着什么:前者会生成一个 vnode 实例,在这个 vnode 实例中会访问 data、computed 等选项中与视图内容有关的属性,这也就意味着会触发相关数据的 getter,然后使 watcher 实例和 dep 实例之间产生关联,从此每当相关的数据发生变更,watcher 实例就会 update;后者将 vnode 实例 patch 为 DOM。

最后还差一个环节:尽管知道那些数据变为响应式肯定是在 mounted hook 之前,但具体哪些数据是响应式的,以及它们分别是在何时变为响应式的?

组件 mount 过程中数据的响应式化

前面说过,响应式的关键是在 dep 实例,而 dep 实例的创建只有 defineReactive$$1observe 两个途径,所以源码中全局搜一下就好了,与二者相关的有:

// 1
function initInjections() {
  // ...
}

// 2
function initRender() {
  // ...
  defineReactive$$1(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true);
  defineReactive$$1(vm, '$listeners', options._parentListeners || emptyObject, null, true);
}

// 3
function initProps() {
  // ...
}

// 4
function initData() {
  // ...
  observe(data, true)
}

其中 2 发生在 beforeCreate hook 之前, 同属于 initState 的 3 和 4,以及 1 发生在 created hook 之前。这同时也解答了上面的问题:每个 vm 实例创建时至少会新建 3 个 dep 实例,分别是在 2 和 4 中,因为 injection 和 props 不一定有。

小结

以上便是响应式的基本原理:在 vm._init 中把 vm.$attrsvm.$listeners 以及 injectionspropsdata 里的数据变成响应式(主要体现在为这些数据创建 dep 实例),然后在处理 computedwatch 以及组件 mount 过程中创建各种类型的 watcher 实例,watcher 实例激活期间对响应式数据的访问(getter)会使对应的 dep 实例与自己发生关联,一旦响应式数据发生变化(setter),经由 dep 实例 notify 之后,订阅了相关 dep 实例的 watcher 实例得以自行 update。

组件 patch 的算法与 update 相关的内容

从组件 mounted 时创建的 render watcher 实例可知,在响应式数据发生变化时,function updateComponent() { vm._update(vm.render()) } 会得到执行,之前 mount 过程已简单说过 vm._update 方法中只与 mount 相关的内容,而 vm._update 中与 patch 相关方法的是 vm.__patch__(由 createPatchFunction 函数生成),它主要接受两个参数,一个是将要被替换的 oldVnode,一个是新的 vnode,patch 的过程中,Vue 会找出新旧两个 vnode 之前的差异,以合适的代价更新 DOM,而这更新 DOM 的算法则是借鉴 snabbdom

vnode hooks 和 modules

整个 patch 过程中,vnode 的从无到有,然后到生成对应的真实 node 以及被插入对应位置,再到最终真实 node 被从 DOM 上移除,这一系列的时机也有对应的 hooks(这可以算是 vnode 的 hooks,与 vm 的 hooks 是两回事,不过大体差不多,比如 vm 的 mounted hook 就对应 vnode 的 insert hook),通过这些 hook 可以做很多事,将 vnode 属性转换为真实 node 的属性的处理就有赖于那些 hooks,比如将写在 vnode.data 里的 style、class、props、attribues、event-listeners 转化成对应真实 node 的属性。处理这些的东西被称之为 modules,modules 实际是一个以一些专门用于 modules 的 hook(Vue 里面包括:create、activate、update、remove、destroy。另外还有些用于 vnode.data.hook 属性,包括:init、create、prepatch、insert、update、postpatch、remove、destroy、postpatch。具体意义参见 snabbdom)为属性名的函数,Vue 里有以下几个 modules:

// platform modules
const attrs = {
  create: function updateAttrs(oldVnode, vnode) { /* ... */ },
  update: function updateAttrs(oldVnode, vnode) { /* ... */ }
}
const klass = { /* ... */  }
const events = { /* ... */  }
const domProps = { /* ... */  }
const style = { /* ... */  }
const transition = { /* ... */  }

// base modules
const ref = { /* ... */  }
const directives = { /* ... */  }

见名知义。可想而知,如果没有这些 modules,patch 的出来的 DOM 除了能让我们看到包含什么内容以外,不具备任何功能,添加类名、行内样式、事件绑定这些原生 DOM 的功能全都不具备,更别说 v-model、v-if 这样的指令能生效了。虽然这些 modules 对强大的 Vue 来说不可或缺,但在这里并不准备去探究它们内部的机制,只是简单提一下它们的在 patch 过程中的作用:

const nodeOps = {
  // 由 DOM 原生 API 实现的操作节点的方法,在别的平台则用对应平台的 API 实现
}
const modules = [attrs, klass, events, domProps, style, transition, ref, directives]
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

function createPatchFunction({ nodeOps, modules }) {
  const cbs = {}
  let i, j

  // 将所有 modules 依据对应的 hook 加载到 cbs 里
  // 然后对应的 hook 触发时,依次执行 cbs[hook] 里面的函数
  for(i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    
    for(j = 0; j < modules.length; ++j) {
      if(modules[j][hooks[i]]) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

  /* 一堆函数 */

  return function patch(oldVnode, vnode, hydrate) {
    /* ... */
  }
}

Vue.prototype.__patch__ = createPatchFunction({ nodeOps, modules })

modulesnodeOps 被当做参数传给 createPatchFunction,这个函数内部声明的函数和最终返回的 patch 都会用到 modulesnodeOps。之前说了 patch 与 mount 相关的内容,这次来看与 update 相关的内容:

patch 大致流程

function patch(oldVnode, vnode) {
  // destroy 相关
  if(isUndef(vnode)) {
    if(isDef(oldVnode)) {
      invokeDestroyHook(oldVnode)
    }
    return
  }
  
  const insertedVnodeQueue = []
  
  if(isUndef(oldVnode)) {
    // ...
    // mount 相关
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    
    if(!isRealElement && sameVnode(oldVnode, vnode)) {
      // update 相关
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // ...
      // mount 和 destroy 相关
    }
  }
  
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

patch 的总体处理逻辑:

  • vnode 为空且 oldVnode 不为空,触发 oldVnode 的 destroy hook
  • vnode 不为空且 oldVnode 为空,根据 vnode 生成真实 node
  • vnode 不为空且与 oldVnode 为同一 vnode,对它们调用 patchVnode
  • vnode 不为空且与 oldVnode 为不同 vnode,直接 destroy oldVnode,然后根据 vnode 重新生成真实 node

具体看 patchVnode

function sameVnode(a, b) {
  // 满足下列条件时,被视作为同一 vnode
  return a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
}

function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index) {
  if(oldVnode === vnode) {
    return
  }
  
  if(isDef(vnode.elm) && isDef(ownerArray)) {
    vnode = ownerArray[index] = cloneVNode(vnode)
  }
  
  const elm = vnode.elm = oldVnode.elm
  
  let i
  const data = vnode.data
  
  if(isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  
  if(isDef(data) && isPatchable(vnode)) {
    for(i = 0; i < cbs.update.length; ++i) {
      cbs.update[i](oldVnode, vnode)
    }
    
    if(isDef(i = data.hook) && (i = i.update)) {
      i(oldVnode, vnode)
    }
  }
  
  // 通过源码中查找 new VNode,normalizeChildren 和 new VNode 的传参
  // 保证了 patch 时的 vnode.text 和 vnode.children 二者最多只有一个不为空
  if(isUndef(vnode.text)) {
    const ch = vnode.children
    const oldCh = oldVnode.children
    
    if(isDef(ch) && isDef(oldCh)) {
      if(ch !== oldCh) {
        updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      }
    } else if(isDef(ch)) {
      if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if(isDef(oldCh)) {
      removeVnodes(oldCh, 0, oldCh.length - 1)
    } else if(isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if(vnode.text !== oldVnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  
  if(isDef(data) && isDef(i = data.hook) && isDef(i = hook.postpatch)) {
    i(oldVnode, vnode)
  }
}

function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
  for(; startIdx <= endIdx; ++startIdx) {
    createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
  }
}

function removeVnodes(vnodes, startIdx, endIdx) {
  for(; startIdx <= endIdx; ++startIdx) {
    const ch = vnodes[startIdx]
    if(isDef(ch)) {
      if(isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch)
        invokeDestroyHook(ch)
      } else {
        removeNode(ch.elm)
      }
    }
  }
}

patchVnode 的处理逻辑:

  • 如果 vnode.text 为空:
    • 如果 vnode.childrenoldVnode.children 都不为空且二者不相等,则调用 updateChildren
    • 如果 vnode.children 不为空而 oldVnode.children 为空,则将 elm.textContent 赋值为空字符串(如果 oldVnode.text 不为空的话),然后依次为 vnode.children 生成真实 node,append 到 oldVnode.elm
    • 如果 vnode.children 为空而 oldVnode.children 不为空,依次移除 oldVnode.elm 的各个子 node
    • 如果二者都为空且 oldVnode.text 不为空时,将 oldVnode.elm.textContent 赋值为空字符串
  • 如果 vnode.text 不为空且与 oldVnode.text 不相等,将 oldVnode.elm.textContent 赋值为 vnode.text

处理过程中还会触发 prepatch、update、postpatch 等 hooks。

updateChildren

function updateChildren(parentElm, oldCh, ch, insertedVnodeQueue) {
  let oldStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newStartIdx = 0
  let newEndIdx = ch.length - 1
  let newStartVnode = ch[newStartIdx]
  let newEndVnode = ch[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  
  while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if(isUndef(oldCh[oldStartIdx])) {
      // 跳过已经处理的无效 vnode
      oldStartVnode = oldCh[++oldStartIdx]
    } else if(isUndef(oldCh[oldEndIdx])) {
      // 跳过被已经处理的无效 vnode
      oldEndVnode = oldCh[--oldEndIdx]
    } else if(sameVnode(oldStartVnode, newStartVnode)) {
      // 新旧 vnode 首首对应,对应 elm 前后位置不变
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, ch, newStartIdx)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = ch[++newStartIdx]
    } else if(sameVnode(oldEndVnode, newEndVnode)) {
      // 新旧 vnode 尾尾对应,对应 elm 前后位置不变
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, ch, newEndIdx)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = ch[--newEndIdx]
    } else if(sameVnode(oldEndVnode, newStartVnode)) {
      // 新旧 vnode 首尾对应,意味着旧尾对应 elm 移动到了旧首对应的 elm 处
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, ch, newStartIdx)
      nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = ch[++newStartIdx]
    } else if(sameVnode(oldStartVnode, newEndVnode)) {
      // 新旧 vnode 尾首对应,意味着旧首对应 elm 移动到了旧尾对应的 elm 处
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, ch, newEndIdx)
      nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = ch[--newEndIdx]
    } else {
      // 首尾都不对应,先看是否有 key 对应,若无,则 createElm(newStartVnode)
      // 否则看对应后是否满足 sameVnode,若否,则 createElm(newStartVnode)
      // 否则,patchVnode 二者,然后将 newStartVnode 对应的 elm,移动到 oldStartVnode 位置处
      if(isUndef(oldKeyToIdx)) {
        // createKeyToOldIdx 的返回值为一个对象,对象里以剩下未被处理的 oldCh 的每个元素的 key 为键,下标为值
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      
      idxInOld = isDef(newStartVnode.key) 
        ? oldKeyToIdx[newStartVnode.key]
        : oldCh.slice(oldStartIdx, oldEndIdx + 1).findIndex(item => sameVnode(item, newStartVnode))
      
      if(isUndef(idxInOld) || idxInOld === -1) {
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, ch,
                  newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        
        if(isUndef(vnodeToMove.key) || sameVnode(vnodeToMove, newStartVnode)) {
          // 无 key 说明是从 findIndex 里找来的,此时已通过 sameVnode 校验
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, ch, newStartIdx)
          // 置为空,表示已经处理过
          oldCh[idxInOld] = undefined
          nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, ch,
                    newStartIdx)
        }
      }

      newStartVnode = ch[++newStartIdx]
    }
  }
  
  if(oldStartIdx < oldEndIdx) {
    // oldCh 先处理完,说明 ch 可能还有没被处理的 vnode
    // 而且这些 vnode 需要新增到 parenElm 上的
    // ch[newEndIdx + 1] 不为空说明 --newEndIdx 这条语句执行过
    // 说明 ch[newEndIdx + 1] 匹配上 oldCh 中的某个 vnode
    // 且对应的 elm 已经插入正确位置,剩下的新生成的 elm 应该插入到 ch[newEndIdx + 1].elm 前面
    refElm = isUnDef(ch[newEndIdx + 1]) ? null ch[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if(newStartIdx > newEndIdx) {
    // ch 先处理完,说明 oldCh 可能还有没被处理的 vnode
    // 而这些 vnode 是需要从 parenElm 剔除的
    removeVnodes(oldCh, oldStartIdx, oldEndIdx)
  }
}

虽然看起内容不少,但 updateChildren 的处理逻辑还是很明了的:

  1. 依次将 oldChch 里的元素对比:
    • 如果通过 sameVnode 校验则对二者调用 patchVnode,同时看命中的 vnode 位置是否发生改变,如果发生改变,将对应的 elm 调整到新的位置
    • 如果在 oldCh 中没有与 newStartVnode 相同的,则依照 newStartVnode 生成新的 elm 插入到其父节点对应位置
  2. 处理 oldChch 中没被处理完的元素,或移除多余的或插入新生的

小结

本节简单提及了 patch 过程中 vnode 的 hooks 的用处(通过 modules 处理与 DOM 相关的属性和用于 vnode.data.hook),梳理了 patch 的总体流程,以及详细介绍了与组件 update hook 相关的算法:patchVnodeupdateChildren 的内部逻辑。可以说是从「如何将 vnode 高效 patch 为真实 node 」的角度为问题 2:得知 statestate 变化后,ff 如何以合适的代价重新生成正确的 UIUI的答案补充了一方面的内容。另一方面的内容则由异步更新补充。

异步更新的原理

最后我们来看看 Vue 的异步更新,先来解释何为异步更新,看一段示例:

<template>
  <div id="app">hello, {{ msg }}</div>
</template>

<script>
export default {
  name: 'App',

  data() {
    return {
      msg: 'app'
    }
  },

  mounted() {
    this.msg = 'lala'
    console.log(document.getElementById('app').textContent)
  }
}
</script>

前面已经说过,this.msg = 'lala' 会导致当前 vm 对应的视图发生更新,那后面紧接着的 console.log 会打印什么内容呢?'hello, lala' 吗?嗯,你已经猜到了,打印的其实是 'hello, app'。这就是异步更新:响应式数据的变化并不会使对应的视图立即更新成与变化后的数据对应的内容。(而是将一个更新对应视图的回调函数存储到一个队列,待适当时机统一触发。)还记得我们之前 watcher 实例里在被 dep 实例 notify 后调用的 update,当时没去理会 update 的内容,现在是时候了。可以试着想一下如果我们想让视图同步更新,update 应该怎样实现?

watcher.update

class Watcher {
// ...

  update() {
    // 求新值,换旧值,将新、旧值做参数触发回调
    const value = this.get()
    const oldValue = this.value
    
    this.value = value
    this.cb.call(this.vm, value, oldValue)
  }
}

上面实现的 update 的就可以保证视图的同步更新。异步更新的坏处在上面实例中已经体现:响应式数据发生变化后我们无法保证同步访问对应 DOM 内容时得到正确的数据。如果只说其坏处不说好处的话似乎太不公平,如你所料:异步更新的机制使得我们可以拿到所有准备触发的回调,这意味着我们可以对这些回调做一些处理,比如假若两个回调做的事情完全是一模一样,那我们只用触发其中一个,这样一来在视图更新频繁的场合就能减少很多不必要的过程,从而提升性能,这同时也是同步的缺点。闲言少叙,看 Watcher 里剩下的与异步更新相关的方法:

class Watcher {
  // ...
  update() {
    if(this.lazy) {
      // lazy-watcher
      this.dirty = true
    } else if(this.sync) {
      // 同步 watcher,update 时直接 run()
      this.run()
    } else {
      // 默认的异步 watcher
      queueWatcher(this)
    }
  }
  
  run() {
    // active 为 false 时意味着当前 watcher 需要被 teardown
    // 对应的回调也不用执行了
    if(this.active) {
      const value = this.get()
      
      if(value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value

        this.value = value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
  
  // 只用于 lazy-watcher
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
  
  teardown() {
    if(this.active) {
      if(!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      
      for(let i = this.deps.length - 1; i >= 0; --i) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

可以看到 update 分三种情况,异步更新的那种情况由 queueWatcher 处理。

queueWatcher

可以想见 queueWatcher 肯定有直接或间接调用 watcher.run()

// 需要 run 的 watcher 队列
const queue = []

// 在 queue 中的 watcher has[id] === true
let has = {}
// 是否应该 flushSchedulerQueue
let waiting = false
// 是否正在 flush
let flushing = false
// 正在处理的 watcher 在 queue 中的下标
let index = 0


function queueWatcher(watcher) {
  const id = watcher.id
  
  if(!has[id]) {
    // queue 中的 watcher 为 id 唯一,对应 id 记录在 has 里
    // has[id] === true 意味着对应的 watcher 已经在 queue 里,不用重复处理
    has[id] = true
    
    if(!flushing) {
      queue.push(watcher)
    } else {
      // flushing 为 true 时,将 watcher 插入到合适的位置
      let i = queue.length - 1
      
      while(i > index && queue[i].id > watcher.id) {
        --i
      }
      
      queue.splice(i + 1, 0, watcher)
    }
    
    if(!waiting) {
      waiting = true
      
      if(process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
      } else {
        nextTick(flushSchedulerQueue)
      }
    }
  }
}

function flushSchedulerQueue() {
  flushing = true
  let watcher, id
  
  // watcher 按 id 从小到大排列
  // 1.父组件的 watcher 在子组件之前
  // 2.非 render-watcher 在 render-watcher 之前
  // 3.子组件在父组件的 watcher 中被 destroy 时,对应 watcher.active 为 false
  queue.sort((a, b) => a.id - b.id)
  
  for(index = 0; index < queue.length; ++index) {
    watcher = queue[index]
    watcher.before && watcher.before()
    id = watcher.id
    has[id] = null
    watcher.run()
  }
  
  const updatedQueue = queue.slice()
  
  resetSchedulerState()
  callUpdatedHooks(updatedQueue)
}

function resetSchedulerState() {
  queue.length = index = 0
  has = {}
  waiting = flushing = false
}

function callUpdatedHooks(queue) {
  for(let l = queue.length - 1; l >= 0; --l) {
    const vm = watcher.vm
    
    if(vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

queueWatcher 的逻辑:判断传进来的 watcher 是否在 queue 里,只有不在其中才将之添加到 queue 里,然后看 waiting 是否为 false,是的话,置为 true,表示正准备执行 flushSchedulerQueueflushSchedulerQueue 的执行有两种,一种是同步,一种是异步。同步的话很好理解:除非在 watcher.before()watcher.run() 里嵌套触发 queueWatcher,否则 queue 的长度最大只可能等于 1,里面唯一一个 watcher 执行 run()resetSchedulerState(),最终退出 queueWatcher 然后等待下一次 queueWatcher。异步就稍微复杂点,通过 nextTick 来实现,显然内部肯定要依赖可用的异步 API。

nextTick

nextTick 是 Vue 实现异步更新的核心,不考虑兼容的话,可以用 Promise.resolve() 来实现:

const callback = []
let pending = false

function flushCallbacks() {
  pending = false
  // 因为 copies[i]() 过程中也可能调用 nextTick
  const copies = callback.slice()
  
  callback.length = 0
  
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

function nextTick(cb, ctx) {
  let _resolve
  
  callback.push(() => {
    if(cb) {
      cb.call(ctx)
    } else if(_resolve) {
      _resolve(ctx)
    }
  })
  
  if(!pending) {
    // pending 和 queueWatcher 里的 waiting 作用类似
    pending = true
    Promise.resolve().then(flushCallbacks)
  }
  
  if(!cb) {
    // 当 nextTick 的第一个参数为空,即 nextTick(undefined, ctx) 时
    // 返回值为 Promise 实例,且传给 then 中的回调的参数值为 ctx
    // 即 nextTick(undefined, ctx).then(f),f 可以拿到 ctx
    return new Promise(resolve => _resolve = resolve)
  }
}

nextTick 的逻辑也很简单:

  1. 定义一个 _resolve
  2. 将一个函数 push 到全局的 callback 队列,函数的内容为:如果第一个参数不为空,作为函数调用之;否则,如果 _resolve 不为空,调用 _resolve
  3. 如果非 pending,置为真,然后通过 Promise.resolve().then(flushCallbacks)flushCallbacks 当做回调 push 到微任务队列,这就实现了异步:把当前同步任务中的所有 nextTick(cb) 中的 cb,全都积攒到 callback 里面,等到下次微任务运行时统一执行
  4. 最后,如果 cb 为空的话,返回一个 promise 实例,实例在对应的 callback 执行时会 resolve(ctx)

然后 flushCallbacks 所做的就是首先将 pending 置为假同时将 callback 清空,这意味着下次调用 nextTick 时又会向微任务队列中加入一个新的 flushCallbacks,然后将上一次微任务运行时积攒的 cb 依次执行。换句话说就是:依次执行上一次微任务运行时所积攒的 cb;开始积攒在下一次微任务运行时中执行的 cb。对比起来会发现,其实 queueWatchernextTick 之间的逻辑以及 flushSchedulerQueueflushCallbacks 简直几乎没什么太大差别。

小结

每个响应式数据 setter 触发时都会由与之一一对应的 dep 实例通知订阅这个数据的 watcher 们,每个 watcher 执行对应的回调时即触发更新。如果 setter1 和 setter2 同时被 watcher1 订阅,那么当 setter1 和 setter2 同一时间都发生变化时,如果按照同步的处理方式,那么 watcher1 的回调会执行两次,显然有一次是多余的。这时就是异步更新登场的时机,异步意味着有可操作性的空间,它可以使得我们拿到某次运行时中所有应该执行回调的 watcher 实例,通过删除重复的、标记将要被 teardown 的,一些不必要的工作得以减免,最终性能得以提升。这可以说从「如何避免重复 render」的角度回答为问题 2 的答案提供了另一部分内容。

总结

组件的 update 即对应视图的 update,但这一切的原因是数据的 update,数据 update 时,依赖数据的视图也应该 update,过去需要人们手动完成,而具备响应式功能的 Vue、React 使得开发者们不用自己去维护数据与视图的关系,包括两个过程:

  1. 数据 update -> 组件 update
  2. 组件 update -> 视图 update

两个过程的原理即对应开头两个问题。