這是個人剖析 React 源碼的第六篇文章。這篇文章鏈接上篇,將會帶着你們學習組件更新過程相關的內容,而且儘量的脫離源碼來了解原理,下降你們的學習難度。html
文章分爲三部分,在這部分的文章中你能夠學習到以下內容:react
三篇文章並無強相關性,固然仍是推薦閱讀下 前一篇文章。git
組件更新歸結到底仍是 DOM 的更新。對於 React 來講,這部分的內容會分爲兩個階段:github
這一小節的內容將會集中在調和階段,提交階段這部分的內容將會在下一篇文章中寫到。另外你們所熟知的虛擬 DOM 的 diff 算法在新版本中其實已經徹底被重寫了。算法
這一小節的內容會有點難度,若是你以爲難以讀懂個人文章或者是別的問題,歡迎在下方評論區與我互動! 數據結構
有個例子能更好地幫助理解,咱們就經過如下組件的更新來了解整個調和的過程。函數
class Test extends React.Component {
state = {
data: [{ key: 1, value: 1 }, { key: 2, value: 2 }]
};
componentDidMount() {
setTimeout(() => {
const data = [{ key: 0, value: 0 }, { key: 2, value: 2 }]
this.setState({
data
})
}, 3000);
}
render() {
const { data } = this.state;
return (
<> { data.map(item => <p key={item.key}>{item.value}</p>) } </> ) } } 複製代碼
在前一篇文章中咱們瞭解到了整個更新過程(不包括渲染)就是在反覆尋找工做單元並運行它們,那麼具體體現到代碼中是怎麼樣的呢?學習
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
複製代碼
上述代碼的 while
循環只有當找不到工做單元或者應該打斷的時候纔會終止。找不到工做單元的狀況只有當循環完全部工做單元纔會觸發,打斷的狀況是調度器觸發的。動畫
當更新任務開始時,root
永遠是第一個工做單元,不管以前有沒有被打斷過工做。this
循環尋找工做單元的這個流程其實很簡單,就是自頂向下再向上的一個循環。這個循環的規則以下:
root
永遠是第一個工做單元,無論以前有沒有被打斷過任務如下動畫是例子代碼的工做循環過程的一個示例:
瞭解了工做循環流程之後,咱們就來深刻學習一下工做單元是如何工做的。爲了精簡流程,咱們就直接認爲當前的工做單元爲 Test
組件實例。
在工做單元工做的這一階段中實際上是分爲不少分支的,由於涉及到不一樣類型組件及 DOM 的處理。Test
是 class
組件,另外這也是最經常使用的組件類型,所以接下來的內容會着重介紹 class
組件的調和過程。
class
組件的調和過程大體分爲兩個部分:
class
組件生命週期函數最早被處理的生命週期函數是 componentWillReceiveProps
。
可是觸發這個函數的條件有兩個:
props
先後有差異getDerivedStateFromProps
或者 getSnapshotBeforeUpdate
這兩個新的生命週期函數。使用其一則 componentWillReceiveProps
不會被觸發知足以上條件該函數就會被調用。所以該函數在 React 16 中已經不被建議使用。由於調和階段是有可能會打斷的,所以該函數會重複調用。
凡是在調和階段被調用的函數基本是不被建議使用的。
接下來須要處理 getDerivedStateFromProps
函數來獲取最新的 state
。
而後就是判斷是否須要更新組件了,這一塊的判斷邏輯分爲兩塊:
shouldComponentUpdate
函數,存在就調用PureComponent
。若是是的話,就淺比較先後的 props
及 state
得出結果若是得出結論須要更新組件的話,那麼就會先調用 componentWillUpdate
函數,而後處理 componentDidUpdate
及 getSnapshotBeforeUpdate
函數。
這裏須要注意的是:調和階段並不會調用以上兩個函數,而是打上 tag 以便未來使用位運算知曉是否須要使用它們。effectTag
這個屬性在整個更新的流程中都是相當重要的一員,凡是涉及到函數的延遲調用、devTool 的處理、DOM 的更新均可能會使用到它。
if (typeof instance.componentDidUpdate === 'function') {
workInProgress.effectTag |= Update;
}
if (typeof instance.getSnapshotBeforeUpdate === 'function') {
workInProgress.effectTag |= Snapshot;
}
複製代碼
處理完生命週期後,就會調用 render
函數獲取新的 child
,用於在以後與老的 child
進行對比。
在繼續學習以前咱們先來熟悉三個對象,由於它們在後續的內容中會反覆出現:
child
。若是你還記得 fiber 的數據結構的話,應該知道每一個 fiber 都有一個 sibling
屬性指向它的兄弟節點。所以知道第一個子節點就能知道全部的同級節點。render
出來的內容。首先咱們會判斷 newChild
的類型,知道類型就能夠進行相應的 diff 策略了。它可能會是一個 Fragment 類型,也多是 object
、number
或者 string
類型。這幾個類型都會有相應的處理,但這不是咱們的重點,而且它們的處理也至關簡單。
咱們的重點會放在可迭代類型上,也就是 Array
或者 Iterator
類型。這二者的核心邏輯是一致的,所以咱們就只講對 Array
類型的處理了。
如下內容是對於 diff 算法的詳解,雖然有三次 for
循環,可是本質上只是遍歷了一次整個 newChild
。
第一輪遍歷的核心邏輯是複用和當前節點索引一致的老節點,一旦出現不能複用的狀況就跳出遍歷。
那麼如何複用以前的節點呢?規則以下:
如下是我簡化後的第一輪遍歷代碼:
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 找到下一個老的子節點
nextOldFiber = oldFiber.sibling;
// 經過 oldFiber 和 newChildren[newIdx] 判斷是否能夠複用
// 並給複用出來的節點的 return 屬性賦值 returnFiber
const newFiber = reuse(
returnFiber,
oldFiber,
newChildren[newIdx]
);
// 不能複用,跳出
if (newFiber === null) {
break;
}
}
複製代碼
那麼回到上文中的例子中,咱們老的第一個節點的 key 爲 1,新的節點的 key 爲 0。key 不相同不能複用,所以直接跳出循環,此時 newIdx
仍 爲 0。
當第一輪遍歷結束後,會出現兩種狀況:
newChild
已經遍歷完當出現 newChild
已經遍歷完的狀況時只須要把全部剩餘的老節點都刪除便可。刪除的邏輯也就是設置 effectTag
爲 Deletion
,另外還有幾個 fiber 節點屬性須要說起下。
當出現須要在渲染階段進行處理的節點時,會把這些節點放入父節點的 effect
鏈表中,好比須要被刪除的節點就會把加入進鏈表。這個鏈表的做用是能夠幫助咱們在渲染階段迅速找到須要更新的節點。
當出現老的節點已經遍歷完了的狀況時,就會開始第二輪遍歷。這輪遍歷的邏輯很簡單,只須要把剩餘新的節點所有建立完畢便可。
這輪遍歷在咱們的例子中是不會執行的,由於咱們以上兩種狀況都不符合。
第三輪遍歷的核心邏輯是找出能夠複用的老節點並移動位置,不能複用的話就只能建立一個新的了。
那麼問題又再次回到瞭如何複用節點並移動位置上。首先咱們會把全部剩餘的老節點都丟到一個 map
中。
咱們例子中的代碼剩餘的老節點爲:
<p key={1}>1</p>
<p key={2}>2</p>
複製代碼
那麼這個 map
的結構就會是這樣的:
// 節點的 key 做爲 map 的 key
// 若是節點不存在 key,那麼 index 爲 key
const map = {
1: {},
2: {}
}
複製代碼
在遍歷的過程當中會尋找新的節點的 key 是否存在於這個 map
中,存在便可複用,不存在就只能建立一個新的了。其實這部分的複用及建立的邏輯和第一輪中的是如出一轍的,因此也就再也不贅述了。
那麼若是複用成功,就應該把複用的 key
從 map
中刪掉,而且給複用的節點移動位置。這裏的移動依舊不涉及 DOM 操做,而是給 effectTag
賦值爲 Placement
。
此輪遍歷結束後,就把還存在於 map
中的全部老節點刪除。
以上就是 diff 子節點的所有邏輯,對比 React 15 的 diff 策略而言我的認爲代碼好懂了許多。
閱讀源碼是一個很枯燥的過程,可是收益也是巨大的。若是你在閱讀的過程當中有任何的問題,都歡迎你在評論區與我交流。
另外寫這系列是個很耗時的工程,須要維護代碼註釋,還得把文章寫得儘可能讓讀者看懂,最後還得配上畫圖,若是你以爲文章看着還行,就請不要吝嗇你的點贊。
最後,以爲內容有幫助能夠加羣一同交流與學習。