網(wǎng)站首頁 編程語言 正文
Diff算法的設(shè)計思路
試想,Diff
算法需要考慮多少種情況呢?大體分三種,分別是:
節(jié)點屬性變化,比如:
// 更新前 <ul> <li key="0" className="before">0</li> <li key="1">1</li> </ul> // 更新后 <ul> <li key="0" className="after">0</li> <li key="1">1</li> </ul>
節(jié)點增刪,比如:
// 更新前 <ul> <li key="0">0</li> <li key="1">1</li> <li key="2">2</li> </ul> // 更新后 情況1 —— 新增節(jié)點 <ul> <li key="0">0</li> <li key="1">1</li> <li key="2">2</li> <li key="3">3</li> </ul> // 更新后 情況2 —— 刪除節(jié)點 <ul> <li key="0">0</li> <li key="1">1</li> </ul>
節(jié)點移動,比如:
// 更新前 <ul> <li key="0">0</li> <li key="1">1</li> </ul> // 更新后 <ul> <li key="1">1</li> <li key="0">0</li> </ul>
該如何設(shè)計Diff
算法呢?考慮到只有以上三種情況,一種常見的設(shè)計思路是:
- 首先判斷當(dāng)前節(jié)點屬于哪種情況
- 如果是增刪,執(zhí)行增刪邏輯
- 如果是屬性變化,執(zhí)行屬性變化邏輯
- 如果是移動,執(zhí)行移動邏輯
按這個方案,其實有個隱含的前提—— 不同操作的優(yōu)先級是相同的。但在日常開發(fā)中,節(jié)點移動發(fā)生較少,所以Diff
算法會優(yōu)先判斷其他情況。
基于這個理念,主流框架(React、Vue)的Diff
算法都會經(jīng)歷多輪遍歷,先處理常見情況,后處理不常見情況。
所以,這就要求處理不常見情況的算法需要能給各種邊界case
兜底。
換句話說,完全可以僅使用處理不常見情況的算法完成Diff
操作。主流框架之所以沒這么做是為了性能考慮。
本文會砍掉處理常見情況的算法,保留處理不常見情況的算法。
這樣,只需要40行代碼就能實現(xiàn)Diff
的核心邏輯。
Demo介紹
首先,我們定義虛擬DOM
節(jié)點的數(shù)據(jù)結(jié)構(gòu):
type Flag = 'Placement' | 'Deletion'; interface Node { key: string; flag?: Flag; index?: number; }
key
是node
的唯一標(biāo)識,用于將節(jié)點在變化前、變化后關(guān)聯(lián)上。
flag
代表node
經(jīng)過Diff
后,需要對相應(yīng)的真實DOM
執(zhí)行的操作,其中:
-
Placement
對于新生成的node
,代表對應(yīng)DOM
需要插入到頁面中。對于已有的node
,代表對應(yīng)DOM
需要在頁面中移動 -
Deletion
代表node
對應(yīng)DOM
需要從頁面中刪除
index
代表該node
在同級node
中的索引位置
注:本Demo
僅實現(xiàn)為node標(biāo)記flag,沒有實現(xiàn)根據(jù)flag執(zhí)行DOM操作。
我們希望實現(xiàn)的diff
方法,接收更新前
與更新后
的NodeList
,為他們標(biāo)記flag
:
type NodeList = Node[]; function diff(before: NodeList, after: NodeList): NodeList { // ...代碼 }
比如對于:
// 更新前 const before = [ {key: 'a'} ] // 更新后 const after = [ {key: 'd'} ] // diff(before, after) 輸出 [ {key: "d", flag: "Placement"}, {key: "a", flag: "Deletion"} ]
{key: "d", flag: "Placement"}
代表d對應(yīng)DOM
需要插入頁面。
{key: "a", flag: "Deletion"}
代表a對應(yīng)DOM
需要被刪除。
執(zhí)行后的結(jié)果就是:頁面中的a變?yōu)閐。
再比如:
// 更新前 const before = [ {key: 'a'}, {key: 'b'}, {key: 'c'}, ] // 更新后 const after = [ {key: 'c'}, {key: 'b'}, {key: 'a'} ] // diff(before, after) 輸出 [ {key: "b", flag: "Placement"}, {key: "a", flag: "Placement"} ]
由于b
之前已經(jīng)存在,{key: "b", flag: "Placement"}
代表b對應(yīng)DOM
需要向后移動(對應(yīng)parentNode.appendChild
方法)。abc
經(jīng)過該操作后變?yōu)?code>acb。
由于a
之前已經(jīng)存在,{key: "a", flag: "Placement"}
代表a對應(yīng)DOM
需要向后移動。acb
經(jīng)過該操作后變?yōu)?code>cba。
執(zhí)行后的結(jié)果就是:頁面中的abc變?yōu)閏ba。
Diff算法實現(xiàn)
核心邏輯包括三步:
- 遍歷前的準(zhǔn)備工作
- 遍歷
after
- 遍歷后的收尾工作
function diff(before: NodeList, after: NodeList): NodeList { const result: NodeList = []; // ...遍歷前的準(zhǔn)備工作 for (let i = 0; i < after.length; i++) { // ...核心遍歷邏輯 } // ...遍歷后的收尾工作 return result; }
遍歷前的準(zhǔn)備工作
我們將before
中每個node
保存在以node.key
為key
,node
為value
的Map
中。
這樣,以O(1)
復(fù)雜度就能通過key
找到before
中對應(yīng)node
:
// 保存結(jié)果 const result: NodeList = []; // 將before保存在map中 const beforeMap = new Map<string, Node>(); before.forEach((node, i) => { node.index = i; beforeMap.set(node.key, node); })
遍歷after
當(dāng)遍歷after
時,如果一個node
同時存在于before
與after
(key
相同),我們稱這個node
可復(fù)用。
比如,對于如下例子,b是可復(fù)用的:
// 更新前 const before = [ {key: 'a'}, {key: 'b'} ] // 更新后 const after = [ {key: 'b'} ]
對于可復(fù)用的node
,本次更新一定屬于以下兩種情況之一:
- 不移動
- 移動
如何判斷可復(fù)用的node
是否移動呢?
我們用lastPlacedIndex
變量保存遍歷到的最后一個可復(fù)用node在before中的index:
// 遍歷到的最后一個可復(fù)用node在before中的index let lastPlacedIndex = 0;
當(dāng)遍歷after
時,每輪遍歷到的node
,一定是當(dāng)前遍歷到的所有node
中最靠右的那個。
如果這個node
是可復(fù)用的node
,那么nodeBefore
與lastPlacedIndex
存在兩種關(guān)系:
注:
nodeBefore
代表該可復(fù)用的node
在before
中的對應(yīng)node
nodeBefore.index < lastPlacedIndex
代表更新前該node
在lastPlacedIndex對應(yīng)node
左邊。
而更新后該node
不在lastPlacedIndex對應(yīng)node
左邊(因為他是當(dāng)前遍歷到的所有node中最靠右的那個)。
這就代表該node
向右移動了,需要標(biāo)記Placement
。
nodeBefore.index >= lastPlacedIndex
該node
在原地,不需要移動。
// 遍歷到的最后一個可復(fù)用node在before中的index let lastPlacedIndex = 0; for (let i = 0; i < after.length; i++) { const afterNode = after[i]; afterNode.index = i; const beforeNode = beforeMap.get(afterNode.key); if (beforeNode) { // 存在可復(fù)用node // 從map中剔除該 可復(fù)用node beforeMap.delete(beforeNode.key); const oldIndex = beforeNode.index as number; // 核心判斷邏輯 if (oldIndex < lastPlacedIndex) { // 移動 afterNode.flag = 'Placement'; result.push(afterNode); continue; } else { // 不移動 lastPlacedIndex = oldIndex; } } else { // 不存在可復(fù)用node,這是一個新節(jié)點 afterNode.flag = 'Placement'; result.push(afterNode); }
遍歷后的收尾工作
經(jīng)過遍歷,如果beforeMap
中還剩下node
,代表這些node
沒法復(fù)用,需要被標(biāo)記刪除。
比如如下情況,遍歷完after
后,beforeMap
中還剩下{key: 'a'}
:
// 更新前 const before = [ {key: 'a'}, {key: 'b'} ] // 更新后 const after = [ {key: 'b'} ]
這意味著a
需要被標(biāo)記刪除。
所以,最后還需要加入標(biāo)記刪除的邏輯:
beforeMap.forEach(node => { node.flag = 'Deletion'; result.push(node); });
完整代碼見在線Demo地址
總結(jié)
整個Diff
算法的難點在于lastPlacedIndex
相關(guān)邏輯。
跟著Demo
多調(diào)試幾遍,相信你能明白其中原理。
原文鏈接:https://juejin.cn/post/7086634898953338911
相關(guān)推薦
- 2022-07-31 Android?中的類文件和類加載器詳情_Android
- 2022-06-08 Python使用PyYAML庫讀寫yaml文件的方法_python
- 2022-09-07 如何應(yīng)用?SOLID?原則在?React?中整理代碼之開閉原則_React
- 2022-10-29 關(guān)于torch.load加載預(yù)訓(xùn)練模型時 造成的 臨時分配的顯存 不釋放
- 2022-10-10 VMware?Workstation與Device/Credential?Guard不兼容的解決_V
- 2022-07-20 centos 安裝jenkins 實現(xiàn)自動部署到遠程服務(wù)器 (樹莓派可用)
- 2022-03-04 element-ui 固定彈窗底部的按鈕
- 2022-07-01 python讀取nc數(shù)據(jù)并繪圖的方法實例_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支