[译] Fiber内幕:深入概述React新的协调算法
原文地址:medium.com/react-in-de…
如何以及為何從React組件到Fiber節點的一切內容
React使用一個構建用戶界面的JavaScript庫,它的核心機制是跟蹤組件狀態的的變化,然后將更新的狀態投影在屏幕上。在React中,我們把這個過程稱為協調。我們調用setState方法后,框架會檢測state和prop是否發生變化,并重新渲染UI組件。
React文檔關于這個機制提供了很好的高層面概述: React元素的角色,生命周期方法,render方法,以及應用于子組件的diff算法。由render方法返回的不可變的React元素普遍被認為是React的“虛擬DOM”。那個術語早期幫助React解釋給人們,但它也造成了一些困惑,也不再在React文檔中使用,這篇文章中,我會繼續稱之為React元素的樹。
除了React元素的樹,框架總還有用于保留狀態的內部實例(組件,DOM節點等)的一棵樹。從16版本開始,React推出了內部實例樹的新實現,以及管理它的算法(代碼上稱為Fiber)。想要得知Fiber架構帶來的好處,可以參見The how and why on React’s usage of linked list in Fiber。
這篇文章花費了我很多時間,而且要是沒有 Dan Abramov! ?的個幫助,也不會講解地如此全面。
這是給你講解React內部架構系列的第一篇文章。這篇文章中,我想提供算法中重要概念和數據結構的深度概述。一旦我們有足夠的背景,我們就可以探索這個算法以及用于遍歷和操作fiber樹的主要方法。系列中的下一篇文章將示范React如何使用這個算法來初始render以及操作state和props的更新,從那里我們將了解到調度(scheduler)的細節、子協調(child reconciliation)操作以及構建更新鏈表(effect list)。
這里我將給你講述相當高級的內容,我保證你閱讀后可以理解到并發(Concurrent)React內部工作背后的神奇。如果你想成為React的貢獻者的話,這個系列的文章也可以作為你的向導。我一個逆向代碼的虔誠者(就是喜歡死磕源碼),所以這里有很多關于React@16.6.0的資源鏈接。
這確實牽扯很多內容,所以如果你沒有馬上理解也不必有很大壓力,一切都值得花時間。需要注意的是你不必了解這些來使用React,這篇文章是關于React如何內部工作的。
設置一個背景
這里有個我們在整個系列中都會使用到的簡單應用。我們有個button,簡單的增加數字,然后渲染到屏幕上。
這是實現:
class ClickCounter extends React.Component {constructor(props) {super(props);this.state = {count: 0};this.handleClick = this.handleClick.bind(this);}handleClick() {this.setState((state) => {return {count: state.count + 1};});}render() {return [<button key="1" onClick={this.handleClick}>Update counter</button>,<span key="2">{this.state.count}</span>]} } 復制代碼你可以在這里去執行它。正如你看到的,它是一個簡單組件,通過render方法返回button和span兩個子組件。只要你點擊button,組件的狀態就會在處理器中更新,這繼而導致span元素中的text的更新。
React在**協調(reconciliation)**期間有執行很多活動,例如,React在第一次render時執行的操作,以及在我們這個簡單的應用中狀態更新之后:
- 更新ClickCounter的state中的count參數
- 獲取和比較ClickCounter的子組件以及他們的props
- 更新span元素的props
在協調期間還執行其他活動,像聲明周期方法或者更新refs。所有這些活動在Fiber架構中統一起來被定義為一個“工作(work)”。工作的類型通常取決于React元素(element)的類型,例如,對于一個類組件(class component),React需要創建實例,而對于方法組件(function component)則不需要這樣。正如你所知,React中有很多種元素,如類組件、方法組件、host組件(DOM節點)以及Portal等。元素的類型被定義在createElement方法中的第一個參數,這個方法通常用在render方法中來場景一個元素。
在我們探索這些執行的活動以及主要的Fiber算法時,我們先來對React內部使用的數據結構有個認識。
從React元素到Fiber節點
React中每個組件是一個UI表示,我們可以叫它視圖(view)或者模板(template),它由render方法返回。這里便是我們ClickCounter的模板:
<button key="1" onClick={this.onClick}>Update counter</button> <span key="2">{this.state.count}</span> 復制代碼React元素(Elements)
一旦模板經過JSX編譯,最終獲得一串React元素。這就是React組件的render方法真實返回的東西,而不是HTML。因為我們沒有要求使用JSX,所以ClickCounter組件的render方法也可以寫成:
class ClickCounter {...render() {return [React.createElement('button',{key: '1',onClick: this.onClick},'Update counter'),React.createElement('span',{key: '2'},this.state.count)]} } 復制代碼render方法中的React.createElement調用可以創建兩個數據結構:
[{$$typeof: Symbol(react.element),type: 'button',key: "1",props: {children: 'Update counter',onClick: () => { ... }}},{$$typeof: Symbol(react.element),type: 'span',key: "2",props: {children: 0}} ] 復制代碼你可以看到React在這些對象上添加$$typeof當作React元素的來唯一標示,且我們還有type、key和props來描述這個元素,這些值由你傳給了React.createElement方法。這里注意,React是如何把文本內容表達成span和button節點的孩子,click處理如何成為button元素props的一部分,這里還有React元素上其他一些字段如ref已經超出了本文的范疇。
React元素ClickCounter沒有任何props或者key:
{$$typeof: Symbol(react.element),key: null,props: {},ref: null,type: ClickCounter } 復制代碼Fiber節點
在**協調(reconciliation)**期間,由render方法返回的每個React元素都將合并到fiber節點的樹中,每個React元素都有相對應的fiber節點,不像React元素,fiber不會在每次render時重新創建。這些可變的數據結構帶有組件的狀態以及DOM。
我們之前討論的是框架根據React元素的類型來執行不同的活動,在我們簡單的應用中,對于類組件ClickCounter,它調用生命周期和render方法,而spanhost組件(DOM節點)則執行DOM變化,所以每個React元素轉化成類型相對應的Fiber節點,這些類型描述了需要完成的工作。
當React元素首次被轉化成fiber節點時,React在createFiberFromTypeAndProps方法中使用這個元素中的數據創建fiber,在之后發生的更新中,React重用這個fiber節點,且通過相對應的React元素中數據更新必要的屬性。
React也可能需要基于key屬性在層級中移動節點,或者如果對應的React元素不再由render方法返回時,則刪除掉它。
找出ChildReconciler方法,你可以看到所有活動的列表,以及React在當前存在fiber節點上執行的對應方法。
因為React為每個React元素都創建了一個fiber,所以只要我們有這些元素的一棵樹,那我們就會有fiber節點的一棵樹。在我們簡單應用案例中,它看起來如下:
所有fiber節點通過一個鏈表鏈接起來,這個鏈表使用了fiber節點中屬性:child、sibling和return。關于如何和為何這種方式,可以查閱我的文章The how and why on React’s usage of linked list in Fiber。
當前和正在執行的樹(Current and work in progress trees)
在第一次渲染(render)之后,React最后得到了一顆fiber樹,它反映了用于渲染UI的應用的狀態,這顆樹被當作current。當React開始處理更新時,它構建所謂的workInProgress樹來反映將來刷新屏幕的狀態。
所有工作都在來自workInProgress樹的fiber上執行。當React經過當前樹時,對于每一個先存在的fiber節點,它都會創建一個替代(alternate)節點,這些節點組成了workInProgress樹。這個節點是使用render方法返回的React元素的數據創建的。一旦更新處理完以及所有相關工作完成,React就有一顆替代樹來準備刷新屏幕。一旦這顆workInProgress樹渲染(render)在屏幕上,它便成了當前樹。
React的設計原則之一是連貫性。React總是一次性更新DOM,而不是只顯示部分結果。這顆workInProgress樹為當做是‘草稿’,它對用戶是不可見的,以至于React可以先處理所有組件,然后再刷新他們的改變到屏幕上。
在這個代碼中,可以看到很多方法,這些方法持有來著current和workInProgress樹的fiber節點,這是一個這樣方法的簽名:
function updateHostComponent(current, workInProgress, renderExpirationTime) {...} 復制代碼每個fiber節點中的alternate字段持有它的一個副本,這個副本節點表示current樹指向workInProgress樹的,反之亦然,代碼如下:
function createWorkInProgress(current, ...) {let workInProgress = current.alternate;if (workInProgress === null) {workInProgress = createFiber(...);}...workInProgress.alternate = current;current.alternate = workInProgress;...return workInProgress; } 復制代碼副作用(side-effects)
我們可以認為React中組件是使用state和props的方法,用于計算UI展示。每個其他活動,像DOM變化或者調用生命周期方法,應當認為是一個副作用,或者一個簡單的作用。作用(Effects)也在這個文檔中提及。
你以前可能做過請求數據,訂閱,或者在React組件中手動修改DOM,我們把這些操作叫做副作用(或者簡說作用),因為它們會影響其他組件,且不能在渲染時完成。
你可以看到很多state和props是如何造成副作用的,
既然應用副作用是一個工作的類型,那一個fiber節點就是除了更新之外還用于跟蹤作用的簡明機制。每一個fiber節點可以有很多關聯的作用,它們被編碼到effectTag字段中(effectTag使用位運算的妙處啦)。
所以Fiber中的作用(effects)基本上定義了一個組件實例在其更新操作之后需要完成的工作(work),對于host組件(DOM元素),這個工作包括更新、添加和刪除元素;對于類組件,React可能需要更新refs,以及調用componentDidMount和componentDidUpdate生命周期方法。這里當然還有其他一些與fiber類型相對應的作用。
作用列表(Effects list)
React處理更新很快,為了實現這個層次的性能,它采用了個別有趣的技巧,比如,將含有作用的fiber節點用線性列表表示,從而可以快速迭代。迭代線性列表比樹要快,且可以不必花時間在沒有副作用的節點上。
這個列表的目的是用于標記一些節點,這些節點有DOM更新或者與其關聯的副作用。這個列表是finishedWork的子集,且通過nextEffect屬性鏈接起來,而不是current和workInProgress樹中使用的child屬性。
Dan Abramov對作用列表作了一個類比,就像一顆圣誕樹中通過“圣誕燈”來把所有作用節點連接起來。為了虛擬化它,我們設想以下這顆fiber樹,其中點亮的節點有一些工作要做,例如,我們更新使得c2插入DOM中、d2和c1改變屬性,以及d2觸發生命周期方法,那作用列表就講它們連接起來,以至于React之后可以濾過其他節點:
你可以看到含有作用的節點和如何連接起來。當要遍歷這些節點時,React使用firstEffect得出列表從哪里開始,那上述的示意圖可以用線性列表如下表示:
正如你所見,React執行作用的順序是從子向上到父的。
Fiber樹的根節點(Root of the fiber tree)
每個React應用有一個或多個DOM元素作為容器,在我們的例子中,它是ID是container的div元素:
const domContainer = document.querySelector('#container'); ReactDOM.render(React.createElement(ClickCounter), domContainer); 復制代碼React為這些容器的每個創建一個fiber根節點,你可以通過DOM元素的引用訪問它:
const fiberRoot = query('#container')._reactRootContainer._internalRoot 復制代碼這個fiber根節點就是React持有fiber樹引用的地方,它保存在fiber根節點的current屬性上:
const hostRootFiberNode = fiberRoot.current 復制代碼fiber樹開始于HostRoot的fiber節點的一個特殊類型,它由內部創建并將頂層組件作為父節點。這里有一個通過從stateNode屬性從HostRootfiber節點返回到FiberRoot的連接:
fiberRoot.current.stateNode === fiberRoot; // true 復制代碼你可以通過fiber根節點獲取HostFiber節點來探索fiber樹,或者你可以像這樣從組件實例中獲取獨立的fiber節點:
compInstance._reactInternalFiber 復制代碼Fiber節點結構
我們來看一下由ClickCounter組件創建的fiber節點的數據結構:
{stateNode: new ClickCounter,type: ClickCounter,alternate: null,key: null,updateQueue: null,memoizedState: {count: 0},pendingProps: {},memoizedProps: {},tag: 1,effectTag: 0,nextEffect: null } 復制代碼以及spanDOM元素:
{stateNode: new HTMLSpanElement,type: "span",alternate: null,key: "2",updateQueue: null,memoizedState: null,pendingProps: {children: 0},memoizedProps: {children: 0},tag: 5,effectTag: 0,nextEffect: null } 復制代碼fiber節點中有許多字段,我已經在前面有描述字段alternate、effectTag和nextEffect的目的,現在我看看為何還需要其他字段。
stateNote
持有類組件實例、DOM節點或者其他與這個fiber節點關聯的React元素類型的引用,一般來說,我們可以說這個屬性用來持有與fiber相關的本地狀態。
type
定義與這個fiber關聯的方法或者類,對于類組件,它指向類的構造方法;對于DOM元素,它具體為HTML標簽;我經常用這個字段來理解fiber節點關聯的是什么元素。
tag
定義了fiber的類型,這個用來在協調算法中定義那些工作需要完成。如之前所說,工作的不同取決于React元素類型,方法createFiberFromTypeAndProps映射了一個React元素到相對應fiber節點類型。在我們的例子應用中,ClickCounter組件的tag屬性值為1,代表了ClassComponent,以及span組件的是5,代表了HostComponent。
updateQueue
一個包括狀態更新、callbacks以及DOM更新的隊列。
memoizedState
fiber中用于創建輸出的狀態,當處理更新時,它反映了當前已經渲染在屏幕上的狀態。
memoizedProps
fiber中在前一次渲染時用于創建輸出的props。
pendingProps
由React元素中的新數據而來的已經更新過的props,且需要應用于子組件或者DOM元素。
key
一組子組件的唯一標示,用于React得出列表中哪個改變了、添加了或者刪除了。這個與React中在這里描述的的“列表與key”的功能相關。
你可以從這里得到fiber節點的整個數據結構。我濾過了一些上面解釋過的字段。特別是我跳過了**child、sibling和return,這些在我前一篇文章中介紹過了。還有一類字段像expirationTime、childExpirationTime以及mode特定用于調度(Scheduler)**。
整體算法
React執行工作主要有兩個階段:render 和 commit。
在第一個render階段,React執行由setState或者React.render調度的組件上的更新,且得出在UI上哪些需要被更新,如果它是初始渲染,那React會為每個有render方法返回的元素創建一個新的fiber節點,在后續的更新中,當前存在React元素的fiber會被重用和更新。這個階段的結果是一顆fiber節點被標記副作用的樹。這些作用被描述為在接下來的commit階段中需要完成的工作,在這個階段,React取標記作用的fiber樹并把它們應用到實例上,遍歷作用列表且執行DOM更新以及其他用戶可見的變化。
理解在第一個render階段中執行的工作可以是異步的很重要。React在可用的時間內能處理一個或多個fiber節點,然后停止來保存完成的工作并妥協于一些事件(比如優先級高的UI事件),它之后可以在之前離開的方法在繼續執行,然而有時可能會丟棄已完成的工作,并從頂層重來。由于這個階段的執行的工作不會導致用戶可見的變化(如DOM更新),所以這個暫停是可行的。不同的是,接下來的commit階段總是同步的,因為這個階段的執行的工作會導致用戶可見的變化,這也是為什么React一把完成它們的原因。
調用生命周期方法是React執行的一種工作類型,一些方法執行在render階段,一些執行在commit階段,下面是在render階段中執行的生命周期方法列表:
- [UNSAFE_]componentWillMount (deprecated)
- [UNSAFE_]componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- [UNSAFE_]componentWillUpdate (deprecated)
- render
正如你所見,一些在render階段中被遺留的方法從16.3版本開始被標記為UNSAFE,它們在16.x的release版本中被棄用掉了,而它們不帶UNSAFE前綴的副本在17.0中將被移除,你可以在這里關于這些改變,以及遷移建議的內容。
你好奇這個的原因嗎?
那,我們已經得知**render階段不會造成副作用(如DOM更新),且React可以對組件異步處理更新(且在的說,甚至可以在多線程中執行)。然后被標記了UNSAFE的聲明周期總是被誤解或者不易察覺的誤用,開發者傾向于把有副作用的邏輯放在這些方法中,這在新的異步渲染策略中可能會導致一些問題。盡管只有他們未標記UNSAFE**前綴的副本被移除掉了,但它們仍然可能在未來的并發模型(Concurrent Mode)中造成問題,當然這個模式你可以不啟用。
這里是**commit**階段執行的生命周期方法列表:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
因為這些方法在同步的**commit**階段執行,所有它們可以包含副作用以及觸控DOM。
好,我們現在已經有了一定的基礎去看看用于遍歷樹和執行工作的算法。
Render階段
這個協調算法總是開始于頂層的HostRootfiber節點,這個節點由renderRoot方法創建,然而React能夠跳過已經處理過的fiber節點,直到它找到尚未完成工作的節點,例如,如果你在一個組件樹深處調用setState,React將從頂部開始,但是很快就跳過一些節點,找到調用setState方法的組件。
工作循環(work loop)中的主要步驟
所有的fiber節點都會在work loop做處理,這里是這個循環的同步部分的實現:
function workLoop(isYieldy) {if (!isYieldy) {while (nextUnitOfWork !== null) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);}} else {...} } 復制代碼在上面代碼中,nextUnitOfWork持有一個fiber節點,這個節點來自還有一些工作需要做的workInProgress樹,正如React遍歷fiber樹一樣,它使用這個變量來知道是否還有其他未完成工作的fiber節點,當前fiber節點處理之后,這個遍歷將要么獲取下一個fiber節點的引用,要么為null,在**null**的情況下,React將退出工作循環,并準備提交更新。
這里有4個主要的方法,用于遍歷樹,以及初始化或者完成工作:
- performUnitOfWork
- beginWork
- completeUnitOfWork
- completeWork
為了示例它們是怎么用的,看看下面遍歷fiber樹的動畫。我以及用demo把這些方法做了個簡單是的實現,每個方法都會取fiber節點來處理,正如React沿著樹往下走時,你可以看到當前活躍fiber節點的變化,這個視頻中你可以清晰地看到算法是如何從一個樹枝走到另一個樹枝的,它在移動到父節點前,首先得先完成子節點的工作。
注意:垂直直線連接代表兄弟,拐彎連接代表父子,如**b1沒有子節點,而b2有一個c1**孩子。
這是視頻連接,其中你可以暫停播放,查看當前節點和方法的狀態。概念上,你可以把“begin”當作進入組件,把“complete”當作離開組件,你也可以在這里執行這個例子和實現,正如我解釋這些方法所做的事情。
我們從頭兩個方法**performUnitOfWork和beginWork**開始:
function performUnitOfWork(workInProgress) {let next = beginWork(workInProgress);if (next === null) {next = completeUnitOfWork(workInProgress);}return next; }function beginWork(workInProgress) {console.log('work performed for ' + workInProgress.name);return workInProgress.child; } 復制代碼performUnitOfWork方法從workInProgress中接受一個fiber節點,調用beginWork方法來開始工作。fiber節點上需要執行的所有活動都將從這個方法開始,對于這個示例的目的,我們只打印一下組件名稱,就當是工作已經完成了。beginWork總是返回一個指針,指向循環中要處理的下一個子節點,或者指向null。
如果有下一個子節點,它會在**workLoop方法中賦值給nextUnitOfWork變量,然后,如果沒有子節點,React知道到達了樹枝的末尾,所有就可以完成(complete)當前這個節點。一個節點只要完成了,它就會需要從兄弟和父級節點繼續執行工作,這在completeUnitOfWork**方法中進行:
function completeUnitOfWork(workInProgress) {while (true) {let returnFiber = workInProgress.return;let siblingFiber = workInProgress.sibling;nextUnitOfWork = completeWork(workInProgress);if (siblingFiber !== null) {// If there is a sibling, return it// to perform work for this siblingreturn siblingFiber;} else if (returnFiber !== null) {// If there's no more work in this returnFiber,// continue the loop to complete the parent.workInProgress = returnFiber;continue;} else {// We've reached the root.return null;}} }function completeWork(workInProgress) {console.log('work completed for ' + workInProgress.name);return null; } 復制代碼你可以從中看到方法大致是一個大的**while循環,React當workInProgress節點沒有孩子時就進入這個方法。在完成當前fiber的工作后,它檢查是否有還有兄弟,如果有,React退出這個方法,并返回指向兄弟的指針,它將會賦值給nextUnitOfWork**變量,然后React將通過兄弟節點在新樹枝上開始執行工作。重要的是明白這種情況中React只有是前面的兄弟節點完成了工作,而它還沒有完成父節點的工作,只有所有開始于子節點的樹枝上的工作完成了,它才算是為父節點完成了工作,然后原路返回。
正如你從實現中所見,**performUnitOfWork和completeUnitOfWork方法的目的幾乎是迭代,而主要活動發生在beginWork和completeWork方法中。在這個系列接下來的文章中,我們將知道,當React進入beginWork和completeWork方法中時,ClickCounter組件和span**節點會發生什么。
Commit階段
這個階段開始于completeRoot方法,這里React便會更新DOM,以及調用前前后置突變生命周期方法。
當React進入這個階段時,它有兩顆樹和一個作用列表,第一顆樹表示了當前渲染在屏幕上的狀態,而這里還有在**render階段構建的一顆替代樹,它調用代碼中finishedWork和workInProgress,表示需要在屏幕上反應出來的狀態,這顆替代樹鏈接方式類似當前樹,通過child和sibling**指針鏈接。
還有作用列表——通過**nextEffect連接起來的finishedWork樹的節點子集。記住作用列表是在render**階段生成,整個渲染(rendering)的要點就是得出哪些節點需要插入、更新、刪除,以及哪些組件需要執行它們的生命周期方法,這便是作用列表要告訴我們的,這是會在commit階段中被迭代的節點集合。
為了debugging,當前樹可以通過fiber根節點**current屬性方法,finishedWork樹可以通過當前樹上的HostFiber節點的alternate**來訪問。
主要運行在commit階段的方法是commitRoot,大致如下操作:
- 標記了**Snapshot作用的節點執行getSnapshotBeforeUpdate**生命周期方法。
- 標記了**Deletion作用的節點執行componentWillUnmount**生命周期方法。
- 執行所有DOM的插入、更新和刪除。
- 把**finishedWork**樹置為當前樹。
- 標記了**Placement作用的節點執行componentDidMount**生命周期方法。
- 標記了**Update作用的節點執行componentDidUpdate**生命周期方法。
調用前置突變方法**getSnapshotBeforeUpdate之后,React提交了樹中所有副作用。它以兩個步驟來做,第一步是執行所有DOM(host)的插入、更新和刪除以及ref的卸載,然后React把finishedWork樹賦值給FiberRoot,即當workInProgress樹為current樹,這在commit階段的第一步和第二步之間執行,便于之前的樹在componentWillUnmount是還是當前樹,而在componentDidMount/Update**時,完成樹(finished work)為當前樹。在第二步中,React調用其他所有生命周期方法和ref回調,這些方法在單獨步驟中執行,以致整個樹中所有的替換、更新和刪除已經被調用。
這里運行上述描述方法的大意:
function commitRoot(root, finishedWork) {commitBeforeMutationLifecycles()commitAllHostEffects();root.current = finishedWork;commitAllLifeCycles(); } 復制代碼每個子方法都實現了一個循環來迭代作用列表以及檢查作用類型,當發現和這個方法目的有關的作用,就應用它。
前置突變(Pre-mutation)生命周期方法
例如這里的一個代碼,迭代作用樹,并檢查一個節點是否是**Snapshot**作用:
function commitBeforeMutationLifecycles() {while (nextEffect !== null) {const effectTag = nextEffect.effectTag;if (effectTag & Snapshot) {const current = nextEffect.alternate;commitBeforeMutationLifeCycles(current, nextEffect);}nextEffect = nextEffect.nextEffect;} } 復制代碼對于類組件來說,這個作用意味著調用**getSnapshotBeforeUpdate**方法。
DOM更新
commitAllHostEffects是React執行DOM更新的方法,這個方法定義了節點需要完成操作的類型,且執行它:
function commitAllHostEffects() {switch (primaryEffectTag) {case Placement: {commitPlacement(nextEffect);...}case PlacementAndUpdate: {commitPlacement(nextEffect);commitWork(current, nextEffect);...}case Update: {commitWork(current, nextEffect);...}case Deletion: {commitDeletion(nextEffect);...}} } 復制代碼有趣的是,React在刪除操作中,把**commitDeletion方法中調用componentWillUnmount**方法當作其中一部分。
后置突變(Post-mutation)生命周期方法
commitAllLifecycles是React調用所有剩余生命周期方法**componentDidUpdate和componentDidMount**的方法。
這里我們就講完了。
轉載于:https://juejin.im/post/5cdb5b205188252035420c7f
總結
以上是生活随笔為你收集整理的[译] Fiber内幕:深入概述React新的协调算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Anordighos手绘休闲脚本字体
- 下一篇: 一看就懂的极简MVVM