Continuation如何在JS中的應(yīng)用

這篇文章給大家分享的是有關(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é)束了。

Continuation如何在JS中的應(yīng)用 

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)用棧的限制了呢?

Continuation如何在JS中的應(yīng)用

首先,因?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:

Continuation如何在JS中的應(yīng)用

每個(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&aring;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)站建設(shè)網(wǎng)站維護(hù)公司