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

本文是Vue实现系列的第五篇,详细介绍了如何将Vue实例的render函数转化为真实DOM,并讲解了mount和update阶段的DOM操作,特别是patch函数、createElm和diff算法的工作原理,通过对比两棵虚拟DOM树实现最小化DOM操作,提高性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

写在前面

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

正文

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

 1export default class YourVue{
 2  update(){
 3    if(this.$options.template){
 4      if(this._isMounted){
 5        const vnode = this.$options.render()
 6        patch(this.vnode, vnode)
 7        this.vnode = vnode
 8      }else{
 9        this.vnode = this.$options.render()
10        let el = this.$options.el
11        this.el = el && query(el)
12        patch(this.vnode, null, this.el)
13        this._isMounted = true
14      }
15    }
16  }
17}

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

 1export function patch (oldVnode, vnode, el) {
 2  if(isUndef(vnode)){
 3      createElm(oldVnode, el)
 4      return
 5  }
 6  if (oldVnode === vnode) {
 7      return 
 8  }
 9  if(sameVnode(oldVnode, vnode)){
10        patchVnode(oldVnode, vnode)
11  }else{
12      const parentElm = oldVnode.elm.parentNode;
13      createElm(vnode,parentElm,oldVnode.elm)
14      removeVnodes(parentElm,[oldVnode],0,0)
15  }
16}

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

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

 1function createElm (vnode, parentElm, afterElm = undefined) {
 2  let element
 3  if(!vnode.tag && vnode.text){
 4    element = document.createTextNode(vnode.text);
 5  }else{
 6    element = document.createElement(vnode.tag)
 7    if(vnode.props.attrs){
 8      const attrs = vnode.props.attrs
 9      for(let key in attrs){
10        element.setAttribute(key, attrs[key])
11      }
12    }
13    if(vnode.props.on){
14      const on = vnode.props.on
15      const oldOn = {}
16      updateListeners(element, on, oldOn, vnode.context)
17    }
18    for(let child of vnode.children){
19        if(child instanceof VNode){
20            createElm(child, element)
21        }else if(Array.isArray(child)){
22          for (let i = 0; i < child.length; ++i) {
23            createElm(child[i], element)
24          }
25        }
26    }
27  }
28  vnode.elm = element;
29  if(isDef(afterElm)){
30    insertBefore(parentElm, element, afterElm)
31  }else if(parentElm){
32    parentElm.appendChild(element)
33  }
34  return element;
35}

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

  • 如果新树有 child 旧树没有,则新建 child

  • 如果新树没有 child,旧树没有,则删掉 child

  • 如果都有 children,就到了虚拟 dom 中非常有名的 diff 算法。

 1function patchVnode(oldVnode, vnode){
 2  if (oldVnode === vnode) {
 3    return
 4  }
 5  const ch = vnode.children
 6  const oldCh = oldVnode.children
 7  const elm = vnode.elm = oldVnode.elm
 8  if(isUndef(vnode.text)){
 9    if(isDef(ch) && isDef(oldCh)){
10        updateChildren(elm,oldCh,ch)
11    }else if(isDef(ch)){
12        if (isDef(oldVnode.text)) setTextContent(elm, '')
13        addVnodes(oldVnode, ch, 0, ch.length - 1)
14    }else if(isDef(oldCh)){
15        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
16    }
17  }else{
18      setTextContent(elm, vnode.text);
19  }
20}

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

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

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

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

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

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

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

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

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

 1function updateChildren(parentElm, oldCh, newCh,){
 2  let oldStartIdx = 0
 3  let newStartIdx = 0
 4  let oldEndIdx = oldCh.length - 1
 5  let oldStartVnode = oldCh[0]
 6  let oldEndVnode = oldCh[oldEndIdx]
 7  let newEndIdx = newCh.length - 1
 8  let newStartVnode = newCh[0]
 9  let newEndVnode = newCh[newEndIdx]
10  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
11
12  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
13      if (isUndef(oldStartVnode)) {
14        oldStartVnode = oldCh[++oldStartIdx] 
15      } else if (isUndef(oldEndVnode)) {
16        oldEndVnode = oldCh[--oldEndIdx]
17      } else if (sameVnode(oldStartVnode, newStartVnode)) {
18        patchVnode(oldStartVnode, newStartVnode)
19        oldStartVnode = oldCh[++oldStartIdx]
20        newStartVnode = newCh[++newStartIdx]
21      } else if (sameVnode(oldEndVnode, newEndVnode)) {
22        patchVnode(oldEndVnode, newEndVnode)
23        oldEndVnode = oldCh[--oldEndIdx]
24        newEndVnode = newCh[--newEndIdx]
25      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
26        patchVnode(oldStartVnode, newEndVnode)
27        insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
28        oldStartVnode = oldCh[++oldStartIdx]
29        newEndVnode = newCh[--newEndIdx]
30      } else if (sameVnode(oldEndVnode, newStartVnode)) { 
31        patchVnode(oldEndVnode, newStartVnode)
32        insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
33        oldEndVnode = oldCh[--oldEndIdx]
34        newStartVnode = newCh[++newStartIdx]
35      } else {
36        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
37        idxInOld = isDef(newStartVnode.key)
38          ? oldKeyToIdx[newStartVnode.key]
39          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
40        if (isUndef(idxInOld)) {
41          createElm(newStartVnode, parentElm, oldStartVnode.elm)
42        } else {
43          vnodeToMove = oldCh[idxInOld]
44          if (sameVnode(vnodeToMove, newStartVnode)) {
45            patchVnode(vnodeToMove, newStartVnode)
46            oldCh[idxInOld] = undefined
47            insertBefore(parentElm,vnodeToMove.elm, oldStartVnode.elm)
48          } else {
49            createElm(newStartVnode, parentElm, oldStartVnode.elm)
50          }
51        }
52        newStartVnode = newCh[++newStartIdx]
53      }
54  }
55  if (oldStartIdx > oldEndIdx) {
56      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
57      addVnodes(parentElm, newCh, newStartIdx, newEndIdx, refElm)
58  } else if (newStartIdx > newEndIdx) {
59      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
60  }
61}

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

https://github.com/buppt/YourVue/tree/master/oldSrc/4.vdom

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值