React實(shí)現(xiàn)核心Diff算法的示例代碼
Diff算法的設(shè)計(jì)思路
試想,Diff
算法需要考慮多少種情況呢?大體分三種,分別是:
節(jié)點(diǎn)屬性變化,比如:
// 更新前 <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é)點(diǎn)增刪,比如:
// 更新前 <ul> <li key="0">0</li> <li key="1">1</li> <li key="2">2</li> </ul> // 更新后 情況1 —— 新增節(jié)點(diǎn) <ul> <li key="0">0</li> <li key="1">1</li> <li key="2">2</li> <li key="3">3</li> </ul> // 更新后 情況2 —— 刪除節(jié)點(diǎn) <ul> <li key="0">0</li> <li key="1">1</li> </ul>
節(jié)點(diǎn)移動(dòng),比如:
// 更新前 <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è)計(jì)Diff
算法呢?考慮到只有以上三種情況,一種常見的設(shè)計(jì)思路是:
- 首先判斷當(dāng)前節(jié)點(diǎn)屬于哪種情況
- 如果是增刪,執(zhí)行增刪邏輯
- 如果是屬性變化,執(zhí)行屬性變化邏輯
- 如果是移動(dòng),執(zhí)行移動(dòng)邏輯
按這個(gè)方案,其實(shí)有個(gè)隱含的前提—— 不同操作的優(yōu)先級(jí)是相同的。但在日常開發(fā)中,節(jié)點(diǎn)移動(dòng)發(fā)生較少,所以Diff
算法會(huì)優(yōu)先判斷其他情況。
基于這個(gè)理念,主流框架(React、Vue)的Diff
算法都會(huì)經(jīng)歷多輪遍歷,先處理常見情況,后處理不常見情況。
所以,這就要求處理不常見情況的算法需要能給各種邊界case
兜底。
換句話說,完全可以僅使用處理不常見情況的算法完成Diff
操作。主流框架之所以沒這么做是為了性能考慮。
本文會(huì)砍掉處理常見情況的算法,保留處理不常見情況的算法。
這樣,只需要40行代碼就能實(shí)現(xiàn)Diff
的核心邏輯。
Demo介紹
首先,我們定義虛擬DOM
節(jié)點(diǎn)的數(shù)據(jù)結(jié)構(gòu):
type Flag = 'Placement' | 'Deletion'; interface Node { key: string; flag?: Flag; index?: number; }
key
是node
的唯一標(biāo)識(shí),用于將節(jié)點(diǎn)在變化前、變化后關(guān)聯(lián)上。
flag
代表node
經(jīng)過Diff
后,需要對(duì)相應(yīng)的真實(shí)DOM
執(zhí)行的操作,其中:
Placement
對(duì)于新生成的node
,代表對(duì)應(yīng)DOM
需要插入到頁面中。對(duì)于已有的node
,代表對(duì)應(yīng)DOM
需要在頁面中移動(dòng)Deletion
代表node
對(duì)應(yīng)DOM
需要從頁面中刪除
index
代表該node
在同級(jí)node
中的索引位置
注:本Demo
僅實(shí)現(xiàn)為node標(biāo)記flag,沒有實(shí)現(xiàn)根據(jù)flag執(zhí)行DOM操作。
我們希望實(shí)現(xiàn)的diff
方法,接收更新前
與更新后
的NodeList
,為他們標(biāo)記flag
:
type NodeList = Node[]; function diff(before: NodeList, after: NodeList): NodeList { // ...代碼 }
比如對(duì)于:
// 更新前 const before = [ {key: 'a'} ] // 更新后 const after = [ {key: 'd'} ] // diff(before, after) 輸出 [ {key: "d", flag: "Placement"}, {key: "a", flag: "Deletion"} ]
{key: "d", flag: "Placement"}
代表d對(duì)應(yīng)DOM
需要插入頁面。
{key: "a", flag: "Deletion"}
代表a對(duì)應(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對(duì)應(yīng)DOM
需要向后移動(dòng)(對(duì)應(yīng)parentNode.appendChild
方法)。abc
經(jīng)過該操作后變?yōu)?code>acb。
由于a
之前已經(jīng)存在,{key: "a", flag: "Placement"}
代表a對(duì)應(yīng)DOM
需要向后移動(dòng)。acb
經(jīng)過該操作后變?yōu)?code>cba。
執(zhí)行后的結(jié)果就是:頁面中的abc變?yōu)閏ba。
Diff算法實(shí)現(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
中每個(gè)node
保存在以node.key
為key
,node
為value
的Map
中。
這樣,以O(1)
復(fù)雜度就能通過key
找到before
中對(duì)應(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
時(shí),如果一個(gè)node
同時(shí)存在于before
與after
(key
相同),我們稱這個(gè)node
可復(fù)用。
比如,對(duì)于如下例子,b是可復(fù)用的:
// 更新前 const before = [ {key: 'a'}, {key: 'b'} ] // 更新后 const after = [ {key: 'b'} ]
對(duì)于可復(fù)用的node
,本次更新一定屬于以下兩種情況之一:
- 不移動(dòng)
- 移動(dòng)
如何判斷可復(fù)用的node
是否移動(dòng)呢?
我們用lastPlacedIndex
變量保存遍歷到的最后一個(gè)可復(fù)用node在before中的index:
// 遍歷到的最后一個(gè)可復(fù)用node在before中的index let lastPlacedIndex = 0;
當(dāng)遍歷after
時(shí),每輪遍歷到的node
,一定是當(dāng)前遍歷到的所有node
中最靠右的那個(gè)。
如果這個(gè)node
是可復(fù)用的node
,那么nodeBefore
與lastPlacedIndex
存在兩種關(guān)系:
注:
nodeBefore
代表該可復(fù)用的node
在before
中的對(duì)應(yīng)node
nodeBefore.index < lastPlacedIndex
代表更新前該node
在lastPlacedIndex對(duì)應(yīng)node
左邊。
而更新后該node
不在lastPlacedIndex對(duì)應(yīng)node
左邊(因?yàn)樗?strong>當(dāng)前遍歷到的所有node中最靠右的那個(gè))。
這就代表該node
向右移動(dòng)了,需要標(biāo)記Placement
。
nodeBefore.index >= lastPlacedIndex
該node
在原地,不需要移動(dòng)。
// 遍歷到的最后一個(gè)可復(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) { // 移動(dòng) afterNode.flag = 'Placement'; result.push(afterNode); continue; } else { // 不移動(dòng) lastPlacedIndex = oldIndex; } } else { // 不存在可復(fù)用node,這是一個(gè)新節(jié)點(diǎn) 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é)
整個(gè)Diff
算法的難點(diǎn)在于lastPlacedIndex
相關(guān)邏輯。
跟著Demo
多調(diào)試幾遍,相信你能明白其中原理。
到此這篇關(guān)于React實(shí)現(xiàn)核心Diff算法的示例代碼的文章就介紹到這了,更多相關(guān)React Diff算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react?路由權(quán)限動(dòng)態(tài)菜單方案配置react-router-auth-plus
這篇文章主要為大家介紹了react路由權(quán)限動(dòng)態(tài)菜單方案react-router-auth-plus傻瓜式配置詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08React?Hooks--useEffect代替常用生命周期函數(shù)方式
這篇文章主要介紹了React?Hooks--useEffect代替常用生命周期函數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09使用React實(shí)現(xiàn)一個(gè)簡單的待辦任務(wù)列表
這篇文章主要給大家介紹了使用React和Ant Design庫構(gòu)建的待辦任務(wù)列表應(yīng)用,它包含了可編輯的表格,用戶可以添加、編輯和完成任務(wù),以及保存任務(wù)列表數(shù)據(jù)到本地存儲(chǔ),文中有相關(guān)的代碼示例,需要的朋友可以參考下2023-08-08React、Vue中key的作用詳解 (key的內(nèi)部原理解析)
key是虛擬DOM對(duì)象的標(biāo)識(shí),當(dāng)狀態(tài)中的數(shù)據(jù)發(fā)生變化時(shí),Vue會(huì)根據(jù)[新數(shù)據(jù)]生成[新的虛擬DOM],本文給大家介紹React、Vue中key的作用詳解 (key的內(nèi)部原理解析),感興趣的朋友一起看看吧2023-10-10Objects are not valid as a Rea
這篇文章主要為大家介紹了Objects are not valid as a React child報(bào)錯(cuò)解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12