新聞中心
正文從這開始~~

成都創(chuàng)新互聯(lián)公司是一家專注于做網(wǎng)站、網(wǎng)站建設(shè)與策劃設(shè)計(jì),魚臺(tái)網(wǎng)站建設(shè)哪家好?成都創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:魚臺(tái)等地區(qū)。魚臺(tái)做網(wǎng)站價(jià)格咨詢:028-86922220
React 新近發(fā)布的 Hooks、Suspense、Concurrent Mode 等新功能讓人眼前一亮,甚至驚嘆 JS 居然有如此魔力。同時(shí),這幾個(gè)功能或多或少附帶一些略顯奇怪的規(guī)則,沒有更深層次理解的話難以把握。其實(shí)這里面并沒有什么“黑科技”,就大的趨勢(shì)來講,前端整體上還是在不斷借鑒計(jì)算機(jī)其它領(lǐng)域的優(yōu)秀實(shí)踐,來幫助我們更方便地解決人機(jī)交互問題。本文著眼于支撐這些功能的一個(gè)底層編程概念 Continuation(譯作“續(xù)延”),期望能夠在了解它之后,大家對(duì)這幾個(gè)功能有進(jìn)一步的理解和掌握。當(dāng)然,Continuation 在 React 之外也有很多的應(yīng)用,可以一眼窺豹。
Continuation 是什么?
有些人對(duì) continuation 并不陌生,因?yàn)橛袝r(shí)候在談到 Callback Hell(回調(diào)地獄)時(shí)會(huì)有提到這一概念。但其實(shí)它和回調(diào)函數(shù)大相徑庭。
維基百科對(duì)它的定義是:
A continuation is an abstract representation of the control state of a computer program.
即,continuation 是計(jì)算機(jī)程序控制狀態(tài)的抽象表示。一個(gè)坊間更通俗的說法是:它代表程序的剩余部分。像 continue、break 這類控制流操作符一樣,continuation 能夠暴露給用戶程序從而可以在恰當(dāng)時(shí)機(jī)恢復(fù)執(zhí)行,這種基本能力大大擴(kuò)展了編程語(yǔ)言使用者的發(fā)揮空間,也為 excpetion handling、generators、coroutines、algebraic effects 等提供了堅(jiān)實(shí)基礎(chǔ)。
相信很多人和我一樣,對(duì)這樣不明就里的官方解釋迷惑不解。沒關(guān)系,我們首先舉一個(gè)現(xiàn)實(shí)生活中的例子——Continuation 三明治:
默認(rèn) Continuation
事實(shí)上,所有的程序都自帶一個(gè)默認(rèn)的 continuation,那就是調(diào)用棧(Call Stack)。調(diào)用棧中存放著當(dāng)前程序的一系列剩余任務(wù),每個(gè)任務(wù)在調(diào)用棧中表示為一個(gè)棧幀(Stack Frame),用以存放任務(wù)的數(shù)據(jù)、變量和調(diào)用信息。當(dāng)調(diào)用棧為空時(shí),意味著整個(gè)程序執(zhí)行結(jié)束了。
- function main() {
- foo();
- bar();
- }
- function foo() {
- bar();
- }
- function bar() {
- // do something
- }
可以看出,調(diào)用棧是嚴(yán)格按照后進(jìn)先出的方式運(yùn)行的,無法靈活調(diào)整執(zhí)行順序。此外,控制流的控制權(quán)也被運(yùn)行環(huán)境牢牢掌握,程序員無能為力。
現(xiàn)在,讓我們?cè)O(shè)想下,如果未來有一天我們能夠?qū)⒄{(diào)用任務(wù)以鏈表的方式存儲(chǔ)在堆中,是不是就可以突破調(diào)用棧的限制了呢?
首先,因?yàn)槿蝿?wù)以調(diào)用楨的形式存儲(chǔ)在堆中,并通過指針相互關(guān)聯(lián),形成一個(gè)調(diào)用幀鏈表。當(dāng)前任務(wù)完成時(shí),運(yùn)行時(shí)可以使用這些指針跳到下一個(gè)調(diào)用幀。得益于鏈表這一組織形式,執(zhí)行程序有能力調(diào)整調(diào)用幀之間的結(jié)構(gòu)順序。
Continuation-Passing Style (CPS)
為了獲得更多控制權(quán),廣大程序員們進(jìn)行了艱苦卓絕的努力。CPS 即是第一種有意義的嘗試。簡(jiǎn)單來說,它是將程序控制流通過 continuation 的形式進(jìn)行顯示傳遞的一種編程方式,具體有三個(gè)典型特征:
- 每個(gè)函數(shù)的最后一個(gè)參數(shù)都是它的 continuation
- 函數(shù)內(nèi)部不能顯示地使用 return
- 函數(shù)只能通過調(diào)用 continuation 以傳遞它完成的計(jì)算結(jié)果
舉個(gè)栗子:
- function double(x, next) {
- next(x * 2);
- }
- function add(x, y, next) {
- next(x + y);
- }
- function minus(x, y, next) {
- next(x - y);
- }
- // ((1 + 2) - 5) * 2
- add(1, 2, resultAdd => {
- minus(resultAdd, 5, resultMinus => {
- double(resultMinus, resultDouble => {
- console.log(`result: ${resultDouble}`);
- });
- });
- });
這不就是我們前端工程師耳熟能詳?shù)幕卣{(diào)函數(shù)么,最后的調(diào)用也再次讓我們想起了恐怖的回調(diào)地獄。表面上看的確如此,但是從控制流的角度來進(jìn)一步考慮,這種模式的確賦予了程序員更多控制權(quán),因?yàn)樗械挠?jì)算步驟(函數(shù))的 continuation 都是顯示傳遞的。
例如,假設(shè)我們希望能夠在計(jì)算的中間點(diǎn)進(jìn)行檢查,一旦計(jì)算結(jié)果小于 0 則直接返回該結(jié)果?;?CPS 的三點(diǎn)特征,我們可以定義如下一個(gè) evaluate 的計(jì)算過程:
- function evaluate(frames, operate, next) {
- let output;
- const run = (index) => {
- // Finish all frames, go to run the top level continuation
- if (index === frames.length) return next(output);
- // Pick up the next frame and run it with assembled arguments
- const { fn, args = [] } = frames[index];
- const fnArgs = index > 0 ? [output, ...args] : [...args];
- fnArgs.push((result) => {
- output = result;
- operate(output, next, () => run(++index));
- });
- fn(...fnArgs);
- };
- // Kick off
- run(0);
- }
- // ((1 + 2) - 5) * 2
- evaluate(
- [
- { fn: add, args: [1, 2] },
- { fn: minus, args: [5] },
- { fn: double },
- ],
- (output, abort, next) => {
- if (output < 0) return abort(`the intermedia result is less than zero: ${output}`);
- next(output);
- },
- (output) => {
- console.log(`output: ${output}`);
- },
- );
示例:https://jsbin.com/bidayeg/3/edit?js,console
可以看出,一方面,通過合理組織計(jì)算步驟模型,evaluate 可以幫助避免回調(diào)地獄的問題,另一方面,evaluate 的第二個(gè)參數(shù)會(huì)在每個(gè)計(jì)算步驟完成時(shí)進(jìn)行檢查,并且有能力 abort 后續(xù)所有計(jì)算步驟,直接調(diào)用頂層 continuation 返回中間結(jié)果。
這個(gè)示例展示了 CPS 為我們拓展的控制流操作能力,除此之外,CPS 還有如下優(yōu)點(diǎn):
- 尾調(diào)用。每個(gè)函數(shù)都是在最后一個(gè)動(dòng)作調(diào)用 continuation 返回計(jì)算結(jié)果,因此執(zhí)行上下文不需要被保存到當(dāng)前調(diào)用棧,編譯器可以針對(duì)這種情況做尾調(diào)用消除(Tail Call Elimination)的優(yōu)化,這種優(yōu)化在函數(shù)式語(yǔ)言編譯器中大量應(yīng)用
- 異步友好。眾所眾知,JavaScript 是單線程的,如果使用直接函數(shù)調(diào)用來處理遠(yuǎn)程請(qǐng)求等操作,那么我們將不得不暫停這唯一的線程直到異步操作結(jié)果返回,這意味著用戶的其它交互得不到及時(shí)響應(yīng)。CPS 或者換言之的回調(diào)模式提供了一種有效易用的方式來處理這類問題
然而,程序終究是人來編寫和維護(hù)的,CPS 雖然有眾多好處,但讓所有人都遵循這樣嚴(yán)格的方式編程非常困難,目前這種技術(shù)更多地在編譯器中作為中間表示層應(yīng)用。
CallCC
目前 Continuation 的主流應(yīng)用方式是通過形如 callCC(call with current continuation)的過程調(diào)用以捕獲當(dāng)前 continuation,并在之后適時(shí)執(zhí)行它以恢復(fù)到 continuation 所在上下文繼續(xù)執(zhí)行后續(xù)計(jì)算從而實(shí)現(xiàn)各種控制流操作。
Scheme、Scala 等語(yǔ)言提供了 call/cc 或等效控制流操作符,JS 目前并沒有原生支持,但是通過后續(xù)介紹的兩種方式可以間接實(shí)現(xiàn)。
現(xiàn)在假設(shè)我們已經(jīng)可以在 JS 中使用 callCC 操作符,讓我們?cè)囋嚳此寄転槲覀儙硎裁礃拥念^腦風(fēng)暴吧。
小試牛刀
讓我們從一個(gè)非常簡(jiǎn)單的例子開始,了解下 callCC 如何運(yùn)作:
- const x = callCC(function (cont) {
- for (let i = 0; i < 10; i++) {
- if (i === 3) {
- cont('done');
- }
- console.log(i);
- }
- });
- console.log(x);
- // output:
- // 0
- // 1
- // 2
- // done
從輸出結(jié)果可以看出,程序的 for 循環(huán)并沒有全部完成,而是在 i 為 3 時(shí)執(zhí)行 callCC 捕獲的 continuation 過程時(shí)直接退出了整個(gè) callCC 調(diào)用,并將 'done' 返回給了變量 x。我們可以總結(jié)下 callCC 方法的邏輯:
- 接受一個(gè)函數(shù)為唯一參數(shù)
- 該函數(shù)也有唯一一個(gè)參數(shù) cont,代表 callCC 的后續(xù)計(jì)算,在這個(gè)例子中,即將 callCC 的計(jì)算結(jié)果賦值給 x,然后執(zhí)行最后的 console.log(x) 打印結(jié)果
- callCC 會(huì)立即調(diào)用其函數(shù)參數(shù)
- 在該函數(shù)參數(shù)執(zhí)行過程中,cont 可以接受一個(gè)參數(shù)作為 callCC 的返回值,一旦調(diào)用,則忽略后續(xù)所有計(jì)算,程序控制流跳轉(zhuǎn)會(huì) callCC 的調(diào)用處繼續(xù)執(zhí)行
得益于 James Long 開發(fā)的 Unwinder 在線編譯工具,非常推薦各位去 Simple 示例 嘗試在瀏覽器里執(zhí)行下,你甚至可以打斷點(diǎn)然后單步執(zhí)行哦~
重新實(shí)現(xiàn)列表 some 方法
進(jìn)一步地,讓我們檢驗(yàn)下剛剛介紹的對(duì) callCC 的理解,重新實(shí)現(xiàn)下列表的 some 方法:
- function some(predicate, arr) {
- const x = callCC(function (cont) {
- for (let index = 0; index < arr.length; index++) {
- console.log('testing', arr[index]);
- if (predicate(arr[index])) {
- cont(true);
- }
- }
- return false;
- });
- return x;
- }
- console.log(some(x => x >= 2, [1, 2, 3, 4]));
- console.log(some(x => x >= 2, [1, -5]));
- // output:
- // testing 1
- // testing 2
- // true
- // testing 1
- // testing -5
- // false
在第一個(gè) some 函數(shù)調(diào)用中,當(dāng) predicate 返回為 true 時(shí),cont(true) 執(zhí)行后程序控制流跳轉(zhuǎn)到 callCC 調(diào)用處,然后 some 函數(shù)返回 true 并被打印。然而在第二個(gè) some 調(diào)用中,因?yàn)樗?predicate 都為 false,沒有 cont 被調(diào)用,因此 callCC 返回了其函數(shù)參數(shù)的最后一個(gè) return 語(yǔ)句的結(jié)果。
在這個(gè)例子中,我們進(jìn)一步了解了 callCC 的運(yùn)行原理,并能用它實(shí)現(xiàn)一些工具方法。
重新實(shí)現(xiàn) Try-Catch
接下來,讓我們挑戰(zhàn)一個(gè)難度更大的 callCC 應(yīng)用:重寫 try-catch。
- const tryStack = [];
- function Try(body, handler) {
- const ret = callCC(function (cont) {
- tryStack.push(cont);
- return body();
- });
- tryStack.pop();
- if (ret.__exc) {
- return handler(ret.__exc);
- }
- return ret;
- }
- function Throw(exc) {
- if (tryStack.length > 0) {
- tryStack[tryStack.length - 1]({ __exc: exc });
- }
- console.log("unhandled exception", exc);
- }
Try 函數(shù)接受兩個(gè)參數(shù):body 是接下來準(zhǔn)備執(zhí)行的主體邏輯,handler 是異常處理邏輯。關(guān)鍵點(diǎn)在于 Try 內(nèi)部在執(zhí)行 body 前會(huì)先將捕獲的 cont 壓入到堆棧 tryStack 中,以便在 Throw 時(shí)獲取 cont 從而繼續(xù)從 callCC 調(diào)用處恢復(fù),從而實(shí)現(xiàn)類似 try-catch 語(yǔ)句的功能。
下面是一個(gè) Try-Catch 的應(yīng)用示例:
- function bar(x) {
- if (x < 0) {
- Throw(new Error("error!"));
- }
- return x * 2;
- }
- function foo(x) {
- return bar(x);
- }
- Try(
- function () {
- console.log(foo(1));
- console.log(foo(-1));
- console.log(foo(2));
- },
- function (ex) {
- console.log("caught", ex);
- }
- );
- // output:
- // 2
- // caught Error: error!
和我們預(yù)期的效果一致,異常處理函數(shù)可以捕獲 Throw 拋出的異常,同時(shí)主體邏輯 body 中的剩余部分也不再執(zhí)行。另外,Throw 也像 JavaScript 原生的 throw 一樣,能夠擊穿多層函數(shù)調(diào)用,直到被 Try 語(yǔ)句的異常處理邏輯處理。
可恢復(fù)的 Try-Catch
基于上一小節(jié)中 Try-Catch 實(shí)現(xiàn),我們現(xiàn)在嘗試一個(gè)真正的能體現(xiàn) continuation 魔力的改造:讓 Try-Catch 在捕獲異常后,能夠從拋出異常的地方恢復(fù)執(zhí)行。
為了實(shí)現(xiàn)這一效果,我們只需要對(duì) Throw 進(jìn)行改造,使其也通過 callCC 過程捕獲調(diào)用 Throw 時(shí)的 continuation,并將該 continuation 賦值給異常對(duì)象以供 Resume 過程調(diào)用從而實(shí)現(xiàn)異?;謴?fù):
- function Throw(exc) {
- if (tryStack.length > 0) {
- return callCC(function (cont) {
- exc.__cont = cont;
- tryStack[tryStack.length - 1]({ __exc: exc });
- });
- }
- throw exc;
- }
- function Resume(exc, value) {
- exc.__cont(value);
- }
實(shí)際使用的例子如下:
- function double(x) {
- console.log('x is', x);
- if (x < 0) {
- x = Throw({ BAD_NUMBER: x });
- }
- return x * 2;
- }
- function main(x) {
- return double(x);
- }
- Try(
- function () {
- console.log(main(1));
- console.log(main(-2));
- console.log(main(3));
- },
- function (ex) {
- if (typeof ex.BAD_NUMBER !== 'undefined') {
- Resume(ex, Math.abs(ex.BAD_NUMBER));
- }
- console.log('caught', ex);
- }
- );
- // output:
- // x is 1
- // 2
- // x is -2
- // 4
- // x is 3
- // 6
從上例輸出中,我們可以清晰地注意到,在執(zhí)行 main(-2) 時(shí)拋出的錯(cuò)誤被準(zhǔn)確地識(shí)別并且恢復(fù)為正確的正整數(shù),并最終執(zhí)行完所有主體邏輯。
Algebraic Effects
這種異常恢復(fù)的機(jī)制,也被稱作 Algebraic Effects。它有一個(gè)非常核心的優(yōu)勢(shì):將主體邏輯與異?;謴?fù)邏輯分離。例如我們可以在 UI 組件中拋出一個(gè)數(shù)據(jù)讀取的異常,然后在更上層的異常處理邏輯中嘗試獲取該數(shù)據(jù)后恢復(fù)執(zhí)行,這樣既簡(jiǎn)化了 UI 組件的復(fù)雜度,也將數(shù)據(jù)獲取的邏輯交給了調(diào)用方,更加靈活高效。
實(shí)際上 Algebraic Effects 還有著諸多的應(yīng)用,Eff、Ocaml 等編程語(yǔ)言對(duì) Algebraic Effects 有著豐富的支持。React 有不少團(tuán)隊(duì)成員是 Ocaml 的擁躉,新近推出的 Hooks、Suspense 都深受這種思想啟發(fā),能夠讓我們類似線性同步地調(diào)用各種狀態(tài)讀取、數(shù)據(jù)獲取等異步過程。
下面我們來分析一個(gè) Suspense 示例,體會(huì)下背后解決思路的相似之處:
- function ProfilePage() {
- return (
Loading profile...}> - );
- }
- function ProfileDetails() {
- // Try to read user info, although it might not have loaded yet
- const user = resource.user.read();
- return
{user.name}
;- }
- const rootElement = document.getElementById("root");
- ReactDOM.createRoot(rootElement).render(
- );
在 ProfileDetails 組件中,執(zhí)行 resource.user.read() 時(shí),由于當(dāng)前數(shù)據(jù)并不存在,所以需要 throw 一個(gè) promise 實(shí)例。位于上層的 Suspense 在捕獲這個(gè) promise 后會(huì)先展示 fallback 指定的 UI,然后等待 promise resolve 后再次嘗試渲染 ProfileDetails 組件。雖然對(duì)比基于 Continuation 實(shí)現(xiàn)的異?;謴?fù)仍然有一定差距,并不能精確地從主體邏輯中拋出異常的語(yǔ)句處恢復(fù),而是將主體邏輯重新執(zhí)行一遍。不過 React 內(nèi)部做了大量?jī)?yōu)化,盡最大可能地避免不必要開銷。
CallCC 實(shí)現(xiàn)
相信很多讀者在一覽 callCC 的強(qiáng)大能力之后,已經(jīng)忍不住想要盡快了解下它的實(shí)現(xiàn)方式,很難想象土鱉的 JS 是如何能做到這一切的。這一章節(jié)我們就為大家揭開它的神秘面紗。
編譯
類似 Babel 幫助我們將各種 JS 新標(biāo)準(zhǔn)甚至是草案階段的語(yǔ)言特性轉(zhuǎn)化為主流瀏覽器都能運(yùn)行的最終代碼一樣,我們可以借助增加一個(gè)編譯階段將含有 callCC 調(diào)用的代碼轉(zhuǎn)化為普通瀏覽器都能運(yùn)行的代碼。
Prettier 作者 James Long 早些年開發(fā)網(wǎng)頁(yè)游戲編輯器時(shí)曾打算制作一款交互式代碼調(diào)試工具,種種嘗試之后,他在友人的指導(dǎo)下學(xué)習(xí)了 Exceptional Continuations in JavaScript 論文中介紹的高性能方法,并基于當(dāng)時(shí) Facebook 剛剛開源不久的編譯 generator 利器 Regenerator,開發(fā)了 Unwinder 來編譯 callCC,同時(shí)還提供了一個(gè)運(yùn)行時(shí)以及實(shí)時(shí)在線 debug 工具。
Unwinder 或者說 Regenerator 的核心是狀態(tài)機(jī),即將源代碼中的所有計(jì)算步驟打散,相互之間的跳轉(zhuǎn)通過狀態(tài)變換來進(jìn)行。例如下面這段簡(jiǎn)短的代碼:
- function foo() {
- var x = 5;
- var y = 6;
- return x + y;
- }
在經(jīng)過狀態(tài)機(jī)轉(zhuǎn)換后,變成了如下形式:
- function foo() {
- let $__next = 0, x, y;
- while (1) {
- switch($__next) {
- case 0:
- x = 5;
- $__next = 1;
- break;
- case 1:
- y = 6;
- $__next = 2;
- break;
- case 2:
- return x + y;
- }
- }
- }
基于這種核心能力,輔以 Exceptional Continuations 特有的 try-catch、restore 等邏輯支持,Unwinder 能夠很好地實(shí)現(xiàn) Continuation。不過后續(xù)作者并沒有再對(duì)其進(jìn)行維護(hù),同時(shí)它在異步操作方面的支持有一定缺陷,導(dǎo)致目前并不是非常流行。
Generator
另外一派是直接采用 Generator 來實(shí)現(xiàn),這非常符合直覺,畢竟 Generator 就是一種轉(zhuǎn)移控制流的非常獨(dú)特的方式。
Yassine Elouafi 在系列文章 Algebraic Effects in JavaScript 中系統(tǒng)性地介紹了 Continuation、CPS、使用 Generator 改造 CPS 并實(shí)現(xiàn) callCC、進(jìn)一步支持 Delimited Continuation 以及最終支持 Algebraic Effects 等內(nèi)容,行文順暢,內(nèi)容示例夯實(shí),是研究 JS Continuation 上乘的參考資料。
限于篇幅,本文不再對(duì)其原理進(jìn)行深入介紹,感興趣的同學(xué)可以讀一下他的系列文章。下面是非常核心的 callcc 實(shí)現(xiàn)部分:
- function callcc(genFunc) {
- return function(capturedCont) {
- function jumpToCallccPos(value) {
- return next => capturedCont(value);
- }
- runGenerator(genFunc(jumpToCallccPos), null, capturedCont);
- };
- }
為了支持類似上文中提到的 Try-Catch,我們可以定義如下方法:
- const handlerStack = [];
- function* trycc(computation, handler) {
- return yield callcc(function*(k) {
- handlerStack.push([handler, k]);
- const result = yield computation;
- handlerStack.pop();
- return result;
- });
- }
- function* throwcc(exception) {
- const [handler, k] = handlerStack.pop();
- const result = yield handler(exception);
- yield k(result);
- }
從實(shí)現(xiàn)層面來看,Generator 方式比編譯方式更加簡(jiǎn)單,核心代碼不到百行。但是因?yàn)?Generator 本身的認(rèn)知復(fù)雜度導(dǎo)致一定門檻,另外所有調(diào)用 callCC 的相關(guān)代碼都必須使用 Generator 才能夠順利運(yùn)行,這對(duì)于應(yīng)用開發(fā)來說太過艱難,更不必說需要改造的海量的第三方模塊。
缺點(diǎn)
Continuation 并非銀彈,究其本質(zhì),它是一個(gè)高級(jí)版本的能夠處理函數(shù)表達(dá)式的 Goto 語(yǔ)句。眾所眾知,由于高度靈活導(dǎo)致的難以理解和調(diào)試,Goto 語(yǔ)句在各個(gè)語(yǔ)言中都屬于半封禁甚至封禁狀態(tài)。Continuation 面臨類似的窘境,需要使用者思慮周全,慎之又慎,將其應(yīng)用控制在一定合理范圍,甚至像 React 這樣完全封裝在自身實(shí)現(xiàn)內(nèi)部。
結(jié)語(yǔ)
Continuation 是個(gè)非常復(fù)雜的概念,為了能夠由淺入深、結(jié)合 JS 實(shí)際地來系統(tǒng)性闡述這一概念,筆者花費(fèi)了自專欄開設(shè)以來最長(zhǎng)的時(shí)間做各種梳理準(zhǔn)備。不期望大家讀過這篇文章后就馬上開始使用 Continuation 或者 Algebraic Effects。如前文所述,目前 Continuation 還存在各方面的問題,應(yīng)該實(shí)事求是,因地制宜,取其精華去其糟粕。正如 React Hooks、Suspense 一樣,它們并沒有真的搞了內(nèi)部的編譯器或者引入 Generator,而是結(jié)合實(shí)際,神似而形不同,最大限度地滿足了設(shè)計(jì)目標(biāo)。此外,期望這篇長(zhǎng)文能幫助大家理解一些設(shè)計(jì)背后的思路,拓展一點(diǎn)前端工程師的技術(shù)視野,了解到整個(gè)編程領(lǐng)域內(nèi)的優(yōu)秀實(shí)踐。
彩蛋
React Fiber 是 React 16 引入的最為重要的底層變化,主要解決阻塞渲染的問題。為了實(shí)現(xiàn)這一目標(biāo),F(xiàn)iber 化整為零,將組件中的每一個(gè)子組件或者子元素都視為一個(gè) Fiber,通過類似 DOM Tree 的組織方式形成一個(gè) Fiber Tree:
每個(gè) Fiber 都有獨(dú)立的 render 過程和狀態(tài)存儲(chǔ),在渲染時(shí),我們可以把整個(gè) Fiber Tree 的渲染過程理解成遍歷整個(gè) Fiber Tree 的過程,每個(gè) Fiber 的渲染工作可以理解為一個(gè)函數(shù)調(diào)用,為了不阻塞頁(yè)面交互,React 核心的任務(wù)調(diào)度算法是這樣的:
- function workLoop(deadline) {
- let shouldYield = false;
- while (nextUnitOfWork && !shouldYield) {
- nextUnitOfWork = performUnitOfWork(
- nextUnitOfWork
- );
- shouldYield = deadline.timeRemaining() < 1;
- }
- if (!nextUnitOfWork && wipRoot) {
- commitRoot();
- }
- requestIdleCallback(workLoop);
- }
- requestIdleCallback(workLoop);
在每個(gè)瀏覽器 idle 的時(shí)間片內(nèi),workLoop 會(huì)盡可能多地執(zhí)行 Fiber 渲染任務(wù),如果時(shí)間到期且仍然有未完成任務(wù)時(shí),nextUnitOfWork 會(huì)更新到最后一個(gè)待執(zhí)行任務(wù),然后等待下一個(gè) idle 時(shí)間片繼續(xù)執(zhí)行。
雖然這部分代碼并沒有明確地使用我們前文提到的種種 Continuation 方式,但是究其本質(zhì),React 是將 Fiber 引入之前的遞歸調(diào)用實(shí)現(xiàn)一次性完整渲染改變成以 Fiber Tree 為基礎(chǔ)的虛擬任務(wù)堆棧(或許不應(yīng)該稱為棧,因?yàn)樗且粋€(gè)樹形結(jié)構(gòu)),從而實(shí)現(xiàn)了對(duì)渲染任務(wù)的靈活調(diào)度。因此,nextUnitOfWork 在這里可以視作某種程度上的 Continuation,它代表著 React 渲染任務(wù)的“剩余部分”。
聯(lián)想到前面提到的 React Hooks、Suspense 背后借鑒的 Algebraic Effects 思想,難怪 React 團(tuán)隊(duì)核心成員 Sebastian Markb?ge 曾經(jīng)放言:
React is operating at the level of a language feature
文章名稱:Continuation在JS中的應(yīng)用
文章源于:http://fisionsoft.com.cn/article/dpcdhhs.html


咨詢
建站咨詢
