Skip to content

Latest commit

 

History

History
495 lines (449 loc) · 20.9 KB

11.md

File metadata and controls

495 lines (449 loc) · 20.9 KB

入口开始,解读Vue源码(十)—— $mount 内部实现 --- patch

通过前文的介绍,我们知道需要将VNode转换成真实的node节点,需要通过patch函数来实现:

vm.$el = vm.__patch__(prevVnode, vnode)

__patch__是在platforms/runtime/index.js中定义的:

Vue.prototype.__patch__ = inBrowser ? patch : noop

这里主要是为了判断当前环境是否是在浏览器环境中,也就是是否存在Window对象。这里也是为了做跨平台的处理,如果是在server render环境,那么patch就是一个空操作。 我们接着去找render的实现:

export function createPatchFunction (backend) {
  ...
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
      // 如果vnode不存在但oldVnode存在,则表示要移除旧的node
      // 那么就调用invokeDestroyHook(oldVnode)来进行销毁
      if (isUndef(vnode)) {
        if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
        return
      }

      let isInitialPatch = false
      const insertedVnodeQueue = []
      // 如果oldVnode不存在,vnode存在,则创建新节点
      if (isUndef(oldVnode)) {
        isInitialPatch = true
        createElm(vnode, insertedVnodeQueue, parentElm, refElm)
      } else {
        // nodeType 节点的类型,详细:https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType
        const isRealElement = isDef(oldVnode.nodeType)
        // 如果oldVnode、vnode都存在
        // 如果oldVnode与Vnode是同一节点是就调用patchVnode处理去比较两个节点的差异
        if (!isRealElement && sameVnode(oldVnode, vnode)) {
          patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
        } else {
          if (isRealElement) {
            // 如果存在真实的节点,存在data-server-rendered属性
            if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
              oldVnode.removeAttribute(SSR_ATTR)
              hydrating = true
            }
            // 需要用hydrate函数将虚拟DOM和真实DOM进行映射
            if (isTrue(hydrating)) {
              if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                invokeInsertHook(vnode, insertedVnodeQueue, true)
                return oldVnode
              }
              ...
            }
            // 如果不是server-rendered 或者hydration失败
            // 创建一个空VNode,代替oldVnode
            oldVnode = emptyNodeAt(oldVnode)
          }
          // 将oldVnode设置为对应的虚拟dom,找到oldVnode.elm的父节点
          // 根据vnode创建一个真实dom节点并插入到该父节点中oldVnode.elm的位置
          const oldElm = oldVnode.elm
          const parentElm = nodeOps.parentNode(oldElm)
          createElm(
            vnode,
            insertedVnodeQueue,
            oldElm._leaveCb ? null : parentElm,
            nodeOps.nextSibling(oldElm)
          )

          // 递归更新父级占位节点元素,
          if (isDef(vnode.parent)) {
           ...
          }

          // 销毁旧节点
          if (isDef(parentElm)) {
            removeVnodes(parentElm, [oldVnode], 0, 0)
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode)
          }
        }
      }

      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
      // 返回节点
      return vnode.elm
    }
}

这里通过createPatchFunction函数,来创建返回一个patch函数。path接收6个参数:

  1. oldVnode: 旧的虚拟节点或旧的真实dom节点
  2. vnode: 新的虚拟节点
  3. hydrating: 是否要跟真是dom混合
  4. removeOnly: 特殊flag,用于组件
  5. parentElm:父节点
  6. refElm: 新节点将插入到refElm之前 具体解析看代码注释~抛开调用生命周期钩子和销毁就节点不谈,我们发现代码中的关键在于sameVnodecreateElmpatchVnode 方法。

sameVnode

判断2个节点,是否是同一个节点

/**
 * 节点 key 必须相同
 * tag、注释、data是否存在、input类型是否相同
 * 如果isAsyncPlaceholder是true,则需要asyncFactory属性相同
 */
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)
      )
    )
  )
}

createElm

function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
  vnode.isRootInsert = !nested // for transition enter check
  // 用于创建组件,在调用了组件初始化钩子之后,初始化组件,并且重新激活组件。
  // 在重新激活组件中使用 insert 方法操作 DOM
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    // 错误检测,主要用于判断是否正确注册了component,这个错误还是比较常见
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        inPre++
      }
      if (
        !inPre &&
        !vnode.ns &&
        !(
          config.ignoredElements.length &&
          config.ignoredElements.some(ignore => {
            return isRegExp(ignore)
              ? ignore.test(tag)
              : ignore === tag
          })
        ) &&
        config.isUnknownElement(tag)
      ) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    // nodeOps 封装的操作dom的合集
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode) // 用于为 scoped CSS 设置作用域 ID 属性

    // weex处理
    if (__WEEX__) {
      ...
    } else {

      // 用于创建子节点,如果子节点是数组,则遍历执行 createElm 方法.
      // 如果子节点的 text 属性有数据,则使用 nodeOps.appendChild(...) 在真实 DOM 中插入文本内容。
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // insert 用于将元素插入真实 DOM 中
      insert(parentElm, vnode.elm, refElm)
    }
    ...
  } 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)
  }
}

通过以上的注释,我们可以知道:createElm 方法的最终目的就是创建真实的 DOM 对象

patchVnode

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // 如果新老 vnode 相等
  if (oldVnode === vnode) {
    return
  }

  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
  }

  // 复用新老节点被标记为static,新老节点key相同,新 vnode 是克隆所得;新 vnode 有 v-once 的属性
  // 如果新节点没有被克隆,这意味着渲染函数已经被hot-reload-api重置,我们需要做一个适当的重新渲染。
  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
  // 执行 data.hook.prepatch 钩子
  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)) {
    // 遍历调用 cbs.update 钩子函数
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    // 执行 data.hook.update 钩子
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  // 旧 vnode 的 text 选项为 undefined
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      // 新老节点的 children 不同,执行 updateChildren 方法。
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      // 旧 vnode children 不存在 执行 addVnodes 方法
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 新 vnode children 不存在 执行 removeVnodes 方法
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 如果新旧 vnode 都是 undefined,且老节点存在 text,清空文本
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新老节点文本不同,更新文本内容
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    // 执行 data.hook.postpatch 钩子,至此 patch 完成
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

让我们来画张图屡一下大致的流程: patchVnode

addVnodesremoveVnodes都比较好理解,一个是增加一个节点元素,一个是删除节点元素。主要来看一下updateChildren方法:

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

  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)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      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)
      } else {
        vnodeToMove = oldCh[idxInOld]
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
          warn(
            'It seems there are duplicate keys that is causing an update error. ' +
            'Make sure each v-for item has a unique key.'
          )
        }
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
          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)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }

这里说起来可能会比较复杂,下面开始用图来解释一下整体的流程:

1. 定义初始变量

  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] // 新列表终点值

2. 定义循环

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  ...
}

进行循环遍历,遍历条件为 oldStartIdx <= oldEndIdxnewStartIdx <= newEndIdx,在遍历过程中,oldStartIdxnewStartIdx 递增,oldEndIdxnewEndIdx 递减。当条件不符合跳出遍历循环

3. oldStartVnode、oldEndVnode 存在检测

if (isUndef(oldStartVnode)) {
  oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {
  oldEndVnode = oldCh[--oldEndIdx]
}

如果oldStartVnode不存在,oldCh起始点向后移动。如果oldEndVnode不存在,oldCh终止点向前移动。

4. oldStartVnode 和 newStartVnode 是 sameVnode

else if (sameVnode(oldStartVnode, newStartVnode)) {
  patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
  oldStartVnode = oldCh[++oldStartIdx]
  newStartVnode = newCh[++newStartIdx]
}

如果oldStartVnodenewStartVnode 是sameVnode,则patchVnode,同时彼此向后移动一位

5. oldEndVnode 和 newEndVnode 是 sameVnode

else if (sameVnode(oldEndVnode, newEndVnode)) {
  patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
  oldEndVnode = oldCh[--oldEndIdx]
  newEndVnode = newCh[--newEndIdx]
}

如果oldEndVnodenewEndVnode 是sameVnode,则patchVnode,同时彼此向前移动一位

6. oldStartVnode 和 newEndVnode 是 sameVnode

else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
  canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
  oldStartVnode = oldCh[++oldStartIdx]
  newEndVnode = newCh[--newEndIdx]
}

如果oldStartVnodenewEndVnode 是 sameVnode,则先 patchVnode,然后把oldStartVnode移到oldCh最后的位置即可,然后oldStartIdx向后移动一位,newEndIdx向前移动一位

7. oldEndVnode 和 newStartVnode 是 sameVnode

else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
  canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
  oldEndVnode = oldCh[--oldEndIdx]
  newStartVnode = newCh[++newStartIdx]
}

如果oldEndVnodenewStartVnode 是 sameVnode,则先 patchVnode,然后把oldEndVnode移到oldCh最前的位置即可,然后newStartIdx向后移动一位,oldEndIdx向前移动一位

8. 如果没有相同的 key,执行 createElm 方法创建元素。

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)
}

如果以上条件都不匹配,则查找oldVnode中与vnode具有相同key的节点,并将查找的结果赋值给elmToMove。如果找不到相同key的节点,则表示是新创建的节点

9. 如果有相同的 key,就判断这两个节点是否为sameNode

vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
  patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
  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)
}
newStartVnode = newCh[++newStartIdx]

若为同一类型就调用patchVnode,就将对应下标处的oldVnode设置为undefined,把vnodeToMove插入到oldCh之前,newStartIdx继续向后移动。如果两个 vnode 不相似,视为新元素,执行 createElm创建。

10. 如果老 vnode 数组的开始索引大于结束索引,说明新 node 数组长度大于老 vnode 数组,执行 addVnodes 方法添加这些新 vnode 到 DOM 中

if (oldStartIdx > oldEndIdx) {
  refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}

11. 如果老 vnode 数组的开始索引小于结束索引,说明老 node 数组长度大于新 vnode 数组,执行 removeVnodes 方法从 DOM 中移除老 vnode 数组中多余的 vnode。

else if (newStartIdx > newEndIdx) {
  removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}

总结

到这里,patch的主要功能也基本讲完了,我们发现,在本篇中,大量出现了一个key字段。经过上面的调研,其实我们已经知道Vue的diff算法中其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
  2. 同一层级的一组节点,他们可以通过唯一的id进行区分 基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n),当页面的数据发生变化时,Diff算法只会比较同一层级的节点:

如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则。 比如一下这个情况:

我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:

即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率? 所以我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。

所以一句话,key的作用主要是为了高效的更新虚拟DOM。另外vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

参考文章: Vue2.0 v-for 中 :key 到底有什么用?

Vue.js 源码学习六 —— VNode虚拟DOM学习