最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 虚拟 DOM 和 diff 算法 -02

    正文概述 掘金(亦黑迷失)   2021-05-24   452

    上一篇《虚拟 DOM 和 diff 算法 -01》我们手写实现了 h 函数,本篇着重介绍 diff 算法。

    diff 算法的特点

    • 如果是往数组的最后面添加节点,那么前面的节点不会改动

    比如有如下新(vnode2)旧(vnode1)两个节点,那么执行 patch(vnode1, vnode2) 会发现仅仅浏览器只是追加了一个节点 <div>东风破</div>, 不会改变前两个。

    const vnode1 = h("div", {}, [
      h("div", "七里香"),
      h("div", "东风破")
    ])
    const vnode2 = h("div", {}, [
      h("div", "七里香"),
      h("div", "东风破"),
      h("div", "兰亭序")
    ])
    

    可以通过在浏览器调试工具里直接将“七里香”改成“七里不香”,然后通过点击按钮执行 patch(vnode1, vnode2) 会发现“七里不香”依旧没变。

    虚拟 DOM 和 diff 算法 -02

    • key 很重要,key 作为节点的标识,告诉 diff 算法在更改前后节点是否为同一个

    如果是往数组的开头添加节点,则所有的节点都会被改动,想要做到最小化更新,需要给每个节点添加 key 属性,这样 <div>七里香</div><div>东风破</div> 两个节点就不会被改动了

    const vnode1 = h("div", {}, [
      h("div", { key: 1 }, "七里香"),
      h("div", { key: 2 }, "东风破")
    ])
    const vnode2 = h("div", {}, [
      h("div", { key: 3 }, "兰亭序"),
      h("div", { key: 1 }, "七里香"),
      h("div", { key: 2 }, "东风破")
    ])
    
    • 只有是同一个虚拟节点,才进行精细化比较,否则直接删除旧节点,插入新节点

    判断两个节点是否为同一个,是根据比较选择器,也就是 sel 的值和 key 的值是否都相同,都相等则判断为同一个虚拟节点

    • 只进行同层比较

    新旧节点的层级要相同,比如下面的例子里新节点比旧节点多了层 div,则不会进行精细化比较,直接删除旧节点插入新节点

    const vnode2 = h("div", {}, [
      h('div', [
        h("div", { key: 3 }, "兰亭序"),
        h("div", { key: 1 }, "七里香"),
        h("div", { key: 2 }, "东风破")
      ])
    ])
    

    手写 patch 函数

    diff 算法是通过 patch 函数实现的,在开始手写之前,我们先来理清 patch 函数做了什么

    函数功能分析

    可以通过之前下载到 node_modules 里的 snabbdom 查看源码,patch 函数被定义在了 snabbdom 下的 src 目录下的 init.ts 里

    // init.ts
    return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
        // ...忽略部分代码
        // 通过 isVnode 函数判断旧节点是否为虚拟节点
        if (!isVnode(oldVnode)) {
          oldVnode = emptyNodeAt(oldVnode); // 不是则通过 emptyNodeAt 包装为虚拟节点
        }
        // 通过 sameVnode 函数判断新旧节点是否为为同一个节点
        if (sameVnode(oldVnode, vnode)) {
          // 相同...
        } else {
          // 不同...
        }
        // ...忽略部分代码
      };
    

    根据源码得到如下流程图

    虚拟 DOM 和 diff 算法 -02

    手写第一次上树

    新建 patch.js 文件,引入 vnode 函数用于将非虚拟节点的 oldVnode 包装为虚拟节点

    // patch.js
    import vnode from './vnode.js'
    import creatElement from './creatElement.js'
    
    export default (oldVnode, newVnode) => {
      // 判断 oldVnode 是否为虚拟节点
      if (oldVnode.sel === undefined) {
        // oldVnode 不是虚拟节点,则包装成虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase, {}, [], undefined, oldVnode)
      } 
      // 判断 oldVnode, newVnode 是否为同一节点
      if (oldVnode.sel === newVnode.sel && oldVnode.key === newVnode.key) {
        // 同一节点
      } else {
        // 不是同一节点
        const domNode = creatElement(newVnode)
        // 将新节点上树
        oldVnode.elm.parentNode?.insertBefore(domNode, oldVnode.elm)
        // 删除旧节点
        oldVnode.elm.parentNode?.removeChild(oldVnode.elm)
      }
    }
    

    新建 creatElement.js 并在 patch.js 引入 creatElement 函数,用于创建新节点,并将对应的虚拟节点的 elm 属性赋值为创建出的新节点

    /**
     * creatElement.js 
     * 将 vnode 创建为真正的 DOM 节点(但是没上树的孤儿节点)
     */
    export default function createElement (vnode) {
      const domNode = document.createElement(vnode.sel)
      vnode.elm = domNode
      // 判断 vnode 有子节点(children)还是文本(text)
      if (vnode.children !== undefined && vnode.children.length && vnode.text === undefined) {
        // 有子节点
        vnode.children.forEach(item => {
          // 调用 createElement 意味着创建出了 DOM,并且将改虚拟节点的 elm 属性指向了这个 DOM,
          // 但这个 DOM 是个孤儿节点,还没上树
          const childNode = createElement(item)
          item.elm = childNode
          domNode.appendChild(childNode)
        })
      } else {
        // 内部为文本
        domNode.innerText = vnode.text
        vnode.elm = domNode
      }
      return domNode
    }
    

    至此,我们已经完成了上面 patch 函数流程图中除了“精细化比较”之外的内容。接下来就开始着手当 oldVnode 和 newVnode 是同一节点的情况下的精细化比较的内容,这部分将有较多的图示,写在本篇难免会导致页面过长,我将在下篇继续分享~

    One More Thing

    本文有用到一些插入节点的方法,现在就此做一个扩展总结

    • innerHTML(属性):获取标签内部的HTML内容
    • outerHTML(属性):获取包括目标标签在内,以及内部HTML的内容
    • appendChild(函数):向目标标签末尾添加子节点,返回参数节点
    • insertBefore(函数):向目标节点的第二个参数位置添加第一个参数为子节点,返回第一个参数
    • insertAdjacentHTML(函数):向目标节点的指定位置添加节点

    虚拟 DOM 和 diff 算法 -02

    虚拟 DOM 和 diff 算法 -02


    起源地下载网 » 虚拟 DOM 和 diff 算法 -02

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元