寫在前面
本文首發於公眾號:符合預期的CoyPan
先來看一段很常見的程式碼:
const arr = [1, 2, 3];
for(const i of arr) {
console.log(i); // 1,2,3
}
複製程式碼
上面的程式碼中,用for...of來遍歷一個陣列。其實這裡說遍歷不太準確,應該是說:for...of語句在可迭代物件(包括 Array,Map,Set,String,TypedArray,arguments 物件等等)上建立一個迭代迴圈,呼叫自定義迭代鉤子,併為每個不同屬性的值執行語句。
iterator
ECMAScript 2015規定了關於迭代的協議,這些協議可以被任何遵循某些約定的物件來實現。如果一個js物件想要能被迭代,那麼這個物件或者其原型鏈物件必須要有一個Symbol.iterator的屬性,這個屬性的值是一個無參函式,返回一個符合迭代器協議的物件。這樣的物件被稱為符合【可迭代協議】。
typeof Array.prototype[Symbol.iterator] === 'function'; // true
typeof Array.prototype[Symbol.iterator]() === 'object'; // true
複製程式碼
陣列之所以可以被for...of迭代,就是因為陣列的原型物件上擁有Symbol.iterator屬性,這個屬性返回了一個符合【迭代器協議】的物件。
一個符合【迭代器協議】的物件必須要有一個next屬性,next屬性也是一個無參函式,返回一個物件,這個物件至少需要有兩個屬性:done, value, 大概長成下面這樣:
{
next: function(){
return {
done: boolean, // 布林值,表示迭代是否完成,如果沒有這個屬性,則預設為false
value: any // 迭代器返回的任何javascript值。如果迭代已經完成,value屬性可以被省略
}
}
}
複製程式碼
依舊來看一下陣列:
typeof Array.prototype[Symbol.iterator]().next === 'function' // true
Array.prototype[Symbol.iterator]().next() // {value: undefined, done: true}
const iteratorObj = [1,2,3][Symbol.iterator]();
iteratorObj.next(); // { value: 1, done: false }
iteratorObj.next(); // { value: 2, done: false }
iteratorObj.next(); // { value: 3, done: false }
iteratorObj.next(); // { value: undefined, done: true }
複製程式碼
我們自己來實現一個可以迭代的物件。
const myIterator = {
[Symbol.iterator]: function() {
return {
i: 0,
next: function() {
if(this.i < 2) {
return { value: this.i++ , done: false };
} else {
return { done: true };
}
}
}
}
}
for(const item of myIterator) {
console.log(item);
}
// 0
// 1
複製程式碼
不光for...of會使用物件的iterator介面,下面這些用法也會預設使用物件的iteretor介面。 (1) 解構賦值 (2) 擴充套件運算子 (3) yield*
generator
生成器物件和生成器函式
generator表示一個生成器物件。這個物件符合【可迭代協議】和【迭代器協議】,是由生成器函式(generator function)返回的。
什麼是生成器函式呢?MDN上的描述如下:
生成器函式在執行時能暫停,後面又能從暫停處繼續執行。 呼叫一個生成器函式並不會馬上執行它裡面的語句,而是返回一個這個生成器的 迭代器 (iterator )物件。當這個迭代器的 next() 方法被首次(後續)呼叫時,其內的語句會執行到第一個(後續)出現yield的位置為止,yield 後緊跟迭代器要返回的值。或者如果用的是 yield*(多了個星號),則表示將執行權移交給另一個生成器函式(當前生成器暫停執行)。next()方法返回一個物件,這個物件包含兩個屬性:value 和 done,value 屬性表示本次 yield 表示式的返回值,done 屬性為布林型別,表示生成器後續是否還有 yield 語句,即生成器函式是否已經執行完畢並返回。
看下面的例子:
function* gen() { // gen一個生成器函式
yield 1;
yield 2;
yield 3;
}
const g = gen(); // g是一個生成器物件,是可迭代的
Object.prototype.toString.call(g) === "[object Generator]" // true
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }
複製程式碼
因為生成器物件符合可迭代協議和迭代器協議,我們可以用for...of來進行迭代。for…of會拿到迭代器返回值的value,也就是說,在迭代generator時,for…of拿到的是yield後面緊跟的那個值。
function* gen2() {
yield 'a';
yield 'b';
yield 'c';
}
const g2 = gen2();
for(const i of g2) {
console.log(i);
}
// a
// b
// c
複製程式碼
生成器函式的"巢狀"
function *gen1(i) {
yield i+1;
yield i+2;
yield *gen2(i+2); // 將執行權移交給gen2
yield i+3;
}
function *gen2(i) {
yield i*2;
}
const g = gen1(0);
g.next(); // { value: 1, done: false }
g.next(); // { value: 2, done: false }
g.next(); // { value: 4, done: false }
g.next(); // { value: 3, done: false }
g.next(); // { value: undefined, done: true }
複製程式碼
生成器函式裡的引數傳遞
function* gen3() {
let a = yield 1;
console.log('a:', a);
let b = yield a + 1;
yield b + 10;
}
const g = gen3();
g.next(); // { value: 1, done: false } 這個時候,程式碼執行到gen3裡第一行等號右邊
g.next(100); // a: 100 , { value: 101, done: false }。程式碼執行第一行等號的左邊,我們傳入了100,這個100會作為a的值,接著執行第二行的log, 然後執行到第三行等號的右邊。
g.next(); // { value: NaN, done: false }。程式碼執行第三行等號的左半部分,由於我們沒有傳值,b就是undefined, undefined + 10 就是NaN了。
g.next(); // { value: undefined, done: true }
複製程式碼
如果我們使用for...of來遍歷上述的生成器物件,由於for…of拿到的是迭代器返回值的value,所以會得到以下的結果:
function* gen4() {
let a = yield 1;
let b = yield a + 1;
yield b + 10;
}
const g4 = gen4();
for(const i of g4) {
console.log(i);
}
// 1
// NaN
// NaN
複製程式碼
下面是一個使用generator和for...of輸出斐波拉契數列的經典例子:
function* fibonacci() {
let [prev, curr] = [0, 1];
while(1){
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 100) {
break
}
console.log(n);
}
複製程式碼
稍微總結一下,generator給了我們控制暫停程式碼執行的能力,我們可以自己來控制程式碼執行。那是否可以用generator來寫非同步操作呢 ?
iterator,generator與非同步操作
一個很常見的場景: 頁面發起一個ajax請求,請求返回後,執行一個回撥函式。在這個回撥函式裡,我們使用第一個請求返回的url,再次發起一個ajax請求。(這裡先不考慮使用Promise)
// 我們先定義發起ajax的函式,這裡用setTimeout模擬一下
function myAjax(url, cb) {
setTimeout(function(){
const data = 'ajax返回了';
cb && cb(resData);
}, 1000);
}
// 一般情況下,要實現需求,一般可以這樣寫
myAjax('https://xxxx', function(url){
myAjax(url, function(data){
console.log(data);
});
});
複製程式碼
我們嘗試用generator的寫法來實現上面的需求.
// 先把ajax函式改造一下, 把url提出來作為一個引數,然後返回一個只接受回撥函式作為引數的newAjax函式
// 這種只接受回撥函式作為引數的函式被稱為thunk函式。
function thunkAjax(url) {
return function newAjax(cb){
myAjax(url, cb);
}
}
// 我們定義一個generator function
function* gen() {
const res1 = yield thunkAjax('http://url1.xxxx');
console.log('res1', res1);
const res2 = yield thunkAjax(res1);
console.log('res2', res2);
}
// 實現需求。
const g = gen();
const y1 = g.next(); // y1 = { value: ƒ, done: false }. 這裡的value,就是一個newAjax函式,接受一個回撥函式作為引數
y1.value(url => { // 執行y1.value這個函式,並且傳入了一個回撥函式作為引數
const y2 = g.next(url); // 傳入url作為引數,最終會賦值給上面程式碼中的res1。 y2 = { value: f, done: false }
y2.value(data => {
g.next(data); // 傳入data作為引數,會賦值給上面程式碼中的res2。至此,迭代也完成了。
});
});
// 最終的輸出為:
// 1s後輸出:res1 ajax返回了
// 1s後輸出:res2 ajax返回了
複製程式碼
在上面的程式碼中,我們使用generator實現了依次執行兩個非同步操作。上面的程式碼看起來是比較複雜的。整個的邏輯在gen這個generator function裡,然後我們手動執行完了g這個generator。按照上面的程式碼,如果我們想再加入一個ajax請求,需要先修改generator function,然後修改generator的執行邏輯。我們來實現一個自動的流程,只需要定義好generator,讓它自動執行。
function autoRun(generatorFun) {
const generator = generatorFun();
const run = function(data){
const res = generator.next(data);
if(res.done) {
return;
}
return res.value(run);
}
run();
}
複製程式碼
這下,我們就可以專注於generator function的邏輯了。
function* gen() {
const res1 = yield thunkAjax('http://url1.xxxx');
console.log('res1', res1);
const res2 = yield thunkAjax(res1);
console.log('res2', res2);
const res3 = yield thunkAjax(res2);
console.log('res3', res3);
...
}
// 自動執行
autoRun(gen);
複製程式碼
著名的co就是一個自動執行generator的庫。
上面的程式碼中,gen函式體內,我們用同步程式碼的寫法,實現了非同步操作。可以看到,用gererator來執行非同步操作,在程式碼可讀性、可擴充套件性上面,是很有優勢的。如今,我們或許會像下面這樣來寫上面的邏輯:
const fn = async function(){
const res1 = await func1;
console.log(res1);
const res2 = await func2;
console.log(res2);
...
}
fn();
複製程式碼
寫在後面
本文從for..of入手,梳理了javascript中的兩個重要概念:iterator和generator。並且介紹了兩者在非同步操作中的應用。符合預期。下一篇文章中,將介紹async、await,任務佇列的相關內容,希望能對js中的非同步程式碼及其寫法有一個更深入,全面的認識。