一、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的基礎工作原理應該就有了大概的認知了。
如果想加深一點理解(皮一下),可以隨意調換一下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部落格】