从零写一个 Vue(五)DOM 生成与更新

649 阅读4分钟

写在前面

本篇是从零实现vue2系列第五篇,将 YourVue 实例的 render 函数转换成真实 dom 和更新算法。

文章会最先更新在公众号:BUPPT。

正文

上篇文章我们把 render 函数挂在了 options 属性上,执行 render() 就可以得到 template 对应的虚拟 dom 树了。

export default class YourVue{
    update(){
        if(this.$options.template){
            if(this._isMounted){
                const vnode = this.$options.render()
                patch(this.vnode, vnode)
                this.vnode = vnode
            }else{
                this.vnode = this.$options.render()
                let el = this.$options.el
                this.el = el && query(el)
                patch(this.vnode, null, this.el)
                this._isMounted = true
            }
        }
    }
}

Vue 将虚拟 dom 转换成真实 dom 有两种阶段,一个是 mount,一个是 update。都是通过 patch 函数来操作 dom 的。

export function patch (oldVnode, vnode, el) {
  if(isUndef(vnode)){
      createElm(oldVnode, el)
      return
  }
  if (oldVnode === vnode) {
      return 
  }
  if(sameVnode(oldVnode, vnode)){
        patchVnode(oldVnode, vnode)
  }else{
      const parentElm = oldVnode.elm.parentNode;
      createElm(vnode,parentElm,oldVnode.elm)
      removeVnodes(parentElm,[oldVnode],0,0)
  }
}

如果是 mount 阶段,会执行 createElm,如果是 update 阶段,先判断两个根节点的 vnode 是否相同,如果不同则直接创建新的 dom,如果相同则执行 patchVnode。

先看 mount 阶段的 createElm,就是createElementsetAttributeupdateListeners 就是第一篇文章中事件绑定到 dom 的方法。最后将生成的 dom 插入到指定的位置。

function createElm (vnode, parentElm, afterElm = undefined) {
  let element
  if(!vnode.tag && vnode.text){
    element = document.createTextNode(vnode.text);
  }else{
    element = document.createElement(vnode.tag)
    if(vnode.props.attrs){
      const attrs = vnode.props.attrs
      for(let key in attrs){
        element.setAttribute(key, attrs[key])
      }
    }
    if(vnode.props.on){
      const on = vnode.props.on
      const oldOn = {}
      updateListeners(element, on, oldOn, vnode.context)
    }
    for(let child of vnode.children){
        if(child instanceof VNode){
            createElm(child, element)
        }else if(Array.isArray(child)){
          for (let i = 0; i < child.length; ++i) {
            createElm(child[i], element)
          }
        }
    }
  }
  vnode.elm = element;
  if(isDef(afterElm)){
    insertBefore(parentElm, element, afterElm)
  }else if(parentElm){
    parentElm.appendChild(element)
  }
  return element;
}

update 阶段,就是对比两棵虚拟 dom 树的阶段。Vue 对比两棵虚拟 dom 树时是按层对比的,如果根节点相同,判断 children 是否相同:

  • 如果新树有 child 旧树没有,则新建 child
  • 如果新树没有 child,旧树没有,则删掉 child
  • 如果都有 children,就到了虚拟 dom 中非常有名的 diff 算法。

function patchVnode(oldVnode, vnode){
  if (oldVnode === vnode) {
    return
  }
  const ch = vnode.children
  const oldCh = oldVnode.children
  const elm = vnode.elm = oldVnode.elm
  if(isUndef(vnode.text)){
    if(isDef(ch) && isDef(oldCh)){
        updateChildren(elm,oldCh,ch)
    }else if(isDef(ch)){
        if (isDef(oldVnode.text)) setTextContent(elm, '')
        addVnodes(oldVnode, ch, 0, ch.length - 1)
    }else if(isDef(oldCh)){
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
  }else{
      setTextContent(elm, vnode.text);
  }
}

diff 算法步骤比较多,但是也都不复杂,核心思想就是使用四个指针分别指向新 children 和旧 children 数组的头和尾,尽量找到和新树相同的节点,通过移动进行元素复用,将旧树变换成新树的结构,减少新建 dom 节点的操作。

当两个头指针指向的节点相同时,头指针后移。当两个尾指针节点相同时,尾指针前移。

当头和头,尾和尾都不同时,先比较旧树的头和新树的尾,如果相同,就把旧树的头指针指向的节点移动到尾指针指向节点的后面。旧头指针后移,新尾指针前移。

当前面都不同,旧树的尾和新树的头相同时,把旧树的尾移动到旧树的头前面,旧尾指针前移,新头指针后移。

当头尾指针都不同的时候,vue 还会遍历旧树剩余节点的 key 与新树的头节点的 key 进行比较,也就是 v-for 时必须要写的 key 的值,如果有相同的 key,就将旧树的节点移到旧头前面。

如果都没有,就在旧树的头前面新建新树的头节点,新树头指针后移。

最后当旧树头尾指针相遇,新树头尾指针之间仍有元素节点时,新建这些节点。

当新树头尾指针相遇,旧树头尾指针之间还有元素时,删除这些节点。

这样就通过元素节点的移动和新建,将旧的 dom 结构转换成新的 dom 树结构啦!理解思路后,再看代码就清晰了。

function updateChildren(parentElm, oldCh, newCh,){
  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

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        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)
        insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { 
        patchVnode(oldEndVnode, newStartVnode)
        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)) {
          createElm(newStartVnode, parentElm, oldStartVnode.elm)
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode)
            oldCh[idxInOld] = undefined
            insertBefore(parentElm,vnodeToMove.elm, oldStartVnode.elm)
          } else {
            createElm(newStartVnode, parentElm, oldStartVnode.elm)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
  }
  if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, newCh, newStartIdx, newEndIdx, refElm)
  } else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

虚拟 dom 完成实现。综合本篇和上篇文章的代码:

github.com/buppt/YourV…

求关注~ 求star~