Skip to content

实现

javascript
// 通过 updateComponent 更新视图,内部先调用 vm._render 生成新的 vnode,再调用
// vm._update,内部会先 vm._vnode 取出上次的 vnode,称为 prevVnode,
// 再 vm._vnode = vnode 将 vnode 修改为新生成的 vnode,如果 prevVnode 未定义,说明是
// 初始化渲染,直接 patch(vm.$el, vnode),否则是更新,直接 patch(prevVnode,vnode) 
// 进行 diff,patch 内部先判断 oldVnode.nodeType 是否有定义,有说明是初始化渲染,
// 否则如果 oldVnode 和 vnode 满足 sameVnode 条件,执行 patchVnode 进行 diff。
Vue.prototype._update = function (vnode: VNode) {
  const vm: Component = this
  // 之前的 vnode
  const prevVnode = vm._vnode
  // 新的 vnode
  vm._vnode = vnode

  if (!prevVnode) {
    // 初始化渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 后续更新,diff
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

function patch(oldVnode, vnode, hydrating, removeOnly) {
  if (sameVnode(oldVnode, vnode)) {
    // 进行 diff
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  }
}
javascript
// patchVnode通过比较oldVnode和vnode的差异从而更新真实DOM,并通过const elm = vnode.elm = oldVnode.elm实现DOM的复用。在进行同级比较前通过updateStyle和updateAttrs完成样式和属性的更新,先取出各自的vnode.data.staticStyle,即oldStyle和newStyle,遍历oldStyle所有属性,如果newStyle[name]未定义,说明该样式被移除了,设置el.style[name] = '',再遍历newStyle所有属性,如果属性值和oldStyle[name]不相等,说明该样式被更新了,以新的为准,设置el.style[name] = newStyle[name],updateAttrs的流程和updateStyle流程一样,通过removeAttribute和setAttribute完成属性的移除和更新
javascript
function updateStyle (oldVnode, vnode) {
  const data = vnode.data || {}
  const oldData = oldVnode.data || {}
  if (isUndef(data.staticStyle) && isUndef(oldData.staticStyle)) {
    return
  }

  let cur, name
  const el = vnode.elm
  const oldStyle = oldData.staticStyle || {}
  const newStyle = data.staticStyle || {} // 先子后父 确保父组件能重写子组件样式

  for (name in oldStyle) { // 遍历旧的样式
    if (isUndef(newStyle[name])) { // 新的没有旧的属性
      el.style[name] = '' // dom移除该属性
    }
  }

  for (name in newStyle) { // 遍历新的样式
    cur = newStyle[name]
    if (cur !== oldStyle[name]) { // 新的和旧的不相等 => 更新 / 删除
      el.style[name] = cur // 以新的为准
    }
  }
}
javascript
// diff,首先 oldVnode 和 vnode 一定是相同类型的虚拟节点,因为 patch 中只有
// oldVnode 和 vnode 满 足sameVnode 才进入 patchVnode。
// 内部先复用 DOM,即 vnode.elm = oldVnode.elm
// 再先判断 vnode.text 是否有定义,如果有定义说明它们都是文本 vnode,
// 如果 oldVnode.text !== vnode.text,则 elm.textContent = vnode.text 进行更新。
// 如果 vnode.text 未定义说明它们都是元素 vnode,对它们的 children 进行diff,
// 如果只有 oldVnode 有 children,说明子元素都被移除了,直接 elm.innerHTML = '',
// 如果只有 vnode 有 children,说明原来没有子元素,现在添加了子元素,直接遍历 children
// 调用 createElm 生成 DOM 并添加到 elm 中,即
// vnode.elm.appendChild(createElm(ch[i]))
// 如果 oldVnode 和 vnode 都有 children,进行 updateChildren,进行真正的 diff。
pacthVnode(oldVnode, vnode) {
  let elm = (vnode.elm = oldVnode.elm) // 复用真实DOM
  let oldCh = oldVnode.children
  let ch = vnode.children
  updateProps(oldVnode, vnode)
  // oldVnode, vnode是相同类型节点
  if (isUndef(vnode.text)) { 
    // 都是元素节点
    if (isDef(oldCh) && isDef(ch) && oldCh !== ch) {
      // 都有子元素, 去 diff
      updateChildren(elm, oldCh, ch)
    } else if (isDef(ch)) {
      // 添加子元素
      elm.appendChild(createElm(ch[i]))
    } else if (isDef(oldCh)) {
      // 移除子元素
      elm.innerHTML = ''
    }
  } else if (oldVnode.text !== vnode.text) {
    // 都是文本节点
    elm.textContent = vnode.text
  }
}
javascript
// updateChildren 中,通过 oldStartIdx、oldEndIdx、newStartIdx、newEndIdx 四个指针
// 分别标识 oldCh 的头和尾、ch 的头和尾,当
// oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx,即新旧指针都没越界时不断
// 进行比较,对 parentElm 进行对应更新。

// 先通过 oldCh 和 ch 头尾两两比较来实现更新
// - 旧头和新头,命中说明 DOM 位置不用改变,递归调用 patchVnode 对它们内部进行diff,
// 最后 oldStartIdx 和 newStartIdx 都右移
// - 旧尾和新尾,命中说明 DOM 位置不用改变,递归调用 patchVnode 对它们内部进行diff,
// 最后 oldEndIdx 和 newEndIdx 都左移
// - 旧头和新尾,命中说明旧头对应的 DOM 元素被移动到后面了,递归调用 patchVnode 对
// 它们内部进行 diff,再将 oldStartIdx 对应的 elm 移动到 oldEndVnode 对应的 elm 的
// nextSibling 的前面,最后 oldStartIdx 右移,newEndIdx 左移
// - 旧尾和新头,命中说明旧尾对应的 DOM 元素被移动到前面了,递归调用 patchVnode 对
// 它们内部进行 diff,再将 oldEndIdx 对应的 elm 移动到 oldStartVnode 对应的elm的前面,
// 最后 oldEndIdx 左移,newStartIdx 右移

// 如果上面四种情况都不命中,说明有可能出现了新增的 DOM,用一张表 oldKeyToIdx 记录
// oldStartIdx 到 oldEndIdx 中有定义 key 的 vnode 在 oldCh 中的位置,之后尝试进行
// DOM 的复用,先判断 newStartVnode 的 key 是否有定义,如果有定义去表中取这个 key 
// 对应的值,即被用来复用的 vnode 在 oldCh 中的下标,如果没有定义也要尽最大能力复用,
// 遍历 oldStartIdx 到 oldEndIdx 的每个 oldVnode,如果 oldVnode 有定义
// (可能因为移动被置为了 undefined)并且 newStartVnode 和该 oldVnode 满足
// sameVnode,就可以进行复用,返回这个 oldVnode 在 oldCh 中的下标,通过 idxInOld 
// 保存下标,如果 idxInOld 未定义,说明没法复用,根据 newStartVnode 生成 DOM 并插入
// 到 oldStartVnode.elm 的前面,有定义,说明可以复用,oldCh[idxInOld] 找出该 vnode
// 并用 vnodeToMove 接收,判断 vnodeToMove 和 newStartVnode 是否满足 sameVnode
// (如果这个 idxInOld 是尽最大能力复用得到的必然满足,但如果是表中找到的只能保证它们
// 的 key 相同),如果不满足直接当作新节点处理,根据 newStartVnode 生成 DOM 并插入
// 到 oldStartVnode.elm 的前面。如果满足则递归调用 patchVnode 对它们内部进行 diff,
// 再将 oldCh[idxInOld] 置为undefined,表示该 vnode 已经处理过了,被移动到了其他位置,
// 再将 vnodeToMove 对应的 elm 移动到 newStartVnode 对应的 elm 的前面,最后
// newStartIdx 右移。同时因为产生了 undefined,条件中增加如果 oldStartVnode 未定义,
// oldStartIdx 就右移,如果oldEndVnode未定义,oldEndIdx 就左移。
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (isUndef(oldStartVnode)) { // 跳过oldCh中因移动产生的undefined
    oldStartVnode = oldCh[++oldStartIdx]
  } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
  } else if (sameVnode(oldStartVnode, newEndVnode)) {
    patchVnode(oldStartVnode, newEndVnode)
    parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
  } else if (sameVnode(oldEndVnode, newStartVnode)) {
    patchVnode(oldEndVnode, newStartVnode)
    parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
  } else { // 都不命中 用一张表记录oldVnode中有key的vnode在oldCh中的位置
    if (isUndef(oldKeyToIdx)) {
      oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx
        let i, key
        const map = {}
        for (i = beginIdx; i <= endIdx; ++i) {
          key = children[i].key
          if (isDef(key)) map[key] = i
        }
        return map
      )
    }
    // 复用
    idxInOld = isDef(newStartVnode.key)
      ? oldKeyToIdx[newStartVnode.key] 
      : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx 
        for (let i = start; i < end; i++) {
          const c = oldCh[i]
          if (isDef(c) && sameVnode(node, c)) return i
        }
      )
    if (isUndef(idxInOld)) { // 是新增节点
      parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
    } else {
      vnodeToMove = oldCh[idxInOld]
      if (sameVnode(vnodeToMove, newStartVnode)) {
        patchVnode(vnodeToMove, newStartVnode)
        oldCh[idxInOld] = undefined // 置为undefined, 表示处理过了
        parentElm.insertBefore(vnodeToMove.elm, newStartVnode.elm)
      } else {
        parentElm.insertBefore(createElm(newStartVnode), oldStartVnode.elm)
      }
    }
    newStartVnode = newCh[++newStartIdx]
  }
}
javascript
// 当指针越界后跳出循环,此时如果 oldStartIdx > oldEndIdx,说明旧的处理完了,而新的
// 没处理完,我们要以新的为准,说明 newStartIdx 到 newEndIdx 都是新增,如果
// newCh[newEndIdx + 1] 未定义,createElm 生成的 DOM 都插入到最后。有定义说明
// 新增的 DOM 位置在 newCh[newEndIdx + 1].elm 的前面,让新增的 DOM 都插入到这个
// vnode 对应 elm 之前。
// 如果 newStartIdx > newEndIdx,说明新的处理完了,而旧的没处理完,
// 我们要以新的为准,说明 oldStartIdx 到 oldEndIdx 都是要删除的 DOM,遍历每一项,如果
// 有定义就通过 removeChild 删除该 vnode 对应的 elm
if (oldStartIdx > oldEndIdx) { // 旧的结束了, 新的没结束, 说明有新增
  // newStartIdx - newEndIdx 都是新增的节点
  // newCh[newEndIdx + 1] 是 vnode, 新增到这个 vnode 对应的 elm 的前面
  // newCh[newEndIdx + 1] 不是 vnode, 新增到最后后面
  let refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
  for (let i = newStartIdx; i < newEndIdx; i++) {
    parentElm.insertBefore(createElm(newCh[i]), refElm)
  }
} else if (newStartIdx > newEndIdx) { // 新的结束了, 旧的没结束, 说明有删除
  for (let i = oldStartIdx; i < oldEndIdx; i++) {
    if (oldCh[i]) {
      parentElm.removeChild(oldCh[i].elm)
    }
  }
}