新聞中心
引言
相信大家對 React 都已經非常熟悉了,像 React,Vue 這樣的現代前端框架已經是我們日常開發(fā)離不開的工具了,這篇文章主要是從源碼的角度剖析 React 的核心渲染原理。我們將從用戶編寫的組件代碼開始,一步一步分析 React 是如何將它們變成真實 DOM ,這個過程主要可以分成兩個階段:render 階段和 commit 階段。文章的核心內容也正是對這兩個階段的分析。

專業(yè)從事成都網站制作、網站設計、外貿網站建設,高端網站制作設計,小程序開發(fā),網站推廣的成都做網站的公司。優(yōu)秀技術團隊竭力真誠服務,采用HTML5+CSS3前端渲染技術,自適應網站建設,讓網站在手機、平板、PC、微信下都能呈現。建站過程建立專項小組,與您實時在線互動,隨時提供解決方案,暢聊想法和感受。
一、前置知識
聲明式渲染
『聲明式渲染』,顧名思義,就是讓使用者只需要「聲明或描述」我需要渲染的東西是什么,然后就把具體的渲染工作交給機器去做,與之相對的是『命令式渲染』。
『命令式渲染』則是由用戶去一步一步地命令機器下一步該怎么做。
舉個簡單的例子:
如果我們需要在網頁上渲染一個有三個節(jié)點的列表,命令式的做法是手動操作 dom,首先創(chuàng)建一個容器節(jié)點,再利用循環(huán)每次先創(chuàng)建一個新節(jié)點,填充內容,然后將新節(jié)點新增到容器節(jié)點下,最后再將容器節(jié)點新增到 body 標簽下:
- const list = [1,2,3];
- const container = document.createElement('div');
- for (let i = 0; i < list.length; i ++) {
- const newDom = document.createElement('div');
- newDom.innerHTML = list[i];
- container.appendChild(newDom);
- }
- document.body.appendChild(container);
而聲明式的做法應該是:
- const list = [1,2,3];
- const container = document.createElement('div');
- const Demo = () =>
- (
)- {list.map((item) =>
{item})}- ReactDom.render(
, container);
可以看到在這個例子中,聲明式寫法以 HTML 語法直接告訴機器,我需要的視圖應該是長這個樣子,然后具體的 DOM 操作全部交由機器去完成。開發(fā)者只需要專注于業(yè)務邏輯的實現。
這便是聲明式渲染。
聲明式渲染是現代前端框架的比較普遍的設計思路。
JSX 和 ReactElement
相信大家最初學 React 的時候都有這樣的疑問,為什么我們能夠以類似 HTML 的語法編寫組件,這個東西又是怎么轉換成 JavaScript 語法的?答案就是 Babel。根據官網介紹,這種語法被稱為 JSX,是一個 JavaScript 的語法擴展。能夠被 Babel 編譯成 React.createElement 方法。舉個例子:
通過查閱源碼我們可以看到 「React.createElement」 方法。
- export function createElement(type, config, children) {
- let propName;
- // Reserved names are extracted
- const props = {};
- let key = null;
- let ref = null;
- let self = null;
- let source = null;
- ...
- return ReactElement(
- type,
- key,
- ref,
- self,
- source,
- ReactCurrentOwner.current,
- props,
- );
- }
- const ReactElement = function(type, key, ref, self, source, owner, props) {
- const element = {
- // This tag allows us to uniquely identify this as a React Element
- $typeof: REACT_ELEMENT_TYPE,
- // Built-in properties that belong on the element
- type: type,
- key: key,
- ref: ref,
- props: props,
- // Record the component responsible for creating this element.
- _owner: owner,
- };
- ...
- return element;
- }
可以看到 React 是使用了 element 這種結構來代表一個節(jié)點,里面就只有簡單的 6 個字段。我們可以看個實際的例子,下面 Count 組件對應的 element 數據結構:
- function Count({count, onCountClick}) {
- return
{ onCountClick()}}>- count: {count}
- }
可以看到,element 結構只能反映出 jsx 節(jié)點的層級結構,而組件里的各種狀態(tài)或者返回 jsx 等都是不會記錄在 element 中。
目前我們知道,我們編寫的 jsx 會首先被處理成 element 結構。
jsx -> element
那 React 又是如何處理 element 的,如剛剛說的,element 里包含的信息太少,只靠 element 顯然是不足以映射到所有真實 DOM 的,因此我們還需要更精細的結構。
Fiber 樹結構
Fiber 這個單詞相信大家多多少少都有聽過,它是在 React 16 被引入,關于 Fiber 如何實現任務調度在這篇文章不會涉及,但是 Fiber 的引入不僅僅帶來了任務調度方面的能力,整個 React 實現架構也因此重構了一遍,而我們之前經常提到的虛擬 DOM 樹在新的 React 架構下被稱為 Fiber 樹,上面提到的每個 element 都有一個所屬的 Fiber。
首先我們先看看源碼中 Fiber 的構造函數:
- function FiberNode(
- tag: WorkTag,
- pendingProps: mixed,
- key: null | string,
- mode: TypeOfMode,
- ) {
- // Instance
- this.tag = tag; // 標識節(jié)點類型,例如函數組件、類組件、普通標簽等
- this.key = key;
- this.elementType = null; // 標識具體 jsx 標簽名
- this.type = null; // 類似 elementType
- this.stateNode = null; // 對應的真實 DOM 節(jié)點
- // Fiber
- this.return = null; // 父節(jié)點
- this.child = null; // 第一個子節(jié)點
- this.sibling = null; // 第一個兄弟節(jié)點
- this.index = 0;
- this.ref = null;
- this.pendingProps = pendingProps; // 傳入的 props
- this.memoizedProps = null;
- this.updateQueue = null; // 狀態(tài)更新相關
- this.memoizedState = null;
- this.dependencies = null;
- this.mode = mode;
- // Effects
- this.flags = NoFlags;
- this.subtreeFlags = NoFlags;
- this.deletions = null;
- this.lanes = NoLanes;
- this.childLanes = NoLanes;
- this.alternate = null;
- ...
- }
可以看到 Fiber 節(jié)點中的屬性很多,其中不僅僅包含了 element 相關的實例信息,還包含了組成 Fiber 樹所需的一些“指針”,組件內部的狀態(tài)(memorizedState),用于操作真實 DOM 的副作用(effects)等等。
我們以上面的 Count 組件為例看一下它對應的 Fiber 結構:
這里我們先主要介紹一下與形成 Fiber 樹相關的三個屬性:child, sibling 和 return。他們分別指向 Fiber 的第一個子 Fiber,下一個兄弟 Fiber 和父 Fiber。
以下面的 jsx 代碼為例:
- // App.jsx
- text
- // Count.jsx
最終形成的 Fiber 樹結構為:
總結一下,我們編寫的 jsx 首先會形成 element ,然后在 render 過程中每個 element 都會生成對應的 Fiber,最終形成 Fiber 樹。
jsx -> element -> Fiber
下面我們正式介紹一下 render 的過程,看看 Fiber 是如何生成并形成 Fiber 樹的。
二、渲染(render)過程
核心流程
通常 React 運行時會有兩個 Fiber 樹,一個是根據當前最新組件狀態(tài)構建出來的,另一個則是上一次構建出來的 Fiber 樹,當然如果是首次渲染就沒有上一次的 Fiber 樹,這時就只有一個了。簡單來說,render 過程就是 React 「對比舊 Fiber 樹和新的 element」 然后「為新的 element 生成新 Fiber 樹」的一個過程。
從源碼中看,React 的整個核心流程開始于 「performSyncWorkOnRoot」 函數,在這個函數里會先后調用 「renderRootSync」 函數和 「commitRoot」 函數,它們兩個就是分別就是我們上面提到的 render 和 commit 過程。來看 renderRootSync 函數,在 「renderRootSync」 函數里會先調用 「prepareFreshStack」 ,從函數名字我們不難猜出它主要就是為接下來的工作做前置準備,初始化一些變量例如 workInProgress(當前正在處理的 Fiber 節(jié)點) 等,接著會調用 「workLoopSync」 函數。(這里僅討論傳統(tǒng)模式,concurrent 模式留給 Fiber 任務調度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下來就會調用 commitRoot 進入 commit 階段。
因此整個 render 過程的重點在 「workLoopSync」 中,從 「workLoopSync」 簡單的函數定義里我們可以看到,這里用了一個循環(huán)來不斷調用 「performUnitOfWork」 方法,直到 workInProgress 為 null。
- function workLoopSync() {
- // Already timed out, so perform work without checking if we need to yield.
- while (workInProgress !== null) {
- performUnitOfWork(workInProgress);
- }
- }
而 「performUnitOfWork」 函數做的事情也很簡單,簡單來說就是為傳進來的 workInProgress 生成下一個 Fiber 節(jié)點然后賦值給 workInProgress。通過不斷的循環(huán)調用 「performUnitOfWork」,直到把所有的 Fiber 都生成出來并連接成 Fiber 樹為止。
現在我們來看 「performUnitOfWork」 具體是如何生成 Fiber 節(jié)點的。
前面介紹 Fiber 結構的時候說過,Fiber 是 React 16 引入用于任務調度提升用戶體驗的,而在此之前,render 過程是遞歸實現的,顯然遞歸是沒有辦法中斷的,因此 React 需要使用循環(huán)來模擬遞歸過程。
「performUnitOfWork」 正是使用了 「beginWork」 和 「completeUnitOfWork」 來分別模擬這個“遞”和“歸”的過程。
render 過程是深度優(yōu)先的遍歷,「beginWork」 函數則會為遍歷到的每個 Fiber 節(jié)點生成他的所有子 Fiber 并返回第一個子 Fiber ,這個子 Fiber 將賦值給 workInProgress,在下一輪循環(huán)繼續(xù)處理,直到遍歷到葉子節(jié)點,這時候就需要“歸”了。
「completeUnitOfWork」 就會為葉子節(jié)點做一些處理,然后把葉子節(jié)點的兄弟節(jié)點賦值給 workInProgress 繼續(xù)“遞”操作,如果連兄弟節(jié)點也沒有的話,就會往上處理父節(jié)點。
同樣以上面的 Fiber 樹例子來看,其中的 Fiber 節(jié)點處理順序應該如下:
beginWork
在介紹概覽的時候說過,React 通常會同時存在兩個 Fiber 樹,一個是當前視圖對應的,一個則是根據最新狀態(tài)正在構建中的。這兩棵樹的節(jié)點一一對應,我們用 current 來代表前者,我們不難發(fā)現,當首次渲染的時候,current 必然指向 null。實際上在代碼中也確實都是通過這個來判斷當前是首次渲染還是更新。
「beginWork」 的目的很簡單:
- 更新當前節(jié)點(workInProgress),獲取新的 children。
- 為新的 children 生成他們對應的 Fiber,并「最終返回第一個子節(jié)點(child)」。
在 「beginWork」 執(zhí)行中,首先會判斷當前是否是首次渲染。
- 如果是首次渲染:
- 則下來會根據當前正在構建的節(jié)點的組件類型做不同的處理,源碼中這塊邏輯使用了大量的 switch case。
- switch (workInProgress.tag) {
- case FunctionComponent: {
- ...
- }
- case ClassComponent: {
- ...
- }
- case HostRoot: {
- ...
- }
- case HostComponent: {
- ...
- }
- ...
- }
- 如果非首次渲染:
- React 會使用一些優(yōu)化手段,而符合優(yōu)化的條件則是「當前節(jié)點對應組件的 props 和 context 沒有發(fā)生變化」并且**當前節(jié)點的更新優(yōu)先級不夠,**如果這兩個條件均滿足的話可以直接復制 current 的子節(jié)點并返回。如果不滿足則同首次渲染走一樣的邏輯。
- if (current !== null) {
- // 這里處理一些依賴
- if (
- enableLazyContextPropagation &&
- !includesSomeLane(renderLanes, updateLanes)
- ) {
- const dependencies = current.dependencies;
- if (dependencies !== null && checkIfContextChanged(dependencies)) {
- updateLanes = mergeLanes(updateLanes, renderLanes);
- }
- }
- const oldProps = current.memoizedProps;
- const newProps = workInProgress.pendingProps;
- if (
- oldProps !== newProps ||
- hasLegacyContextChanged() ||
- // Force a re-render if the implementation changed due to hot reload:
- (__DEV__ ? workInProgress.type !== current.type : false)
- ) {
- // 如果 props 或者 context 變了
- didReceiveUpdate = true;
- } else if (!includesSomeLane(renderLanes, updateLanes)) {
- didReceiveUpdate = false;
- // 走到這里則說明符合優(yōu)化條件
- switch (workInProgress.tag) {
- case HostRoot:
- ...
- break;
- case HostComponent:
- ...
- break;
- case ClassComponent: {
- ...
- break;
- }
- case HostPortal:
- ...
- break;
- case ContextProvider: {
- ...
- break;
- }
- ...
- }
- return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
- } else {
- ...
- didReceiveUpdate = false;
- }
- } else {
- didReceiveUpdate = false;
- }
更新優(yōu)化策略應用
開發(fā)過程中我們常常希望利用 React 非首次渲染的優(yōu)化策略來提升性能,如下代碼,B 組件是個純展示組件且內部沒有依賴任何 Demo 組件的數據,因此有些同學可能會想當然認為當 Demo 重新渲染時這個 B 組件是符合 React 優(yōu)化條件的。但結果是,每次 Demo 重新渲染都會導致 B 組件重新渲染。每次渲染時 B 組件的 props 看似沒發(fā)生變化,但由于 Demo 重新執(zhí)行后會生成全新的 B 組件(下面會介紹),所以新舊 B 組件的 props 肯定也是不同的。
- function App() {
- return
- }
- function Demo() {
- const [v, setV] = useState();
- return (
- );
- }
那有什么辦法可以保持住 B 組件不變嗎,答案是肯定的,我們可以把 B 組件放到 Demo 組件外層,這樣一來,B 組件是在 App 組件中生成并作為 props 傳入 Demo 的,因為不管 Demo 組件狀態(tài)怎么變化都不會影響到 App 組件,因此 App 和 B 組件就只會在首次渲染時會執(zhí)行一遍,也就是說 Demo 獲取到的 props.children 的引用一直都是指向同一個對象,這樣一來 B 組件的 props 也就不會變化了。
- function App() {
- return
- }
- function Demo(props) {
- const [v, setV] = useState();
- return (
- );
- }
更新當前節(jié)點
通過上面的解析我們知道,當不走優(yōu)化邏輯時 「beginWork」 使用大量的 switch...case 來分別處理不同類型的組件,下來我們以我們熟悉的 Function Component 為例。
「核心就是通過調用函數組件,得到組件的返回的 element?!?/p>
類似地,對于類組件,則是調用組件實例的 render 方法得到 element。
而對于我們普通的組件,例如
,則是直接取 props.children 即可。
- function updateFunctionComponent(
- current,
- workInProgress,
- Component,
- nextProps: any,
- renderLanes,
- ) {
- let context;
- if (!disableLegacyContext) {
- const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
- context = getMaskedContext(workInProgress, unmaskedContext);
- }
- let nextChildren;
- prepareToReadContext(workInProgress, renderLanes);
- // 執(zhí)行組件函數獲取返回的 element
- nextChildren = renderWithHooks(
- current,
- workInProgress,
- Component,
- nextProps,
- context,
- renderLanes,
- );
- // React DevTools reads this flag.
- workInProgress.flags |= PerformedWork;
- reconcileChildren(current, workInProgress, nextChildren, renderLanes);
- return workInProgress.child;
- }
得到組件返回的 element(s) 之后,下一步就是為他們生成 Fiber,我們查看源碼可以看到,不論是函數組件或是類組件或是普通組件,最后返回的 element(s) 都會作為參數傳入到 「reconcileChildren」 中。
介紹 「reconcileChildren」 之前我們先用一張圖總結一下 「beginWork」 的大致流程:
生成子節(jié)點
經過上一步得到 workInProgress 的 children 之后,接下來需要為這些 children element 生成 Fiber ,這就是 「reconcileChildFibers」 函數做的事情,這也是我們經常提到的 diff 的過程。
這個函數里主要分兩種情況處理,如果是 newChild(即 children element)是 object 類型,則進入單節(jié)點 diff 過程(「reconcileSingleElement」),如果是數組類型,則進入多節(jié)點 diff 過程(「reconcileChildrenArray」)。
- function reconcileChildFibers(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- newChild: any,
- lanes: Lanes,
- ): Fiber | null {
- if (typeof newChild === 'object' && newChild !== null) {
- switch (newChild.$typeof) {
- case REACT_ELEMENT_TYPE:
- return placeSingleChild(
- reconcileSingleElement(
- returnFiber,
- currentFirstChild,
- newChild,
- lanes,
- ),
- );
- ...
- }
- if (isArray(newChild)) {
- return reconcileChildrenArray(
- returnFiber,
- currentFirstChild,
- newChild,
- lanes,
- );
- }
- throwOnInvalidObjectType(returnFiber, newChild);
- }
- }
單節(jié)點diff
- function reconcileSingleElement(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- element: ReactElement,
- lanes: Lanes,
- ): Fiber {
- const key = element.key;
- let child = currentFirstChild;
- while (child !== null) {
- // 首先比較 key 是否相同
- if (child.key === key) {
- const elementType = element.type;
- ...
- // 然后比較 elementType 是否相同
- if (child.elementType === elementType) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props);
- existing.ref = coerceRef(returnFiber, child, element);
- existing.return = returnFiber;
- return existing;
- }
- // Didn't match.
- deleteRemainingChildren(returnFiber, child);
- break;
- } else {
- deleteChild(returnFiber, child);
- }
- // 遍歷兄弟節(jié)點,看能不能找到 key 相同的節(jié)點
- child = child.sibling;
- }
- if (element.type === REACT_FRAGMENT_TYPE) {
- const created = createFiberFromFragment(
- element.props.children,
- returnFiber.mode,
- lanes,
- element.key,
- );
- created.return = returnFiber;
- return created;
- } else {
- const created = createFiberFromElement(element, returnFiber.mode, lanes);
- created.ref = coerceRef(returnFiber, currentFirstChild, element);
- created.return = returnFiber;
- return created;
- }
- }
本著盡可能復用舊節(jié)點的原則,在單節(jié)點 diff 在這里,我們會遍歷舊節(jié)點,對每個遍歷到的節(jié)點會做一下兩個判斷:
- key 是否相同
- key 相同的情況下,elementType 是否相同
延伸下來有三種情況:
- 如果 key 不相同,則直接調用 「deleteChild」 將這個 child 標記為刪除,但是我們不用灰心,可能只是我們還沒有找到那個對的節(jié)點,所以要繼續(xù)執(zhí)行child = child.sibling;遍歷兄弟節(jié)點,直到找到那個對的節(jié)點。
- 如果 key 相同,elementType 相同,那就是最理想的情況,找到了可以復用的節(jié)點,直接調用 「deleteRemainingChildren」 把剩余的兄弟節(jié)點標記刪除,然后直接復用 child 返回。
- 如果 key 相同,但 elementType 不同,這是最悲情的情況,我們找到了那個節(jié)點,可惜的是這個節(jié)點的 elementType 已經變了,那我們也不需要再找了,把 child 及其所有兄弟節(jié)點標記刪除,跳出循環(huán)。直接創(chuàng)建一個新的節(jié)點。
多節(jié)點diff
- function reconcileChildrenArray(
- returnFiber: Fiber,
- currentFirstChild: Fiber | null,
- newChildren: Array<*>,
- lanes: Lanes,
- ) {
- let resultingFirstChild: Fiber | null = null;
- let previousNewFiber: Fiber | null = null;
- let oldFiber = currentFirstChild;
- let lastPlacedIndex = 0;
- let newIdx = 0;
- let nextOldFiber = null;
- for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
- const newFiber = updateSlot(
- returnFiber,
- oldFiber,
- newChildren[newIdx],
- lanes,
- );
- if (newFiber === null) {
- break;
- }
- lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
- if (previousNewFiber === null) {
- resultingFirstChild = newFiber;
- } else {
- previousNewFiber.sibling = newFiber;
- }
- previousNewFiber = newFiber;
- oldFiber = nextOldFiber;
- }
- if (newIdx === newChildren.length) {
- ...
- }
- if (oldFiber === null) {
- ...
- }
- for (; newIdx < newChildren.length; newIdx++) {
- ...
- }
- return resultingFirstChild;
- }
- function updateSlot(
- returnFiber: Fiber,
- oldFiber: Fiber | null,
- newChild: any,
- lanes: Lanes,
- ): Fiber | null {
- const key = oldFiber !== null ? oldFiber.key : null;
- ...
- if (newChild.key === key) {
- return updateElement(returnFiber, oldFiber, newChild, lanes);
- } else {
- return null;
- }
- }
從源碼我們可以看到,在 「reconcileChildrenArray」 中,出現了兩個循環(huán)。
第一輪循環(huán)中邏輯如下:
- 同時遍歷 oldFiber 鏈和 newChildren,判斷 oldFiber 和 newChild 的 key 是否相同。
- 如果 key 相同。
- 判斷雙方 elementType 是否相同。
- 如果相同則復用 oldFiber 返回。
- 如果不同則新建 Fiber 返回。
- 如果 key 不同則直接跳出循環(huán)。
可以看到第一輪循環(huán)只要碰到新舊的 key 不一樣時就會跳出循環(huán),換句話說,第一輪循環(huán)里做的事情都是基于 key 相同,主要就是「更新」的工作。
跳出循環(huán)后,要先執(zhí)行兩個判斷:
- newChildren 已經遍歷完了:這種情況說明新的 children 全都已經處理完了,只要把 oldFiber 和他所有剩余的兄弟節(jié)點刪除然后返回頭部的 Fiber 即可。
- 已經沒有 oldFiber :這種情況說明 children 有新增的節(jié)點,給這些新增的節(jié)點逐一構建 Fiber 并鏈接上,然后返回頭部的 Fiber 即可。
如果以上兩種情況都不是,則進入第二輪循環(huán)。
在執(zhí)行第二輪循環(huán)之前,先把剩下的舊節(jié)點和他們對應的 key 或者 index 做成映射,方便查找。
第二輪循環(huán)沿用了第一輪循環(huán)的 newIdx 變量,說明第二輪循環(huán)是在第一輪循環(huán)結束的地方開始再次遍歷剩下的 newChildren。
- const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
- for (; newIdx < newChildren.length; newIdx++) {
- const newFiber = updateFromMap(
- existingChildren,
- returnFiber,
- newIdx,
- newChildren[newIdx],
- lanes,
- );
- if (newFiber !== null) {
- if (shouldTrackSideEffects) {
- if (newFiber.alternate !== null) {
- existingChildren.delete(
- newFiber.key === null ? newIdx : newFiber.key,
- );
- }
- }
- lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
- if (previousNewFiber === null) {
- resultingFirstChild = newFiber;
- } else {
- previousNewFiber.sibling = newFiber;
- }
- previousNewFiber = newFiber;
- }
- }
- function placeChild(
- newFiber: Fiber,
- lastPlacedIndex: number,
- newIndex: number,
- ): number {
- newFiber.index = newIndex;
- if (!shouldTrackSideEffects) {
- // Noop.
- return lastPlacedIndex;
- }
- const current = newFiber.alternate;
- if (current !== null) {
- const oldIndex = current.index;
- if (oldIndex < lastPlacedIndex) {
- // This is a move.
- newFiber.flags |= Placement;
- return lastPlacedIndex;
- } else {
- // This item can stay in place.
- return oldIndex;
- }
- } else {
- // This is an insertion.
- newFiber.flags |= Placement;
- return lastPlacedIndex;
- }
- }
第二輪循環(huán)主要調用了 「updateFromMap」 來處理節(jié)點,在這里需要用 newChild 的 key 去 existingChildren 中找對應的 Fiber。
- 能找到 key 相同的,則說明這個節(jié)點只是位置變了,是可以復用的。
- 找不到 key 相同的,則說明這個節(jié)點應該是新增的。
不管是復用還是新增,「updateFromMap」 都會返回一個 newFiber,然后我們需要為這個 newFiber 更新一下它的位置(index),但是僅僅更新這個 Fiber 的 index 還不夠,因為這個 Fiber 有可能是復用的,如果是復用的就意味著它已經有對應的真實 DOM 節(jié)點了,我們還需要復用它的真實 DOM,因此需要對應更新這個 Fiber 的 flag,但是真的需要對每個 Fiber 都去設置 flag 嗎,我們舉個例子:
- // 舊
- [, , ]
- // 新
- [, , ]
如果按照我們剛剛說的做法,這里的 a, b, c 都會被打上 flag,這樣一來,在 commit 階段,這三個 DOM 都會被移動,可是我們知道,這里顯然只需要移動一個節(jié)點即可,退一萬步說我們移動兩個節(jié)點也比移動所有節(jié)點要來的聰明。
其實在這個問題上主要就是我們得區(qū)分一下到底哪個節(jié)點才是移動了的,這就需要一個參照點,我們要保證在參照點左邊都是已經排好順序了的。而這個參照點就是 lastPlacedIndex。有了它,我們在遍歷 newChildren 的時候可能會出現下面兩種情況:
- 生成(或復用)的 Fiber 對應的老 index < lastPlacedIndex,這就說明這個 Fiber 的位置不對,因為 lastPlacedIndex 左邊的應該全是已經遍歷過的 newChild 生成的 Fiber。因此這個 Fiber 是需要被移動的,打上 flag。
- 如果 Fiber 對應的老 index >= lastPlacedIndex,那就說明這個 Fiber 的相對位置是 ok 的,可以不用移動,但是我們需要更新一下參照點,把參照點更新成這個 Fiber 對應的老 index。
我們舉一個例子:
- // 舊
- [, , , ]
- // 新
- [, , , , ]
lastPlacedIndex 初始值為 0,
首先處理
文章標題:一文讀懂React組件渲染核心原理
本文網址:http://www.dlmjj.cn/article/dpjoepc.html


咨詢
建站咨詢
