首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

写在前面

本篇是从零实现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和setAttribute,updateListeners就是第一篇文章中事件绑定到 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 

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 

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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200619A06BXN00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券