摘要:不需要多線程的鎖機制線程由系統控制切換,協程是由用戶控制切換。協程的中斷實際上是掛起的概念協程發起異步操作意味著該協程將會被掛起,為了保證喚醒時能正常運行,需要正確保存并恢復其運行時的上下文。
博客 github 地址: https://github.com/HCThink/h-blog/blob/master/js/syncAndAsync/generator/readme.md
github 首頁(star+watch,一手動態直達): https://github.com/HCThink/h-blog
掘金 link , 掘金 專欄
segmentfault 主頁
原創禁止私自轉載
Generator可以隨心所欲的交出和恢復函數的執行權,yield交出執行權,next()恢復執行權Generator 函數是一個狀態機,封裝了多個內部狀態,執行一個Generator函數會返回一個迭代器對象,可以依次遍歷 Generator 函數內部的每一個狀態
調用一個生成器函數并不會馬上執行它里面的語句,而是返回一個這個 generator 的 迭代器 (iterator )對象。當這個迭代器的 next() 方法被首次(后續)調用時,其內的語句會執行到第一個(后續)出現yield的位置為止,yield 后緊跟迭代器要返回的值。
或者如果用的是 yield*(多了個星號),則表示將執行權移交給另一個生成器函數(當前生成器暫停執行)。
next()方法返回一個對象,這個對象包含兩個屬性:value 和 done,value 屬性表示本次 yield 表達式的返回值,done 屬性為布爾類型,表示生成器后續是否還有 yield 語句,即生成器函數是否已經執行完畢并返回。
典型場景co
async/await
依賴 async 的上層庫和應用不勝枚舉,比如 koa
koa 等依賴其上層語法糖封裝: koa
基本使用 codefunction* 這種聲明方式(function關鍵字后跟一個星號)會定義一個生成器函數 (generator function),它返回一個 Generator 對象。
更多 demo 參考: 迭代器 , 常見數列生成器
// 斐波那契豎列生成器 function* fib() { let [x, y]: [number, number] = [0, 1]; while (true) { [x, y] = [y, x + y]; yield x; } } const generator: Generator = fib(); // 階乘 function* factorial() { let x: number = 1; let fac: number = 1; while (true) { yield fac; fac = fac * ++x; } }Generator 對象
調用一個生成器函數并不會馬上執行它里面的語句,而是返回這個生成器的 迭代器 (實現 iterator )的對象 Generator, 所以它符合可迭代協議和迭代器協議。如上述代碼中 const generator: Generator = fib(); 接受 fib() 的類型即: Generator
Generator 對象
Generator.prototype.next()
返回一個由 yield表達式生成的值。
Generator.prototype.return()
返回給定的值并結束生成器。
Generator.prototype.throw()
向生成器拋出一個錯誤。yield 優先級
操作符優先級匯總
yield 僅僅比 展開運算符: ..., 逗號: , 的優先級高,所以注意區分 yield fn() + 10 中 fn() + 10 才是 yield 表達式。
generator 中斷 的入參和返回generator 入參返回
注意,不是要說整個 generator 的出入參,而是 yield 和 next,這個問題,其實困擾我蠻久的,原因是 generator 和傳統 js 的函數調用區別很大, 如果你很熟悉普通函數調用的出入參,在這里往往轉不過彎。
返回: next() 返回類 { done: boolean, value: any } 對象, 其中 value 則是 yield 表達式的值。
實際上返回會好理解一些,當我們執行 generator 函數之后獲得一個 Generator 對象當我們第一次調用 GeneratorObj.next() 時,函數才會開始執行,直到第一個 yield 表達式執行完成, 并將 yield 表達式結果提供給 next 進行返回。【注意 yield 表達式此時開始執行】,然后進入中斷。
function pi(n: number): number { return Math.PI * n; } function* fn(n: number) { // 第一個 next 調用后 yield 表達式【pi(n) + 10, 注意優先級】執行并將結果: 13.1415... 進行包裝 // { value: 13.14..., done: false } let g1 = yield pi(n) + 10; // 同理這里就是: { value: 3.141592653589793, done: false } g1 = (yield pi(n)) + 10; // return 等價一最后一個 yield。 return 100; } const fnGenx: Generator = fn(1); Log(fnGenx.next()); // { value: 13.141592653589793, done: false } Log(fnGenx.next()); // { value: 3.141592653589793, done: false } Log(fnGenx.next()); // { value: 100, done: true }
當在生成器函數中顯式 return 時,會導致生成器立即變為完成狀態,即調用 next() 方法返回的對象的 done 為 true。如果 return 后面跟了一個值,那么這個值會作為當前調用 next() 方法返回的 value 值。
調用 next 時會立即獲得 yield 表達式的執行結果。也就是說 yield 不能多帶帶處理異步,因為 yield 其實不在意其后的表達式所有代碼執行結束的時間點。因此也無法確定下次 next 的調用時間點。
入參: next 方法也可以通過接受一個參數用以向生成器傳值。請注意,首次調用 next 方法時參數會被丟棄。next 入參規則如下:
調用 next()方法時,如果傳入了參數,那么這個參數會作為上一條執行的 yield 語句的返回值
實際上往往會誤認為 let g1 = (yield x10(n)) + 10; 中 yield 表達式的值就會直接賦值給 g1 其實并不是這樣的,yield 表達式的值是 next 的返回值,當下次 next(100) 傳入的值會替代上一個 yield 表達式的值。也就等價于 g1 = (100) + 10
function x10(n: number): number { return 10 * n; } function* fn(n: number) { // yield x10(n) + 10 結果為:30, 下次 next 時傳入的值做了 +10, 則 g1 值為: 40 let g1 = yield x10(n) + 10; Log(g1); // 40 // 同理: (yield x10(g1)) 結果為: 40 * 10 = 400, 下次 next 時傳入的值: 400 + 10 = 410 // 代入中斷的點: g1 = 410(yield x10(g1)) + 10 = 420 g1 = (yield x10(g1)) + 10; Log(g1); // 420 } // 第一個參數由生成器提供 const fnGenx: Generator = fn(2); let genObj = fnGenx.next(100); // 第一次入參會被丟棄, 因為他沒有上一個 yield while (!genObj.done) { Log("outer: ", genObj.value); genObj = fnGenx.next(genObj.value + 10); } // outer: 30 // 40 // outer: 400 // 420一個官方 demo
/** gen函數運行解析: * i=0 時傳入參數(0),并將參數0賦給上一句yield的返回賦值,由于沒有上一句yield語句,這步被忽略 * 執行var val =100,然后執行yield val,此時g.next(i)返回{ value: 100, done: false } * 然后console.log(i,g.next(i).value),打印出0 100 * * i=1 時傳入參數(1),并將參數1賦給上一句yield的返回賦值,即(val = 1) * 然后執行console.log(val),打印出1。 * 接著進入第二次while循環,調用yield val,此時g.next(i)返回{ value: 1, done: false } * 然后console.log(i,g.next(i).value),打印出1 1 * * i=2 ....(省略) */ function* gen() { var val =100; while(true) { val = yield val; console.log(val); } } var g = gen(); for(let i =0;i<5;i++){ console.log(i,g.next(i).value); } // 返回: // 0 100 // 1 // 1 1 // 2 // 2 2 // 3 // 3 3 // 4 // 4 4yield*
如果 yield* Generator, 可以等價認為將 這個 Generator 的所有 yield 插入到 當前位置
function* anotherGenerator(i) { yield i + 1; yield i + 2; yield i + 3; } function* generator(i){ yield i; yield* anotherGenerator(i);// 移交執行權 yield i + 10; } // 等價于 function* generator(i){ yield i; // yield* anotherGenerator(i);// 移交執行權 yield i + 1; yield i + 2; yield i + 3; yield i + 10; }注意點
next 的參數會作為上一條執行的 yield 語句的返回值: let first = yield 1; 中 first 不是直接賦值為 yield 表達式的值, 而是 下次 next 傳入的值。
生成器函數不能當做構造器使用。
function* f() {} var obj = new f; // throws "TypeError: f is not a constructor"
yield 表達式是立即執行的,并且返回表達式值, 如果 yield 表達式是異步的,你需要在恰當的時機觸發 next 才能達到 async 的執行順序。在『重要問題 generator & 異步』中有詳細講解
generator 和異步機制不同,只是配合 generator + 執行器可以 "同步化" 處理異步, Generator 函數是ES6提供的一種異步編程解決方案
“中斷”是 Generator 的重要特征 ———— Generator 能讓一段程序執行到指定的位置先中斷,啟動。
babel 轉譯參考: demo
function *gen(p) { console.log(p) const de1 = yield fn(p); console.log(de1) const de2 = yield fn(de1); console.log(de2) } function fn(p) { return Math.random() * p; }
通過 babel 編譯為
"use strict"; var _marked = /*#__PURE__*/regeneratorRuntime.mark(gen); function gen(p) { var de1, de2; return regeneratorRuntime.wrap(function gen$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: console.log(p); _context.next = 3; return fn(p); case 3: de1 = _context.sent; console.log(de1); _context.next = 7; return fn(de1); case 7: de2 = _context.sent; console.log(de2); case 9: case "end": return _context.stop(); } } }, _marked, this); } function fn(p) { return Math.random() * p; }
可以看到 babel 使用了一個諸如的對象: regeneratorRuntime 在不支持的環境 polyfill,這個對象解析出現在 babel 的 babel-plugin-transform-runtime 插件中.
插件目錄
source
const moduleName = injectCoreJS2 ? "@babel/runtime-corejs2" : "@babel/runtime"; let modulePath = moduleName; if (node.name === "regeneratorRuntime" && useRuntimeRegenerator) { path.replaceWith( this.addDefaultImport( `${modulePath}/regenerator`, "regeneratorRuntime", ), ); return; }
繼續跟進到 babel-runtime-corejs2/regenerator/index.js, babel-runtime/regenerator/index.js 文件中, 兩個文件均只有一行代碼: module.exports = require("regenerator-runtime"); 都使用了 fackbook 的 regenerator
regenerator
regenerator 源碼分析參考
支撐思想: 協程協程,又稱微線程,纖程. 是一種非搶占式資源調度單元, 是一個無優先級的輕量級的用戶態線程前期知識準備
現代操作系統是分時操作系統,資源分配的基本單位是進程,CPU調度的基本單位是線程。
簡單來說,進程(Process), 線程(Thread)的調度是由操作系統負責,線程的睡眠、等待、喚醒的時機是由操作系統控制,開發者無法精確的控制它們。使用協程,開發者可以自行控制程序切換的時機,可以在一個函數執行到一半的時候中斷執行,讓出CPU,在需要的時候再回到中斷點繼續執行。
上下文: 指的是程序在執行中的一個狀態。通常我們會用調用棧來表示這個狀態——棧記載了每個調用層級執行到哪里,還有執行時的環境情況等所有有關的信息。
調度: 指的是決定哪個上下文可以獲得接下去的CPU時間的方法。
進程: 是一種古老而典型的上下文系統,每個進程有獨立的地址空間,資源句柄,他們互相之間不發生干擾。
線程: 是一種輕量進程,實際上在linux內核中,兩者幾乎沒有差別,除了線程并不產生新的地址空間和資源描述符表,而是復用父進程的。 但是無論如何,線程的調度和進程一樣,必須陷入內核態。
協程傳統的編程語言,早有多任務的解決方案,其中有一種叫做"協程"(coroutine),意思是多個線程互相協作,完成異步任務, 這和普通的搶占式線程有所不同。
JS 中 generator 就類似一個語言層面實現的非搶占式的輕量級"線程"。 線程包含于進程,而協程包含于線程
所以協程具有極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯。
不需要多線程的鎖機制
線程由系統控制切換,協程是由用戶控制切換。
從更高的層面來講,協程和多線程是兩種解決“多任務”編程的技術。多線程使得 "同一時刻貌似" 有多個線程在并發執行,不過需要在多個線程間協調資源,因為多個線程的執行進度是“不可控”的。而協程則避免了多線程的問題,同一時刻實質上只有一個“線程”在執行,所以不會存在資源“搶占”的問題。
不過在 JS 領域,貌似不存在技術選擇的困難,因為 JS 目前還是“單線程”的,所以引入協程也是很自然的選擇吧。
協程 & 函數棧大多語言都是層級調用,比如A調用B,B在執行過程中又調用了C,C執行完畢返回,B執行完畢返回,最后是A執行完畢。所以子程序調用是通過棧實現的,一個線程就是執行一個子程序。子程序調用總是一個入口,一次返回,調用順序是明確的。
而協程的調用和子程序不同。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接著執行。
協程的中斷: 實際上是掛起的概念協程發起異步操作意味著該協程將會被掛起,為了保證喚醒時能正常運行,需要正確保存并恢復其運行時的上下文。記錄步驟為:
保存當前協程的上下文(運行棧,返回地址,寄存器狀態)
設置將要喚醒的協程的入口指令地址到IP寄存器
恢復將要喚醒的協程的上下文
可以參考 libco 騰訊開源的一個C++協程庫,作為微信后臺的基礎庫,經受住了實際的檢驗: libco
JS 協程: generatorjs 的生成器也是一種特殊的協程,它擁有 yield 原語,但是卻不能指定讓步的協程,只能讓步給生成器的調用者或恢復者。由于不能多個協程跳來跳去,生成器相對主執行線程來說只是一個可暫停的玩具,它甚至都不需要另開新的執行棧,只需要在讓步的時候保存一下上下文就好。因此我們認為生成器與主控制流的關系是不對等的,也稱之為非對稱協程(semi-coroutine)。
因此一般的協程實現都會提供兩個重要的操作 Yield 和 Resume(next)。
Generator 實現協程的問題在協程執行中不能有阻塞操作,否則整個線程被阻塞(協程是語言級別的,線程,進程屬于操作系統級別)
需要特別關注全局變量、對象引用的使用
yield 僅能存在于 生成器內部[對比 node-fibers]
真.協程所謂的真協程是相對 generator 而言的, node-fibers 庫提供了對應的實現,我們用一個例子部分代碼說明二者區別
import Fiber from "fibers" function fibersCo () { /* 基于 fibers 的執行器 ..... */ } fibersCo(() => { let foo1 = a => { console.log("read from file1"); let ret = Fiber.yield(a); return ret; }; let foo2 = b => { console.log("read from file2"); let ret = Fiber.yield(b); return ret; }; let getSum = () => { let f1 = foo1(readFile("/etc/fstab")); let f2 = foo2(readFile("/etc/shells")); return f1.toString().length + f2.toString().length; }; let sum = getSum(); });
通過這個代碼可以發現,在第一次中斷被恢復的時候,恢復的是一系列的執行棧!從棧頂到棧底依次為:foo1 => getSum => fibersCo 里的匿名函數;而使用生成器,我們就無法寫出這樣的程序,因為 yield 原語只能在生產器內部使用, SO
無論什么時候被恢復,都是簡單的恢復在生成器內部,所以說生成器的中斷是不開調用棧滴。重要問題 generator & 異步
generator 處理異步
generator 機制和異步有所不同, Generator 和普通函數本質區別在于 Generator 能讓一段程序執行到指定的位置,然后交出執行棧,調用下次 next 的時候又會從之前中斷的位置繼續開始執行,配合這種機制處理異步,則會產生同步化異步處理的效果。
generator 的問題
但其實很快發現 generator 不能多帶帶處理異步問題,原因在于
generator 無法獲取下次 next 的時機。
generator 無法自執行
generator 處理異步的思路 + 實踐
使用 Generator 函數來處理異步操作的基本思想就是在執行異步操作時暫停生成器函數的執行,然后在階段性異步操作完成的狀態中通過生成器對象的next方法讓Generator函數從暫停的位置恢復執行,如此往復直到生成器函數執行結束。簡單來講其實就是將異步串行化了。
也正是基于這種思想,Generator函數內部才得以將一系列異步操作寫成類似同步操作的形式,形式上更加簡潔明了。
而要讓Generator函數按順序自動完成內部定義好的一系列異步操作,還需要配套的執行器。與之配套的有兩種思路
thunk
promise
其實在 async/await 之前就已經有了 co 庫使用此兩種方案實現類似 async 的機制。參考 co 源碼分析
co 源碼分析
優勢: 異常捕獲。 generator 的異常捕獲模型,優于 promise。
function* gen(x){ try { var y = yield x + 2; } catch (e){ console.log(e); } return y; }generator 的 yield 會產生調用函數棧么?
因為 yield 原語只能在生產器內部使用, 所以無論什么時候被恢復,都是簡單的恢復在生成器內部。所以說生成器的中斷是不開調用棧滴。
參考上述章節
真.協程
中斷/函數棧
上層應用async / await
async / await, co
并發通信: 多個generator函數結合在一起,讓他們獨立平行的運行,并且在它們執行的過程中來來回回得傳遞信息
csp : 并行(多個 generator 微線程通訊)
參考function* api
Generator api
https://cnodejs.org/topic/58d...
https://www.cnblogs.com/wangf...
https://github.com/Jocs/jocs....
https://cnodejs.org/topic/58d...
https://brooch.me/2016/12/30/...
https://blog.csdn.net/shenlei...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/100012.html
摘要:學習開發,無論是前端開發還是都避免不了要接觸異步編程這個問題就和其它大多數以多線程同步為主的編程語言不同的主要設計是單線程異步模型。由于異步編程可以實現非阻塞的調用效果,引入異步編程自然就是順理成章的事情了。 學習js開發,無論是前端開發還是node.js,都避免不了要接觸異步編程這個問題,就和其它大多數以多線程同步為主的編程語言不同,js的主要設計是單線程異步模型。正因為js天生的與...
摘要:實現方案首先小程序目前還是不支持的和的,那么如何讓它支持呢點擊下載,并把下載好的文件夾放到自己小程序的目錄下,包總共才多,體積很小的。如果想使用這些新的對象和方法,必須使用,為當前環境提供一個墊片。用于實現瀏覽器并不支持的原生的代碼。 前言 在平常的項目開發中肯定會遇到同步異步執行的問題,還有的就是當執行某一個操作依賴上一個執行所返回的結果,那么這個時候你會如何解決這個問題呢; 1.是...
摘要:然而異步編程真正發展壯大,的流行功不可沒。于是從異步編程誕生的那一刻起,它就和回調函數綁在了一起。這個函數會起一個定時器,在超過指定時間后執行指定的函數。我們知道是異步編程的未來。 什么是異步(Asynchrony) 按照維基百科上的解釋:獨立于主控制流之外發生的事件就叫做異步。比如說有一段順序執行的代碼 void function main() { fA(); fB(); }...
摘要:對于而言,異步編程我們可以采用回調函數,事件監聽,發布訂閱等方案,在之后,又新添了,,的方案??偨Y本文闡述了從回調函數到的演變歷史。參考文檔深入掌握異步編程系列理解的 對于JS而言,異步編程我們可以采用回調函數,事件監聽,發布訂閱等方案,在ES6之后,又新添了Promise,Genertor,Async/Await的方案。本文將闡述從回調函數到Async/Await的演變歷史,以及它們...
摘要:大家都一直在嘗試使用更好的方案來解決這些問題。這是一個用同步的思維來解決異步問題的方案。當我們發出了請求,并不會等待響應結果,而是會繼續執行后面的代碼,響應結果的處理在之后的事件循環中解決。我們可以用一個兩人問答的場景來比喻異步與同步。 在實際開發中總會遇到許多異步的問題,最常見的場景便是接口請求之后一定要等一段時間才能得到結果,如果遇到多個接口前后依賴,那么問題就變得復雜。大家都一直...
閱讀 2584·2023-04-25 20:50
閱讀 3929·2023-04-25 18:45
閱讀 2213·2021-11-17 17:00
閱讀 3323·2021-10-08 10:05
閱讀 3073·2019-08-30 15:55
閱讀 3487·2019-08-30 15:44
閱讀 2355·2019-08-29 13:51
閱讀 1111·2019-08-29 12:47