本文內容涉及到不少渲染鏈路中的原理以及源碼方法,因此在看本文以前,須要對於React
的render
渲染流程有大體的瞭解。不清楚的同窗能夠先看個人第一篇源碼解析文章。react
寫給本身看的React源碼解析(一):你的React代碼是怎麼渲染成DOM的? git
本文主要解析fiber
架構更新鏈路的雙緩衝
模式以及Concurrent
模式下時間切片
,優先級
的實現原理。github
雙緩衝模式的主要用處,是可以幫咱們較大限度地實現Fiber
節點的複用,減小性能方面的開銷。瀏覽器
在以前的文章中,咱們知道在首次渲染的時候會建立出兩顆樹,current
樹與workInProgress
樹。current
樹與workInProgress
樹,其實就是兩套緩衝數據:當current
樹被渲染到頁面上時,全部的數據更新都會由workInProgress
樹來承接。workInProgress
樹將會在內存裏悄悄地完成全部改變,直到下次進行渲染的commit
階段執行完畢以後,fiberRoot
對象的current
會指向workInProgress
樹,workInProgress
樹就會變成渲染到頁面上的current
樹。性能優化
咱們用一個實際例子來幫助理解:markdown
import { useState } from 'react';
function App() {
const [state, setState] = useState(0)
return (
<div className="App"> <div onClick={() => { setState(state + 1) }}> <p>{state}</p> </div> </div>
);
}
複製代碼
這個例子的功能很簡單,就是點擊一次,數字加1。上面的demo在render
階段結束後,commit
階段結束前的兩顆fiber
樹以下圖所示架構
commit
階段完成,workInProgress
樹被渲染到頁面上,這時候fiberRoot
對象的current
會指向workInProgress
樹,這個當前被渲染的fiber
樹。app
點擊一次數字,咱們進入第一次的更新流程。重點看beginWork
調用鏈路中的createWorkInProgress
方法。異步
上圖中,workInProgress
樹下面的子節點的current.alternate
對應的就是current
樹的子節點,可是current
樹目前沒有子節點,因此爲null,進入等於null的流程。按照workInProgress
的子節點的屬性給current
樹建立出相同的子節點。ide
而後在commit
階段結束後,current
樹會被渲染到頁面上,fiberRoot
對象的current
會指回到current
樹,具體以下圖
再點擊一次數字,觸發state的第二次更新,仍是看以前的createWorkInProgress
方法。
這時候,由於兩顆樹都已經構建完成,因此current.alternate
是存在的。因此以後每次經過beginWork
觸發createWorkInProgress
調用時,都會一致地走入else
裏面的邏輯,也就是直接複用現成的節點。 這也就是雙緩衝機制實現節點複用的方法。
React
源碼解析第一篇分析了首次渲染的鏈路,更新的鏈路其實跟首次渲染大體同樣。
首次渲染能夠理解爲一種特殊的更新,ReactDOM.render
,setState
,useState
同樣,都是一種觸發更新的姿式。這些方法發起的調用鏈路很類似,是由於它們最後「異曲同工」,都會經過建立update
對象來進入同一套更新工做流。
按demo的流程來,點擊數字以後,會觸發一個dispatchAction
方法,在該方法中,會完成update
對象的建立
update
建立完成以後,會跟首次渲染同樣,進入updateContainer
方法(首次渲染鏈路中的update
會在這個方法裏建立),這裏主要是兩個方法
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current, lane, eventTime);
複製代碼
enqueueUpdate
:將update
入隊。每個Fiber
節點都會有一個屬於它本身的updateQueue
,用於存儲多個更新,這個updateQueue
是以鏈表的形式存在的。在render
階段,updateQueue
的內容會成爲 render
階段計算Fiber
節點的新state
的依據。
scheduleUpdateOnFiber
:調度update
。這個方法後面緊跟的就是performSyncWorkOnRoot
所觸發的render
階段。
這裏有一個點須要提示一下:dispatchAction
中,調度的是當前觸發更新的節點,這一點和掛載過程須要區分開來。在掛載過程當中,updateContainer
會直接調度根節點。其實,對於更新這種場景來講,大部分的更新動做確實都不是由根節點觸發的,而render
階段的起點則是根節點。因此在scheduleUpdateOnFiber
中,有這樣一個方法
它會從當前Fiber
節點開始,向上遍歷直至根節點,並將根節點返回。因此,咱們說React
的更新流程,是從根節點開始,從新遍歷整個fiber
樹,這也是爲何咱們平時的性能優化的重點都在減小組件的從新render
上。
在scheduleUpdateOnFiber
中,還有一個重要的判斷,那就是對於同步和異步的判斷邏輯。
以前咱們分析同步的首次渲染流程的時候,走的是performSyncWorkOnRoot
方法,可是對於異步模式,會運行ensureRootIsScheduled
方法。來看下一段核心邏輯
if (newCallbackPriority === SyncLanePriority) {
// 同步更新的 render 入口
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 將當前任務的 lane 優先級轉換爲 scheduler 可理解的優先級
var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
// 異步更新的 render 入口
newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
複製代碼
從這段邏輯中咱們能夠看出,React
會以當前更新任務的優先級類型爲依據,決定接下來是調度 performSyncWorkOnRoot
仍是performConcurrentWorkOnRoot
。這裏調度任務用到的函數分別是 scheduleSyncCallback
和scheduleCallback
,這兩個函數在內部都是經過調用 unstable_scheduleCallback
方法來執行任務調度的。這個方法是Scheduler
(調度器)中導出的一個核心方法。
Scheduler
的核心能力,就是讓fiber
架構實現了時間切片
與優先級調度
這兩個核心特徵。
先來了解一下時間切片究竟是作了什麼事情?
import React from 'react';
function App() {
const arr = new Array(1000).fill(0);
return (
<div className="App"> <div className="container"> { arr.map((i, index) => <p>{`測試文本第${index}行`}</p>) } </div> </div>
);
}
複製代碼
上面的代碼就是渲染1000條p
標籤到頁面上,當咱們使用ReactDOM.render
進行渲染,由於它是一個同步的過程,全部的鏈路都會在一個宏任務裏執行掉。根據不一樣用戶電腦和瀏覽器的性能不一樣,這個宏任務的執行時間,多是100ms、200ms、300ms甚至更多。由於js
線程和渲染線程是互斥的,在執行這個比較長時間的宏任務時,咱們瀏覽器的渲染線程將被阻塞。咱們知道瀏覽器的刷新頻率爲60Hz
也就是說每16.6ms
就會刷新一次,這種長時間的宏任務致使的渲染線程阻塞,將會產生明顯的卡頓、掉幀。
而時間切片,就是把這段須要較長時間運行的宏任務「切」開,變成一段段儘可能保證運行時間在瀏覽器刷新間隔時間之下的宏任務。給渲染線程留出時間,保證渲染的流暢度。咱們來看兩張圖,第一張是同步模式下的調用棧
下一張是把ReactDOM.render
調用改成createRoot
,用Concurrent
(異步)模式來進行渲染
咱們能夠看到,原本一個長時間的「大任務」被切成了一個個短期的「小任務」。
根據上文對scheduleUpdateOnFiber
的分析,在同步的模式下,React
會調用performSyncWorkOnRoot
,在這個鏈路下,會經過workLoopSync
方法來循環建立Fiber
節點、構建Fiber
樹。
function workLoopSync() {
// 若 workInProgress 不爲空
while (workInProgress !== null) {
// 針對它執行 performUnitOfWork 方法
performUnitOfWork(workInProgress);
}
}
複製代碼
這是一個沒法中斷的過程,開始了就沒法中止。
而在異步的模式下,React
會調用performConcurrentWorkOnRoot
,經過renderRootConcurrent
調用 workLoopConcurrent
來構建Fiber
樹。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
複製代碼
咱們能夠發現,異步的方法裏,其實就只是多了一個shouldYield()
方法,當shouldYield()
爲true
的時候,while循環將中止,將主線程讓給渲染線程。
shouldYield
的本體其實也是調度器裏導出的一個方法Scheduler.unstable_shouldYield
,方法很簡單。源碼地址
export function unstable_shouldYield() {
return getCurrentTime() >= deadline;
}
複製代碼
就是噹噹前時間大於deadline
這個當前時間切片的到期時間時,就返回true
,中止workLoopConcurrent
循環。
咱們來看下deadline
是怎麼定義的
deadline = getCurrentTime() + yieldInterval;
複製代碼
getCurrentTime()
就是當前時間,而yieldInterval
是一個常量,5ms,源碼地址
const yieldInterval = 5;
複製代碼
因此說,時間切片的間隔是5ms
(實際應該都是比5ms
稍大,由於必須等當前的fiber
節點構建完成以後,纔會經過shouldYield()
方法判斷是否到期)
當workLoopConcurrent
循環中斷以後,React
會從新發起調度(setTimeout
或者MessageChannel
方式),檢查是否存在事件響應、更高優先級任務或其餘代碼須要執行,若是有則執行,若是沒有則從新建立工做循環workLoopConcurrent
,執行剩下的工做中Fiber
節點構建。
在更新鏈路中,不管是scheduleSyncCallback
仍是scheduleCallback
,最終都是經過調用 unstable_scheduleCallback
來發起調度的。 unstable_scheduleCallback
是Scheduler
導出的一個核心方法,它將結合任務的優先級信息爲其執行不一樣的調度邏輯。源碼地址
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 獲取當前時間
var currentTime = getCurrentTime();
// 聲明 startTime,startTime 是任務的預期開始時間
var startTime;
// 如下是對 options 入參的處理
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
// 若入參規定了延遲時間,則累加延遲時間
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
// timeout 是 expirationTime 的計算依據
var timeout;
// 根據 priorityLevel,肯定 timeout 的值
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 優先級越高,timout 越小,expirationTime 越小
var expirationTime = startTime + timeout;
// 建立 task 對象
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// 若當前時間小於開始時間,說明該任務可延時執行(未過時)
if (startTime > currentTime) {
// 將未過時任務推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中沒有可執行的任務,而當前任務又是 timerQueue 中的第一個任務
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 那麼就派發一個延時任務,這個延時任務用於檢查當前任務是否過時
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 裏處理的是當前時間大於 startTime 的狀況,說明這個任務已過時
newTask.sortIndex = expirationTime;
// 過時的任務會被推入 taskQueue
push(taskQueue, newTask);
......
// 執行 taskQueue 中的任務
requestHostCallback(flushWork);
}
return newTask;
}
複製代碼
unstable_scheduleCallback
的主要工做是針對當前任務建立一個task
,而後結合startTime
信息將這個task
推入timerQueue
或taskQueue
,最後根據timerQueue
和taskQueue
的狀況,執行延時任務或即時任務。
這裏須要知道幾個概念
堆是一種特殊的徹底二叉樹。若是對一棵徹底二叉樹來講,它每一個結點的結點值都不大於其左右孩子的結點值,這樣的徹底二叉樹就叫「小頂堆」。小頂堆自身特有的插入和刪除邏輯,決定了不管咱們怎麼增刪小頂堆的元素,其根節點必定是全部元素中值最小的一個節點。
咱們來看下核心邏輯
if (startTime > currentTime) {
// 將未過時任務推入 "timerQueue"
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 若 taskQueue 中沒有可執行的任務,而當前任務又是 timerQueue 中的第一個任務
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
......
// 那麼就派發一個延時任務,這個延時任務用於檢查當前任務是否過時
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// else 裏處理的是當前時間大於 startTime 的狀況,說明這個任務已過時
newTask.sortIndex = expirationTime;
// 過時的任務會被推入 taskQueue
push(taskQueue, newTask);
......
// 執行 taskQueue 中的任務
requestHostCallback(flushWork);
}
複製代碼
若判斷當前任務是未過時任務,那麼該任務會在sortIndex
屬性被賦值爲startTime
後,被推入timerQueue
。taskQueue
裏存儲的是已過時的任務,peek(taskQueue)
取出的任務若爲空,則說明taskQueue
爲空、當前並無已過時任務。在沒有已過時任務的狀況下,若當前任務(newTask
)就是timerQueue
中須要最先被執行的未過時任務,那麼unstable_scheduleCallback
會經過調用requestHostTimeout
,爲當前任務發起一個延時調用。
注意,這個延時調用(也就是handleTimeout
)並不會直接調度執行當前任務——它的做用是在當前任務到期後,將其從 timerQueue
中取出,加入taskQueue
中,而後觸發對flushWork
的調用。真正的調度執行過程是在flushWork
中進行的。flushWork
中將調用workLoop
,workLoop
會逐一執行taskQueue
中的任務,直到調度過程被暫停(時間片用盡,將從新發起Task
調度)或任務所有被清空。
當下React
發起Task
調度的姿式有兩個:setTimeout
、MessageChannel
。在宿主環境不支持MessageChannel
的狀況下,會降級到setTimeout
。但不論是setTimeout
仍是MessageChannel
,它們發起的都是異步任務(宏任務,將在下次eventLoop
中被調用)。
若是本文對你有所幫助,請幫忙點個贊,感謝!