react?diff?算法實現(xiàn)思路及原理解析
前面幾節(jié)我們學(xué)習(xí)了解了 react 的渲染機制和生命周期,本節(jié)我們正式進入基本面試必考的核心地帶 -- diff 算法,了解如何優(yōu)化和復(fù)用 dom 操作的,還有我們常見的 key 的作用。
diff算法使用在子都是數(shù)組的情況下,這點和vue是一樣的。如果元素是其他類型的話直接替換就好。
事例分析
按照之前的 diff 寫法,如果元素不同我們是直接刪了 a 再插入的:

按照上面圖的結(jié)構(gòu),我們需要知道那個元素變化了,其實右邊相對左邊只是把 A 做了移動,沒有 dom 元素的刪除和新增。
diff 特點
- 同級對比
On - 類型不一樣銷毀老的,創(chuàng)建新的
- 通過
key標(biāo)識
key這里需要標(biāo)識,主要是為了列表中有刪除新增時有優(yōu)化效果,如果純靜態(tài)列表,只是展示作用,key意義不大。
diff 思路
- 使用
map存儲節(jié)點狀態(tài),格式如下:
let map = {
keyA: ADOM,
keyB: BDOM
}- 定義 lastPlacedIndex 記錄上一個不需要移動的老節(jié)點
默認(rèn) lastPlacedIndex = 0 ,上一個不需要移動的節(jié)點,在循環(huán)新的子虛擬 dom 時,如果老節(jié)點的掛載索引小于當(dāng)前值,則改變 lastPlacedIndex。這里有點類似 vue 的最長遞增子序列,最大的保證不變的 dom 元素,只是判斷方式不同。
- 循環(huán)新數(shù)組
- 先出
A,map中如果有A,表示可以復(fù)用- 判斷
A的老掛載索引和lastPlacedIndex對比,如果索引值大,A節(jié)點不需要移動,更新lastPlacedIndex的值;否則循環(huán)到B,掛載索引小,需要移動B;循環(huán)到G,map中沒有值,需要新增;新的數(shù)組節(jié)點循環(huán)完,未用到的老節(jié)點全部刪除。
- 判斷

實現(xiàn) diff 算法
修改入口文件
// src/index.js
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = {list: ['A','B', 'C', 'D', 'E', 'F']}
}
handleClick = () => {
this.setState({
list: ['A', 'C', 'E', 'B', 'G']
})
}
render() {
// 使用空標(biāo)簽
return <React.Fragment>
<ul>
{this.state.list.map(item => {
// 這里使用 key 標(biāo)識
return <li key={item}>{item}</li>
})}
</ul>
<button onClick={this.handleClick}>add 1</button>
</React.Fragment>
}
}實現(xiàn) React.Fragment
Fragment就是代碼片段,不占用dom結(jié)構(gòu)。簡寫<></>,對應(yīng)dom操作為createDocumentFragment。
- 是用原生庫打印,看結(jié)構(gòu)

可以發(fā)現(xiàn)就是一個簡單的 Symbol,所以需要定義新的類型:
為什么一個簡單的
Symbol可以被渲染成片段呢?依賴于babel解析。
// src/constants.js
export const REACT_FRAGMENT = Symbol("react.fragment") // React.Fragment 標(biāo)簽
// 備用,diff 時做 patch 的 type 定義
// 新的插入
export const PLACEMENT = 'PLACEMENT'
// 復(fù)用的移動
export const MOVE = 'MOVE'在創(chuàng)建元素的時候進行類型判斷,記得 react.js 中導(dǎo)出
// src/react-dom.js
// createDOM 方法
else if (type === REACT_FRAGMENT) {
// fragment 片段
dom = document.createDocumentFragment()
}
// updateElement 方法
else if (oldVdom.type === REACT_FRAGMENT) {
// fragment 不需要對比,直接對比 子 就可以了
const currentDOM = newVdom.dom = findDOM(oldVdom)
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)
}我們需要修改 children 對比
之前邏輯:
// src/react-dom.js
// diff 沒有做復(fù)用,直接做的替換
function updateChildren(parentDOM, oldVChildren, newVChildren) {
// 拿到最長的
let maxLength = Math.max(oldVChildren.length, newVChildren.length);
for (let i = 0; i < maxLength; i++) {
// 不能直接 appendChild 進父,需要找到當(dāng)前操作的節(jié)點的下一個節(jié)點。在其前面插入
const nextVdom = oldVChildren.find((item, index) => index > i && item && findDOM(item))
compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom));
}
}新的邏輯(參考上面的流程):
// diff
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren];
newVChildren = Array.isArray(newVChildren) ? newVChildren : [newVChildren];
// 1.循環(huán)老結(jié)構(gòu), 構(gòu)建map存儲 key: dom
const keydOldMap = {}
let lastPlacedIndex = 0
oldVChildren.forEach((oldVChild, index) => {
let oldKey = oldVChild?.key || index // 寫key 了就用key,沒寫默認(rèn) index
keydOldMap[oldKey] = oldVChild
})
// 2. 創(chuàng)建 dom 補丁包,收集 dom 操作
const patch = []
newVChildren.forEach((newVChild, index) => {
newVChild.mountIndex = index // 為新元素每個添加索引標(biāo)識
const newKey = newVChild?.key || index
const oldVChild = keydOldMap[newKey] // 看有沒有存
if(oldVChild) {
// 如果有老的,就去更新老節(jié)點 這里直接可以復(fù)用
updateElement(findDOM(oldVChild).parentNode, oldVChild, newVChild)
if(oldVChild.mountIndex < lastPlacedIndex) {
patch.push({
type: MOVE,
oldVChild,
newVChild,
mountIndex: index // 舊的移動到新的的位置
})
}
// 復(fù)用過了 刪除掉
delete keydOldMap[newKey]
lastPlacedIndex = Math.max(lastPlacedIndex, oldVChild.mountIndex)// 取最大
} else {
// 新的
patch.push({
type: PLACEMENT,
newVChild,
mountIndex: index
})
}
})
// 找到需要移動的老節(jié)點
const moveVChildren = patch.filter(action => action.type === MOVE).map(action => action.oldVChild)
// 把要刪除的節(jié)點 和 要移動的節(jié)點先全刪除 (頁面里沒有了,但是內(nèi)存中還存在 patch 中有存)
Object.values(keydOldMap).concat(moveVChildren).forEach(oldVdom => {
let currentDOM = findDOM(oldVdom)
currentDOM.remove()
})
patch.forEach(action => {
const {type, oldVChild, newVChild, mountIndex} = action
// 老的真實子節(jié)點
const childNodes = parentDOM.childNodes
// 新的插入
if (type === PLACEMENT) {
let newDOM = createDOM(newVChild)
let childNode = childNodes[mountIndex] // 老真實節(jié)點
if (childNode) {
// 往 老的父對應(yīng)位置插入
parentDOM.insertBefore(newDOM, childNode)
} else {
parentDOM.appendChild(newDOM)
}
} else if (type === MOVE) {
// 移動不用創(chuàng)建 新 dom,復(fù)用
let oldDOM = findDOM(oldVChild)
let childNode = childNodes[mountIndex] // 老真實節(jié)點
if (childNode) {
// 往 老的父對應(yīng)位置插入
parentDOM.insertBefore(oldDOM, childNode)
} else {
parentDOM.appendChild(oldDOM)
}
}
})
}實現(xiàn)如下跟原生一致,可以看到,三個節(jié)點實現(xiàn)了復(fù)用,即 A, C, E

如果沒有寫 key,我們在看效果:

可以看到只有第一個節(jié)點實現(xiàn)了復(fù)用,因為默認(rèn)索引都使用的 0。所以這也是為什么不建議我們使用索引當(dāng) key 的原因。動態(tài)列表 key 意義不大。
本節(jié)代碼不是很多,主要是 diff 算法的思路和實現(xiàn)原理。如果了解了 vue 的 diff 算法,相信理解起來更好,也能更好的對比。下一小節(jié)我們學(xué)習(xí)下 react 新的生命周期。
到此這篇關(guān)于react diff 算法實現(xiàn)思路及原理解析的文章就介紹到這了,更多相關(guān)react diff 算法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React?Hook?Form?優(yōu)雅處理表單使用指南
這篇文章主要為大家介紹了React?Hook?Form?優(yōu)雅處理表單使用指南,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
React Router 中實現(xiàn)嵌套路由和動態(tài)路由的示例
React Router 是一個非常強大和靈活的路由庫,它為 React 應(yīng)用程序提供了豐富的導(dǎo)航和 URL 管理功能,能夠幫助我們構(gòu)建復(fù)雜的單頁應(yīng)用和多頁應(yīng)用,這篇文章主要介紹了React Router 中如何實現(xiàn)嵌套路由和動態(tài)路由,需要的朋友可以參考下2023-05-05
使用VScode 插件debugger for chrome 調(diào)試react源碼的方法
這篇文章主要介紹了使用VScode 插件debugger for chrome 調(diào)試react源碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
react搭建在線編輯html的站點通過引入grapes實現(xiàn)在線拖拉拽編輯html
Grapes插件是一種用于Web開發(fā)的開源工具,可以幫助用戶快速創(chuàng)建動態(tài)和交互式的網(wǎng)頁元素,它還支持多語言和多瀏覽器,適合開發(fā)響應(yīng)式網(wǎng)頁和移動應(yīng)用程序,這篇文章主要介紹了react搭建在線編輯html的站點通過引入grapes實現(xiàn)在線拖拉拽編輯html,需要的朋友可以參考下2023-08-08
React實現(xiàn)生成和導(dǎo)出Word文檔的方法詳解
React是一個流行的JavaScript庫,用于構(gòu)建現(xiàn)代前端應(yīng)用程序,本文將深入探討如何在React中生成和導(dǎo)出Word文檔,感興趣的小伙伴可以學(xué)習(xí)一下2023-09-09
React語法中設(shè)置TS校驗規(guī)則的步驟詳解
這篇文章主要給大家介紹了React語法中如何設(shè)置TS校驗規(guī)則,文中有詳細(xì)的代碼示例供大家參考,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10

