从 vue 源码看问题 —— vue 中如何进行 patch ?

前言

当组件进行更新时,new Watcher(vm, updateComponent, ...) 中的 updateComponent 方法会被执行,而更新时的 updateComponent 方法如下:

// 负责更新组件
updateComponent = () => {
  // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode 
  vm._update(vm._render(), hydrating)
}

首先会执行 vm._render() 得到组件的 VNode,接着将 VNode 传递给 vm._update 方法,就正式开始 patch 阶段.

vue1.x 到 vue2.x 的转变

vue1.x

1.x 中并没有 2.x 中对应的 VNodediff 的概念,因为 1.x 的核心是 响应式,即 Object.definePropertyDepWatcher.

  • Object.defineProperty: 负责数据的拦截(本质就是对象属性的劫持),对数据属性 key 设置 gettersetter,在 getter触发时进行依赖收集,在 setter 触发时通过 dep 通知对应的 watcher 进行更新
  • Dep:每个 dep 实例和 data 选项中返回对象的 key一对一 的关系
  • Watcherdata 选项中返回对象的 keywatcher一对多 的关系,模版中每使用一次 key 就会生成一个对应的 watcher

由于 watcherDOM 属于 一对一 的关系,当数据发生更新时,dep 会通知 watcher 直接更新对应的 DOM,即 定向更新
所以更新的效率是非常高的,因为 watcher 可以明确知道与它对应的 key 在组件模版中的具体位置(即对应的 dom 元素).

但是这种高效的更新并不适用于复杂场景,因为当页面足够复杂时(即包含很多组件),对应的页面会就产生大量的 watcher 与真实 dom 进行强关联,这是非常耗资源.

vue2.x

由于 vue1.x 存在的问题,于是 Vue 2.0 就引入了 VNodediff 算法解决问题.

针对复杂页面 watcher 太多导致性能下降的问题,Vue 2.0 中将 watcher 的粒度放大,即一个组件对应一个 watcher(渲染 watcher),这样 watcher 的维度就属于是组件级别,而不是单个 DOM 级别.

当响应式数据更新时,dep 通知 watcher 去更新组件内容,这对于 vue1.x 来说是很简单的,但是 vue2.0 中的 watcher 是组件级别,因此这个 watcher 并不知道要更新模板中的哪些位置.

于是 vue2.0 中通过引入 VNode 来查找本次组件需要更新的内容

当组件中数据更新时,会通过调用 render 方法为组件生成一个新的 VNode,将新的 VNode 与 旧的 VNode 通过 diff 算法进行比较,查找本次需要更新的内容,接着执行 DOM 操作去更新对应节点.

深入源码

入口

文件位置:src\core\instance\lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
     ...
    }
  } else {
    // 负责更新组件
    updateComponent = () => {
      // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode 
      vm._update(vm._render(), hydrating)
    }
  }
  ...
 if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

vm._update()

文件位置:src\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.

    // prevVnode 不存在,代表是初始化渲染
    if (!prevVnode) {
      // patch 阶段:patch、diff 算法
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
       // prevVnode 存在,代表是后续更新阶段
      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.
  }

vm.__patch__()

文件位置:src\platforms\web\runtime\index.js
Vue 原型上挂载 __patch__ 方法

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch()

文件位置:src\platforms\web\runtime\patch.js

通过 createPatchFunction() 工厂函数,为其传入平台特有的一些操作,然后返回一个 patch 函数

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

nodeOps

文件位置:src\platforms\web\runtime\node-ops.js

/**
 * web 平台的 DOM 操作 API
 */


// 创建标签名为 tagName 的元素节点
export function createElement (tagName: string, vnode: VNode): Element {
  // 创建元素
  const elm = document.createElement(tagName)

  // 非 select 元素直接返回
  if (tagName !== 'select') {
    return elm
  }
  
  // false or null will remove the attribute but undefined will not
  // 如果是 select 元素,则为它设置 multiple 属性
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }

  return elm
}

// 创建带命名空间的元素节点
export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

// 创建文本节点
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

// 创建注释节点
export function createComment (text: string): Comment {
  return document.createComment(text)
}

// 在指定节点前插入节点
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

// 移除子节点
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}

// 添加子节点
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}

// 返回指定节点的父节点
export function parentNode (node: Node): ?Node {
  return node.parentNode
}

// 返回指定节点的下一个兄弟节点
export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}

// 返回指定节点的标签名 
export function tagName (node: Element): string {
  return node.tagName
}

// 为指定节点设置文本
export function setTextContent (node: Node, text: string) {
  node.textContent = text
}

// 为节点设置指定的 scopeId 属性,属性值为 '',如 <div scopeId></div>
export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}

modules

文件位置:src\core/vdom/modules/index.js + web/runtime/modules/index.js

平台特有的一些操作,如:attr、class、style、event 等,还有核心的 directiveref,它们会向外暴露一些特有的方法,比如:create、activate、update、remove、destroy,这些方法在 patch 阶段时会被调用,从而做相应的操作,比如创建 attr、指令等.

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)

createPatchFunction()

文件位置:src\core\vdom\patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

/* 
  工厂函数:
     注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数 
*/ 
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}

  /*
    modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 }
    nodeOps: { 对元素的增删改查 API }
  */
  const { modules, nodeOps } = backend

  /*
    hooks = ['create', 'activate', 'update', 'remove', 'destroy']
    遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,
    比如:directives 中的 create、update、destroy 方法
    让这些方法放到 cbs[hook] = [hook 方法] 中,比如: cb.create = [fn1, fn2, ...]
    然后在合适的时间调用相应的钩子方法完成对应的操作
  */
  for (i = 0; i < hooks.length; ++i) {
    // 比如 cbs.create = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  
 ...
 
  /*
    vm.__patch__
      1、新节点不存在,老节点存在,调用 destroy,销毁老节点
      2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
      3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
  */
 return function patch (oldVnode, vnode, hydrating, removeOnly){
  ...
 }
}

createPatchFunction 中返回的 patch()

  /*
    vm.__patch__
      1、新节点不存在,老节点存在,调用 destroy,销毁老节点
      2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
      3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
  */
  function patch(oldVnode, vnode, hydrating, removeOnly) {

    // 新节点不存在,老节点存在,则调用 destroy,销毁老节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    // 老的 VNode 不存在
    if (isUndef(oldVnode)) {
      /*
        老的 VNode 不存在,新的 VNode 存在,这种情况会在一个组件初次渲染的时候出现,
        比如:<div id="app">
                <comp></comp>
              </div>
        这里的 comp 组件初次渲染时就会走这儿
        empty mount (likely as component), create new root element
      */
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 老的 VNode 存在
      
      // 判断老的 VNode 是否是一个真实的 dom 节点
      const isRealElement = isDef(oldVnode.nodeType)

      /*
        老的 VNode 不是真实元素,但是老节点和新节点是同一个节点,
        则属于更新阶段,需要执行 patch 更新节点
        patch existing root node
      */
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {

        // 老的 VNode 是真实元素
        if (isRealElement) {
          /* 
            挂载到真实元素以及处理服务端渲染的情况
            oldVnode.nodeType === 1 代表的是 html 元素
          */
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }

          /*
           走到这儿说明不是服务端渲染,或者 hydration 失败,
           则根据 oldVnode 创建一个 vnode 节点替换 oldVnode
          */ 
          oldVnode = emptyNodeAt(oldVnode)
        }

        // replacing existing element
        // 获取老节点的真实元素
        const oldElm = oldVnode.elm
        // 获取老节点的父元素,即 body 元素
        const parentElm = nodeOps.parentNode(oldElm)

        // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
        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,
          nodeOps.nextSibling(oldElm)
        )

        // 递归更新父占位符节点元素
        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
          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)
              }
              // #6513
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                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)
        }
      }
    }

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

invokeDestroyHook()

  /*
    销毁节点:
      执行组件的 destroy 钩子,即执行 $destroy 方法 
      执行组件各个模块(style、class、directive 等)的 destroy 方法
      如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
   */
  function invokeDestroyHook(vnode) {
    let i, j
    const data = vnode.data
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
    }
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j])
      }
    }
  }

sameVnode()

function sameVnode(a, b) {
  return (
    // key 必须相同,注意 undefined === undefined 也为 true
    a.key === b.key &&
    a.asyncFactory === b.asyncFactory && (
      (
        // 标签相同
        a.tag === b.tag &&
        // 都属于注释节点
        a.isComment === b.isComment &&
        // 都有 data 属性
        isDef(a.data) === isDef(b.data) &&
        // input 标签类型要一致
        sameInputType(a, b)
      ) || (
        // 异步占位符节点
        isTrue(a.isAsyncPlaceholder) &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

emptyNodeAt()

  // 为元素(elm) 创建一个空的 vnode
  function emptyNodeAt(elm) {
    return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
  }

createElm()

  // 基于 vnode 创建整棵 DOM 树,并插入到父节点上
  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

    /*
       1、如果 vnode 是一个组件,则执行 init 钩子,创建组件实例并挂载,
          然后为组件执行各个模块的 create 钩子
          如果组件被 keep-alive 包裹,则激活组件

       2、如果是一个普通元素,则什么也不做
   */
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    // 获取 data 对象
    const data = vnode.data
    // 获取所有的孩子节点
    const children = vnode.children
    const tag = vnode.tag

    // 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)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        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)
        }
        // 将节点插入父节点
        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)
    }
  }

createComponent()

  /**
   * 如果 vnode 是一个组件,则执行 init 钩子,创建组件实例,并挂载
   * 然后为组件执行各个模块的 create 方法
   * 
   * @param {*} vnode 组件新的 vnode
   * @param {*} insertedVnodeQueue 数组
   * @param {*} parentElm oldVnode 的父节点
   * @param {*} refElm oldVnode 的下一个兄弟节点
   * @returns 如果 vnode 是一个组件并且组件创建成功,则返回 true,否则返回 undefined
   */
  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    // 获取 vnode.data 对象
    let i = vnode.data
    if (isDef(i)) {
      // 判断 组件实例是否已经存在 && 被 keep-alive 包裹
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive

      /*
        执行 vnode.data.init 钩子函数
        1. 如果是被 keep-alive 包裹的组件:
             则再执行 prepatch 钩子,用 vnode 上的各个属性更新 oldVnode 上的相关属性
        2. 如果是组件没有被 keep-alive 包裹或者首次渲染,则初始化组件,并进入挂载阶段
      */
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }

      /*
        调用 init hook 之后,如果 vnode 是子组件,它应该创建一个子实例并挂载它
        若子组件还设置 vnode.elm 占位符,在这种情况下,只需返回元素就可以了
      */
      if (isDef(vnode.componentInstance)) {
        /*
         如果 vnode 是一个子组件,则调用 init 钩子之后会创建一个组件实例并进行挂载
         这时就可以给组件执行各个模块的 create 钩子
        */
        initComponent(vnode, insertedVnodeQueue)
        // 将组件的 DOM 节点插入到父节点内
        insert(parentElm, vnode.elm, refElm)
        
        if (isTrue(isReactivated)) {
          // 组件被 keep-alive 包裹的情况,激活组件
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

insert()

  // 向父节点插入节点 
  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)
      }
    }
  }

removeVnodes()

  // 移除指定索引范围(startIdx —— endIdx)内的节点 
  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)
        }
      }
    }
  }

patchVnode()

  /*
    更新节点:
      1. 全量的属性更新
      2. 如果新老节点都有孩子,则递归执行 diff
      3. 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
      4. 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
      5. 更新文本节点
  */ 
  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))
    ) {
      /*
        新旧节点都是静态的而且两个节点的 key 一样,并且
        新节点被 clone 或者 新节点有 v-once 指令
        则重用这部分节点
      */   
      vnode.componentInstance = oldVnode.componentInstance
      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

    // 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
    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)) {
        // 如果新老节点都有孩子,则递归执行 diff 过程
        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(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)
    }
  }

updateChildren()

  /*
   diff 过程:
     diff 优化:
               1. 四种假设:
                          newStart === oldStart
                          newEnd === oldEnd
                          newStart === oldEnd
                          newEnd === oldStart

               2. 假设新老节点开头结尾有相同节点的情况: 
                - 一旦命中假设,就避免了一次循环,以提高执行效率
                - 如果没有命中假设,则执行遍历,从老节点中找到新开始节点
                  找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置

     如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
     如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
  */
  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 是一个特殊的标志,仅由 <transition-group> 使用,
    // 以确保被移除的元素在离开转换期间保持在正确的相对位置
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      // 检查新节点的 key 是否重复
      checkDuplicateKeys(newCh)
    }

    // 遍历新老两组节点,只要有一组遍历完(开始索引超过结束索引)则跳出循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 老开始节点和新开始节点是同一个节点,执行 patch
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        // patch 结束后老开始和新开始的索引分别加 1,开始下一个节点
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 老结束和新结束是同一个节点,执行 patch
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // patch 结束后老结束和新结束的索引分别减 1,开始下一个节点
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // 老开始和新结束是同一个节点,执行 patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        // 处理被 transtion-group 包裹的组件时使用
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // patch 结束后老开始索引加 1,新结束索引减 1,开始下一个节点
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // 老结束和新开始是同一个节点,执行 patch
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // patch 结束后,老结束的索引减 1,新开始的索引加 1,开始下一个节点
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引

        // 找到老节点中每个节点 key 和 索引之间的关系映射:
        // 如 oldKeyToIdx = { key1: idx1, ... }
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 在映射中找到新开始节点在老节点中的位置索引
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        if (isUndef(idxInOld)) { // New element
          // 在老节点中没找到新开始节点,则说明是新创建的元素,执行创建
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 在老节点中找到新开始节点了
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // 如果这两个节点是同一个,则执行 patch
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            // patch 结束后将该老节点置为 undefined
            oldCh[idxInOld] = undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // same key but different element. treat as new element
            // 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点,
            // 则视为新元素,执行创建
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        // 老节点向后移动一个
        newStartVnode = newCh[++newStartIdx]
      }
    }

    // 走到这里,说明老节点或者新节点被遍历完了
    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)
    }
  }

checkDuplicateKeys()

  // 检查一组元素的 key 是否重复 
  function checkDuplicateKeys(children) {
    const seenKeys = {}
    for (let i = 0; i < children.length; i++) {
      const vnode = children[i]
      const key = vnode.key
      if (isDef(key)) {
        if (seenKeys[key]) {
          warn(
            `Duplicate keys detected: '${key}'. This may cause an update error.`,
            vnode.context
          )
        } else {
          seenKeys[key] = true
        }
      }
    }
  }

addVnodes()

  // 在指定索引范围(startIdx —— endIdx)内添加节点
  function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)
    }
  }

createKeyToOldIdx()

// 得到指定范围(beginIdx —— endIdx)内节点的 key 和 索引之间的关系映射 => { key1: idx1, ... }
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
}

findIdxInOld()

  // 找到新节点(vnode)在老节点(oldCh)中的位置索引 
  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
    }
  }

invokeCreateHooks()

  /*
    调用各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等,
    然后执行组件的 mounted 生命周期方法
  */ 
  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)
      // 调用组件的 insert 钩子,执行组件的 mounted 生命周期方法
      if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
    }
  }

createChildren()

  // 创建所有子节点,并将子节点插入父节点,形成一棵 DOM 树
  function createChildren(vnode, children, insertedVnodeQueue) {
    if (Array.isArray(children)) {
      // children 是数组,表示是一组节点
      if (process.env.NODE_ENV !== 'production') {
        // 检测这组节点的 key 是否重复
        checkDuplicateKeys(children)
      }
      // 遍历这组节点,依次创建这些节点然后插入父节点,形成一棵 DOM 树
      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)))
    }
  }

总结

Vue 中的 patch 都做了些什么?
Vuepatch 负责组件的 首次渲染后续更新销毁组件

  • 如果老的 VNode 是真实元素,则表示首次渲染,创建整棵 DOM 树,并插入 body,然后移除老的模版节点
  • 如果老的 VNode 不是真实元素,并且新的 VNode 也存在,则表示更新阶段,执行 patchVnode
    • 首先是全量更新所有的属性
    • 如果新老 VNode 都有孩子,则递归执行 updateChildren,进行 diff 过程
    • 老的 VNode 没孩子,如果新的 VNode 有孩子,则新增这些新孩子节点
    • 如果老的 VNode 有孩子,新的 VNode 没孩子,则删除这些老孩子节点
    • 如果不符合上面的几种,说明属于更新文本节点
  • 如果新的 VNode 不存在,老的 VNode 存在,则调用 destroy 销毁老节点

diff 过程结合 DOM 特点的优化?

  • 同层比较(降低时间复杂度)深度优先(递归)
  • 根据 DOM 节点做了四种假设,假设新老 VNode 的开头结尾存在相同节点
    • 如果命中假设,就可避免一次循环,降低了 diff 时间复杂度,提高执行效率
    • 如果没有命中假设,则执行遍历,从老的 VNode 中找到新的 VNode 的开始节点
  • 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
  • 如果老的 VNode 先于新的 VNode 遍历结束,则剩余的新的 VNode 执行新增节点操作
  • 如果新的 VNode 先于老的 VNode 遍历结束,则剩余的老的 VNode 执行删除操作,移除老节点
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值