最近看的代碼 yield 比較多,上次看到這么多 function* 還是在 koa1 時代,腦子中滿是 yield 和 next,而我自己用這個用的較少,就水個文章學習一下。
原文地址: https://github.com/vorshen/blog/blob/master/yield/index.md
Prerequisites
yield 英文直譯有著「提供」、「退讓」的意思,先了解直譯,對后面內容理解有幫助。
本文主要談一些我的思想理解,不會詳細描述 api 怎么用,所以完全不了解 yield 的同學,可以先看一下 api 使用文檔稍微了解一下。
- javascript yield https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/yield
- c++ yield https://zh.cppreference.com/w/cpp/thread/yield
yield 在 javascript 中
我最早在項目中接觸 yield 是因為項目用了 koa,但后面 koa2 將 yield 全面替換為 async、await,當時我以為 yield 是 async、await 的中間臨時方案,或者說 async、await 是 yield 的語法糖,其實并不能這么簡單的去看。
我們先看一下 yield 在 javascript 中的定義:
yield 關鍵字用來暫停和恢復一個生成器函數。
這句話里面有兩個概念需要了解:
- 生成器函數
- 暫停和恢復
生成器函數就是 function*,(普通的 function 關鍵字后面增加了一個星號)。那它有什么用呢?
生成器函數在執行時能暫停,后面又能從暫停處繼續執行。
好家伙,這直接讓人想到協程了。不過我們不要去想著底層是如何實現的,還是把注意力放在暫停和恢復這兩個行為上。
在前端代碼中,我們讓程序暫停和恢復的次數非常多,舉例最大的點就是發送請求,等待 ajax 返回。
1 2 |
const response = await axios.get('/drink?id=yori'); // balabala 操作 response |
當執行到如上代碼的時候,需要發送 IO 請求,在 response 沒有回來的時候,cpu 沒啥事干,就去其他應用程序里打工去了,此時相當于整個函數執行變成了暫停狀態,然后 response 回來,整個函數恢復執行。
yield 與異步
這樣一看,首先 yield 能解決異步問題,我們可以寫一個代碼感受一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
function* main() { yield console.log("利利準備"); yield* drink(); yield console.log("利利喝不動了"); } function* drink() { yield console.log("利利噸噸噸"); yield new Promise(function(resolve, reject) { setTimeout(function() { console.log('過了 3s'); resolve(); }, 3000); }); } function run(gen) { // 類似 co const t = gen.next(); const { value, done } = t; if (done) { console.log('End'); return; } if (value instanceof Promise) { value.then((e) => run(gen)) } else { run(gen) } } const gen = main(); run(gen); |
異步出現在 drink 函數里面那個封裝成 Promise 的 setTimeout。問題來了!yield 本身和 Promise 并沒有什么火花,對于 yield 來說它只是把 Promise 當作一個普通的 expression。
此時,反倒是 yield 配套的 next 起到了關鍵性的作用,雖然需要我們自己調用 next 很繁瑣,但這同是將操作權給了我們,可以創造無限可能。
我們可以通過一個 if 條件,判斷 yield 結果是否是一個 Promise,如果是 Promise,那就可以就地進行 then 等待,而并非立刻繼續 next。
這便是 co 函數的核心,但真正的 co 函數還有很多細節,感興趣同學自行查閱。
理解了上面這個示例代碼,我們便知道了,用 yield 來將異步回調函數的寫法轉為同步的能力,是一種取巧的方案,必須依賴 co 函數進行輔助。所以相比較 async、await,yield 確實是一種臨時方案,koa2 進行全面替換也無可厚非。
誰要是異步代碼不用 async、await,用 yield,頭都給捶爛??!
yield 與迭代
上面一章已經說了,yield 本身并不是給異步用的,那 yield 有自己存在的價值么?當然還是有的!
從網上關于 function* 的示例,基本上都是作為迭代使用,比如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function* getValue() { let list = [1, 2, 3, 4, 5]; for (let i = 0; i < list.length; i++) { yield list[i]; } } // 自己調用 next 的方式 const gen = getValue(); let t; while (t = gen.next(), !t.done) { console.log(t.value); } // 采用 for of 的方式 for (t of getValue()) { console.log(t); } |
你可能覺得就這??遍歷數據我 for 循環不行么?當然可以,但是這違背了泛型編程的思想。
數組的 for 循環是一種寫法,鏈表的 for 循環是一種寫法,二叉樹的 for 循環也是一種寫法,自定義的數據結構迭代又會是一種寫法。
不過對于前端而言,也不一定有那么強烈的泛型編程場景,還是得結合實際考慮。
整體來說 yield 在迭代場景中使用還是比較簡單,不過需要注意幾點:
- 生成器一定要相互獨立,切勿隨意復用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function* getValue() { let list = [1, 2, 3, 4, 5]; for (let i = 0; i < list.length; i++) { yield list[i]; } } // 沒問題,generator 都相互獨立 const gen = getValue(); let t; while (t = gen.next(), !t.done) { for (d of getValue()) { console.log(d); } console.log(t.value); } // 有問題 const gen = getValue(); let t; while (t = gen.next(), !t.done) { for (d of gen) { // ??這里用了同一個 gen console.log(d); } console.log(t.value); } |
這個不用多說,輸出的結果一定不會是你想要的。
- 不要在迭代過程中修改迭代的元素
1 2 3 4 5 6 7 |
let list = [1, 2, 3, 4, 5]; for (let v of list) { console.log(v); if (Math.random() < 0.5) { list.unshift(6); } } |
看代碼應該就能理解,會有元素被輸出多次,而 6 這個元素永遠不會出現。當然,如果你覺得自己可以控制好修改元素的位置,并且很有自信,也是可以刀尖舔血的,但一定要記得,v8 don't know what you want。
- 避免上層對 next 的調用
next 是一把雙刃劍,它的靈活性讓 yield 可以勝任很多騷操作(前面說的異步就是一種),但是我的建議是不要將這個東西給上層使用,請封裝好,就像 co 函數,就像 for...of。
再回到最開始說的 yield 英文直譯,可以理解「提供」和「退讓」的語義在哪了么?
yield 在 C++ 中
這一節就簡單了,先直接上代碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void wait() { while (true) { this_thread::yield(); // 進行 yield 調用,讓出 cpu } } void calc() { int d = 0; for (int n = 0; n < 10000; ++n) { for (int m = 0; m < 100000; ++m) { d += d * n * m; } } } int main(int argc, char* argv[]) { thread t1(wait); thread t2(calc); t1.detach(); t2.join(); return 0; } |
calc 是一個耗時的函數,在我機器上大概耗時 4s,此時如果我不在 wait 函數中加 yield 執行,注意執行時 taskset 要設置成單 cpu 執行。
1 2 3 |
real 0m7.920s user 0m7.916s sys 0m0.000s |
這里 user 的時間不止 4s,可以看出 while true 占據了不少 cpu 時間片。
然后我們加上 yield 的后結果就不一樣了:
1 2 3 |
real 0m7.910s user 0m5.012s sys 0m2.896s |
這里 user 時間下降到 5s,wait 函數線程占據的 cpu 時間明顯下降,但是 sys 時間上升,因為我們通過 yield 將時間交還給了內核,所以可以看到 sys 的時間有所增長。
再回到最開始說的 yield 英文直譯,應該可以很容易理解 C++ 中的「退讓」語義。
總結
javascript 中不要使用 yield 去處理異步,請老老實實的使用 async/await。
當使用迭代場合的時候,提供了三點建議。
c++ 中 yield 比較簡單,沒啥好總結的。
有問題什么的可以一起討論~