最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 手写 Vue2 系列 之 patch —— diff

    正文概述 掘金(李永宁)   2021-07-08   545

    前言

    上一篇文章 手写 Vue2 系列 之 初始渲染 中完成了原始标签、自定义组件、插槽的的初始渲染,当然其中也涉及到 v-bind、v-model、v-on 指令的原理。完成首次渲染之后,接下来就该进行后续的更新了:

    响应式数据发生更新 -> setter 拦截到更新操作 -> dep 通知 watcher 执行 update 方法 -> 进而执行 updateComponent 方法更新组件 -> 执行 render 生成新的 vnode -> 将 vnode 传递给 vm._update 方法 -> 调用 patch 方法 -> 执行 patchVnode 进行 DOM diff 操作 -> 完成更新

    目标

    所以,本篇的目标就是实现 DOM diff,完成后续更新。涉及知识点只有一个:DOM diff。

    实现

    接下来就开始实现 DOM diff,完成响应式数据的后续更新。

    patch

    /**
     * 负责组件的首次渲染和后续更新
     * @param {VNode} oldVnode 老的 VNode
     * @param {VNode} vnode 新的 VNode
     */
    export default function patch(oldVnode, vnode) {
      if (oldVnode && !vnode) {
        // 老节点存在,新节点不存在,则销毁组件
        return
      }
    
      if (!oldVnode) { // oldVnode 不存在,说明是子组件首次渲染
      } else {
        if (oldVnode.nodeType) { // 真实节点,则表示首次渲染根组件
        } else {
          // 后续的更新
          patchVnode(oldVnode, vnode)
        }
      }
    }
    
    

    patchVnode

    /**
     * 对比新老节点,找出其中的不同,然后更新老节点
     * @param {*} oldVnode 老节点的 vnode
     * @param {*} vnode 新节点的 vnode
     */
    function patchVnode(oldVnode, vnode) {
      // 如果新老节点相同,则直接结束
      if (oldVnode === vnode) return
    
      // 将老 vnode 上的真实节点同步到新的 vnode 上,否则,后续更新的时候会出现 vnode.elm 为空的现象
      vnode.elm = oldVnode.elm
    
      // 走到这里说明新老节点不一样,则获取它们的孩子节点,比较孩子节点
      const ch = vnode.children
      const oldCh = oldVnode.children
    
      if (!vnode.text) { // 新节点不存在文本节点
        if (ch && oldCh) { // 说明新老节点都有孩子
          // diff
          updateChildren(ch, oldCh)
        } else if (ch) { // 老节点没孩子,新节点有孩子
          // 增加孩子节点
        } else { // 新节点没孩子,老节点有孩子
          // 删除这些孩子节点
        }
      } else { // 新节点存在文本节点
        if (vnode.text.expression) { // 说明存在表达式
          // 获取表达式的新值
          const value = JSON.stringify(vnode.context[vnode.text.expression])
          // 旧值
          try {
            const oldValue = oldVnode.elm.textContent
            if (value !== oldValue) { // 新老值不一样,则更新
              oldVnode.elm.textContent = value
            }
          } catch {
            // 防止更新时遇到插槽,导致报错
            // 目前不处理插槽数据的响应式更新
          }
        }
      }
    }
    
    

    updateChildren

    /**
     * diff,比对孩子节点,找出不同点,然后将不同点更新到老节点上
     * @param {*} ch 新 vnode 的所有孩子节点
     * @param {*} oldCh 老 vnode 的所有孩子节点
     */
    function updateChildren(ch, oldCh) {
      // 四个游标
      // 新孩子节点的开始索引,叫 新开始
      let newStartIdx = 0
      // 新结束
      let newEndIdx = ch.length - 1
      // 老开始
      let oldStartIdx = 0
      // 老结束
      let oldEndIdx = oldCh.length - 1
      // 循环遍历新老节点,找出节点中不一样的地方,然后更新
      while (newStartIdx <= newEndIdx || oldStartIdx <= oldEndIdx) { // 根为 web 中的 DOM 操作特点,做了四种假设,降低时间复杂度
        // 新开始节点
        const newStartNode = ch[newStartIdx]
        // 新结束节点
        const newEndNode = ch[newEndIdx]
        // 老开始节点
        const oldStartNode = oldCh[oldStartIdx]
        // 老结束节点
        const oldEndNode = oldCh[oldEndIdx]
        if (sameVNode(newStartNode, oldStartNode)) { // 假设新开始和老开始是同一个节点
          // 对比这两个节点,找出不同然后更新
          patchVnode(oldStartNode, newStartNode)
          // 移动游标
          oldStartIdx++
          newStartIdx++
        } else if (sameVNode(newStartNode, oldEndNode)) { // 假设新开始和老结束是同一个节点
          patchVnode(oldEndNode, newStartNode)
          // 将老结束移动到新开始的位置
          oldEndNode.elm.parentNode.insertBefore(oldEndNode.elm, oldCh[newStartIdx].elm)
          // 移动游标
          newStartIdx++
          oldEndIdx--
        } else if (sameVNode(newEndNode, oldStartNode)) { // 假设新结束和老开始是同一个节点
          patchVnode(oldStartNode, newEndNode)
          // 将老开始移动到新结束的位置
          oldStartNode.elm.parentNode.insertBefore(oldStartNode.elm, oldCh[newEndIdx].elm.nextSibling)
          // 移动游标
          newEndIdx--
          oldStartIdx++
        } else if (sameVNode(newEndNode, oldEndNode)) { // 假设新结束和老结束是同一个节点
          patchVnode(oldEndNode, newEndNode)
          // 移动游标
          newEndIdx--
          oldEndIdx--
        } else {
          // 上面几种假设都没命中,则老老实的遍历,找到那个相同元素
        }
      }
      // 跳出循环,说明有一个节点首先遍历结束了
      if (newStartIdx < newEndIdx) { // 说明老节点先遍历结束,则将剩余的新节点添加到 DOM 中
    
      }
      if (oldStartIdx < oldEndIdx) { // 说明新节点先遍历结束,则将剩余的这些老节点从 DOM 中删掉
    
      }
    }
    
    

    sameVNode

    /**
     * 判断两个节点是否相同
     * 这里的判读比较简单,只做了 key 和 标签的比较
     */
    function sameVNode(n1, n2) {
      return n1.key == n2.key && n1.tag === n2.tag
    }
    
    

    结果

    好了,到这里,虚拟 DOM 的 diff 过程就完成了,如果你能看到如下效果图,则说明一切正常。

    手写 Vue2 系列 之 patch —— diff

    可以看到,页面已经完全做到响应式数据的初始渲染和后续更新。其中关于 Computed 计算属性的内容仍然没有正确的显示出来,这很正常,因为还没实现这个功能,所以接下来就会去实现 conputed 计算属性,也就是下一篇内容 手写 Vue2 系列 之 computed。

    关注

    欢迎大家关注我的 掘金账号 和 B站,如果内容有帮到你,欢迎大家点赞、收藏 + 关注

    链接

    • 精通 Vue 技术栈的源码原理

    • 配套视频

    • 学习交流群


    起源地下载网 » 手写 Vue2 系列 之 patch —— diff

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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