這篇文章給大家分享的是有關(guān)Continuation如何在JS中的應(yīng)用的內(nèi)容。小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過(guò)來(lái)看看吧。
網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)公司!專(zhuān)注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、小程序制作、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶(hù)創(chuàng)新互聯(lián)還提供了科爾沁左翼免費(fèi)建站歡迎大家使用!
正文從這開(kāi)始~~
React 新近發(fā)布的 Hooks、Suspense、Concurrent Mode 等新功能讓人眼前一亮,甚至驚嘆 JS 居然有如此魔力。同時(shí),這幾個(gè)功能或多或少附帶一些略顯奇怪的規(guī)則,沒(méi)有更深層次理解的話難以把握。其實(shí)這里面并沒(méi)有什么“黑科技”,就大的趨勢(shì)來(lái)講,前端整體上還是在不斷借鑒計(jì)算機(jī)其它領(lǐng)域的優(yōu)秀實(shí)踐,來(lái)幫助我們更方便地解決人機(jī)交互問(wèn)題。本文著眼于支撐這些功能的一個(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è)坊間更通俗的說(shuō)法是:它代表程序的剩余部分。像 continue、break 這類(lèi)控制流操作符一樣,continuation 能夠暴露給用戶(hù)程序從而可以在恰當(dāng)時(shí)機(jī)恢復(fù)執(zhí)行,這種基本能力大大擴(kuò)展了編程語(yǔ)言使用者的發(fā)揮空間,也為 excpetion handling、generators、coroutines、algebraic effects 等提供了堅(jiān)實(shí)基礎(chǔ)。
相信很多人和我一樣,對(duì)這樣不明就里的官方解釋迷惑不解。沒(méi)關(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)行的,無(wú)法靈活調(diào)整執(zhí)行順序。此外,控制流的控制權(quán)也被運(yùn)行環(huán)境牢牢掌握,程序員無(wú)能為力。
現(xiàn)在,讓我們?cè)O(shè)想下,如果未來(lái)有一天我們能夠?qū)⒄{(diào)用任務(wù)以鏈表的方式存儲(chǔ)在堆中,是不是就可以突破調(diào)用棧的限制了呢?
首先,因?yàn)槿蝿?wù)以調(diào)用楨的形式存儲(chǔ)在堆中,并通過(guò)指針相互關(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)單來(lái)說(shuō),它是將程序控制流通過(guò) continuation 的形式進(jìn)行顯示傳遞的一種編程方式,具體有三個(gè)典型特征:
每個(gè)函數(shù)的最后一個(gè)參數(shù)都是它的 continuation
函數(shù)內(nèi)部不能顯示地使用 return
函數(shù)只能通過(guò)調(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)地獄。表面上看的確如此,但是從控制流的角度來(lái)進(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ì)算過(guò)程:
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
可以看出,一方面,通過(guò)合理組織計(jì)算步驟模型,evaluate 可以幫助避免回調(diào)地獄的問(wèn)題,另一方面,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)用來(lái)處理遠(yuǎn)程請(qǐng)求等操作,那么我們將不得不暫停這唯一的線程直到異步操作結(jié)果返回,這意味著用戶(hù)的其它交互得不到及時(shí)響應(yīng)。CPS 或者換言之的回調(diào)模式提供了一種有效易用的方式來(lái)處理這類(lèi)問(wèn)題
然而,程序終究是人來(lái)編寫(xiě)和維護(hù)的,CPS 雖然有眾多好處,但讓所有人都遵循這樣嚴(yán)格的方式編程非常困難,目前這種技術(shù)更多地在編譯器中作為中間表示層應(yīng)用。
CallCC
目前 Continuation 的主流應(yīng)用方式是通過(guò)形如 callCC(call with current continuation)的過(guò)程調(diào)用以捕獲當(dāng)前 continuation,并在之后適時(shí)執(zhí)行它以恢復(fù)到 continuation 所在上下文繼續(xù)執(zhí)行后續(xù)計(jì)算從而實(shí)現(xiàn)各種控制流操作。
Scheme、Scala 等語(yǔ)言提供了 call/cc 或等效控制流操作符,JS 目前并沒(méi)有原生支持,但是通過(guò)后續(xù)介紹的兩種方式可以間接實(shí)現(xiàn)。
現(xiàn)在假設(shè)我們已經(jīng)可以在 JS 中使用 callCC 操作符,讓我們?cè)囋嚳此寄転槲覀儙?lái)什么樣的頭腦風(fēng)暴吧。
小試牛刀
讓我們從一個(gè)非常簡(jiǎn)單的例子開(kāi)始,了解下 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)并沒(méi)有全部完成,而是在 i 為 3 時(shí)執(zhí)行 callCC 捕獲的 continuation 過(guò)程時(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í)行過(guò)程中,cont 可以接受一個(gè)參數(shù)作為 callCC 的返回值,一旦調(diào)用,則忽略后續(xù)所有計(jì)算,程序控制流跳轉(zhuǎn)會(huì) callCC 的調(diào)用處繼續(xù)執(zhí)行
得益于 James Long 開(kāi)發(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,沒(méi)有 cont 被調(diào)用,因此 callCC 返回了其函數(shù)參數(shù)的最后一個(gè) return 語(yǔ)句的結(jié)果。
在這個(gè)例子中,我們進(jìn)一步了解了 callCC 的運(yùn)行原理,并能用它實(shí)現(xiàn)一些工具方法。
重新實(shí)現(xiàn) Try-Catch
接下來(lái),讓我們挑戰(zhàn)一個(gè)難度更大的 callCC 應(yīng)用:重寫(xiě) 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 是接下來(lái)準(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)類(lèi)似 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)行改造,使其也通過(guò) callCC 過(guò)程捕獲調(diào)用 Throw 時(shí)的 continuation,并將該 continuation 賦值給異常對(duì)象以供 Resume 過(guò)程調(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ī)制,也被稱(chēng)作 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ā),能夠讓我們類(lèi)似線性同步地調(diào)用各種狀態(tài)讀取、數(shù)據(jù)獲取等異步過(guò)程。
下面我們來(lái)分析一個(gè) Suspense 示例,體會(huì)下背后解決思路的相似之處:
function ProfilePage() { return ( <Suspense fallback={<h2>Loading profile...</h2>}> <ProfileDetails /> </Suspense> ); } function ProfileDetails() { // Try to read user info, although it might not have loaded yet const user = resource.user.read(); return <h2>{user.name}</h2>; } const rootElement = document.getElementById("root"); ReactDOM.createRoot(rootElement).render( <ProfilePage /> );
在 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í)行一遍。不過(guò) React 內(nèi)部做了大量?jī)?yōu)化,盡最大可能地避免不必要開(kāi)銷(xiāo)。
CallCC 實(shí)現(xiàn)
相信很多讀者在一覽 callCC 的強(qiáng)大能力之后,已經(jīng)忍不住想要盡快了解下它的實(shí)現(xiàn)方式,很難想象土鱉的 JS 是如何能做到這一切的。這一章節(jié)我們就為大家揭開(kāi)它的神秘面紗。
編譯
類(lèi)似 Babel 幫助我們將各種 JS 新標(biāo)準(zhǔn)甚至是草案階段的語(yǔ)言特性轉(zhuǎn)化為主流瀏覽器都能運(yùn)行的最終代碼一樣,我們可以借助增加一個(gè)編譯階段將含有 callCC 調(diào)用的代碼轉(zhuǎn)化為普通瀏覽器都能運(yùn)行的代碼。
Prettier 作者 James Long 早些年開(kāi)發(fā)網(wǎng)頁(yè)游戲編輯器時(shí)曾打算制作一款交互式代碼調(diào)試工具,種種嘗試之后,他在友人的指導(dǎo)下學(xué)習(xí)了 Exceptional Continuations in JavaScript 論文中介紹的高性能方法,并基于當(dāng)時(shí) Facebook 剛剛開(kāi)源不久的編譯 generator 利器 Regenerator,開(kāi)發(fā)了 Unwinder 來(lái)編譯 callCC,同時(shí)還提供了一個(gè)運(yùn)行時(shí)以及實(shí)時(shí)在線 debug 工具。
Unwinder 或者說(shuō) Regenerator 的核心是狀態(tài)機(jī),即將源代碼中的所有計(jì)算步驟打散,相互之間的跳轉(zhuǎn)通過(guò)狀態(tài)變換來(lái)進(jìn)行。例如下面這段簡(jiǎn)短的代碼:
function foo() { var x = 5; var y = 6; return x + y; }
在經(jīng)過(guò)狀態(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。不過(guò)后續(xù)作者并沒(méi)有再對(duì)其進(jìn)行維護(hù),同時(shí)它在異步操作方面的支持有一定缺陷,導(dǎo)致目前并不是非常流行。
Generator
另外一派是直接采用 Generator 來(lái)實(shí)現(xiàn),這非常符合直覺(jué),畢竟 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); }; }
為了支持類(lèi)似上文中提到的 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)層面來(lái)看,Generator 方式比編譯方式更加簡(jiǎn)單,核心代碼不到百行。但是因?yàn)?Generator 本身的認(rèn)知復(fù)雜度導(dǎo)致一定門(mén)檻,另外所有調(diào)用 callCC 的相關(guān)代碼都必須使用 Generator 才能夠順利運(yùn)行,這對(duì)于應(yīng)用開(kāi)發(fā)來(lái)說(shuō)太過(guò)艱難,更不必說(shuō)需要改造的海量的第三方模塊。
缺點(diǎn)
Continuation 并非銀彈,究其本質(zhì),它是一個(gè)高級(jí)版本的能夠處理函數(shù)表達(dá)式的 Goto 語(yǔ)句。眾所眾知,由于高度靈活導(dǎo)致的難以理解和調(diào)試,Goto 語(yǔ)句在各個(gè)語(yǔ)言中都屬于半封禁甚至封禁狀態(tài)。Continuation 面臨類(lèi)似的窘境,需要使用者思慮周全,慎之又慎,將其應(yīng)用控制在一定合理范圍,甚至像 React 這樣完全封裝在自身實(shí)現(xiàn)內(nèi)部。
結(jié)語(yǔ)
Continuation 是個(gè)非常復(fù)雜的概念,為了能夠由淺入深、結(jié)合 JS 實(shí)際地來(lái)系統(tǒng)性闡述這一概念,筆者花費(fèi)了自專(zhuān)欄開(kāi)設(shè)以來(lái)最長(zhǎng)的時(shí)間做各種梳理準(zhǔn)備。不期望大家讀過(guò)這篇文章后就馬上開(kāi)始使用 Continuation 或者 Algebraic Effects。如前文所述,目前 Continuation 還存在各方面的問(wèn)題,應(yīng)該實(shí)事求是,因地制宜,取其精華去其糟粕。正如 React Hooks、Suspense 一樣,它們并沒(méi)有真的搞了內(nèi)部的編譯器或者引入 Generator,而是結(jié)合實(shí)際,神似而形不同,最大限度地滿(mǎn)足了設(shè)計(jì)目標(biāo)。此外,期望這篇長(zhǎng)文能幫助大家理解一些設(shè)計(jì)背后的思路,拓展一點(diǎn)前端工程師的技術(shù)視野,了解到整個(gè)編程領(lǐng)域內(nèi)的優(yōu)秀實(shí)踐。
彩蛋
React Fiber 是 React 16 引入的最為重要的底層變化,主要解決阻塞渲染的問(wèn)題。為了實(shí)現(xiàn)這一目標(biāo),F(xiàn)iber 化整為零,將組件中的每一個(gè)子組件或者子元素都視為一個(gè) Fiber,通過(guò)類(lèi)似 DOM Tree 的組織方式形成一個(gè) Fiber Tree:
每個(gè) Fiber 都有獨(dú)立的 render 過(guò)程和狀態(tài)存儲(chǔ),在渲染時(shí),我們可以把整個(gè) Fiber Tree 的渲染過(guò)程理解成遍歷整個(gè) Fiber Tree 的過(guò)程,每個(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í)行。
雖然這部分代碼并沒(méi)有明確地使用我們前文提到的種種 Continuation 方式,但是究其本質(zhì),React 是將 Fiber 引入之前的遞歸調(diào)用實(shí)現(xiàn)一次性完整渲染改變成以 Fiber Tree 為基礎(chǔ)的虛擬任務(wù)堆棧(或許不應(yīng)該稱(chēng)為棧,因?yàn)樗且粋€(gè)樹(shù)形結(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
感謝各位的閱讀!關(guān)于“Continuation如何在JS中的應(yīng)用”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
文章名稱(chēng):Continuation如何在JS中的應(yīng)用
分享網(wǎng)址:http://muchs.cn/article38/ihddsp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)公司、定制開(kāi)發(fā)、企業(yè)網(wǎng)站制作、移動(dòng)網(wǎng)站建設(shè)、靜態(tài)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
網(wǎng)頁(yè)設(shè)計(jì)公司知識(shí)