React踩坑筆記 —— 差分演算法(一)
目錄
概要
- 《React踩坑筆記 —— 差分演算法(二)》
React提供了宣告式的API,以至於我們不需要擔心每次更新具體發生了什麼更改。這使得我們開發應用變得很容易,但始終無法清楚React內部是如何實現的。本文解釋了在
在理解 “差分演算法” 之前,首先我們需要去理解:
- document與Dom nodes的關係;
- document與react元件的關係;
- react元件與react元素樹的關係;
render()
函式的作用與生命週期;ReactDOM.render()
函式的作用。
Document與React元素樹
JSX程式碼
import React from "react";
import ReactDOM from 'react-dom';
const First = () => (<div>
<h1>第一棵樹</h1>
<div>附加資訊</div>
</div>);
const Second = () => (<div>
<h1>第二棵樹</h1>
<Third />
</div>);
const Third = () => (<div>
<h1>第三棵樹</h1>
<div>附加資訊</div>
</div> );
const App = () => (<div>
<First />
<Second />
</div>);
ReactDOM.render(<App />, document.getElementById('root'));
圖示
說明
萬變不離其宗,對於一個HTML頁面,document與Node永遠會是它的基礎組成部分(實際上document也只是一個特別的Node),如果把document看做一張紙,那麼Node可以看成點綴在紙上的文字、圖片或符號。
在React中,React元件是由真實的Dom Node組成,最終會被轉化為Dom Node並在document中渲染出來(事件\樣式\選擇器,只屬於真實的DOM Node)。
當你使用React的時候,你可以認為:
- 每一個React元件都對應一棵React元素樹。
- 或者說,在某一時間點上每一個
render()
函式都建立了一棵React元素樹。 - 這裡的
render()
函式,見下文。 - 每一個React元件可以由一個或多個React元件以及
HTMLElement
組成,即是 —— 每一棵樹都可以作為另一棵樹的子樹,正如上圖所示:<Third />是<Second />的子樹,<Second />和<First />又都是<App />的子樹。 - 在上圖中,<App />元件所對應的React元素樹,是最大的一棵樹。通過API ——
ReactDOM.render(element, container[, callback])
將這棵樹掛載到容器(container)
中。 - 所謂
容器(container)
,不過是document中某個指定的Div或其它塊級元素。 - 所以,無論通過
JavaScript選擇器
還是dom操作外掛
還是React提供的Refs
,對Dom node的操作都會被保留在這個document中,並在元件的state
或props
發生改變的時候參與 “差分演算法”。 - 所以,如果當前頁面被重新整理,那麼原來的document也不復存在:
- React元件會被重新掛載
- Dom node會被重新插入
- 如果你想保留某些狀態,你需要高於頁面的儲存方式,例如sessionStorage( 標籤頁 )、localStorage( 瀏覽器 )、cookie( 瀏覽器 )、Web SQL( 瀏覽器 )、伺服器資料( 任何地方 )
- 路由(Router),只會引起某些元件的解除安裝或掛載,而不會顛覆整個document。
render()
對於Function元件(無狀態元件)
就是自身,對於Class元件(有狀態元件)
就是從React.Component
繼承的render()
方法。
返回值型別
當render()
被呼叫時,可以依賴props
和state
來構造返回值,其返回值型別包括如下:
- React elements. 典型地通過JSX語法建立,例如
<div />
、<MyComponent />
- Arrays and fragments. 由於 “差分演算法” ,React要求
render()
必須返回單元素(single),React提供了<></>和<React.Fragment>允許開發者從render()
中返回多元素 - Portals. 有時我們需要將子元件渲染到父元件外的某個Dom node上,例如對話方塊、懸浮卡、提示資訊。Portals 為我們都提供了首選方式。
- String and numbers. 字串和數字被以文字節點(Text nodes)的形式渲染到Dom中
- Booleans or null. 返回Null表示什麼都不做。 (大多數情況是返回
test && <Child />
,這裡test
是布林值(boolean)
純函式
render()
應該是一個純函式,返回值只依賴於state
和props
,並且不會產生副作用,副作用包括:
- 修改
state
和props
- 呼叫非純函式。如
Date.now()
或Math.random()
- 網路請求,路由跳轉
- 瀏覽器互動。
setTimeout()
、console.log()
如果你需要瀏覽器互動或網路請求,可以選擇在componentDidMount()
或其它合適的宣告周期函式中執行。有些條件沒有做硬性要求,但是保證render()
作為純函式,能夠提高元件更新的效能。
生命週期
元件在掛載(Mounting)和更新(Updating)時,render()
都會被執行,併產生一棵新的React元素樹,然後執行 “ 差分演算法” 更新UI(使用者介面)。宣告週期執行順序如下。詳情可見官方文件《component-lifecycle》
Mounting
- constructor()
- static getDerivedStateFromProps()
- UNSAFE_componentWillMount()
- render()
- componentDidMount()
Updating
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- UNSAFE_componentWillUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
ReactDOM.render()
ReactDOM.render(element, container[, callback])
該方法將整個React元素樹渲染到document中指定的<Div />
容器中,並返回根元件(對應最大元素樹)的引用(Function 元件 —— 無狀態元件,沒有例項(Instance)所以返回null)。
如果這個React元素(根元件)先前已經被渲染到容器中,那麼將對它執行更新,按照 “差分演算法” 去呈現它。
如果這個可選的引數—— 回撥函式,被提供。那麼會在元件被渲染或更新後呼叫。
Note
ReactDOM.render()
控制著容器(container)節點的內容,第一次呼叫時,任何存在的Dom元素都會被替換,之後再呼叫會使用React Dom diffing algorithm(差分演算法)進行高效更新。ReactDOM.render()
不修改容器節點(只修改容器的子節點)。ReactDOM.render()
當前返回根元件(root ReactComponent )例項的引用。然而,返回值的使用已經被遺留了,應該避免去使用它,因為在未來的React版本中,在某些情況下React會採用非同步的方式去渲染元件。如果你真的需要引用根元件例項,首選解決方案是為你的根元件新增《callback ref》。- 另外,使用
ReactDOM.render()
去融合服務端渲染容器(server-rendered container)已經被遺棄了,並將在React 17被移除,代替使用hydrate()。