最近在準備面試。複習了一些react的知識點,特此總結。vue
react 16之前的生命週期是這樣的react
組件在首次渲染時會被實例化,而後調用實例上面的componentWillMount,render和componentDidMount函數。組件在更新渲染時能夠調用componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render和componentDidUpdate函數。組件在卸載時能夠調用componentWillUnmount函數。面試
借圖:算法
從 React v16.3 開始,React 建議使用getDerivedStateFromProps
和getSnapshotBeforeUpdate
兩個生命週期函數替代 componentWillMount
,componentWillReceiveProps
和componentWillUpdate
三個生命週期函數。這裏須要注意的是 新增的兩個生命週期 函數和原有的三個生命週期函數必須分開使用,不能混合使用api
目前的生命週期(借圖):數組
componentWillMount存在的問題瀏覽器
有人認爲在componentWillMount中能夠提早進行異步請求,避免白屏。可是react在調用render渲染頁面的時候,render並不會等待異步請求結束,再獲取數據渲染
。這麼寫是有潛在隱患的。緩存
而在react fiber以後 可能在一次渲染中屢次調用。緣由是:react fiber技術使用增量渲染來解決掉幀的問題,經過requestIdleCallback調度執行每一個任務單元,能夠中斷和恢復,生命週期一旦中斷,恢復以後會從新跑一次以前的生命週期
markdown
新的生命週期併發
static getDerivedStateFromProps
getSnapshotBeforeUpdate
因爲React渲染/更新過程一旦開始沒法中斷,持續佔用主線程,主線程忙於執行JS,無暇他顧(佈局、動畫),形成掉幀、延遲響應(甚至無響應)等不佳體驗。fiber應運而生。
Fiber 是對react reconciler(調和) 核心算法的重構。關鍵特性以下:
增量渲染用來解決掉幀的問題,渲染任務拆分以後,每次只作一小段,作完一段就把時間控制權交還給主線程,而不像以前長時間佔用。
Fiber tree
Fiber以前的reconciler(被稱爲Stack reconciler)自頂向下的遞歸mount/update,沒法中斷(持續佔用主線程),這樣主線程上的佈局、動畫等週期性任務以及交互響應就沒法當即獲得處理,影響體驗。
Fiber解決這個問題的思路是把渲染/更新過程(遞歸diff)拆分紅一系列小任務,每次檢查樹上的一小部分,作完看是否還有時間繼續下一個任務,有的話繼續,沒有的話把本身掛起,主線程不忙的時候再繼續。
fiber樹實際上是一個單鏈表結構
,child指向第一個子節點,return指向父節點,sibling指向下個兄弟節點。結構以下:
// fiber tree節點結構
{
stateNode,
child,
return,
sibling,
...
}
複製代碼
Fiber reconciler
reconcile過程分爲2個階段:
1.(可中斷)render/reconciliation 經過構造workInProgress tree得出change
2.(不可中斷)commit 應用這些DOM change(更新DOM樹、調用組件生命週期函數以及更新ref等內部狀態)
構建workInProgress tree的過程就是diff的過程,經過requestIdleCallback來調度執行一組任務,每完成一個任務後回來看看有沒有插隊的(更緊急的),每完成一組任務,把時間控制權交還給主線程,直到下一次requestIdleCallback回調再繼續構建workInProgress tree
生命週期也被分紅了兩個階段:
// 第1階段 render/reconciliation
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
// 第2階段 commit
componentDidMount
componentDidUpdate
componentWillUnmount
複製代碼
第1階段的生命週期函數可能會被屢次調用,默認以low優先級執行,被高優先級任務打斷的話,稍後從新執行。
fiber tree與workInProgress tree
雙緩衝技術:指的是workInProgress tree構造完畢,獲得的就是新的fiber tree,而後把current指針指向workInProgress tree,因爲fiber與workInProgress互相持有引用,舊fiber就做爲新fiber更新的預留空間,達到複用fiber實例的目的。
每一個fiber上都有個alternate屬性,也指向一個fiber,建立workInProgress節點時優先取alternate,沒有的話就建立一個
let workInProgress = current.alternate;
if (workInProgress === null) {
//...
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// We already have an alternate.
// Reset the effect tag.
workInProgress.effectTag = NoEffect;
// The effect list is no longer valid.
workInProgress.nextEffect = null;
workInProgress.firstEffect = null;
workInProgress.lastEffect = null;
}
複製代碼
這麼作的好處:
fiber 中斷 恢復
中斷:檢查當前正在處理的工做單元,保存當前成果(firstEffect, lastEffect),修改tag標記一下,迅速收尾並再開一個requestIdleCallback,下次有機會再作
斷點恢復:下次再處理到該工做單元時,看tag是被打斷的任務,接着作未完成的部分或者重作
P.S.不管是時間用盡「天然」中斷,仍是被高優任務粗暴打斷,對中斷機制來講都同樣。
在代碼中調用setState函數以後,React 會將傳入的參數對象與組件當前的狀態合併,而後觸發所謂的調和過程(Reconciliation)。通過調和過程,React 會以相對高效的方式根據新的狀態構建 React 元素樹而且着手從新渲染整個UI界面。在 React 獲得元素樹以後,React 會自動計算出新的樹與老樹的節點差別,而後根據差別對界面進行最小化重渲染。在差別計算算法中,React 可以相對精確地知道哪些位置發生了改變以及應該如何改變,這就保證了按需更新,而不是所有從新渲染。
setState調用時有時是同步的(settimeout,自定義dom事件),有時是異步的(普通調用)
React事件是經過事件代理,在最外層的 document上對事件進行統一分發,並無綁定在真實的 Dom節點上。 並且react內部對原生的Event對象進行了包裹處理。具備與瀏覽器原生事件相同的接口,包括 stopPropagation()
和 preventDefault()
。
若是有多個同步setState(...)
操做,React 會將它們的更新(update)前後依次加入到更新隊列(updateQueue),在應用程序的 render 階段處理更新隊列時會將隊列中的全部更新合併成一個,合併原則是相同屬性的更新取最後一次的值。若是有異步setState(...)操做,則先進行同步更新,異步更新則遵循 EventLoop 原理後續處理。
React 更新
// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdate(expirationTime, suspenseConfig) {
var update = {
// 過時時間與任務優先級相關聯
expirationTime: expirationTime,
suspenseConfig: suspenseConfig,
// tag用於標識更新的類型如UpdateState,ReplaceState,ForceUpdate等
tag: UpdateState,
// 更新內容
payload: null,
// 更新完成後的回調
callback: null,
// 下一個更新(任務)
next: null,
// 下一個反作用
nextEffect: null
};
{
// 優先級會根據任務體系中當前任務隊列的執行狀況而定
update.priority = getCurrentPriorityLevel();
}
return update;
}
複製代碼
每個更新對象都有本身的過時時間(expirationTime)、更新內容(payload),優先級(priority)以及指向下一個更新的引用(next)。其中當前更新的優先級由任務體系統一指定。
React 更新隊列
// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function createUpdateQueue(baseState) {
var queue = {
// 當前的state
baseState: baseState,
// 隊列中第一個更新
firstUpdate: null,
// 隊列中的最後一個更新
lastUpdate: null,
// 隊列中第一個捕獲類型的update
firstCapturedUpdate: null,
// 隊列中第一個捕獲類型的update
lastCapturedUpdate: null,
// 第一個反作用
firstEffect: null,
// 最後一個反作用
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null
};
return queue;
}
複製代碼
這是一個單向鏈表結構。
當咱們使用setState()時, React 會建立一個更新(update)對象,而後經過調用enqueueUpdate
函數將其加入到更新隊列(updateQueue)
// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
// 每次setState都會建立update併入updateQueue
function enqueueUpdate(fiber, update) {
// 每一個Fiber結點都有本身的updateQueue,其初始值爲null,通常只有ClassComponent類型的結點updateQueue纔會被賦值
// fiber.alternate指向的是該結點在workInProgress樹上面對應的結點
var alternate = fiber.alternate;
var queue1 = void 0;
var queue2 = void 0;
if (alternate === null) {
// 若是fiber.alternate不存在
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// 若是fiber.alternate存在,也就是說存在current樹上的結點和workInProgress樹上的結點都存在
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// 若是兩個結點上面均沒有updateQueue,則爲它們分別建立queue
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(alternate.memoizedState);
} else {
// 若是隻有其中一個存在updateQueue,則將另外一個結點的updateQueue克隆到該結點
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// 若是隻有其中一個存在updateQueue,則將另外一個結點的updateQueue克隆到該結點
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// 若是兩個結點均有updateQueue,則不須要處理
}
}
}
if (queue2 === null || queue1 === queue2) {
// 通過上面的處理後,只有一個queue1或者queue1 == queue2的話,就將更新對象update加入到queue1
appendUpdateToQueue(queue1, update);
} else {
// 通過上面的處理後,若是兩個queue均存在
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// 只要有一個queue不爲null,就須要將將update加入到queue中
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// 若是兩個都不是空隊列,因爲兩個結構共享,因此只在queue1加入update
appendUpdateToQueue(queue1, update);
// 仍然須要在queue2中,將lastUpdate指向update
queue2.lastUpdate = update;
}
}
...
}
function appendUpdateToQueue(queue, update) {
if (queue.lastUpdate === null) {
// 若是隊列爲空,則第一個更新和最後一個更新都賦值當前更新
queue.firstUpdate = queue.lastUpdate = update;
} else {
// 若是隊列不爲空,將update加入到隊列的末尾
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
複製代碼
在enqueueUpdate
函數中,React 將更新加入到更新隊列時會同時維護兩個隊列對象 queue1 和 queue2,其中 queue1 是應用程序運行過程當中 current 樹上當前 Fiber 結點最新隊列,queue2 是應用程序上一次更新時(workInProgress 樹
)Fiber 結點的更新隊列,它們之間的相互邏輯是下面這樣的。
fiber.updateQueue
,queue2 取的是fiber.alternate.updateQueue
;null
,則調用createUpdateQueue(...)
獲取初始隊列;null
,則調用cloneUpdateQueue(...)
從對方中獲取隊列;null
,則將update
做爲lastUpdate
加入 queue1 中。React 處理更新隊列
React 應用程序運行到 render 階段時會處理更新隊列,處理更新隊列的函數是processUpdateQueue
// 源碼位置:packages/react-reconciler/src/ReactUpdateQueue.js
function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime) {
...
// 從隊列中取出第一個更新
var update = queue.firstUpdate;
var resultState = newBaseState;
// 遍歷更新隊列,處理更新
while (update !== null) {
...
// 若是第一個更新不爲空,緊接着要遍歷更新隊列
// getStateFromUpdate函數用於合併更新,合併方式見下面函數實現
resultState = getStateFromUpdate(workInProgress, queue, update, resultState, props, instance);
...
update = update.next;
}
...
// 設置當前fiber結點的memoizedState
workInProgress.memoizedState = resultState;
...
}
// 獲取下一個更新對象並與現有state對象合併
function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
switch (update.tag) {
case UpdateState:
{
var _payload2 = update.payload;
var partialState = void 0;
if (typeof _payload2 === 'function') {
// setState傳入的參數_payload2類型是function
...
partialState = _payload2.call(instance, prevState, nextProps);
...
} else {
// setState傳入的參數_payload2類型是object
partialState = _payload2;
}
// 合併當前state和上一個state.
return _assign({}, prevState, partialState);
}
}
}
複製代碼
processUpdateQueue函數用於處理更新隊列,在該函數內部使用循環的方式來遍歷隊列,經過update.next依次取出更新(對象)進行合併,合併更新對象的方式是:
React 應用程序首次渲染時在 prerender
階段會初始化 current 樹
。最開始的 current 樹只有一個根結點— HostRoot類型的 Fiber 結點。在後面的 render 階段會根據此時的 current 樹建立 workInProgress 樹
。在 workInProgress 樹
上面進行一系列運算(計算更新等),最後將反作用列表(Effect List)傳入到 commit
階段。當 commit 階段運行完成後將當前的 current 樹
替換爲 workInProgress 樹
,至此一個更新流程就完成了。簡述:
current 樹是未更新前應用程序對應的 Fiber 樹,workInProgress 樹是須要更新屏幕的 Fiber 樹。
FiberRootNode
構造函數只有一個實例就是 fiberRoot
對象。而每一個 Fiber 節點都是 FiberNode
構造函數的實例,它們經過return,child和sibling三個屬性鏈接起來,造成了一個巨大鏈表。React 對每一個節點的更新計算都是在這個鏈表上完成的。React 在對 Fiber 節點標記更新標識的時候的作法就是爲節點的effectTag
屬性賦不一樣的值。
借圖:
這兩個api,其實概念上仍是很好理解的,一個是「緩存函數」, 一個是緩存「函數的返回值」。
一旦在條件語句中聲明hooks,在下一次函數組件更新,hooks鏈表結構,將會被破壞,current樹的memoizedState緩存hooks信息,和當前workInProgress不一致,若是涉及到讀取state等操做,就會發生異常。
總結:能夠認爲維護了一個state數組 和一個cursor
指針 。第一次渲染時把當前的值push進states
數組裏,把綁定了指針的setter推動 setters
數組中。每次的後續渲染都會重置指針cursor
的位置,並會從每一個數組中讀取對應的值。每一個 setter 都會有一個對應的指針位置的引用,所以當觸發任何 setter 調用的時候都會觸發去改變狀態數組中的對應的值。若是放在條件語句中。那麼setstate的順序就會發生錯亂。
let first = true;
const [num1, setNum1] = usestate(1);
if (first) {
const [num2, setNum2] = usestate(2);
first = false
}
const [num3, setNum3] = usestate(3);
複製代碼
第一次渲染時,維護的states數組時[1,2,3] ,setters數組指針指向[state[0],state[1],state[2]]。可是當咱們的組件更新時,這時候的states數組是[1,2,3] setters數組是[state[0],state[1]]。顯然咱們的setNum3被設置成了2。
vue react 共同點:
1. 監聽數據變化的實現原理不一樣
2. 數據流的不一樣
3. 組件通訊的區別
4. 模板渲染方式的不一樣