[记录]我的Diff算法学习路径

Catalog

  1. Vue.js
    1.1 patch 流程
    1.2 updateChildren 大概步骤
    1.3 updateChildren 代码实现
    1.4 两端比对 DEMO
  2. React.js
    2.1 updateChildren 大概步骤
    2.2 updateChildren 代码实现
    2.3 节点移动、新增 、移除 DEMO
  3. Reference

Vue.js Diff 算法

patch 流程

- oldVNode 判断 是否存在
  - oldVNode 存在
    - VNode 判断 是否存在
      - VNode 存在
        - patch
          - sameVnode 判断 key && tag && isComment && isDef(a.data) && sameInputType(a, b) 基本属性是否相同
            - sameVnode 相同
              - diff
                - VNode 判断 是否是文本节点
                  - 是,若 oldVNode.text !== VNode.text 则直接进行文本节点替换
                  - 否,进入子节点 diff
                    - oldCh 不存在,cn 存在,清空 oldVNode 的文本节点,同时调用 addVnodes 方法将 cn 添加到 elm 真实的 dom 节点中
                    - oldCh 节点存在,cn 不存在,则删除 elm 真实节点下的 oldCn 子节点
                    - oldCh 存在,cn 存在,调用 updateChildren 对子节点进行 diff
                    - oldVNode 有文本节点,vnode 没有,那么就清空这个文本节点
            - sameVnode 不相同
              - 删除老的 dom 节点,生成新的 dom
      - VNode 不存在
        - 移除 DOM
  - oldVNode 不存在
    - 挂载 DOM

updateChildren 大概步骤

  1. 建立两组首尾索引和节点变量
  2. 当满足 oldStartIdx <= oldEndIdx 并且 newStartIdx <= newEndIdx 条件时进入循环
    1. 双端比较 => 循环中先进行比较找出 key 相同的新旧节点,根据四种情况判断是否移动 DOM 和增减首尾索引变量以及 patch
    2. 有 key 时 => 当双端匹配没有找到 key 相同的节点,通过当前 newStartVNode 的 key 在 nextChildren 中寻找 key 相同的 VNode
      1. key 相同的 VNode 存在时,对当前节点进行移动 DOM、patch 并将旧 children 中该位置的节点设为 undefined,将 newStartIdx 下移一位
      2. key 相同的 VNode 不存在,则当前“第一个”节点为新节点,将其挂载到 oldStartVNode 之前,将 newStartIdx 下移一位
  3. 挂载全新的节点
    1. 当 oldEndIdx < oldStartIdx, 添加新节点
  4. 移除不存在的节点
    1. 当 newEndIdx < newStartIdx, 移除不存在的元素

updateChildren 代码实现

// 建立两组首尾索引
let oldStartIdx = 0;
let oldEndIdx = prevChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = nextChildren.length - 1;
// 索引对应的VNode
let oldStartVNode = prevChildren[oldStartIdx];
let oldEndVNode = prevChildren[oldEndIdx];
let newStartVNode = nextChildren[newStartIdx];
let newEndVNode = nextChildren[newEndIdx];
// 进行执行双端比较-简略流程版
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
    ...
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对
    ...
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对
    ...
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对
    ...
  }else {
    // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
  }
}
// 进行执行双端比较-详细流程版
// 处理捕获,移动DOM,增减序号
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (!oldStartVNode) {
    // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
    oldStartVNode = prevChildren[++oldStartIdx]
  } else if (!oldEndVNode) {
    // 当旧VNode已被diff移到别的位置时,旧VNode节点为undefined需要跳过当前索引
    oldEndVNode = prevChildren[--oldEndIdx]
  } else if (oldStartVNode.key === newStartVNode.key) {
    // 步骤一:oldStartVNode 和 newStartVNode 比对
    // 调用 patch 函数更新
    patch(oldStartVNode, newStartVNode, container)
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newStartVNode = nextChildren[++newStartIdx]
  } else if (oldEndVNode.key === newEndVNode.key) {
    // 步骤二:oldEndVNode 和 newEndVNode 比对
    // 调用 patch 函数更新
    patch(oldEndVNode, newEndVNode, container)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldStartVNode.key === newEndVNode.key) {
    // 步骤三:oldStartVNode 和 newEndVNode 比对
    // 调用 patch 函数更新
    patch(oldStartVNode, newEndVNode, container)
    // 将 oldStartVNode.el 移动到 oldEndVNode.el 的后面,也就是 oldEndVNode.el.nextSibling 的前面
    container.insertBefore(
      oldStartVNode.el,
      oldEndVNode.el.nextSibling
    )
    // 更新索引,指向下一个位置
    oldStartVNode = prevChildren[++oldStartIdx]
    newEndVNode = nextChildren[--newEndIdx]
  } else if (oldEndVNode.key === newStartVNode.key) {
    // 步骤四:oldEndVNode 和 newStartVNode 比对
    // 先调用 patch 函数完成更新
    patch(oldEndVNode, newStartVNode, container)
    // 更新完成后,将容器中最后一个子节点移动到最前面,使其成为第一个子节点
    container.insertBefore(oldEndVNode.el, oldStartVNode.el)
    // 更新索引,指向下一个位置
    oldEndVNode = prevChildren[--oldEndIdx]
    newStartVNode = nextChildren[++newStartIdx]
  } else {
    // 当双端比对没有匹配节点时,遍历旧 children,试图寻找与 newStartVNode 拥有相同 key 值的元素
    const idxInOld = prevChildren.findIndex(
      node => node.key === newStartVNode.key
    )
    if (idxInOld >= 0) {
      // vnodeToMove 就是在旧 children 中找到的节点,该节点所对应的真实 DOM 应该被移动到最前面
      const vnodeToMove = prevChildren[idxInOld]
      // 调用 patch 函数完成更新
      patch(vnodeToMove, newStartVNode, container)
      // 把 vnodeToMove.el 移动到最前面,即 oldStartVNode.el 的前面
      container.insertBefore(vnodeToMove.el, oldStartVNode.el)
      // 由于旧 children 中该位置的节点所对应的真实 DOM 已经被移动,所以将其设置为 undefined
      prevChildren[idxInOld] = undefined
    } else {
      // 挂载新节点,当双端比对与key查找都不匹配时 newStartVNode 即是新节点
      // newStartVNode 是这轮比较中的“第一个”节点,所以把它挂在到 oldStartVNode 前即可
      mount(newStartVNode, container, false, oldStartVNode.el)
    }
    // 将 newStartIdx 下移一位
    newStartVNode = nextChildren[++newStartIdx]
  }
}
// 挂载没有被处理的全新节点
if (oldEndIdx < oldStartIdx) {
  // 循环结束后当 oldEndIdx < oldStartIdx, 添加新节点
  for (let i = newStartIdx; i <= newEndIdx; i++) {
    mount(nextChildren[i], container, false, oldStartVNode.el)
  }
} else if (newEndIdx < newStartIdx) {
  // 循环结束后当 newEndIdx < newStartIdx, 移除不存在的元素
  for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    container.removeChild(prevChildren[i].el)
  }
}

两端比对 DEMO

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { diff... }

VNode startIndex/endIndex
a b c d 0 / 3
d b a c 0 / 3
next next
a b c x 0 / 2
x b a c 1 / 3
next next
a b x x 0 / 0
x b a x 2 / 2
next next
a x x x 1 / 0
x x a x 1 / 0
next next
x x x x 0 / 0
x x x x 0 / 0
complete complete

两端比对 - 添加新元素 DEMO

VNode startIndex/endIndex
a b 0 / 1
a d b 0 / 2
next next
A b 1 / 1
A d b 1 / 2
next next
A B 1 / 0
A d B 1 / 1
next next
A d B 1 / 0
A d B 1 / 1
complete complete

两端比对 - 添加新元素 DEMO2

VNode startIndex/endIndex
a b c 0 / 2
d a b c 0 / 3
next next
a b C 0 / 1
d a b C 0 / 2
next next
a B C 0 / 0
d a B C 0 / 1
next next
A B C 0 / -1
d A B C 0 / 0
next next
d A B C 0 / -1
d A B C 0 / 0
next next
complete complete

两端比对 - 移除不存在的元素 DEMO

VNode startIndex/endIndex
a b c 0 / 2
a c 0 / 1
next next
A b c 1 / 2
A c 1 / 1
next next
A b C 1 / 1
A C 1 / 0
next next
A C 1 / 1
A C 1 / 0
next next
complete complete

React.js Diff 算法

updateChildren 大概步骤

  1. 初始化最大索引值变量(lastIndex)为 0
  2. 遍历新节点数组,初始化是否匹配标识符(find)为 false
    1. 循环旧节点数组,查找 key 相同的节点
      1. 存在,先进行 patch,后判断节点是否需要移动(j<lastIndex)
        1. 需要移动=>移动 DOM
        2. 不需要移动=>更新 lastIndex 变量为当前索引,连接新节点到对应 DOM 上
      2. 不存在,则为新节点,需要挂载 DOM 到上一个新节点的后面
    2. 处理已经不存在的节点
      1. 遍历旧节点数组,移除新节点数组中不存在的节点

updateChildren 代码实现

// 用来存储寻找过程中遇到的旧节点数组中最大索引值
let lastIndex = 0;
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i];
  let j = 0,
    find = false;
  // 遍历旧的 children
  for (j; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j];
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      find = true;
      patch(prevVNode, nextVNode, container);
      // 判断节点是否需要移动
      if (j < lastIndex) {
        // 需要移动
        // refNode 是为了下面调用 insertBefore 函数准备的
        const refNode = nextChildren[i - 1].el.nextSibling;
        // 调用 insertBefor 函数移动 DOM,将当前节点的el 移动到 refNode前
        container.insertBefore(prevVNode.el, refNode);
      } else {
        // 更新 lastIndex
        lastIndex = j;
        // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
        const el = (nextVNode.el = prevVNode.el);
      }
      break; // 这里需要 break
    }
    // 挂载新节点
    if (!find) {
      // 找到 refNode
      const refNode =
        i - 1 < 0 ? prevChildren[0].el : nextChildren[i - 1].el.nextSibling;
      mount(nextVNode, container, false, refNode);
    }
  }
  // 移除已经不存在的节点
  // 遍历旧的节点
  for (let i = 0; i < prevChildren.length; i++) {
    const prevVNode = prevChildren[i];
    // 拿着旧 VNode 去新 children 中寻找相同的节点
    const has = nextChildren.find(
      (nextVNode) => nextVNode.key === prevVNode.key
    );
    if (!has) {
      // 如果没有找到相同的节点,则移除
      container.removeChild(prevVNode.el);
    }
  }
}

节点移动、新增、移除 DEMO

VNode lastIndex 是否需要移动 DOM 是否新增 DOM 是否移除 DOM
a b d / / / /
a d c 0 n n n
next d next next next
a b d / / / /
a d c 2 n n n
next c next next next
a b d / / / /
a d c 2 n y n
next 移除旧 DOM next next next
a b d c / / / y
a d c / / / /
next next next next next
a b c / / / /
a d c / / / /
complete complete complete complete complete

Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354