# 源码阅读vue VirtualDOM 和 diff

# 是什么

VirtualDOM 是根据真实的DOM节点树,抽象出来的一棵用 JavaScript 对象描述节点的抽象树。

通过 VirtualDOM ,可以对比前后节点变化了哪些变化,做到局部更新视图,减少 DOM 操作。

Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

对比过程通过 patch 来实现,而 patch 的核心是 diff 算法。

# 找到 patch

在vue源码入口文件vue/src/core/index.js (opens new window)中,可以看到import Vue from './instance/index',导入了Vue这个对象。

vue/src/core/instance/index.js (opens new window)中,

import { initMixin } from './init'
//...

function Vue (options) {
  //...
  this._init(options)
}

initMixin(Vue)
lifecycleMixin(Vue)
// ...

export default Vue

可以看到Vue是一个函数方法,调用该方法时会调用一个叫_init的初始化方法,并传入options参数,同时这个文件还执行了 initMixin 和 lifecycleMixin 方法。

vue/src/core/instance/init.js (opens new window)中,

// ...
import { initState } from './state'
import { extend, mergeOptions, formatComponentName } from '../util/index'

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this

    // ...

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

看到_init方法就是在initMixin方法中定义的,在_init方法中,声明了常量vm并赋值当前实例,接受了options并做了处理,还调用了$mount方法。

vue/src/platforms/web/runtime/index.js (opens new window)中,

Vue.prototype.__patch__ = inBrowser ? patch : noop

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

看到$mount方法返回mountComponent的执行结果。

同时在这个文件中还定义了__patch__。

vue/src/core/instance/lifecycle.js (opens new window)中,

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // ...

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  //...
}

在mountComponent中声明了一个Watcher对象,在源码分析vue watch侦听器 (opens new window)中我们已经了解到, 构造函数传入的第二个参数expOrFn会赋值给Watcher实例的getter并在get方法中调用,而get在run中调用,run在update中调用,update则通过dep.notify()调用,当data对象的某个属性改变时,通过这个属性的setter就会触发notify()通知,从而达到更新视图的目的。

而这里的第二个参数实际上就是() => { vm._update(vm._render(), hydrating) }

同样在lifecycle.js文件中,可以找到lifecycleMixin,在这里定义了_update,_update调用了vm.__patch__

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }
  //...
}

回到__patch__的定义,Vue.prototype.__patch__ = inBrowser ? patch : noop,在浏览器环境下,__patch__指向下面这个patch函数。

vue/src/platforms/web/runtime/patch.js (opens new window)中,

export const patch: Function = createPatchFunction({ nodeOps, modules })

发现patch函数通过createPatchFunction函数返回。

vue/src/core/vdom/patch.js (opens new window)中,可以找到createPatchFunction,并看到该方法返回的patch函数。

# patch

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
    let i, j
    const cbs = {}

    const { modules, nodeOps } = backend

    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
            if (isDef(modules[j][hooks[i]])) {
                cbs[hooks[i]].push(modules[j][hooks[i]])
            }
        }
    }
    // ...

    return function patch (oldVnode, vnode, hydrating, removeOnly) {
        // vnode不存在
        if (isUndef(vnode)) {
            // oldVnode存在,直接调用DestroyHook
            if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
            // 直接return
            return
        }

        let isInitialPatch = false
        const insertedVnodeQueue = []

        // oldVnode不存在,则创建根节点,设置标志位isInitialPatch为true
        if (isUndef(oldVnode)) {
            isInitialPatch = true
            createElm(vnode, insertedVnodeQueue)
        } 
        // oldVnode存在
        else {
            // 标记oldVnode是否有nodeType
            const isRealElement = isDef(oldVnode.nodeType)
            // 旧节点不是真实节点并且新旧节点符合sameVnode,则调用patchVnode修改旧节点
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
            } 
            // 新旧节点不同
            else {
                if (isRealElement) {
                    // 服务端渲染相关处理
                    // 略...

                    // 非服务端渲染,创建一个空节点替换oldVnode
                    oldVnode = emptyNodeAt(oldVnode)
                }

                // 将oldVnode的真实节点赋值给常量oldElm
                const oldElm = oldVnode.elm
                // 将旧父节点赋值给常量parentElm
                const parentElm = nodeOps.parentNode(oldElm)

                // 创建新节点
                createElm(
                    vnode,
                    insertedVnodeQueue,
                    oldElm._leaveCb ? null : parentElm,
                    nodeOps.nextSibling(oldElm)
                )

                if (isDef(vnode.parent)) {
                    /*组件根节点被替换,遍历更新父节点element*/
                    let ancestor = vnode.parent
                    const patchable = isPatchable(vnode)
                    while (ancestor) {
                        for (let i = 0; i < cbs.destroy.length; ++i) {
                            cbs.destroy[i](ancestor)
                        }
                        ancestor.elm = vnode.elm
                        if (patchable) {
                            for (let i = 0; i < cbs.create.length; ++i) {
                                cbs.create[i](emptyNode, ancestor)
                            }
                            const insert = ancestor.data.hook.insert
                            if (insert.merged) {
                                for (let i = 1; i < insert.fns.length; i++) {
                                    insert.fns[i]()
                                }
                            }
                        } else {
                            registerRef(ancestor)
                        }
                        ancestor = ancestor.parent
                    }
                }

                // 移除旧父节点
                if (isDef(parentElm)) {
                    removeVnodes([oldVnode], 0, 0)
                } else if (isDef(oldVnode.tag)) {
                    invokeDestroyHook(oldVnode) // 调用destroy钩子
                }
            }
        }
        
        // 调用insert 钩子
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
        return vnode.elm
    }
}

nodeOps 是vue/src/platforms/web/runtime/node-ops.js (opens new window)导出的所有方法的一个集合。

modules 来自vue/src/platforms/web/runtime/modules/index.js (opens new window)

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
function sameInputType (a, b) {
  if (a.tag !== 'input') return true
  let i
  const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
  const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
  return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
export const isTextInputType = makeMap('text,number,password,search,email,tel,url')
export function makeMap (
  str: string,
  expectsLowerCase?: boolean
): (key: string) => true | void {
  const map = Object.create(null)
  const list: Array<string> = str.split(',')
  for (let i = 0; i < list.length; i++) {
    map[list[i]] = true
  }
  return expectsLowerCase
    ? val => map[val.toLowerCase()]
    : val => map[val]
}

# createElm

function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    // 略
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      vnode = ownerArray[index] = cloneVNode(vnode)
    }
    // 略
    vnode.isRootInsert = !nested 

    // 创建组件节点
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    // 元素节点
    if (isDef(tag)) {
      // 略
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }

      // 创建虚拟节点对应的元素节点
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      // set scope id attribute for scoped CSS.
      setScope(vnode)

      // if 部分略
      if (__WEEX__) {
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } 
      else {
        // 遍历子节点,创建虚拟子节点对应的元素节点
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue) // 调用create钩子
        }
        // 将元素节点挂到父元素上
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        creatingElmInVPre--
      }
    } 
    // 创建注释节点并插入
    else if (isTrue(vnode.isComment)) {
      vnode.elm = nodeOps.createComment(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    } 
    // 创建文本节点并插入
    else {
      vnode.elm = nodeOps.createTextNode(vnode.text)
      insert(parentElm, vnode.elm, refElm)
    }
}
function createChildren (vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
        if (process.env.NODE_ENV !== 'production') {
            checkDuplicateKeys(children)
        }
        for (let i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
        }
    } else if (isPrimitive(vnode.text)) {
        nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
}
function insert (parent, elm, ref) {
    if (isDef(parent)) {
        if (isDef(ref)) {
            if (nodeOps.parentNode(ref) === parent) {
                nodeOps.insertBefore(parent, elm, ref)
            }
        } else {
            nodeOps.appendChild(parent, elm)
        }
    }
}

组件节点和insertedVnodeQueue暂不深入

# patchVnode

  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 两个节点相同直接返回
    if (oldVnode === vnode) {
      return
    }

    // 略
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      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
    }

    /**
     * 如果新旧节点isStatic都为true,且key相同,
     * 并且新节点是isCloned或者是isOnce(标记了v-once表示只渲染一次),
     * 则替换回旧节点的componentInstance。
    */
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    // data.hook.prepatch 钩子存在则调用
    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
    // 调用update钩子
    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) // data.hook.update 钩子存在则调用
    }
    // 不存在文本节点
    if (isUndef(vnode.text)) {
        // 新旧节点都有子节点且子节点不同,则调用updateChildren
        if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
        } 
        // 仅新节点有子节点
        else if (isDef(ch)) {
            // 非生产环境重复key值校验警告
            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(oldCh, 0, oldCh.length - 1)
        } 
        // 均无子节点,需要清空文本
        else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '')
        }
    }
    // 存在文本节点,且新旧节点文本不同,替换文本内容
    else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    // data.hook.postpatch 钩子存在则调用
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
}

# updateChildren (diff)

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // <transition-group>
    const canMove = !removeOnly

    // 非生产环境重复key值校验警告
    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 旧子节点列表首个节点为空, oldStartVnode 右移, oldStartIdx +1
        oldStartVnode = oldCh[++oldStartIdx]
      } else if (isUndef(oldEndVnode)) {
        // 旧子节点列表末尾节点为空, oldEndVnode 左移, oldEndIdx -1
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新旧列表首个节点符合sameVnode,进行patchVnode
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // oldStartVnode 右移, oldStartIdx +1
        oldStartVnode = oldCh[++oldStartIdx]
        // newStartVnode 右移, newStartIdx +1
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧列表末尾节点符合sameVnode,进行patchVnode
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // oldEndVnode 左移, oldEndIdx -1
        oldEndVnode = oldCh[--oldEndIdx]
        // newEndVnode 左移, newEndIdx -1
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // 旧子节点列表首个节点 和 新子节点列表末尾节点 符合sameVnode,进行patchVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // 将旧子节点列表首个节点,插到末尾
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStartVnode 右移, oldStartIdx +1
        oldStartVnode = oldCh[++oldStartIdx]
        // newEndVnode 左移, newEndIdx -1
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // 旧子节点列表末尾节点 和 新子节点列表首个节点 符合sameVnode,进行patchVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 将旧子节点列表末尾节点,插到列表最前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEndVnode 左移, oldEndIdx -1
        oldEndVnode = oldCh[--oldEndIdx]
        // newStartVnode 右移, newStartIdx +1
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 非首尾两两对比四种结果的情况
        // 初始化 oldKeyToIdx 为一个key值与下标的哈希表
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 如果新子节点列表首个节点存在key值,到哈希表中查找,否则到旧子节点列表中根据sameVnode查找,得到下标
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 没有找到下标则创建新节点
        if (isUndef(idxInOld)) { 
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } 
        // 找到下标
        else {
          // 找到下标对应的旧子节点
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 找的的旧子节点 和 新子节点列表首个节点 符合sameVnode,进行patchVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将该旧子节点设为undefined
            oldCh[idxInOld] = undefined
            // 将该旧子节点插到列表最前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 不符合sameVnode,也就是相同的key但元素不同,则创建新节点
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // newStartVnode 右移, newStartIdx +1
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 旧子节点列表遍历完毕
    if (oldStartIdx > oldEndIdx) {
      // 获取新子节点剩余节点右边节点,用于insert时参照位置
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      // 批量调用createElm增加新子节点列表剩余节点
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } 
    // 新子节点列表遍历完毕
    else if (newStartIdx > newEndIdx) {
      // 删除剩余旧子节点列表
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
}
function createKeyToOldIdx (children, beginIdx, endIdx) {
    let i, key
    const map = {}
    for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
    }
    return map
}
function findIdxInOld (node, oldCh, start, end) {
    for (let i = start; i < end; i++) {
      const c = oldCh[i]
      if (isDef(c) && sameVnode(node, c)) return i
    }
}
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 { // Text node
          removeNode(ch.elm)
        }
      }
    }
}

# 总结

渲染Watcher执行update->run->get,调用了vm._update->vm.__patch__->patch

节点相同判断通过sameVnode方法。

patch:

  • 新节点不存在,则销毁旧节点,结束patch;
  • 旧节点不存在,则创建新节点;
  • 旧节点存在且新旧相同,进行patchVnode;
  • 旧节点存在且新旧不同,创建新节点;

patchVnode:

  • 新节点有文本节点,且与旧文本节点不同,则替换文本;
  • 新节点不存在文本节点:
  • 都有子节点且子节点不同,进行updateChildren;
  • 仅新节点有子节点,增加节点;
  • 仅旧节点有子节点,删除节点;
  • 均无子节点,清空文本;

updateChildren:

  • 首首、尾尾、首尾、尾首比较;
  • 上述四种不匹配,则查找相同key,没找到则新增节点;
  • 找到相同key但节点不同,则新增节点;
  • 找到相同key但节点相同,则移动;
  • 直到新节点列表或旧节点列表有一个遍历完,对多余的旧节点进行删除,新节点进行增加;
export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  //...
}

# 参考

VirtualDOM与diff(Vue实现) (opens new window)