你是否對JS中的Generator及協程真正理解?
本文轉載自微信公眾號「前端三元同學」,作者神三元。轉載本文請聯系前端三元同學公眾號。
生成器(Generator)是 ES6 中的新語法,相對于之前的異步語法,上手的難度還是比較大的。因此這里我們先來好好熟悉一下 Generator 語法。
生成器執行流程
什么是生成器函數?
生成器是一個帶星號的"函數"(注意:它并不是真正的函數),可以通過yield關鍵字暫停執行和恢復執行的
舉個例子:
- function* gen() {
- console.log("enter");
- let a = yield 1;
- let b = yield (function () {return 2})();
- return 3;
- }
- var g = gen() // 阻塞住,不會執行任何語句
- console.log(typeof g) // object 看到了嗎?不是"function"
- console.log(g.next())
- console.log(g.next())
- console.log(g.next())
- console.log(g.next())
- // enter
- // { value: 1, done: false }
- // { value: 2, done: false }
- // { value: 3, done: true }
- // { value: undefined, done: true }
由此可以看到,生成器的執行有這樣幾個關鍵點:
- 調用 gen() 后,程序會阻塞住,不會執行任何語句。
- 調用 g.next() 后,程序繼續執行,直到遇到 yield 程序暫停。
- next 方法返回一個對象, 有兩個屬性: value 和 done。value 為當前 yield 后面的結果,done 表示是否執行完,遇到了return 后,done 會由false變為true。
yield* 語法
當一個生成器要調用另一個生成器時,使用 yield* 就變得十分方便。比如下面的例子:
- function* gen1() {
- yield 1;
- yield 4;
- }
- function* gen2() {
- yield 2;
- yield 3;
- }
我們想要按照1234的順序執行,如何來做呢?
在 gen1 中,修改如下:
- function* gen1() {
- yield 1;
- yield* gen2();
- yield 4;
- }
這樣修改之后,之后依次調用next即可。
生成器實現機制——協程
可能你會比較好奇,生成器究竟是如何讓函數暫停, 又會如何恢復的呢?接下來我們就來對其中的執行機制——協程一探究竟。
什么是協程?
協程是一種比線程更加輕量級的存在,協程處在線程的環境中,一個線程可以存在多個協程,可以將協程理解為線程中的一個個任務。不像進程和線程,協程并不受操作系統的管理,而是被具體的應用程序代碼所控制。
協程的運作過程
那你可能要問了,JS 不是單線程執行的嗎,開這么多協程難道可以一起執行嗎?
答案是:并不能。一個線程一次只能執行一個協程。比如當前執行 A 協程,另外還有一個 B 協程,如果想要執行 B 的任務,就必須在 A 協程中將JS 線程的控制權轉交給 B協程,那么現在 B 執行,A 就相當于處于暫停的狀態。
舉個具體的例子:
- function* A() {
- console.log("我是A");
- yield B(); // A停住,在這里轉交線程執行權給B
- console.log("結束了");
- }
- function B() {
- console.log("我是B");
- return 100;// 返回,并且將線程執行權還給A
- }
- let gen = A();
- gen.next();
- gen.next();
- // 我是A
- // 我是B
- // 結束了
在這個過程中,A 將執行權交給 B,也就是 A 啟動 B,我們也稱 A 是 B 的父協程。因此 B 當中最后return 100其實是將 100 傳給了父協程。
需要強調的是,對于協程來說,它并不受操作系統的控制,完全由用戶自定義切換,因此并沒有進程/線程上下文切換的開銷,這是高性能的重要原因。