非同步(二):Generator深入理解

amandakelake發表於2019-02-25

一、Generator基礎認知

最基礎的原則就是見到yield就暫停,next()就繼續到下一個yield……以此知道函式執行完畢。

 先不上理論,直接看一段程式碼,這裡的step()是一個輔助函式,用來控制迭代器,替代手動next()

var a = 1;
var b = 2;
function* foo() {
  a++;
  yield;
  b = b * a;
  a = (yield b) + 3;
}
function* bar() {
  b--;
  yield;
  a = (yield 8) + b;
  b = a * (yield 2);
}
function step(gen) {
  var it = gen();
  var last;
  return function() {
    // 不管yield出來的是什麼,下一次都把它原樣傳回去!
    last = it.next(last).value;
  };
}

a = 1;
b = 2;

var s1 = step(foo);
var s2 = step(bar);
複製程式碼

yield和next()呼叫有一個數量上的不匹配,就是說,想要完整跑完一個生成器函式,next()呼叫總是比yield的數量多一次


為什麼會有這個不匹配? 

因為第一個 next(..) 總是啟動一個生成器,並執行到第一個 yield 處。不過,是第二個 next(..) 呼叫完成第一個被暫停的 yield 表示式,第三個 next(..) 呼叫完成第二個 yield, 以此類推。


所以上面的程式碼中,foo有兩個yield,bar有三個yield 所以接下來要跑三次s1(),四次s2() 我們在控制檯看每一步的輸出,一步一步來分析

非同步(二):Generator深入理解

分析到這裡,對generator的基礎工作原理應該就有了大概的認知了。

如果想加深一點理解(皮一下),可以隨意調換一下s1和s2的執行順序,總之就是三個s1和四個s2,對於理解多個生成器如何在共享的作用域上併發執行也有指導意義。


二、非同步迭代生成器

這一段,我們來理解一下生成器與非同步程式設計之間的問題,最直接的就是網路請求了

let data = ajax(url); // ajax是假設的封裝過的網路請求方法,並不是原生那傢伙
console.log(data)
複製程式碼

這段程式碼,大家都知道不能正常工作吧,data是underfined 

ajax是一個非同步操作,它並沒有停下來等到拿到資料之後再賦值給data 而是在發出請求之後,直接就執行了下一句console.log(data)

既然知道了問題核心在於“沒有停下來” 那剛好生成器又有“yield”停下來這個操作,那麼二者是不是剛好合拍了呢

看一段程式碼

function foo() {
  ajax(url, (err, data) => {
    if (err) {
      // 向*main()丟擲一個錯誤 it.throw( err );
    } else {
      // 用收到的data恢復*main()
      it.next(data);
    }
  });
}

function* main() {
  try {
    let data = yield foo();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}
複製程式碼

這段程式碼使用了生成器,其實跟上一段程式碼乾的是一樣的事情,雖然更長更復雜,但實際上更好用,具體原因繼續往下看


首先,兩段程式碼的核心區別在於生成器中使用了yield


在yield foo()的時候,呼叫了foo(),沒有返回值(underfined),所以發出了一個ajax請求,雖然依然是yield underfined,但是沒關係,因為這段程式碼不依賴yield的值來做什麼事情,大不了就列印underfined嘛對不對

這裡並不是在訊息傳遞的意義上使用 yield,而只是將其用於流程控制實現暫停 / 阻塞。實 際上,它還是會有訊息傳遞,但只是生成器恢復執行之後的單向訊息傳遞。

所以,生成器在 yield 處暫停,本質上是在提出一個問題:“我應該返回什麼值來賦給變數 data ?”誰來回答這個問題呢?


看foo,如果ajax請求成功,呼叫it.next( data )會用響應資料恢復生成器,意味著我們暫停的 yield 表示式直接接收到了這個值。然後 隨著生成器程式碼繼續執行,這個值被賦給區域性變數 data


在生成器內部有了看似完全同步的程式碼 (除了 yield 關鍵字本身),但隱藏在背後的是,在 foo(..) 內的執行可以完全非同步


這一部分對於理解生成器與非同步程式設計之間的核心非常重要,萬望深刻理解為什麼


三、Generator+Promise處理併發流程與優化

接下來來點高階貨吧,總不能一直停留在理論上 

request是假設封裝好的基於Promise的實現方法 

run也是假設封裝好的能實現重複迭代的驅動Promise鏈的方法

function *foo() {
  let r1 = yield request(url1);
  let r2 = yield request(url2);

  let r3 = yield request(`${url3}/${r1}/${r2}`);

  console.log(r3)
}
run(foo)
複製程式碼

這段程式碼裡,r3是依賴於r1和r2的,同時r1和r2是序列的,但這兩個請求是相對獨立的,那是不是應該考慮併發執行呢? 

但yield 只是程式碼中一個單獨 的暫停點,並不可能同時在兩個點上暫停

這樣試一下

function *foo() {
  let p1 = request(url1);
  let p2 = request(url2);

  let r1 = yield p1;
  let r2 = yield p2;

  let r3 = yield request(`${url3}/${r1}/${r2}`);

  console.log(r3)
}
run(foo)
複製程式碼

看一下yield的位置,p1和p2是併發同時執行的用於 Ajax 請求的 promise,哪一個先完成都無所謂,因為 promise 會按照需要 在決議狀態保持任意長時間 


然後使用接下來的兩個 yield 語句等待並取得 promise 的決議(分別寫入 r1 和 r2)。 

如果p1先決議,那麼yield p1就會先恢復執行,然後等待yield p2恢復。 

如果p2先決 議,它就會耐心保持其決議值等待請求,但是 yield p1 將會先等待,直到 p1 決議。 

不管哪種情況,p1 和 p2 都會併發執行,無論完成順序如何,兩者都要全部完成,然後才 會發出 r3 = yield request..Ajax 請求。


這種流程控制模型和Promise.all([ .. ]) 工具實現的 gate 模式相同

function *foo() {
  let rs = yield Promise.all([
    request(url1),
    request(url2)
  ]);

  let r1 = rs[0];
  let r2 = rs[1];

  let r3 = yield request(`${url3}/${r1}/${r2}`);
  console.log(r3)
}
run(foo)
複製程式碼


四、抽象非同步Promise流,簡化生成器

到目前位置,Promise都是直接暴露在生成器內部的,但生成器實現非同步的要點在於:建立簡單、順序、看似同步的程式碼,將非同步的 細節儘可能隱藏起來。


能不能考慮一下把多餘的資訊都藏起來,特別是看起來比較複雜的Promise程式碼呢?

function bar(url1, url2) {
  return Promise.all([request(url1), request(url2)]);
}

function* foo() {
  // 隱藏bar(..)內部基於Promise的併發細節
  let rs = yield bar(url1, url2);
  let r1 = rs[0];
  let r2 = rs[1];

  let r3 = yield request(`${url3}/${r1}/${r2}`);
  console.log(r3);
}
run(foo);
複製程式碼

把Promise的實現細節都封裝在bar裡面,對bar的要求就是給我們一下rs結果而已,我們也不需要關係底層是用什麼來實現的

非同步,實際上是把Promise,作為一個實現細節看待。

具體到實際生產中,一系列的非同步流程控制有可能就是下面的實現方式

function bar() {
  Promise.all([
    bax(...).then(...),
    Promise.race([...])
  ])
  .then(...)
}
複製程式碼

這些程式碼可能非常複雜,如果把實現直接放到生成器內部的話,那幾乎就失去了使用生成器的理由了


好好記一下這句話:建立簡單、順序、看似同步的程式碼,將非同步的細節儘可能隱藏起來。


後話

感謝您耐心看到這裡,希望有所收穫!

如果不是很忙的話,麻煩點個star⭐【Github部落格傳送門】,舉手之勞,卻是對作者莫大的鼓勵。

我在學習過程中喜歡做記錄,分享的是自己在前端之路上的一些積累和思考,希望能跟大家一起交流與進步,更多文章請看【amandakelake的Github部落格】


相關文章