Vue VNode Patch 分析

作者:taikulawo创建时间:2019-09-11字数统计:3006预计阅读需要8分钟

这里记录一下Vue的 Virtual DOM 比较过程 来自于 cn.vuejs.org patchVnode 函数断点

当我们对于data进行修改之后会产生新的 VDOM 集合 这里是 vnode

oldVnode则代表修改之前的 VDOM

function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

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

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

Vue 会在 updateChildren 中比较 VNode 每次调用sameVnode,当节点相同时调用 patchVNode patchVNode中会再次调用 updateChildren 进行更新

这是一个间接的递归

有个疑问 Vue一直再调用 updateChildren

既然是diff,应该会生成一个差异补丁 但 updateChildren 并未有返回值,那么patch之后最终如何渲染到UI上的?


Vue在模板编译完成之后会生成组件的渲染函数

这个渲染函数调用之后会返回VNode节点

vnode = render.call(vm._renderProxy, vm.$createElement);
updateComponent = function () {
    vm._update(vm._render(), hydrating);
};

vm_render() 调用render function 获得VNode,然后进行_update,根据VNode将DOM渲染到HTML中

也即,render 获得 VDOM_update 根据 VDOM 来渲染 DOM


如果没记错的话 target stack 是 Vue2 中才引入的机制,而 Vue1 中则是仅靠 Dep.target 来进行依赖收集的。根据我自己对 Vue1 和 Vue2 差异的理解,引入 target stack 的原因在于 Vue2 使用了新的视图更新方式。

具体来说,vue1 视图更新采用的是细粒度绑定的方式,而 vue2 采取的是 virtual DOM 的方式。举个例子来说可能比较容易理解,对于下面的模版:

<!-- root -->
<div>
{{ a }}
<my :text="b"></my>
{{ c }}
<div>

<!-- component my -->
<span>{{ b }}</span>

Vue1 的处理方式可以简化理解为:

watch(for a) -> directive(update {{ a }})
watch(for b) -> directive(update {{ b }})
watch(for c) -> directive(update {{ c }})

由于是数据到 DOM 操作操作指令的细粒度绑定,所以不论是指令还是 watcher 都是原子化的。对于上面的模版,在处理完 {{ a }} 的视图绑定后,创建新的 vue 实例 my 并且处理 {{ b }} 的视图绑定,随后继续处理 {{ c }}的绑定。

而在 Vue2 中情况就完全不同,视图被抽象为一个 render 函数,一个 render 函数只会生成一个 watcher,其处理机制可以简化理解为:

renderRoot () {
	...
	renderMy ()
	...
}

可以看到在 Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,有嵌套调用就会有调用栈。当 evaluate root 时,调用到 my 的 render 函数,此时就需要中断 root 而进行 my 的 evaluate,当 my 的 evaluate 结束后 root 将会继续进行,这就是 target stack 的意义。

未完待续。。

0 comments
Anonymous
Markdown is supported

Be the first guy leaving a comment!