文章内容输出来源:拉勾教育大前端高薪训练营

虚拟DOM

使用虚拟DOM的好处 不需要直接操作DOM,提高开发效率

注意:虚拟DOM不一定会提高性能 首次渲染的时候会增加开销

h函数

h函数就是vm.$createElement(tag,data,children,normalizeChildren) vm.$createElement(tag,data,children,normalizeChildren) tag 标签名称或组件对象



const vm = new Vue({
    el: '#app',
    data: {
      msg: 'Hello Word'
    },
    render(h) {
      let vnode = h('h1', {
        attrs: {
          id: 'title'
        }
      }, this.msg)
      return vnode
    }
  })



VNode的核心属性

  • tag
  • data
  • children
  • text
  • elm 对应真实DOM
  • key

creteElement

在执行mountComponent过程中会实例化Watcher,new Watcher时会执行updateComponent,_update执行时会将_render()当做参数传递进来



// core/instance/lifecycle.js
...
  updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
...



_render会调用用户传入的render函数或模板编译生成的render函数,将vm.$createElement传递进来,所以上面代码中的h函数就是vm.$createElement



// core/instance/render.js

export function initRender (vm: Component) {
...
  // 编译生成的render函数
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  // normalization is always applied for the public version, used in
  // user-written render functions.
  // 用户传入的render函数
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
...
Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      // 调用传入的render
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }



createElement就是对参数的处理,使$createElement既可以传递两个参数,也可以传入三个参数。然后调用_createElement



// core/vdom/create-element.js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 数组或原始值
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 用户传入
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}



_createElement主要做了

  • data是响应式对象返回空vnode处理
  • 动态组件处理
  • 开发环境key不是原始值处理
  • children处理 将children转化为一维数组
  • 创建vnode
  • 如果tag是string
  • tag是html保留标签
  • 自定义组件调用createComponent
  • 自定义标签
  • 组件调用createComponent
  • vnode处理
// core/vdom/create-element.js
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // 响应式对象返回空vnode
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // 动态组件
  // <component v-bind:is="currentTabComponent"></component>
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }


  // warn against non-primitive key  
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // children处理
  // 用户传入的render 
  if (normalizationType === ALWAYS_NORMALIZE) {
    // children转化为一维数组
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // html保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } 
    // 自定义组件
    else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}



update



// core/instance/lifecycle.js
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

patch函数是跟平台相关的



// platforms/web/runtime/index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop



patch函数调用了createPatchFunction,传入了跟平台相关的模块和平台无关的模块



// platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
// 平台无关模块 ref directives
import baseModules from 'core/vdom/modules/index'
// 平台模块
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

// 传入了跟平台相关的模块和平台无关的模块
export const patch: Function = createPatchFunction({ nodeOps, modules })



createPatchFunction首先初始化模块的钩子函数



// core/vdom/patch.js
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){
    ...
  }



createPatchFunction返回了patch方法

patch方法主要执行过程

  • 判断vnode是否存在,如果不存在,而oldVnode存在 ,就执行oldVnode的destory钩子函数,返回,
  • oldVnode不存在,创建vnode对应的真实dom(没有插入到页面DOM树上)
  • oldVnode存在
  • 判断oldVnode是否是真实DOM
  • oldVnode如果不存在真实DOM,且oldVnode具有相同vnode,调用patchVnode
  • oldVnode是真实DOM, 将oldVnode转化为对应的Vnode对象,然后创建vnode对应的真实DOM,移除oldVnode
  • 最后调用invokeInsertHook,并返回vnode对应的真实DOM
// core/vdom/patch.js
export function createPatchFunction (backend) {
  ...
   return function patch (oldVnode, vnode, hydrating, removeOnly){
     // 新vnode不存在
    if (isUndef(vnode)) {
      // 旧vnode存在,执行destory钩子函数
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    //记录vnode创建的DOM元素是否添加到DOM树上 true是表示没有
    let isInitialPatch = false
    const insertedVnodeQueue = []

    // 旧vnode不存在
    // 创建vnode对应的真实dom(不插入到页面上)
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      //判断旧节点是否存在真实DOM,存在说明oldVnode是真实的DOM元素
      const isRealElement = isDef(oldVnode.nodeType)
      // oldVnode不是真实DOM并且oldVnode和vnode是相同vnode
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
         ...
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        // 创建vnode对应的DOM元素
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,//null并不会挂载带DOM树上
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
       ...

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}



invokeInsertHook是对vnode的insert钩子函数的处理



// core/vdom/patch.js
  function invokeInsertHook (vnode, queue, initial) {
    // delay insert hooks for component root nodes, invoke them after the
    // element is really inserted
    //initial为true并且vnode.pareent存在,将insertedVnodeQueue保存在pendingInsert上
    if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data.pendingInsert = queue
    } else {
      for (let i = 0; i < queue.length; ++i) {
        queue[i].data.hook.insert(queue[i])
      }
    }
  }



createElm

createElm执行过程

  • 如果是组件,直接返回创建组件
  • 如果vnode的tag存在
  • 创建vnode标签对应的DOM
  • 创建vnode对应的子节点的DOM,如果data存在,调用invokeCreateHooks
  • 将vnode插入到DOM树上
  • tag不存在
  • 如果是注释节点,创建注释节点插入DOM树上
// core/vdom/patch.js
function createElm (
    vnode,
    insertedVnodeQueue,
    parentElm,
    refElm,
    nested,
    ownerArray,
    index
  ) {
    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // This vnode was used in a previous render!
      // now it's used as a new node, overwriting its elm would cause
      // potential patch errors down the road when it's used as an insertion
      // reference node. Instead, we clone the node on-demand before creating
      // associated DOM element for it.
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    vnode.isRootInsert = !nested // for transition enter check
    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标签对应的DOM
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
       ...
      } else {
        // 创建子节点
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          // 触发模块中的钩子函数,
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        // 将vnode插入到dom树上
        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)
    }
  }



createChildren



// 创建vnode对应 的子节点并添加到vnode对应的DOM上
  function createChildren (vnode, children, insertedVnodeQueue) {
    // 如果children是数组
    if (Array.isArray(children)) {
      if (process.env.NODE_ENV !== 'production') {
        // 检查是否存在相同key
        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)) {
      // 如果vnode.是原始值text
      nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
    }
  }



invokeCreateHooks



function invokeCreateHooks (vnode, insertedVnodeQueue) {
    // 触发模块中的钩子函数
    for (let i = 0; i < cbs.create.length; ++i) {
      cbs.create[i](emptyNode, vnode)
    }
    i = vnode.data.hook // Reuse variable
    if (isDef(i)) {
      if (isDef(i.create)) i.create(emptyNode, vnode)//触发hook中的create钩子函数
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)//将hook中的insert钩子函数添加到insertedVnodeQueue中
    }
  }



patchVnode

在前面patch方法中,当oldVnode和vnode是sameVnode(key,tag等相同)



if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  }



patchVnode的主要执行过程

  • 触发用户传入的prepatch钩子函数
  • 触发模块(更新节点的属性样式事件等)和用户传入的钩子函数
  • 判断vnode有没有text属性
  • 没有text属性
  • 如果新旧节点都存在子节点,调用updateChildren
  • 新节点有子节点,旧节点没有
  • 判断oldVnode有没有text属性,有清空text属性
  • 调用addVnodes把创建子节点对应的DOM并插入进来
  • 老节点有子节点,新节点没有子节点,调用removeVnodes删除老节点的子节点
  • 老节点有text属性,清空text属性
  • 有text属性
  • vnode和oldVnode的text属性不相等,直接修改文本内容
  • 触发用户传入的postpatch钩子函数
// core/vdom/patch.js
function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    if (oldVnode === vnode) {
      return
    }
    ...

    // 触发用户传入的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
    if (isDef(data) && isPatchable(vnode)) {
      // 执行模块update钩子,更新节点的属性样式事件等
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 用户传入的update钩子函数
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // vnode没有text属性
    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)
        }
        // oldVnode有text属性,清空oldVnode的DOM的text
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 把子节点转化为真实DOM添加到vnode对应的DOM上
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // oldVnode存在子节点,删除子节点对应的DOM元素 
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有text属性 新节点没有文本也没有子节点
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // vnode有text属性
      // 修改文本
      nodeOps.setTextContent(elm, vnode.text)
    }
    // 执行postpatch钩子函数
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }



updateChildren整体分析

vue.js中的updateChildren的方法和snabbdom中的updateChildren方法相似

主要比较过程




unreal 在umg中创建虚拟actor createelement 虚拟dom_初始化


oldStartVnode/newStartVnode

  • 新旧节点从开始位置开始比较
  • 旧节点第一个元素与新节点第一个元素进行使用sameVnode比较,如果相同调用pathVnode对比和更新节点
  • 比较完成后索引后移,往后进行比较


unreal 在umg中创建虚拟actor createelement 虚拟dom_Vue_02


  • oldEndVnode/newEndVnode(效果与上面相似,只不过是从结束位置开始比较,如果相同,比较完成后索引左移)
  • 新旧节点从结束位置开始比较
  • 如果相同调用pathVnode对比和更新节点
  • 比较完成后索引前移,往前移动进行比较
  • oldStartVnode/newEndVnode
  • 旧节点第一个节点、新节点最后一个节点开始比较
  • 如果相同就把如果相同调用pathVnode对比和更新节点,再把旧节点的第一个节点移动到最后
  • 旧节点索引后移,新节点索引前移,进行比较


unreal 在umg中创建虚拟actor createelement 虚拟dom_Vue_03


  • oldEndVnode/newStartVnode
  • 旧节点第一个节点、新节点的第一个节点开始比较
  • 如果相同就把如果相同调用pathVnode对比和更新节点,再把旧节点的最后一个节点移动到最前面
  • 更新索引,旧节点索引前移,新节点索引后移


unreal 在umg中创建虚拟actor createelement 虚拟dom_初始化_04


  • 如果上面四种情况都不满足
  • 在老节点中查找子节点newStartVnode对应的vnode
  • 没有就创建newStartVnode对应DOM并插入到oldStartVnode对应的DOM的前面
  • 存在,比较newStartVnode在老节点中对应的vnode elmToMove的sel属性是否相同
  • 不相同,直接创建(同上)
  • 相同,调用patchVnode,再将elmToMove对应的DOM插入到oldStartVnode对应的DOM前面


unreal 在umg中创建虚拟actor createelement 虚拟dom_Vue_05


  • 上面执行完毕后,如果新老节点至少有一个没有遍历完成
  • 如果老节点先遍历完成,说明新节点没有遍历完,创建新节点还没有遍历的节点(从newStartIdx,newEndIdx)


unreal 在umg中创建虚拟actor createelement 虚拟dom_sed_06


  • 否则说明老节点有多余的节点,删除老节点还没有遍历的节点(oldStartIdx, oldEndIdx)


unreal 在umg中创建虚拟actor createelement 虚拟dom_js createelement_07


源码


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

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      
      // 当oldStartVnode不存在, ++oldStartIdx索引右移
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 当oldEndVnode不存在, --oldStartIdx索引左移
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 新旧开始节点是sameVnode,调用patchVnode,新旧节点的索引都右移
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧结束节点是sameVnode
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 旧开始节点和新结束节点是sameVnode 
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // 移动旧结束节点对应的DOM到最后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // 旧开始索引右移,新结束索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 旧结束和新开始节点是sameVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // 移动旧结束节点对应的DON到最前面
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // 旧结束节点索引左移,新开始节点右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //包含所有旧节点的对象从 oldStartIdx到oldEndIdx
        // key为oldVnode的子节点的key, 值为索引
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 查找newStartVnode在oldVnode的位置
        // 如果新开始节点存在key,直接通过key从newStartVnode获取在老节点中对应的索引
        // 不存在key ,遍历老节点所有节点查找是否存在于newStartVnode是sameVnode,存在就返回索引
        idxInOld = isDef(newStartVnode.key) ?
          oldKeyToIdx[newStartVnode.key] :
          findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        // 索引不存在,直接创建newStartVnode对应的DOM元素
        if (isUndef(idxInOld)) { // New element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 存在,获取oldVnode对应的vnode
          vnodeToMove = oldCh[idxInOld]
          // 判断是否是sameVnode 是,调用patchVnode 再将找到的vnode放到最前面
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // 将oldCh设置为undefined,防止vnodeToMove对应的DOM被removeVnodes被删掉
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            // 不是sameVnode,创建newStartVnode对应的DOM
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // 索引右移
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 旧节点先遍历完成,创建没有遍历完newCh对应的DOM元素
    if (oldStartIdx > oldEndIdx) {
      // 获取参考节点
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      // 新节点先遍历完成,将未遍历完的老节点删除掉
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }


带key的好处

使用key可以让它能够跟踪每个节点的身份,在进行比较的时候,key值不同是就不会进行子节点的比较,会提高diff比较效率,可以避免频繁更新dom元素

比如下面例子,*如果不带key,sameVnode返回的值一直就是true,就会调用patchVnode对比vnode的差异,导致频繁更新DOM


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<div id="app">
  <ul>
    <li v-for="item in arr" :key="item">{{item}}</li>
  </ul>
  <button @click="add">button</button>
</div>

</body>
<script src="../dist/vue.js"></script>
<script>
let id = 4
const vm = new Vue({
  el: '#app',
  data: {
    msg: 'Hello Word',
    arr: [1, 2, 3]
  },
  methods: {
    add() {
      this.arr.splice(0, 0, id++)
    }
  }
})
</script>

</html>


  • 不设置key总共会更新3次dom,创建一次dom
  • 设置key值创建了一次dom