最近開始深刻學習React
的原理了,後面會出一系列關於React
原理的文章,基本都是我學習其餘前輩的React
源碼分析以及跟隨他們閱讀源碼時的一些思考和記錄,內容大部分非原創,但我會用我本身的方式去總結原理以及相關的流程,並加以補充,看成本身的學習總結。html
本系列內容偏向底層源碼實現,若是你是React
新手,不建議你細看。react
本文的內容是關於React
首次渲染的流程。git
這裏主要有兩個步驟,第一個是從JSX
代碼通過React.createElement
方法變成一個虛擬DOM
,第二個步驟是經過ReactDOM.render
方法把虛擬DOM
變成真實DOM
。github
對於 React.createElement
方法,不少新人可能並不知道,由於咱們在通常的業務邏輯中,比較少會直接使用這個方法。其實在React
官網中,就已經有寫明瞭。數組
JSX 會被編譯爲 React.createElement(), React.createElement() 將返回一個叫做「React Element」的 JS 對象。babel
咱們在babel官網裏能夠寫個JSX
試一下。 markdown
JSX
在通過babel
的編譯以後,變成了嵌套的React.createElement
。JSX 的本質其實就是React.createElement這個 JavaScript 調用的語法糖。 有了JSX
語法糖的存在,咱們可使用咱們最爲熟悉的類 HTML
標籤語法來建立虛擬 DOM
,在下降學習成本的同時,也提高了研發效率與研發體驗。數據結構
接下來,咱們來看下createElement
的源碼架構
export function createElement(type, config, children) {
// propName 變量用於儲存後面須要用到的元素屬性
let propName;
// props 變量用於儲存元素屬性的鍵值對集合
const props = {};
// key、ref、self、source 均爲 React 元素的屬性
let key = null;
let ref = null;
let self = null;
let source = null;
// config 對象中存儲的是元素的屬性
if (config != null) {
// 進來以後作的第一件事,是依次對 ref、key、self 和 source 屬性賦值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此處將 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 裏面的屬性都一個一個挪到 props 這個以前聲明好的對象裏面
for (propName in config) {
if (
// 篩選出能夠提進 props 對象裏的屬性
hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是當前元素的子元素的個數,減去的 2 是 type 和 config 兩個參數佔用的長度
const childrenLength = arguments.length - 2;
// 若是拋去type和config,就只剩下一個參數,通常意味着文本節點出現了
if (childrenLength === 1) {
// 直接把這個參數的值賦給props.children
props.children = children;
// 處理嵌套多個子元素的狀況
} else if (childrenLength > 1) {
// 聲明一個子元素數組
const childArray = Array(childrenLength);
// 把子元素推動數組裏
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最後把這個數組賦值給props.children
props.children = childArray;
}
// 處理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最後返回一個調用ReactElement執行方法,並傳入剛纔處理過的參數
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
複製代碼
總結一下這個函數作的一些事情app
key
,ref
,self
,source
四個值(將key
字符串化,將config
中的ref
賦值給ref
,self
和source
暫不太清楚其功能,能夠忽略,17版本的jsx方法直接刪除了這兩個參數)config
,篩選出能夠賦值到props
裏的屬性props.children
中(若是隻有一個子元素,就直接賦值,若是子元素大於一個,就以數組形式存儲)defaultProps
(若是沒有傳入相關的props
,props
就取設置的默認值)ReactElement
方法,並傳入剛纔處理的參數createElement
其實就是一個數據處理器,把從JSX
獲取到的內容進行格式化,再傳入ReactElement
方法中。
注意:在React 17
版本當中,createElement
會被替換成jsx
(源碼地址)方法。
import React from 'react'; // 在17版本中,能夠不引入這句
function App() {
return <h1>Hello World</h1>;
}
// createElement
function App() {
return React.createElement('h1', null, 'Hello world');
}
// 17版本中的jsx
import {jsx as _jsx} from 'react/jsx-runtime'; // 由編譯器引入
function App() {
// 子元素將直接編譯成config對象裏的children屬性,jsx再也不接收單獨的子元素入參
return _jsx('h1', { children: 'Hello world' });
}
複製代碼
咱們來看看ReactElement
方法作了哪些事,源碼地址。
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一個常量,用來標識該對象是一個ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 內置屬性賦值
type: type,
key: key,
ref: ref,
props: props,
// 記錄創造該元素的組件
_owner: owner,
};
//
if (__DEV__) {
// 省略這些沒必要要的代碼
}
return element;
};
複製代碼
ReactElement
方法也很簡單,其實就是經過這些傳入的參數,建立了一個對象,並把它返回出來。這個ReactElement
對象實例就是createElement
方法最終返回出的內容,它就是React
虛擬DOM
的一個節點。虛擬DOM
其實本質上就是一個存儲了不少用來描述DOM
的屬性的對象。
這裏你要注意,由於每一個節點都會被createElement
方法調用,因此最終返回回來的應該是一個虛擬DOM
的樹。
const App = (
<div className="App"> <h2 className="title">title</h2> <p className="text">text</p> </div>
);
console.log(App);
複製代碼
如上圖所示,全部的節點都會被編譯成ReactElement
對象實例(虛擬DOM
)。
虛擬DOM
有了,但咱們最終的目的仍是要把內容渲染到頁面上,因此咱們還須要經過ReactDOM.render
方法把虛擬DOM
渲染成真實的DOM
。這一塊內容比較多,我會一個函數一個函數的分開討論。
關於爲何要使用虛擬DOM
,虛擬DOM
有哪些優點,這些內容不在本文的討論範圍內容,網上相關的文章不少,你們能夠自行去了解一下。
React
啓動方式在React
的16版本以及17版本中,一直都有三種啓動方式
legacy
模式,ReactDOM.render(<App />, rootNode)
。目前經常使用的模式,渲染過程是同步的blocking
模式,ReactDOM.createBlockingRoot(rootNode).render(<App />)
。過渡的模式,基本不多用concurrent
模式,ReactDOM.createRoot(rootNode).render(<App />)
。異步渲染的模式,還可使用一些新的特性,目前還在實驗中。咱們是解析ReactDOM.render
的渲染流程,因此其實分析的是一個同步的流程。關於concurrent
的異步渲染流程,時間分片以及優先級,其實也是在同步渲染的基礎上作的一些修改,這些內容我會在本系列後續的文章中再總結。
雖說是一個同步的流程,可是在React 16
版本的時候,已經把整個的渲染鏈路重構成了Fiber
的結構。Fiber
架構在 React
中並不可以和異步渲染畫嚴格的等號,它是一種同時兼容了同步渲染與異步渲染的設計。
render
的三個階段ReactDOM.render
方法對應的調用棧很深,涉及到的函數方法也不少,不過咱們能夠只須要看一些關鍵的邏輯,瞭解大體的流程便可。
首次render
能夠大致上分紅三個階段
Fiber
樹中基本實體的建立。從調用ReactDOM.render
開始,到scheduleUpdateOnFiber
方法調用performSyncWorkOnRoot
結束。Fiber
樹。從performSyncWorkOnRoot
方法開始,到commitRoot
方法結束。Fiber
樹,把Fiber
節點映射爲DOM
節點並渲染到頁面上。從commitRoot
方法開始,到渲染結束。上面幾個方法如今看不懂不要緊,接下來會一步步帶你瞭解。只須要有個大體的印象,知道這三個階段分別完成的事情。
如今咱們開始初始化階段,在這個階段,上文中已經說,主要就是完成Fiber
樹中基本實體的建立。可是咱們須要知道什麼是基本實體?有哪些?咱們從源碼中去尋找答案。
咱們只看關鍵的邏輯,咱們先來看看ReactDOM.render
中調用的legacyRenderSubtreeIntoContainer
方法(源碼地址)。
// ReactDOM.render中的調用
return legacyRenderSubtreeIntoContainer(null, element, container, false, callback);
// legacyRenderSubtreeIntoContainer源碼
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
// container 對應的是咱們傳入的真實 DOM 對象
var root = container._reactRootContainer;
// 初始化 fiberRoot 對象
var fiberRoot;
// DOM 對象自己不存在 _reactRootContainer 屬性,所以 root 爲空
if (!root) {
// 若 root 爲空,則初始化 _reactRootContainer,並將其值賦值給 root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
// legacyCreateRootFromDOMContainer 建立出的對象會有一個 _internalRoot 屬性,將其賦值給 fiberRoot
fiberRoot = root._internalRoot;
// 這裏處理的是 ReactDOM.render 入參中的回調函數,你瞭解便可
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
// 進入 unbatchedUpdates 方法
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// else 邏輯處理的是非首次渲染的狀況(即更新),其邏輯除了跳過了初始化工做,與樓上基本一致
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
複製代碼
這個函數主要作了下面幾步
legacyCreateRootFromDOMContainer
方法建立了container._reactRootContainer
並賦值給root
root
的_internalRoot
屬性賦值給fiberRoot
fiberRoot
與一些其餘參數傳入updateContainer
方法updateContainer
的回調內容做爲參數傳入unbatchedUpdates
方法這裏的fiberRoot
的本質是一個FiberRootNode
對象,它的關聯對象是真實DOM
的容器節點,這個對象裏有一個current
對象 如上圖,這個
current
對象是一個FiberNode
實例,其實它就是一個Fiber
節點,並且她仍是當前Fiber
樹的頭部節點。fiberRoot
和它下面的current
對象這兩個節點,將是後續整棵Fiber
樹構建的起點。
接下來咱們看看unbatchedUpdates
方法(源碼地址)。
function unbatchedUpdates(fn, a) {
// 這裏是對上下文的處理,沒必要糾結
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// 重點在這裏,直接調用了傳入的回調函數 fn,對應當前鏈路中的 updateContainer 方法
return fn(a);
} finally {
// finally 邏輯裏是對回調隊列的處理,此處不用太關注
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
複製代碼
這個方法比較簡單,其實就是直接調用了傳入的回調函數fn。而fn,是在legacyRenderSubtreeIntoContainer
中傳入的
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
複製代碼
因此咱們再來看updateContainer
方法
先來看源碼(源碼地址),我會刪除不少無關的邏輯。
function updateContainer(element, container, parentComponent, callback) {
// 這個 current 就是以前說的當前`Fiber`樹的頭部節點
const current = container.current;
// 這是一個 event 相關的入參,此處沒必要關注
var eventTime = requestEventTime();
// 這是一個比較關鍵的入參,lane 表示優先級
var lane = requestUpdateLane(current);
// 結合 lane(優先級)信息,建立 update 對象,一個 update 對象意味着一個更新
var update = createUpdate(eventTime, lane);
// update 的 payload 對應的是一個 React 元素
update.payload = {
element: element
};
// 處理 callback,這個 callback 其實就是咱們調用 ReactDOM.render 時傳入的 callback
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
// 將 update 入隊
enqueueUpdate(current, update);
// 調度 fiberRoot
scheduleUpdateOnFiber(current, lane, eventTime);
// 返回當前節點(fiberRoot)的優先級
return lane;
}
複製代碼
這個方法裏的邏輯有點複雜,總的來講能夠分爲三點
Fiber
節點的lane
(優先級)lane
(優先級),建立當前Fiber
節點的update
對象,並將其入隊rootFiber
)進行更新不過由於本文講解的首次渲染鏈路是同步的,優先級意義不大,因此咱們能夠直接看看調度節點的方法scheduleUpdateOnFiber
。
這個方法內容有點長,我只列出關鍵邏輯(源碼地址)。
// 若是是同步的渲染,將進入這個條件。若是是異步渲染的模式,將進入它的else邏輯中
// React 是經過 fiber.mode 來區分不一樣的渲染模式
if (lane === SyncLane) {
if (
// 判斷當前是否運行在 unbatchedUpdates 方法裏
(executionContext & LegacyUnbatchedContext) !== NoContext &&
// 判斷當前是否已經 render
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
schedulePendingInteractions(root, lane);
// 咱們要關注的關鍵步驟,從這個方法開始。開啓 render 階段
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
}
複製代碼
在以前的步驟中,React
已經完成Fiber
樹中基本實體的建立,其實就是以前幾節說的fiberRoot
和它下面的current
對象這兩個節點。在這個方法中,咱們只須要關注performSyncWorkOnRoot
方法,從它開始,咱們將進入render
階段。
render
階段要作的事情是構建和完善Fiber
樹,其實就是以fiberRoot
和它下面的current
對象這兩個節點爲頂節點,不斷的遍歷,把他們的子元素的Fiber
樹構建出來。
咱們先來看performSyncWorkOnRoot
方法。
這裏重點看兩個邏輯
exitStatus = renderRootSync(root, lanes);
...
commitRoot(root);
複製代碼
renderRootSync
方法是render
階段開始的標誌,而下面的commitRoot
是commit
階段開始的標誌。咱們先進入的是render
階段,因此咱們先看renderRootSync
裏的流程。
這個方法裏須要看兩個邏輯
prepareFreshStack(root, lanes);
...
workLoopSync();
複製代碼
咱們先走prepareFreshStack
的流程,等它走完了,再進入workLoopSync
的遍歷流程。
prepareFreshStack
的做用是重置一個新的堆棧環境,咱們也只須要關注一個邏輯
workInProgress = createWorkInProgress(root.current, null);
複製代碼
createWorkInProgress
是一個比較重要的方法,咱們詳細看一下。
精簡後的源碼以下,源碼地址
// 這裏入參中的 current 傳入的是現有樹結構中的 rootFiber 對象
function createWorkInProgress(current, pendingProps) {
var workInProgress = current.alternate;
// ReactDOM.render 觸發的首屏渲染將進入這個邏輯
if (workInProgress === null) {
// 這是須要你關注的第一個點,workInProgress 是 createFiber 方法的返回值
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// 這是須要你關注的第二個點,workInProgress 的 alternate 將指向 current
workInProgress.alternate = current;
// 這是須要你關注的第三個點,current 的 alternate 將反過來指向 workInProgress
current.alternate = workInProgress;
} else {
// else 的邏輯此處先不用關注
}
// 如下省略大量 workInProgress 對象的屬性處理邏輯
// 返回 workInProgress 節點
return workInProgress;
}
複製代碼
這裏事先說明一下,入參current
就是以前的fiberRoot
對象下的current
對象。
總結一下createWorkInProgress
方法作的事情
createFiber
,workInProgress
是createFiber
方法的返回值workInProgress
的alternate
將指向current
current
的alternate
將反過來指向workInProgress
workInProgress
節點這裏的createFiber
方法,顧名思義,就是用來建立一個Fiber
節點的方法。入參都是current
的值,因此,workInProgress
節點其實就是current
節點的副本。這時候整顆樹的結構應該以下所示:
workInProgress
樹頂點建立完成了,如今運行以前renderRootSync
方法裏第二個關鍵邏輯workLoopSync
。
這個方法很簡單,就是個遍歷的功能
function workLoopSync() {
// 若 workInProgress 不爲空
while (workInProgress !== null) {
// 針對它執行 performUnitOfWork 方法
performUnitOfWork(workInProgress);
}
}
複製代碼
由於後面列出的方法,都是workLoopSync
中不斷遍歷的,因此在解析performUnitOfWork
方法及其子方法以前,我要先對整個遍歷的流程作一個大體的總結,有了一個大體的瞭解以後再去分析裏面的方法。
workLoopSync
作的事情就是經過while
循環反覆判斷workInProgress
是否爲空,並在不爲空的狀況下針對它執行performUnitOfWork
函數。而 performUnitOfWork
函數將觸發beginWork
的調用,建立新的Fiber
節點。若beginWork
所建立的Fiber
節點不爲空,則performUniOfWork
會用這個新的Fiber
節點來更新workInProgress
的值,爲下一次循環作準備。
當workInProgress
爲空時,意味着已經完成對整棵Fiber
樹的構建。
在這個過程當中,每個被建立出來的新Fiber
節點,都會掛載爲以前的workInProgress
樹的後代節點。咱們一步步來看一下。
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
複製代碼
performUnitOfWork
裏其實存在有兩個流程,一個是beginWork
流程(建立新的Fiber
節點),還有一個completeWork
流程(當beginWork
遍歷到當前分支的葉子節點時,next === null,運行completeWork
流程),來負責處理Fiber
節點到DOM
節點的映射邏輯。
咱們先來看beginWork
流程
beginWork
代碼有400多行,實在太多了,只取一些關鍵邏輯。源碼地址
function beginWork(current, workInProgress, renderLanes) {
......
// current 節點不爲空的狀況下,會加一道辨識,看看是否有更新邏輯要處理
if (current !== null) {
// 獲取新舊 props
var oldProps = current.memoizedProps;
var newProps = workInProgress.pendingProps;
// 若 props 更新或者上下文改變,則認爲須要"接受更新"
if (oldProps !== newProps || hasContextChanged() || (
workInProgress.type !== current.type )) {
// 打個更新標
didReceiveUpdate = true;
} else if (xxx) {
// 不須要更新的狀況 A
return A
} else {
if (須要更新的狀況 B) {
didReceiveUpdate = true;
} else {
// 不須要更新的其餘狀況,這裏咱們的首次渲染就將執行到這一行的邏輯
didReceiveUpdate = false;
}
}
} else {
didReceiveUpdate = false;
}
......
// 這坨 switch 是 beginWork 中的核心邏輯,原有的代碼量至關大
switch (workInProgress.tag) {
......
// 這裏省略掉大量形如"case: xxx"的邏輯
// 根節點將進入這個邏輯
case HostRoot:
return updateHostRoot(current, workInProgress, renderLanes)
// dom 標籤對應的節點將進入這個邏輯
case HostComponent:
return updateHostComponent(current, workInProgress, renderLanes)
// 文本節點將進入這個邏輯
case HostText:
return updateHostText(current, workInProgress)
......
// 這裏省略掉大量形如"case: xxx"的邏輯
}
}
複製代碼
beginWork
的核心邏輯是根據fiber
節點(workInProgress
樹下的節點)的tag
屬性(表明當前fiber
屬於什麼類型的標籤)的不一樣,調用不一樣的節點建立函數。
這些節點建立函數,最終都會經過調用reconcileChildren
方法,生成當前節點的子節點。
這個方法也比較簡單
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
// 判斷 current 是否爲 null
if (current === null) {
// 若 current 爲 null,則進入 mountChildFibers 的邏輯
workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
} else {
// 若 current 不爲 null,則進入 reconcileChildFibers 的邏輯
workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
}
}
複製代碼
上面的兩個方法,咱們也能夠找到賦值的地方
var reconcileChildFibers = ChildReconciler(true);
var mountChildFibers = ChildReconciler(false);
複製代碼
這兩個方法都是經過ChildReconciler
方法建立出來的,只是入參有所區別
ChildReconciler
的代碼量也很大,代碼就不放了,源碼地址。
這個方法裏包含了不少關於Fiber
節點的建立、增長、刪除、修改等操做的函數,用來給其餘函數調用。返回值是一個名爲reconcileChildFibers
的函數,這個函數是一個邏輯分發器,它將根據入參的不一樣,執行不一樣的Fiber
節點操做,最終返回不一樣的目標Fiber
節點。
還有一個很重要的邏輯,這個方法會根據入參shouldTrackSideEffects
來決定「是否須要追蹤反作用」,reconcileChildFibers
和mountChildFibers
的不一樣,主要在於對反作用的處理不一樣。shouldTrackSideEffects
爲true
的話,會給新建立的這個Fiber
節點添加一個flags
屬性(17版本以前,這個屬性名是effectTag
),並賦值一個常量。
若是是根節點,會賦值一個Placement
常量,這是一個二進制常量,目的是在渲染真實DOM
的時候告訴渲染器,處理這個fiber
節點時是須要新增DOM節點的。 這種類型的常量還有不少,源碼地址。
這裏先給一個demo,後續的編譯都會以這個demo來實現。
function App() {
return (
<div className="App"> <div className="container"> <h1>我是標題</h1> <p>我是第一段話</p> <p>我是第二段話</p> </div> </div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
複製代碼
回到咱們剛纔的渲染鏈路中來,由於本次循環是第一次,處理的是current
樹和workInProgress
樹的頂部節點,因此current
是存在的,會進入reconcileChildFibers
方法中,它是容許追蹤反作用的。由於當前的workInProgress
是頂部節點,它是沒有一個確切的ReactElement
與之映射,因此它會做爲是JSX
中根組件的父節點,就是這個App
組件的父節點。而後會基於App
組件的ReactElement
(jsx
編譯後的虛擬DOM
)對象信息,建立其對應的FiberNode
,並給它打上Placement
(新增)的反作用標記, 返回給workInProgress.child
。
這樣就將JSX
根組件的Fiber
與以前建立的Fiber
樹頂點關聯起來了,以下圖。
這樣,第一次循環完成,由於App
還有子元素,因此beginWork
中返回的workInProgress
不爲null(workInProgress
其實就是這些jsx
節點編譯以後的fiber
節點),workLoopSync
還會繼續循環。最終的樹結構以下
咱們來看看這些標籤的fiber
節點對象。
上圖分別是App
節點,兩個div子節點,以及p標籤的節點。能夠看到,每個非文本類型的ReactElement
都有了它對應的Fiber
節點。
這些節點之間都是相互有聯繫的,它們是經過child
、return
、sibling
這 3 個屬性創建關係,其中 child
、return
記錄的是父子節點關係,而sibling
記錄的則是兄弟節點關係(sibling 指向的是當前節點的第 1 個兄弟節點)。
具體看下圖:
以上即是workInProgress Fiber
樹的最終形態了。從圖中能夠看出,雖然人們習慣上仍然將眼前的這個產物稱爲Fiber
樹,但它的數據結構本質其實已經從樹變成了鏈表。
下面來看另外一個completeWork
流程。
上文曾經說過,performUnitOfWork
中在beginWork
流程遍歷到葉子節點以後,next
就會變成null,當次beginWork
流程結束,進入相對應的``completeWork`流程。
從新引用一下上面用到的performUnitOfWork
方法裏的代碼
next = beginWork(current, unitOfWork, subtreeRenderLanes);
if (next === null) {
// If this doesn't spawn new work, complete the current work.
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
複製代碼
completeUnitOfWork
是一個遍歷循環的方法,將會遍歷循環下面幾件事
completeWork
方法EffectList
)插入到其父節點的反作用鏈(EffectList
)中return
掉當前調用,觸發兄弟節點對應的performUnitOfWork
邏輯;而遍歷到父節點時,則會直接進入下一輪循環,也就是重複 一、2 的邏輯咱們先來看completeWork
方法。
completeWork
也是一個體量比較大的函數,咱們只抽離關鍵的邏輯
function completeWork(current, workInProgress, renderLanes) {
// 取出 Fiber 節點的屬性值,存儲在 newProps 裏
var newProps = workInProgress.pendingProps;
// 根據 workInProgress 節點的 tag 屬性的不一樣,決定要進入哪段邏輯
switch (workInProgress.tag) {
......
// h1 節點的類型屬於 HostComponent,所以這裏爲你講解的是這段邏輯
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// 判斷 current 節點是否存在,由於目前是掛載階段,所以 current 節點是不存在的
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
} else {
// 針對異常狀況進行 return 處理
......
// 接下來就爲 DOM 節點的建立作準備了
var currentHostContext = getHostContext();
// _wasHydrated 是一個與服務端渲染有關的值,這裏不用關注
var _wasHydrated = popHydrationState(workInProgress);
// 判斷是不是服務端渲染
if (_wasHydrated) {
......
} else {
// 這一步很關鍵, createInstance 的做用是建立 DOM 節點
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
// appendAllChildren 會嘗試把上一步建立好的 DOM 節點掛載到 DOM 樹上去
appendAllChildren(instance, workInProgress, false, false);
// stateNode 用於存儲當前 Fiber 節點對應的 DOM 節點
workInProgress.stateNode = instance;
// finalizeInitialChildren 用來爲 DOM 節點設置屬性
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
......
}
return null;
}
case HostText:
{
......
}
case SuspenseComponent:
{
......
}
case HostPortal:
......
return null;
case ContextProvider:
......
return null;
......
}
}
複製代碼
首先咱們須要知道,進入這個completeWork
的參數是什麼,咱們知道只有當beginWork
結束,也就是遍歷到第一個葉子節點的時候,纔會進入completeWork
方法。因此,第一次運行的時候的參數,實際上是demo中的h1
標籤相對應的fiber
節點對象。這也是completeWork
的一個特色,是嚴格自底向上運行的。
而後咱們再來看completeWork
方法幾個功能要點
completeWork
的核心邏輯是一段體量巨大的switch
語句,在這段switch
語句中,completeWork
將根據workInProgress
節點的tag
屬性的不一樣,進入不一樣的DOM
節點的建立、處理邏輯。Demo
示例中,h1
節點的tag
屬性對應的類型應該是HostComponent
,也就是原生DOM
元素類型。completeWork
中的current
、workInProgress
就是以前說的current
樹和workInProgress
樹上面的節點。其中workInProgress
樹表明的是「當前正在render
中的樹」,而current
樹則表明「已經存在的樹」。
workInProgress
節點和current
節點之間用alternate
屬性相互鏈接。在組件的掛載階段,current
樹只有一個頂部節點,並無其餘內容。所以h1
這個workInProgress
節點對應的current
節點是null
。
帶着這個前提,咱們再來看看completeWork
方法,咱們能夠總結出
completeWork
其實就是負責處理Fiber
節點到DOM
節點的映射邏輯。經過三個步驟
DOM
節點(CreateInstance
)DOM
節點插入到 DOM 樹中(AppendAllChildren
),賦值給workInProgress
節點的stateNode
屬性(並且當前節點運行AppendAllChildren時,會逐個向下查找本身的後代子 Fiber 節點,並把所對應的 DOM 節點掛載到其父 Fiber 節點所對應的 DOM 節點裏去,因此最上級的節點裏的stateNode屬性,就是一個完整的dom樹)DOM
節點設置屬性(FinalizeInitialChildren
)先來看第三步的代碼實現
以當前節點爲起點,循環遍歷其兄弟節點及其父節點。當遍歷到兄弟節點時,將return
掉當前調用,觸發兄弟節點對應的performUnitOfWork
邏輯;而遍歷到父節點時,則會直接進入下一輪循環,也就是重複 一、2 的邏輯
do {
......
// 這裏省略步驟 1 和步驟 2 的邏輯
// 獲取當前節點的兄弟節點
var siblingFiber = completedWork.sibling;
// 若兄弟節點存在
if (siblingFiber !== null) {
// 將 workInProgress 賦值爲當前節點的兄弟節點
workInProgress = siblingFiber;
// 將正在進行的 completeUnitOfWork 邏輯 return 掉
return;
}
// 若兄弟節點不存在,completeWork 會被賦值爲 returnFiber,也就是當前節點的父節點
completedWork = returnFiber;
// 這一步與上一步是相輔相成的,上下文中要求 workInProgress 與 completedWork 保持一致
workInProgress = completedWork;
} while (completedWork !== null);
複製代碼
功能比較簡單,按demo來講,由於beginWork
流程是一個深度優先遍歷,當遍歷到h1
標籤時,遍歷中斷,開始執行completedWork
流程。h1
的兄弟節點p
標籤,其實連beginWork
流程尚未運行過,因此須要從新調用performUnitOfWork
邏輯。
咱們再來講一下第二步
將當前節點的反作用鏈(EffectList
)插入到其父節點的反作用鏈(EffectList
)中。
這一步的目標其實就是找出界面中須要處理的更新。由於在實際的操做中,並非全部的節點上都會產生須要處理的更新。好比在掛載階段,對整棵workInProgress
樹遞歸完畢後,React
會發現實際只須要對App
節點執行一個掛載操做就能夠了;而在更新階段,這種現象更爲明顯。
怎樣作才能讓渲染器又快又好地定位到那些真正須要更新的節點呢?這就是反作用鏈(effectList
)的功能。
每一個Fiber
節點都維護着一個屬於它本身的effectList
,effectList
在數據結構上以鏈表的形式存在,鏈表內的每個元素都是一個Fiber
節點。這些Fiber
節點須要知足兩個共性:
Fiber
節點的後代節點(並不是它自身的更新,而是其須要更新的後代節點)這個effectList
鏈表在Fiber
節點中是經過firstEffect
和lastEffect
來維護。firstEffect
表示effectList
的第一個節點,而lastEffect
則記錄最後一個節點。
由於completeWork
是自底向上執行的,因此在頂部節點上能夠拿到一個存儲了當前Fiber
樹全部effect Fiber
。
按demo來講,只有頂部的節點纔會存在反作用鏈(App組件的fiber
節點),對於App
組件內的全部子節點都不存在反作用鏈。當首次渲染或者更新的時候,渲染器只會去處理反作用鏈上的App fiber
節點(App
做爲一個最小的更新組件,已經包含了內部子元素的dom節點)。固然若是App
裏面還引用了其餘組件,App
組件的fiber
中也會包含該組件的反作用鏈。
commit
會在performSyncWorkOnRoot
中被調用,它是一個絕對同步的過程。
commitRoot(root);
複製代碼
從流程上來講,commi
共分爲 3 個階段:before mutation
、mutation
、layout
。
before mutation
階段,這個階段DOM
節點尚未被渲染到界面上去,過程當中會觸發 getSnapshotBeforeUpdate
,也會處理useEffect
鉤子相關的調度邏輯。
mutation
,這個階段負責DOM
節點的渲染。在渲染過程當中,會遍歷effectList
,根據 flags(effectTag)
的不一樣,執行不一樣的DOM
操做。
layout
,這個階段處理DOM
渲染完畢以後的收尾邏輯。好比調用 componentDidMount/componentDidUpdate
,調用useLayoutEffect
鉤子函數的回調等。除了這些以外,它還會把fiberRoot
的current
指針指向workInProgress Fiber
樹。
若是本文對你有所幫助,請幫忙點個贊,感謝!