上一主题 下一主题
ScriptCat,新一代的脚本管理器脚本站,与全世界分享你的用户脚本油猴脚本开发指南教程目录
返回列表 发新帖
楼主: 李恒道 - 

[油猴脚本开发指南]简明diff(四)

[复制链接]
  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2021-10-9 22:05:29 | 显示全部楼层 | 阅读模式

    前文

    之前我们已经学习了patchVnode的过程

    关于新旧节点的各种判断,但是最核心的情况的函数处理

    我们依然还没有研究过,这节课我们研究

          if (isDef(oldCh) && isDef(ch)) {
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
          } 

    里进行处理的updatechildren

    这里是精华,也是我们diff部分的终篇。

    开干

    先说一个diff的算法结论

    就是把新旧虚拟dom进行比较

    在比较的过程中将真实dom逐步转化为新虚拟dom的状态。

    开始

    直接查代码

    代码如下

      function updateChildren(
        parentElm: Node,
        oldCh: VNode[],
        newCh: VNode[],
        insertedVnodeQueue: VNodeQueue
      ) {
        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: KeyToIndexMap | undefined;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;
    
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldStartVnode, newEndVnode)) {
            // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(
              parentElm,
              oldStartVnode.elm!,
              api.nextSibling(oldEndVnode.elm!)
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
          } else if (sameVnode(oldEndVnode, newStartVnode)) {
            // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
          } else {
            if (oldKeyToIdx === undefined) {
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key as string];
            if (isUndef(idxInOld)) {
              // New element
              api.insertBefore(
                parentElm,
                createElm(newStartVnode, insertedVnodeQueue),
                oldStartVnode.elm!
              );
            } else {
              elmToMove = oldCh[idxInOld];
              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(
                  parentElm,
                  createElm(newStartVnode, insertedVnodeQueue),
                  oldStartVnode.elm!
                );
              } else {
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
              }
            }
            newStartVnode = newCh[++newStartIdx];
          }
        }
        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
          if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            addVnodes(
              parentElm,
              before,
              newCh,
              newStartIdx,
              newEndIdx,
              insertedVnodeQueue
            );
          } else {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
          }
        }
      }

    参数

        parentElm: Node,
        oldCh: VNode[],
        newCh: VNode[],
        insertedVnodeQueue: VNodeQueue

    传入了父节点,旧虚拟dom的子节点数组,新虚拟dom的子节点数组,insertedVnde数组。

        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: KeyToIndexMap | undefined;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;

    这里我们声明了许多变量,有些我们还不知道,但是可以根据名字和作用猜测到大部分的变量作用。

    先说一个结论,diff算法是同时获取新旧虚拟dom的头和尾,并逐步向中间逼近,在逼近的过程中将真实dom逐步变为新虚拟dom对应的真实dom的操作。

        let oldStartIdx = 0;
        let newStartIdx = 0;

    设置新虚拟dom的开始索引以及旧虚拟dom的开始索引为0

        let oldEndIdx = oldCh.length - 1;
        let oldStartVnode = oldCh[0];
        let oldEndVnode = oldCh[oldEndIdx];

    获取旧虚拟dom的结束索引

    获取开始索引的对应虚拟dom

    获取结束索引的对应虚拟dom

        let newEndIdx = newCh.length - 1;
        let newStartVnode = newCh[0];
        let newEndVnode = newCh[newEndIdx];

    获取新虚拟dom的结束索引

    获取新虚拟dom的开始索引位置的虚拟dom

    获取新虚拟dom的结束索引位置的虚拟dom

       let oldKeyToIdx: KeyToIndexMap | undefined;
        let idxInOld: number;
        let elmToMove: VNode;
        let before: any;

    这四个不知道是啥,先不管。

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)

    因为diff算法是分别从左右设置两个索引逐渐向里逼近,直至新旧虚拟dom一方结束才结束

    图片.png

    所以这里用了while,比对是否旧虚拟dom的开始结束索引是否相等

    比较新虚拟dom的开始结束索引是否相等。

    如果都不等的话,证明是可以继续比对的,我们往下看都进行了哪些操作。

          if (oldStartVnode == null) {
            oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
          } else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx];
          } else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx];
          } else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx];
          } 

    这里首先判断了是否为null

    如果开始索引的节点为null

    则开始索引往后移

    如果结束索引的节点为null

    则结束索引往前移

    因为是if else

    所以如果前几个判断为null

    则会直接跳过判断部分,进入下一轮循环。

    那如果四个节点都不为null

    我们会怎么进行判断?

    if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
          }

    这里对旧开始节点和新开始节点进行比较,如果比较成功,则调用patchVnode来对这两个节点进行处理

    然后旧开始节点和新开始节点都往前进一个

    这里调用patchVnode相当于一个递归

    我们一开始对元素用了patchvnode

    然后发现都存在子元素就调用了 updateChildren

    这时候updateChildren又会调用patchVnode对其子元素继续处理。

    一直到穷尽为止

    比较的函数是怎么进行比较的?

    function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
      const isSameKey = vnode1.key === vnode2.key;
      const isSameIs = vnode1.data?.is === vnode2.data?.is;
      const isSameSel = vnode1.sel === vnode2.sel;
    
      return isSameSel && isSameKey && isSameIs;
    }

    通过比较key,data,sel,然后进行结合判断

    这里我们基本懂了,那我们继续

    if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
          }

    这里我们比对旧结束节点以及新结束节点

    如果相等则调用patchVnode

    并把旧结束节点和新结束节点往前推进一个

    每一次推进节点,都代表一个元素的真实dom已经排列到了新虚拟dom的响应位置并正确更新了新虚拟dom中对应的数据。

    if (sameVnode(oldStartVnode, newEndVnode)) {
            // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
            api.insertBefore(
              parentElm,
              oldStartVnode.elm!,
              api.nextSibling(oldEndVnode.elm!)
            );
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
          }

    对比旧开始节点与新结束节点是否相等

    如果相等,则调用patchVnode函数

    并将其插入到旧结束节点之后

    然后将旧开始节点往前推进一个,新结束节点往后推进一个

    这里之所以插入旧结束节点之后

    是因为diff是在不断的比对的过程中移动开始和结束的元素的位置

    并且排列元素

    代表除了开始和结束的元素位置以外的元素都已经排序完成了

    我们来大概模拟一下

    图片.png

    匹配成功~

    图片.png

    然后把元素挪过来

    图片.png

    接下来挪动旧的开始坐标和新的结束坐标,缩小范围

    图片.png

    这个时候新旧的结尾范围外的元素,完全是相等的

    图片.png

    注意,这里的修改位置不是指修改虚拟的dom

    而是指根据新旧虚拟dom进行对比,操控真实dom

    从而使真实dom最后与新虚拟dom所有信息一致。

    如果你了解了这里,那么接下来就基本一路通畅了

    如果前三种情况都不符合呢?

    if (sameVnode(oldEndVnode, newStartVnode)) {
            // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
            api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
          }

    接下来对比旧虚拟dom的结尾以及新虚拟dom的开头节点

    如果相等

    则使用patchVnode函数

    然后插入到旧开始节点之前

    然后将旧结束节点向前提前一位

    将新开始节点向后一位。

    如果这四种情况都不符合呢?

            if (oldKeyToIdx === undefined) {
              oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
            }
            idxInOld = oldKeyToIdx[newStartVnode.key as string];

    首先判断oldkeytoidx是否为undefined

    如果为undefined则设置oldkeytoidx

    我们看看createKeyToOldIdx

    function createKeyToOldIdx(
      children: VNode[],
      beginIdx: number,
      endIdx: number
    ): KeyToIndexMap {
      const map: KeyToIndexMap = {};
      for (let i = beginIdx; i <= endIdx; ++i) {
        const key = children[i]?.key;
        if (key !== undefined) {
          map[key as string] = i;
        }
      }
      return map;
    }

    以旧开始节点和旧结束节点为范围

    设置了一个对象

    属性是每个节点的key

    值则是整个数组的节点索引的位置

    idxInOld = oldKeyToIdx[newStartVnode.key as string];

    获取到了oldKeyToIdx之后,则输入新虚拟dom索引位置的节点的key值,得到index

            if (isUndef(idxInOld)) {
              // New element
              api.insertBefore(
                parentElm,
                createElm(newStartVnode, insertedVnodeQueue),
                oldStartVnode.elm!
              );
            } else {
              elmToMove = oldCh[idxInOld];
              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(
                  parentElm,
                  createElm(newStartVnode, insertedVnodeQueue),
                  oldStartVnode.elm!
                );
              } else {
                patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
              }
            }

    首先判断index是否为未定义,如果为未定义,直接在旧虚拟dom开始索引前插入新创建的元素。

    如果存在定义

    elmToMove = oldCh[idxInOld];

    则获取相应的元素

              if (elmToMove.sel !== newStartVnode.sel) {
                api.insertBefore(
                  parentElm,
                  createElm(newStartVnode, insertedVnodeQueue),
                  oldStartVnode.elm!
                );
              }

    如果获取到的元素的sel是否相等,sel相当于css选择器 如span#app这样

    如果不相等,则证明不是一个元素,直接在旧开始节点前插入一个新元素。

    如果相等,则进行以下处理

               patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
                oldCh[idxInOld] = undefined as any;
                api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);

    通过patchVnode来比对差异并修改

    然后设置相应的节点为undefined

    并插入到旧开始节点前。

    在以上操作都结束后

    会执行

    newStartVnode = newCh[++newStartIdx];

    将新虚拟dom节点索引+1

    因为如果不存在,会插入,可以+1

    如果存在,不相等,会新建并插入,可以+1

    如果存在,相等,插入到新位置,然后将旧虚拟dom相应位置设置为undefined,所以旧的不需要+1了。新的位置+1就可以了。

    那么这里我们已经学习完所有的比对了。

    我们总结一下

    旧开头与新开头对比

    旧结尾与新结尾对比

    旧开头与新结尾对比

    旧结尾与新开头对比

    如果都不符合

    则对开始都结尾的map进行查询,是否有对应key的节点

    如果不存在,新建并插入

    如果存在,不相等,新建并插入

    如果存在,相等,旧虚拟dom相应节点索引位置设置为undefined,并插入。

    那么到这里我们已经学习完了核心内容,接下来有一点收尾工作。

        if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
          if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            addVnodes(
              parentElm,
              before,
              newCh,
              newStartIdx,
              newEndIdx,
              insertedVnodeQueue
            );
          } else {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
          }
        }

    判断新开头和新结尾或旧开头旧结尾之间,是否存在剩余

    如果最后处理只剩两个元素

    图片.png

    我们处理完第一个元素,开头和结尾应该重叠了

    图片.png

    这时候如果再处理最后一个元素,也就是第二个元素,尾元素会向前走一个

    也就是说处理完成的特征就是头>尾

    图片.png

    oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx

    这里首先判断了新和旧是否有任意一个头小于等于尾,代表有剩余元素

    if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
            addVnodes(
              parentElm,
              before,
              newCh,
              newStartIdx,
              newEndIdx,
              insertedVnodeQueue
            );
          } else {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
          }

    判断旧头是否大于旧尾,如果大于,代表是新虚拟dom剩余了元素,这时候把剩余元素全部插入到旧尾节点后就好了

    如果是新节点没有剩余,代表我们本次diff已经完成了,删除旧虚拟剩余dom的对应真实dom即可

    图片.png

    这里相当于新虚拟dom剩余了元素

    无论新旧

    前三个节点一定相等,后两个节点一定相等

    同时尾节点在第三个节点上

    这时候插入剩余节点一定会在相应的位置上

    因为左右遍历的时候,会确定出剩余元素应该插入到哪个位置上。

    那么到这里我们的diff章节正式结束

    我几乎把全程每个细节和代码以及理论都讲出来了

    累死了

    结语

    撒花~

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2021-10-9 22:08:59 | 显示全部楼层
    代码太他妈长了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 13:37
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4196

    回帖

    4061

    积分

    管理员

    积分
    4061

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2021-10-9 22:20:17 | 显示全部楼层
    哥哥太硬核了。。。。不过这和油猴有关系么
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2021-10-9 22:27:41 | 显示全部楼层
    王一之 发表于 2021-10-9 22:20
    哥哥太硬核了。。。。不过这和油猴有关系么

    对后期注入Vue有帮助...
    如果连Vue原理都不懂,根本没法找点注入....
    想一边介绍vue基础
    一边渗透vue原理
    不然到时候webpack源码+nodejs环境+vue原理凑一起太爆炸了

    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    开心
    前天 13:37
  • 签到天数: 213 天

    [LV.7]常住居民III

    305

    主题

    4196

    回帖

    4061

    积分

    管理员

    积分
    4061

    管理员荣誉开发者油中2周年生态建设者喜迎中秋油中3周年挑战者 lv2

    发表于 2021-10-9 22:30:39 | 显示全部楼层
    李恒道 发表于 2021-10-9 22:27
    对后期注入Vue有帮助...
    如果连Vue原理都不懂,根本没法找点注入....
    想一边介绍vue基础

    哥哥tql。。。
    上不慕古,下不肖俗。为疏为懒,不敢为狂。为拙为愚,不敢为恶。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2024-10-28 07:07
  • 签到天数: 193 天

    [LV.7]常住居民III

    712

    主题

    5966

    回帖

    6763

    积分

    管理员

    非物质文化遗产社会摇传承人

    积分
    6763

    荣誉开发者喜迎中秋油中2周年生态建设者

    发表于 2021-10-9 23:48:06 | 显示全部楼层

    哥哥太穷了?
    呜呜呜
    哥哥别骂了别骂了
    我学就是了
    混的人。
    ------------------------------------------
    進撃!永遠の帝国の破壊虎---李恒道

    入驻了爱发电https://afdian.net/a/lihengdao666
    个人宣言:この世界で私に胜てる人とコードはまだ生まれていません。死ぬのが怖くなければ来てください。
    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2023-5-5 13:37
  • 签到天数: 15 天

    [LV.4]偶尔看看III

    117

    主题

    405

    回帖

    711

    积分

    版主

    积分
    711

    油中2周年油中3周年

    发表于 2021-10-10 10:34:57 | 显示全部楼层
    李恒道 发表于 2021-10-9 23:48
    哥哥太穷了?
    呜呜呜
    哥哥别骂了别骂了

    哈哈哈哈。穷鬼
    提及少年一词,应与平庸相斥!
    回复

    使用道具 举报

    发表回复

    本版积分规则

    快速回复 返回顶部 返回列表