摘要:基礎(chǔ)的理論概念這篇文章是我的一次嘗試,希望能夠形式化的介紹關(guān)于本身的一些理念模型。我對于此實(shí)際的理念模型是在每次的更新過程中返回下一個(gè)階段的狀態(tài)。的目標(biāo)是提升對在動(dòng)畫,布局以及手勢方面的友好度。我已經(jīng)邀請了團(tuán)隊(duì)的成員來對本文檔的準(zhǔn)確性進(jìn)行。
前言
本文主要是對收集到的一些官方或者其他平臺的文章進(jìn)行翻譯,中間可能穿插一些個(gè)人的理解,如有錯(cuò)誤疏漏之處,還望批評指正。筆者并未研究過源碼,只是希望本文成為那些inspire你的東西的一部分,從而在今后一起去探討和研究React Fiber。
注:絕大多數(shù)情況下,以下的第一人稱不代表譯者,而是對應(yīng)文章的作者,請注意區(qū)分。
React basic 基礎(chǔ)的理論概念??這篇文章是我的一次嘗試,希望能夠形式化的介紹關(guān)于react本身的一些理念模型。目的在于基于演繹推理的方式,描述那些給我們靈感讓我們進(jìn)行這樣的設(shè)計(jì)的源泉。
??當(dāng)然,這里的一些設(shè)想是具有爭議的,實(shí)際的設(shè)計(jì)也許也會(huì)有bug或者疏漏。但是,這也是一個(gè)好的開始讓我們?nèi)バ问交卣務(wù)撨@些。同時(shí),如果你有更好的想法,也歡迎pr。以下讓我們沿著這個(gè)思路,從簡單到復(fù)雜的去思考這一系列問題,不必?fù)?dān)心,這里沒有太多具體的框架細(xì)節(jié)。
??實(shí)際的關(guān)于React的實(shí)現(xiàn)是充滿務(wù)實(shí)主義的,漸進(jìn)式的,算法優(yōu)化的,新老代碼交替的,各種調(diào)試工具以及任何你能想到的讓他變成更加有用的東西。當(dāng)然,這些東西也像版本迭代一樣,它們的存在是短暫的,如果它們足夠有用,我們就會(huì)不斷的更新他們。再次聲明,實(shí)際的實(shí)現(xiàn)是非常非常復(fù)雜的。
轉(zhuǎn)換??React最核心的前提是,UI僅僅是數(shù)據(jù)->數(shù)據(jù)的映射。相同的輸入意味著相同輸出。非常簡單的純函數(shù)。
function NameBox(name) { return { fontWeight: "bold", labelContent: name }; }
"Sebastian Markb?ge" -> { fontWeight: "bold", labelContent: "Sebastian Markb?ge" };抽象
??但是,并不是所有的UI都能這樣做,因?yàn)?,有些UI是非常復(fù)雜的。所以,很重要的一點(diǎn)是,UI能夠被抽象成許許多多可復(fù)用的小塊,同時(shí)不暴露這些小塊的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。就像在一個(gè)函數(shù)中調(diào)用另一個(gè)函數(shù)一樣。
function FancyUserBox(user) { return { borderStyle: "1px solid blue", childContent: [ "Name: ", NameBox(user.firstName + " " + user.lastName) ] }; }
{ firstName: "Sebastian", lastName: "Markb?ge" } -> { borderStyle: "1px solid blue", childContent: [ "Name: ", { fontWeight: "bold", labelContent: "Sebastian Markb?ge" } ] };組合
??為了實(shí)現(xiàn)可復(fù)用這一特性,僅僅只是簡單復(fù)用葉子節(jié)點(diǎn),每次都為它們創(chuàng)建一個(gè)新的容器是遠(yuǎn)遠(yuǎn)不夠的。同時(shí)我們需要在容器(container)這一層面構(gòu)建抽象,并且組合其它抽象。在我看來,組合就是將兩個(gè)甚至多個(gè)抽象變成一個(gè)新的抽象。
function FancyBox(children) { return { borderStyle: "1px solid blue", children: children }; } function UserBox(user) { return FancyBox([ "Name: ", NameBox(user.firstName + " " + user.lastName) ]); }狀態(tài)
??UI并不僅僅是簡單的服務(wù)或者說業(yè)務(wù)中的邏輯狀態(tài)。事實(shí)上,對于一個(gè)特定的投影而言,很多狀態(tài)是具體的,但是對于其他投影,可能不是這樣。例如,如果你正在文本框中輸入,這些輸入的字符可以被復(fù)制到另外的tab或者移動(dòng)設(shè)備上(當(dāng)然你不想復(fù)制也沒問題,主要是為了和下一句的例子進(jìn)行區(qū)分)。但是,諸如滾動(dòng)條的位置這樣的數(shù)據(jù),你幾乎從來不會(huì)想把它在多個(gè)投影中復(fù)制(因?yàn)樵谶@臺設(shè)備上比如滾動(dòng)條位置是200,但是在其他設(shè)備上滾動(dòng)到200的內(nèi)容通常來說肯定是不同的)。
??我們更趨向于將我們的數(shù)據(jù)模型變?yōu)椴豢勺兊摹?strong>我們在最頂端將所有能更新狀態(tài)的函數(shù)串起來,把它們當(dāng)作一個(gè)原子(說成事務(wù)可能更容易明白)來對待。
function FancyNameBox(user, likes, onClick) { return FancyBox([ "Name: ", NameBox(user.firstName + " " + user.lastName), "Likes: ", LikeBox(likes), LikeButton(onClick) ]); } // Implementation Details var likes = 0; function addOneMoreLike() { likes++; rerender(); } // Init FancyNameBox( { firstName: "Sebastian", lastName: "Markb?ge" }, likes, addOneMoreLike );
注意:這個(gè)例子通過副作用去更新狀態(tài)。我對于此實(shí)際的理念模型是在每次的更新過程中返回下一個(gè)階段的狀態(tài)。當(dāng)然,不這樣做看起來要更簡單一點(diǎn),但是在以后我們最終還是會(huì)選擇改變這個(gè)例子采用的方式(因?yàn)楦弊饔玫娜秉c(diǎn)太多了)。
緩存??我們知道,對于純函數(shù)而言,一次又一次相同的調(diào)用是非常浪費(fèi)時(shí)間和空間的。我們可以對這些函數(shù)建立緩存的版本,追蹤最近一次調(diào)用的輸入和輸出。下一次就可以直接返回結(jié)果,不用再次計(jì)算。
function memoize(fn) { var cachedArg; var cachedResult; return function(arg) { if (cachedArg === arg) { return cachedResult; } cachedArg = arg; cachedResult = fn(arg); return cachedResult; }; } var MemoizedNameBox = memoize(NameBox); function NameAndAgeBox(user, currentTime) { return FancyBox([ "Name: ", MemoizedNameBox(user.firstName + " " + user.lastName), "Age in milliseconds: ", currentTime - user.dateOfBirth ]); }列表/集合
??大多數(shù)UI都是通過很多個(gè)列表組成,通過列表中的每個(gè)元素產(chǎn)生不同的值(比如data.map(item =>
??為了管理每個(gè)列表元素的狀態(tài),我們可以創(chuàng)建一個(gè)Map來管理每個(gè)特定的列表元素。
function UserList(users, likesPerUser, updateUserLikes) { return users.map(user => FancyNameBox( user, likesPerUser.get(user.id), () => updateUserLikes(user.id, likesPerUser.get(user.id) + 1) )); } var likesPerUser = new Map(); function updateUserLikes(id, likeCount) { likesPerUser.set(id, likeCount); rerender(); } UserList(data.users, likesPerUser, updateUserLikes);
注意:現(xiàn)在我們有多個(gè)不同的輸入傳遞給FancyNameBox。那會(huì)破壞我們上一節(jié)提到的緩存策略,因?yàn)槲覀円淮沃荒苡洃浺粋€(gè)值。(因?yàn)樯厦娴膍emoize函數(shù)的形參只有一個(gè))
續(xù)延??不幸的是,在UI中有太多的list相互嵌套,我們不得不用大量的模板代碼去顯式的管理它們。
??我們可以通過延遲執(zhí)行將一部分的模板代碼移到我們的主要邏輯之外。例如,通過利用currying(可以通過bind實(shí)現(xiàn))(當(dāng)然我們知道這樣bind并沒有完整的實(shí)現(xiàn)currying)。然后我們通過在核心函數(shù)之外的地方傳遞狀態(tài),這樣,我們就能擺脫對模板的依賴。
??這并沒有減少模板代碼,但是至少將它們移動(dòng)到了核心邏輯之外。
function FancyUserList(users) { return FancyBox( UserList.bind(null, users) ); } const box = FancyUserList(data.users); const resolvedChildren = box.children(likesPerUser, updateUserLikes); const resolvedBox = { ...box, children: resolvedChildren };
譯注:這里當(dāng)然可以采用
function FancyUserList(users) { return FancyBox( UserList(users, likesPerUser, updateUserLikes) ); }
??但是這樣擴(kuò)展起來就很麻煩,想增加,刪除我們都需要去改FancyUserList里的代碼。最重要的是,如果我們想將likesPerUser和updateUserLikes換成其他的集合和函數(shù)的話,我們必須再創(chuàng)建一個(gè)函數(shù),如:
function FancyUserList2(users) { return FancyBox( UserList(users, likesPerUser2, updateUserLikes2) ); }
當(dāng)然,你肯定會(huì)想到,直接給FancyUserList設(shè)置成接收多個(gè)參數(shù)不就行了。但是這樣依然存在一個(gè)問題,那就是每次你需要用到FancyUserList的時(shí)候,都需要帶上所有的參數(shù)。要解決也是可以的,比如const foo = FancyUserList.bind(null, data.users),后面需要用的話,直接foo(bar1, func1), foo(bar2, func2)就行了。也實(shí)現(xiàn)了設(shè)計(jì)模式中我們常談到的分離程序中變與不變的部分。但是這樣的實(shí)現(xiàn)將bind操作交給了調(diào)用者,這一點(diǎn)上可以改進(jìn),就像示例中提到的那樣。
狀態(tài)映射??我們很早就知道,一旦我們看見相同的部分,我們能夠使用組合去避免一次又一次重復(fù)的去實(shí)現(xiàn)相同的部分。我們可以將提取出來那部分邏輯移動(dòng)并傳遞給更低等級或者說更低層級的函數(shù),這些函數(shù)就是我們經(jīng)常復(fù)用的那些函數(shù)。
function FancyBoxWithState( children, stateMap, updateState ) { return FancyBox( children.map(child => child.continuation( stateMap.get(child.key), updateState )) ); } function UserList(users) { return users.map(user => { continuation: FancyNameBox.bind(null, user), key: user.id }); } function FancyUserList(users) { return FancyBoxWithState.bind(null, UserList(users) ); } const continuation = FancyUserList(data.users); continuation(likesPerUser, updateUserLikes);緩存映射
??想在緩存列表中緩存多個(gè)元素是比較困難的,你必須弄清楚一些在平衡緩存與頻率之間做得很好的緩存算法,然而這些算法是非常復(fù)雜的。
??幸運(yùn)的是,在同一區(qū)域的UI通常是比較穩(wěn)定的,不會(huì)變化的。
??在這里我們依然可以采用像剛剛那種緩存state的技巧,通過組合的方式傳遞memoizationCache
function memoize(fn) { return function(arg, memoizationCache) { if (memoizationCache.arg === arg) { return memoizationCache.result; } const result = fn(arg); memoizationCache.arg = arg; memoizationCache.result = result; return result; }; } function FancyBoxWithState( children, stateMap, updateState, memoizationCache ) { return FancyBox( children.map(child => child.continuation( stateMap.get(child.key), updateState, memoizationCache.get(child.key) )) ); } const MemoizedFancyNameBox = memoize(FancyNameBox);代數(shù)哲學(xué)
??你會(huì)發(fā)現(xiàn),這有點(diǎn)像PITA(一種類似肉夾饃的食物),通過幾個(gè)不同層次的抽象,將你需要的東西(值/參數(shù))一點(diǎn)一點(diǎn)的加進(jìn)去。有時(shí)這也提供了一種快捷的方式,能在不借助第三方的條件下在兩個(gè)抽象之間傳遞數(shù)據(jù)。在React里面,我們把這叫做context.
??有時(shí)候數(shù)據(jù)之間的依賴并不像抽象樹那樣整齊一致。例如,在布局算法中,在完整的確定所有字節(jié)點(diǎn)的位置之前,你需要知道各個(gè)子節(jié)點(diǎn)矩形區(qū)域的大小。
Now, this example is a bit "out there". I"ll use Algebraic Effects as proposed for ECMAScript. If you"re familiar with functional programming, they"re avoiding the intermediate ceremony imposed by monads.
譯注:FP理解不深,所以上面段就不翻譯了,以免誤導(dǎo)
function ThemeBorderColorRequest() { } function FancyBox(children) { const color = raise new ThemeBorderColorRequest(); return { borderWidth: "1px", borderColor: color, children: children }; } function BlueTheme(children) { return try { children(); } catch effect ThemeBorderColorRequest -> [, continuation] { continuation("blue"); } } function App(data) { return BlueTheme( FancyUserList.bind(null, data.users) ); }React Fiber體系結(jié)構(gòu)
譯注:為了比較形象的闡釋,故這里將React Stack vs Fiber的視頻貼在這,而不是放在閱讀更多里面。由于在youtube上,為了方便查看,這里錄制了一張gif(有點(diǎn)大,18M,下載時(shí)請耐心等待)。
簡介??React Fiber是一個(gè)正在進(jìn)行中的對React核心算法的重寫。它是過去兩年React團(tuán)隊(duì)研究成果的一個(gè)頂峰。
??React Fiber的目標(biāo)是提升對在動(dòng)畫,布局以及手勢方面的友好度。它最重要的特性叫做"增量式/漸進(jìn)式"渲染:即,將渲染工作分割為多個(gè)小塊進(jìn)行,并在各個(gè)幀之間傳播。
??其它關(guān)鍵的特性包括,1.擁有了暫停,中止以及當(dāng)有更新來臨的時(shí)候重新恢復(fù)工作的能力。2.不同的能力對于不同類型的更新分配不同的優(yōu)先級。3.新的并發(fā)原語。
關(guān)于本文檔??在Fiber中引入了幾個(gè)新的概念,這些概念僅僅只看代碼是很難真的體會(huì)的。本文檔最初只是我在React項(xiàng)目組時(shí)的收集,收集一些我整理Fiber的實(shí)現(xiàn)的時(shí)候的筆記。隨著筆記的增多,我意識到這可能對其他人來說也是一個(gè)有益的資源。(譯注:本文檔的作者acdlite是Facebook開發(fā)組的一名成員,并不屬于React框架的開發(fā)組(這里指實(shí)際工作中,而不是gh上的team)。React團(tuán)隊(duì)的leader,舊的核心算法及新的核心算法的提出者是sebmarkbage)
??我將嘗試盡可能用簡單的語言來描述,避免一些不必要的術(shù)語。在必要時(shí)也會(huì)給出一些資源的鏈接。
??請注意我并不是React團(tuán)隊(duì)的一員,也不具備足夠的權(quán)威。所以這并不是一份官方文檔。我已經(jīng)邀請了React團(tuán)隊(duì)的成員來對本文檔的準(zhǔn)確性進(jìn)行review。
??Fiber是一項(xiàng)還在進(jìn)行中的工作,在它完成前都很可能進(jìn)行重改。所以本文檔也是如此,隨著時(shí)間很可能發(fā)生變化。歡迎任何的建議。
??我的目標(biāo)是,在閱讀本文檔后,在Fiber完成的時(shí)候,順著它的實(shí)現(xiàn)你能更好的理解它。甚至最終回饋React(譯注:意思是fix bug,pr新特性,解決issue等等)。
準(zhǔn)備??在繼續(xù)閱讀前,我強(qiáng)烈建議你確保自己對以下內(nèi)容已經(jīng)非常熟悉:
??React Components, Elements, and Instances - "組件"通常來說是一個(gè)范圍很大的術(shù)語。牢固的掌握這些術(shù)語是至關(guān)重要的。
??Reconciliation - 對React的協(xié)調(diào)/調(diào)度算法的一個(gè)高度概括。
??React基礎(chǔ)理論概念 - 對React中的一些概念模型的抽象描述,第一次讀的時(shí)候可能不太能體會(huì)。沒關(guān)系,以后終會(huì)明白的。
??React設(shè)計(jì)原則 - 請注意其中的scheduling這一小節(jié),非常好的解釋了React Fiber。
回顧??如果你還沒準(zhǔn)備好的話,請重新閱讀上面的"準(zhǔn)備"一節(jié)。在我們探索之前,讓我們來了解幾個(gè)概念。
什么是協(xié)調(diào)(reconciliation)??reconciliation:是一種算法,React使用它去區(qū)分兩棵樹,從而決定到底哪一部分需要改變。
??update:數(shù)據(jù)的變化會(huì)導(dǎo)致渲染,通常這是setState的結(jié)果,最終會(huì)觸發(fā)重新渲染。
??React API的核心理念是思考/決定/調(diào)度怎樣去update,就好像它會(huì)導(dǎo)致整個(gè)app重新渲染一樣。它讓開發(fā)者能夠聲明式地去思考,而不用去擔(dān)心如何高效的將app從一個(gè)狀態(tài)過渡到另一個(gè)狀態(tài)(A到B,B到C,C再到A等等)。
??事實(shí)上,每次變化都重新渲染整個(gè)app的方式只能工作在非常小的app上。在現(xiàn)實(shí)世界真正的app中,這在性能上花費(fèi)的代價(jià)太大了。React已經(jīng)在這方面做了優(yōu)化,在保持好性能的前提下創(chuàng)造出app重新渲染之后的樣子。絕大部分的優(yōu)化都屬于reconciliation這個(gè)過程的一部分。
??Reconciliation是一個(gè)隱藏在被廣為熟知的稱作"virtual DOM"的背后的算法。概括起來就是:當(dāng)你渲染一個(gè)React應(yīng)用的時(shí)候,就產(chǎn)生了一棵描述這個(gè)應(yīng)用的節(jié)點(diǎn)樹,并存儲(chǔ)在內(nèi)存中。接下來這棵樹會(huì)被刷新,然后翻譯到具體的某個(gè)環(huán)境中。例如,在瀏覽器環(huán)境,它被翻譯成一系列的DOM操作。當(dāng)app有更新的時(shí)候(通常是通過setState),一棵新的樹就產(chǎn)生了。這棵新樹會(huì)與之前的樹進(jìn)行diff,然后計(jì)算出更新整個(gè)app需要哪些操作。
??雖然Fiber是一個(gè)對reconciler完全的重寫,但是React文檔中對核心算法的概括描述仍然是適用的。幾個(gè)關(guān)鍵點(diǎn)為:
不同的組件類型被假定為會(huì)產(chǎn)生本質(zhì)上不同類型的樹。React不會(huì)嘗試對它們進(jìn)行diff,而是完全地替換舊的樹。(譯注:如)
對列表(list,譯注:即組件元素組成的數(shù)組)的diff是采用key來進(jìn)行的。Key應(yīng)該是穩(wěn)定的,可預(yù)測的,且唯一的。
Reconciliation vs rendering??DOM只是React能夠渲染的東西之一,除此之外,主要還有通過React Native產(chǎn)生的IOS和Android的原生控件。(這就是為什么說"virtual DOM"屬于用詞不當(dāng))
??React能支持這么多的渲染目標(biāo)的是因?yàn)镽eact本身的設(shè)計(jì)所導(dǎo)致的,協(xié)調(diào)(reconciliation)和渲染是兩個(gè)不同的,分離的階段。協(xié)調(diào)器(reconciler)做的是計(jì)算樹的哪部分在變化的工作,而渲染器(renderer)做的則是利用協(xié)調(diào)器產(chǎn)生的結(jié)果去更新我們的應(yīng)用的工作。(譯注:即不同平臺/環(huán)境下去更新界面的手段/方式是不同的,所以不能一概而論,但是計(jì)算樹的差異的過程卻是通用的。)
??這種分離意味著React DOM以及React Native既能共享同一個(gè)由React提供的協(xié)調(diào)器的邏輯,又能夠利用它們各自的渲染器去完成渲染。
??Fiber重寫了協(xié)調(diào)器。它并不關(guān)心渲染,盡管渲染器需要相應(yīng)作出一些改變(并且利用)這個(gè)新的算法的某些東西。
調(diào)度??調(diào)度(scheduling):是一個(gè)決定什么時(shí)候該做某個(gè)任務(wù)的過程。
??任務(wù)(work):任何需要執(zhí)行的計(jì)算都屬于任務(wù)。任務(wù)通常是由一次更新所導(dǎo)致的。(如setState)
??React的設(shè)計(jì)原則這篇文檔在這一點(diǎn)上闡釋的非常不錯(cuò),所以我在這引用一小段:
在當(dāng)前版本的實(shí)現(xiàn)中,React在一個(gè)工作輪回中遞歸地遍歷要更新的樹并且調(diào)用render函數(shù)。然而,在將來它也許會(huì)為了避免丟幀而延遲某些更新。
譯注:將來即指Fiber,幀是Fiber里引入的一個(gè)概念,因?yàn)橛玫搅藃equestAnimationFrame。Fiber棧就是用來協(xié)調(diào)對幀的操作(Fiber棧也是Fiber里的概念,是一個(gè)對函數(shù)調(diào)用棧的模擬。)。延遲更新是相對遞歸遍歷而言的,即暫時(shí)中斷遞歸,轉(zhuǎn)去遍歷另外的節(jié)點(diǎn)。可參考演講視頻,或者觀察一下這個(gè)gif(有點(diǎn)大,20M)以及將幀劃分的圖片
這在React的設(shè)計(jì)中是一個(gè)很常見的課題。一些框架實(shí)現(xiàn)了"push"的方式,當(dāng)新的數(shù)據(jù)可用的時(shí)候執(zhí)行計(jì)算。然而,React堅(jiān)持采用"pull"的方式,將計(jì)算延遲執(zhí)行,直到有必要時(shí)才進(jìn)行計(jì)算。
React并不是一個(gè)通用的數(shù)據(jù)處理框架。它是一個(gè)用于構(gòu)建用戶接口的框架。我們認(rèn)為它有自己獨(dú)特的定位,在一個(gè)應(yīng)用中知道哪些相關(guān)的計(jì)算是目前所需要的,哪些是目前不需要的。
如果某些東西不可見(在屏幕外),我們可以延遲執(zhí)行任何和這部分相關(guān)的邏輯。如果數(shù)據(jù)到達(dá)的頻率比幀刷新的頻率還要快,我們可以合并以及批處理這些更新。比起那些優(yōu)先級不太高的任務(wù)(例如渲染從網(wǎng)絡(luò)獲取來的數(shù)據(jù)),我們可以優(yōu)先考慮來自用戶接口的任務(wù)(例如,點(diǎn)擊一個(gè)按鈕觸發(fā)的動(dòng)畫),從而避免丟幀。
幾個(gè)關(guān)鍵點(diǎn)在于:
在UI中,并不是每個(gè)更新都有必要立即展示給用戶。事實(shí)上,這樣做將會(huì)是很浪費(fèi)的,會(huì)造成丟幀以及降低用戶體驗(yàn)。
不同類型的更新具有不同的優(yōu)先級 - 動(dòng)畫過渡需要比更新數(shù)據(jù)更快。
譯注:完整的優(yōu)先級可以參考源碼中的定義
基于push的方式需要app(程序員)去決定怎樣調(diào)度這些任務(wù)。基于pull的方式讓框架(React)變得智能,從而幫助我們做出這些抉擇。
??React目前并沒有非常好地利用調(diào)度,一次更新將會(huì)導(dǎo)致整個(gè)子樹立即被重新渲染。改進(jìn)React的核心算法從而更好的利用調(diào)度是隱藏在Fiber背后的理念驅(qū)動(dòng)。
??現(xiàn)在我們要準(zhǔn)備深入Fiber的實(shí)現(xiàn)了。下一節(jié)會(huì)比我們到目前為止討論的要更有專業(yè)性一點(diǎn)。在你繼續(xù)閱讀前請確保之前的內(nèi)容你基本了解了。
Fiber是什么??我們即將討論React Fiber的核心體系結(jié)構(gòu)。Fiber比起應(yīng)用開發(fā)者通常的認(rèn)知而言,是一個(gè)更加的低得多的抽象層次。如果你發(fā)現(xiàn)自己很難去理解它,不要灰心。繼續(xù)嘗試,最后一定會(huì)撥開云霧見光明。(當(dāng)你最后理解它的理解,請向我建議如何改進(jìn)這一小節(jié))
??我們開始吧~
??我們對Fiber已經(jīng)確立的目標(biāo)是,激活React,讓它具備調(diào)度的能力。具體地來說,我們需要能夠:
暫停及恢復(fù)任務(wù)。
賦予不同的任務(wù)不同的優(yōu)先級。
重用之前已經(jīng)完成的任務(wù)。
中止那些不再需要的任務(wù)。
??要想做到其中的任何一條,我們首先需要一種方式,把工作/任務(wù)分解成許許多多的小單元(units)。從某種意義上來說,那就是fiber。一個(gè)fiber代表了任務(wù)的單位。
??為了進(jìn)一步理解,讓我們回到之前提到的把React組件當(dāng)作數(shù)據(jù)的函數(shù)這一概念,通常表示為:
??v = f(d)
??由此可見,渲染一個(gè)React應(yīng)用與在一個(gè)函數(shù)類調(diào)用另一個(gè)函數(shù)是類似的(譯注:一個(gè)組件的render函數(shù)里面會(huì)調(diào)用另一個(gè)組件的render函數(shù))。這個(gè)類比在思考fiber的時(shí)候是很有用的。
??通常,計(jì)算機(jī)對一個(gè)程序的執(zhí)行/調(diào)用情況的跟蹤的方式是通過調(diào)用棧(call stack)。當(dāng)一個(gè)函數(shù)被執(zhí)行的時(shí)候,一個(gè)新的棧幀(stack frame)被壓入棧中。那個(gè)棧幀就代表了在那個(gè)函數(shù)里被執(zhí)行的任務(wù)。(譯注:聽著可能有點(diǎn)不順暢,不過無論什么語言,調(diào)試的時(shí)候觀察過call stack的同學(xué)應(yīng)該都清楚)
??當(dāng)我們處理UI的時(shí)候,問題在于如果一次有太多的任務(wù)要執(zhí)行,將會(huì)導(dǎo)致動(dòng)畫丟幀以及卡頓。更重要的是,那些任務(wù)當(dāng)中的一部分也許是沒有必要執(zhí)行的,如果新的一次更新對其中一部分進(jìn)行了廢棄的話。這就是UI組件和函數(shù)分解之間有區(qū)別的地方,因?yàn)橥ǔ=M件比函數(shù)有更多具體的需要關(guān)心的東西。
??較新的瀏覽器(以及React Native)實(shí)現(xiàn)了幫助解決這些具體問題的API:requestIdleCallback會(huì)讓一個(gè)低優(yōu)先級的函數(shù)在空閑期被調(diào)用。而requestAnimationFrame會(huì)讓一個(gè)高優(yōu)先級的函數(shù)在下一個(gè)動(dòng)畫幀被調(diào)用。問題在于,為了使用這些API,你需要將渲染工作劃分為增量式的單元。如果你只依賴調(diào)用棧的話,那么直到調(diào)用棧為空之前它都會(huì)一直在工作。
??那么,如果我們能夠自定義調(diào)用棧的行為,對優(yōu)化渲染UI來說是不是就更好了呢?如果我們能任意地中斷調(diào)用棧并且手動(dòng)操作棧幀,是不是也會(huì)更好呢?
??這就是React Fiber的目標(biāo)。Fiber是對于棧的重寫,特別是對于React組件來說。你可以把一個(gè)單一的fiber想象成一個(gè)虛擬的棧幀。
??重寫棧的優(yōu)點(diǎn)是,你能夠在內(nèi)存中保留棧幀(這個(gè)鏈接挺有趣的,值得一看),并且在任何時(shí)候通過任意方式執(zhí)行。這對我們完成調(diào)度來說是至關(guān)重要的。
??除了調(diào)度外,手動(dòng)地處理?xiàng)苍S能夠讓我們擁有一些潛在的特性,例如并發(fā)以及錯(cuò)誤邊界處理。我們會(huì)在后面的小節(jié)討論這些。
Fiber的結(jié)構(gòu)??注意:隨著我們對實(shí)現(xiàn)的細(xì)節(jié)關(guān)注得越具體,也許會(huì)發(fā)現(xiàn)更多的可能性。如果你發(fā)現(xiàn)錯(cuò)誤或者太舊的信息,請給我們提pr。
??在具體的術(shù)語中,一個(gè)fiber是一個(gè)js對象,它包含著一個(gè)組件,以及這個(gè)組件的輸入及輸出。
??一個(gè)fiber與一個(gè)棧幀相對應(yīng),但同時(shí)也與一個(gè)組件的實(shí)例相對應(yīng)。
??這里列出一些屬于fiber的重要的屬性(注意并沒有完全的列舉全):
type和key??fiber的type屬性和key屬性對React元素來講提供的是相同的功能。(事實(shí)上,當(dāng)一個(gè)fiber從一個(gè)元素中被創(chuàng)建的時(shí)候,這兩個(gè)屬性都是復(fù)制過來的(譯注:可參考源碼))
??一個(gè)fiber的type描述了與它相對應(yīng)的組件,對于函數(shù)或者類組件而言,type就是函數(shù)或者類組件本身(譯注:源碼中對type的描述為"與這個(gè)fiber相對應(yīng)的函數(shù)/組件/模塊")。對于宿主組件而言(div,span等等),type就是字符串("div","span")。(譯注:這一點(diǎn)其實(shí)和之前的React是一樣的,沒有區(qū)別,如果你用react-devtools調(diào)試過的話應(yīng)該會(huì)注意到)
??從概念上來講,type是一個(gè)函數(shù)(就像 v = f(d)),這個(gè)函數(shù)的執(zhí)行被棧幀所追蹤。
??和type一起的key,被用在協(xié)調(diào)(reconciliation)過程中,決定這個(gè)fiber是否能被重用。(譯注:源碼中的描述為"這個(gè)child唯一的標(biāo)識符")
child和sibling??這兩個(gè)屬性指向其它的fiber,描述一個(gè)fiber的遞歸樹結(jié)構(gòu)。(譯注:源碼中的描述為"單向鏈表樹結(jié)構(gòu)")
??child屬性對應(yīng)的fiber是與一個(gè)組件的render方法的返回值相對應(yīng)的。所以,在下面的例子中:
function Parent() { return}
??Parent的child屬性就與Child相對應(yīng)。
??sibling屬性解釋了這樣的案例,即在render方法中返回多個(gè)子節(jié)點(diǎn)(一個(gè)在Fiber中的新特性)。(譯注:而且也可以返回一個(gè)字符串。相信都是大家期盼已久的,再也不用套一個(gè)div了。另外一個(gè)大的特性是error boundaries)
function Parent() { return [, ] }
??子fiber形成了一個(gè)單鏈表,單鏈表的頭節(jié)點(diǎn)是數(shù)組中的第一個(gè)元素。所以在上面的例子中,Parent的child屬性是Child1,Child1的sibling屬性是Child2。
??回到我們與函數(shù)的類比上,你可以把一個(gè)子fiber想象成一個(gè)尾調(diào)用函數(shù)。
return??return屬性的值也是一個(gè)fiber,指向處理完當(dāng)前fiber之后的返回值。在概念上與棧幀的返回地址類似。
??如果一個(gè)fiber有多個(gè)子fiber,每一個(gè)子fiber的return屬性都執(zhí)行父fiber。所以在我們上一節(jié)的例子中,Child1和Child2的return屬性的值都是Parent。
pendingProps和memoizedProps??從概念上來說,props就是一個(gè)函數(shù)的arguments。一個(gè)fiber的pendingProps在它最初被調(diào)用的時(shí)候就被設(shè)置了。memoizedProps在執(zhí)行的結(jié)尾被設(shè)置。(譯注:應(yīng)該就類似與對純函數(shù)進(jìn)行cache)
??當(dāng)將要到來的pendingProps和memoizedProps相等的時(shí)候,就標(biāo)志著這個(gè)fiber以前的輸出能夠被重用了,這樣就能避免不必要的任務(wù)執(zhí)行。
pendingWorkPriority??pendingWorkPriority的值代表了這個(gè)任務(wù)的優(yōu)先級。ReactPriorityLevel列出了不同的優(yōu)先級以及它們代表的含義。
??NoWork優(yōu)先級的值是0,優(yōu)先級數(shù)字越大表示優(yōu)先級越低(即0是最高的優(yōu)先級)。例如,你可以利用下面的函數(shù)去檢查一個(gè)fiber的優(yōu)先級是否至少達(dá)到了某個(gè)指定的優(yōu)先級。
function matchesPriority(fiber, priority) { return fiber.pendingWorkPriority !== 0 && fiber.pendingWorkPriority <= priority }
??這個(gè)函數(shù)僅僅只是為了說明使用,并不是真正的React Fiber代碼庫中的一部分。
??調(diào)度器使用priority屬性去搜索下一個(gè)要執(zhí)行的任務(wù)單元。我們將在futrue一節(jié)討論這個(gè)算法。
alternate??flush:刷新一個(gè)fiber就是將它的輸出渲染到屏幕上。
??work-in-progress:代表一個(gè)還未完成的fiber,從概念上來說,類似于一個(gè)還未return的棧幀。
??在任何時(shí)候,一個(gè)組件的實(shí)例最多有2個(gè)fiber與它相關(guān)聯(lián):當(dāng)前的刷新后的fiber以及正在運(yùn)行中(work-in-progress)的fiber。
??當(dāng)前的fiber的備胎(alternate)就是正在運(yùn)行的fiber,正在運(yùn)行的fiber的備胎也是當(dāng)前的fiber。(譯注:可參考源碼)
??一個(gè)fiber的備胎是用一個(gè)叫做cloneFiber的函數(shù)惰式創(chuàng)建的,而不是總是創(chuàng)建一個(gè)新的對象。如果fiber的備胎存在的話,cloneFiber會(huì)嘗試重用這個(gè)fiber的備胎,從而達(dá)到最小化分配內(nèi)存的目的。
??雖然你應(yīng)該把a(bǔ)lternate屬性當(dāng)作一種實(shí)現(xiàn)細(xì)節(jié),但是在源碼中你會(huì)經(jīng)常看到它,所以放到這里討論它是有價(jià)值的。
output??host component:代表一個(gè)React應(yīng)用程序的葉子節(jié)點(diǎn)。不同的渲染環(huán)境下是不同的(例如,在瀏覽器應(yīng)用里面,它們是div,span等等)。在JSX中,它們用小寫名來表示。(譯注:完整的分類可參考源碼)
??從概念上來說,一個(gè)fiber的輸出(output)是一個(gè)函數(shù)的返回值。
??每一個(gè)fiber最終都有一個(gè)輸出,但是只有在宿主環(huán)境的葉子節(jié)點(diǎn)中才會(huì)創(chuàng)建輸出。然后輸出被翻譯/轉(zhuǎn)移到真正的dom樹中。
??輸出就是最終傳給渲染器的東西,以便渲染器能夠在渲染環(huán)境中刷新,從而反映出那些變化。如何創(chuàng)建和更新輸出是渲染器的職責(zé)。
將來的可能??到目前為止我們就談這么多了。但是本文檔還遠(yuǎn)遠(yuǎn)沒有完成。未來我可能將描述一些在更新的生命周期中頻繁使用的算法。它們包括:
調(diào)度器是如何知道下一個(gè)要執(zhí)行的單元是哪一個(gè)的?
在fiber樹中優(yōu)先級是如何被追蹤和傳播的?
調(diào)度器怎么知道何時(shí)暫停和恢復(fù)某個(gè)任務(wù)?
任務(wù)是如何被刷新以及被標(biāo)記為已經(jīng)完成的?
副作用(如生命周期函數(shù))是怎樣工作的?
協(xié)程(coroutine)是什么?它是怎樣被利用從而實(shí)現(xiàn)像context和layout這樣的特性的?
更多推薦React-Future
Fiber Principles: Contributing To Fiber
React 15.5 and 16 Umbrella
Fiber Simplify coroutines by making yields stateless
Fiber Umbrella for remaining features / bugs
React Perf Scenarios
Fiber Compute the Host Diff During Reconciliation
fiber-debugger
Why, What, and How of React Fiber with Dan Abramov and Andrew Clark
Pete Hunt: The Past, Present and Future of React
Dan Codes
另外之前收集過一些dan發(fā)在twitter上的東西,你可以進(jìn)入鏈接然后ctrl+f搜索fiber。
------------------------------------------------------2017-4-16日更新---------------------------------------------------------------
That @reactiflux Q&A from @acdlite,關(guān)于這個(gè)更多的可以看discord里的討論
之前提到acdlite并非React項(xiàng)目組的成員,糾正下,準(zhǔn)確度說應(yīng)該是寫那篇文章的時(shí)候還不是,但是后面加入了React團(tuán)隊(duì)??蓞⒖歼@條tweet中的描述。另外其中也提到當(dāng)時(shí)是作為一個(gè)旁觀者的角度去寫的那篇文章,經(jīng)過在React項(xiàng)目組參與fiber的開發(fā),文章里的很多東西也需要更新了,它后面會(huì)抽時(shí)間更新的,到時(shí)如果我沒忘的話應(yīng)該也會(huì)更新翻譯的。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/82487.html
摘要:業(yè)界動(dòng)態(tài)發(fā)布版本,同時(shí)發(fā)布了版本以及首個(gè)穩(wěn)定版本的。程序人生如何用人類的方式進(jìn)行二關(guān)于如何在中進(jìn)行良好的溝通,避免陷入一些潛在的陷阱。技術(shù)周刊由小組出品,匯聚一周好文章,周刊原文。 業(yè)界動(dòng)態(tài) Angular 5.1 & More Now Available Angular發(fā)布5.1版本,同時(shí)發(fā)布了Angular CLI 1.6版本以及首個(gè)穩(wěn)定版本的Angular Material。CL...
摘要:前端日報(bào)精選借助和緩存及離線開發(fā)中和走進(jìn)之實(shí)現(xiàn)分析總是一知半解的中個(gè)常見的陷阱發(fā)布核心成員發(fā)布了免費(fèi)的學(xué)習(xí)視頻中文譯的函數(shù)式編程是一種反模式掘金譯更好的表單設(shè)計(jì)每一頁,一件事實(shí)例研究掘金打印龍墨并不簡單結(jié)合實(shí)現(xiàn)簡單的加載動(dòng)畫 2017-07-12 前端日報(bào) 精選 借助Service Worker和cacheStorage緩存及離線開發(fā)JavaScript中toString()和valu...
摘要:它的主體特征是增量渲染能夠?qū)秩竟ぷ鞣指畛蓧K,并將其分散到多個(gè)幀中。實(shí)際上,這樣做可能會(huì)造成浪費(fèi),導(dǎo)致幀丟失并降低用戶體驗(yàn)。當(dāng)一個(gè)函數(shù)被執(zhí)行時(shí),一個(gè)新的堆??蚣鼙惶砑拥蕉褩V?。該堆??虮硎居稍摵瘮?shù)執(zhí)行的工作。 原文 react-fiber-architecture 介紹 React Fibre是React核心算法正在進(jìn)行的重新實(shí)現(xiàn)。它是React團(tuán)隊(duì)兩年多的研究成果。 React ...
摘要:在上面我們已經(jīng)知道瀏覽器是一幀一幀執(zhí)行的,在兩個(gè)執(zhí)行幀之間,主線程通常會(huì)有一小段空閑時(shí)間,可以在這個(gè)空閑期調(diào)用空閑期回調(diào),執(zhí)行一些任務(wù)。另外由于這些堆棧是可以自己控制的,所以可以加入并發(fā)或者錯(cuò)誤邊界等功能。 文章首發(fā)于個(gè)人博客 前言 2016 年都已經(jīng)透露出來的概念,這都 9102 年了,我才開始寫 Fiber 的文章,表示慚愧呀。不過現(xiàn)在好的是關(guān)于 Fiber 的資料已經(jīng)很豐富了,...
閱讀 1499·2021-11-22 15:11
閱讀 2903·2019-08-30 14:16
閱讀 2816·2019-08-29 15:21
閱讀 2961·2019-08-29 15:11
閱讀 2519·2019-08-29 13:19
閱讀 3042·2019-08-29 12:25
閱讀 477·2019-08-29 12:21
閱讀 2904·2019-08-29 11:03