從 Interator 講到 Async

Interator,即我們常說的迭代器。在許多編程語言中都有它的身影。而 JavaScript 在 ES6 規範中正式定義了迭代器的標準化接口。

為什麼需要迭代器

這個問題嘛?需要從設計模式講起了。

我們知道設計模式中就有迭代器模式。迭代器模式要解決的問題是這樣的:在遍歷不同集合的時候(數組、Map、Set等),不同的集合有不同的遍歷方式,每次都要針對集合的不同來重新編寫代碼。麻煩!懶惰的程序員們就想啊:是否可以有一種通用的遍歷集合元素的方式呢?

於是,迭代器模式誕生了。它是實現對不同集合進行統一遍歷操作的一種機制。也可以理解為是對集合遍歷行為的一個抽象。

在 happyEnding 的世界裡,只要你實現了迭代器接口,就相當於加入了迭代器大家庭了。而函數 next(),就是一個通行證。

從 Interator 講到 Async/Await

ES6 中的迭代器

在 ES6 中,怎樣才算可以被認定為是一個 可以提供迭代器的對象 呢? 兩個必須滿足的條件:

  • 一個實現了 Interable 接口的函數,該函數必須能夠生成迭代器對象;
  • 迭代器對象中包含有 next() 方法,next() 函數的返回格式是:{ value | Object , done | Boolean }。

我們來看個栗子:

<code>class Users {
\tconstructor(users){
\t\tthis.users = users;
\t}

// 實現 Interable 接口
\t[Symbol.iterator]: function(){
\t\tlet i =0;
\t\tlet users = this.users;

// 返回一個迭代器對象
\t\treturn {

// 必須包含 next() 方法
// 返回值格式符合規範:{ value | Object , done | Boolean }
\t\t\tnext(){
\t\t\t\tif( i < users.length){
\t\t\t\t\treturn {done:false, value:users[i++]};
\t\t\t\t}
\t\t\t\treturn {done : true};
\t\t\t}
\t\t}

\t}
}/<code>

這個栗子是符合 ES6 的規範的。這裡值得注意的是:我們使用了ES6 中預定義的特殊Symbol值 Symbol.iterator,任何 Object 都可以通過添加這個屬性來自定義迭代器的實現。 關於 Symbol ,不是本文的重點哈。

我們來看看如何使用這個可以提供迭代器的對象:

<code>const allUsers = new Users([
\t{name: 'frank'},
\t{name: 'niuniu'},
\t{name: 'niuniu2'}
]);

// 驗證方式1:ES6 的 for...of 它會主動調用 Symbol.iterator
for( let v of allUsers){
\tconsole.log( v );
}
//output:
$:{ name: 'frank' }
\t{ name: 'niuniu' }
\t{ name: 'niuniu2' }

// 驗證方式2:自己調用

// 主動返回一個迭代器對象
const allUsersIterator = allUsers[Symbol.iterator]();

console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
console.log(allUsersIterator.next());
//output:
$:{ done: false, value: { name: 'frank' } }
\t{ done: false, value: { name: 'niuniu' } }
\t{ done: false, value: { name: 'niuniu2' } }
\t{ done: true }/<code>

再囉嗦一下下:ES6 中的 Array、Map、Set 這些內置對象都已實現了 Symbol.iterator,簡言之它們已經實現了迭代器屬性。但,想要顯式地使用對應對象的迭代器特性,還需要自己去調用:

<code>let bar = [1,2,3,4];
//顯式調用生成迭代器
let barIterator = bar[Symbol.iterator]();
//使用迭代器特性
console.log(barIterator.next().value); // output : 1/<code>

從迭代器到生成器

講完了迭代器 Iterator,我們來講講生成器 Generator。為什麼 JavaScript 中需要用到生成器 Generator 呢?

有兩個解釋點:

<code>then
/<code>

來看看上面的 User 對象在生成器下的表現方式:

<code>class Users {
\tconstructor(users){
\t\tthis.users = users;
\t\tthis.length = users.length;
\t}

\t*getIterator(){
\t\tfor( let i=0; i< this.length; i++ ){
\t\t\tyield this.users[i];
\t\t}
\t}
}

const allUsers = new Users([
\t{name: 'frank'},
\t{name: 'niuniu'},
\t{name: 'niuniu2'}
]);

//驗證
let allUsersIterator = allUsers.getIterator();
console.log(allUsersIterator.next()); //{ value: { name: 'frank' }, done: false }/<code>

是不是看起來簡單了一點呢?如果僅僅是這個讓迭代器看起來更加優雅,ES6 根本不需要生成器這種新的函數形式。生成器的語法更加複雜。而這些複雜性之所以存在,是為了應對更多的應用場景的。

且先來看生成器的語法:

通過以下語法來生成生成器函數:

<code>function *foo(){
\t//...
}/<code>

儘管生成器使用了 * 來聲明,但是執行起來還是和普通函數是一樣:

<code>foo();/<code>

也可以傳參給它:

<code>function *foo(x,y){
\t//...
}

foo(24,2);/<code>

主要區別是:執行生成器,比如 foo(24,2) ,並不實際在生成器中執行代碼。相反,它會產生一個迭代器控制這個生成器執行其代碼。(這個執行生成器函數生成迭代器的過程,和文章前面顯示調用生成迭代器的過程類似哦)

要讓代碼生效,需要調用迭代器方法 next() 。

<code>function *foo(){
\t//...
}
//生成迭代器
let fooIterator = foo();

//執行
fooIterator.next();/<code>

可能你會好奇,既然調用了迭代器方法 next() ,函數怎麼知道返回什麼呢?在生成器函數中沒有 return 啊。別急,關鍵字 yield 就是在扮演這個角色的。

yield

yield 關鍵字在生成器中,用來表示 暫停點 。看下面代碼:

<code>function *foo(){
\tlet x=10;
\tlet y=20;
\tyield;
\tlet z = 10 + 20;
}
複製代碼/<code>

在這個生成器中,首次運行前兩行,遇到 yield 會暫停這個生成器。如果恢復的話,會從 yield 處執行。就這樣,只要遇到 yield 就會暫停。生成器中 yield 可以出現任意多次,你甚至可以將它放在循環中。

yield 不僅僅是一個暫停點,它還是一個表達式。 yield 的右邊,是暫停時候的返回值(就是迭代器被調用 next() 後的返回值)。而 yield 在語句的位置,還可以插入 next() 方法中的輸入參數(替換掉 yield 及其右側表達式):

<code>function *foo(){
\tlet x=10;
\tlet y=20;
\tlet z = yield x+y;
\treturn x + y +z;

}
//生成迭代器
let fooIterator = foo();
//第一次執行迭代器的next(),遇到 yield 返回,返回值是 yield 右側的運行結果
console.log(fooIterator.next().value); // 30
//第二次執行迭代器的next(100), yield 及其右側表達式的位置會替換為參數 100
console.log(fooIterator.next(100).value); // 130/<code>
從 Interator 講到 Async/Await

融合 Promise 來控制異步操作

ES5 的 Promise 中包含了異步操作,待操作完成時,會返回一個決議。但是它的寫法 then() 會讓代碼在複雜情況下變得很難看,眾多的 then 嵌套並沒有比回調地獄好看多少。於是我們就想,是否可以通過生成器來更好地控制 Promise 的異步操作呢?

將 Promise 放到 yield 後面,然後迭代器偵聽這個 promise 的決議(完成或拒絕),然後要麼使用完成消息恢復生成器的允許(調用 next()),要麼向生成器拋出一個帶有拒絕原因的錯誤。

這是最為重要的一點: yield 一個 Promise,然後通過這個 Promise 來控制生成器的迭代過程 。

<code>import 'axios';

//步驟一:定義生成器
function *main(){
try {
var text = yield myPromise();
console.log("generator result :", text);
}catch(err){
console.error("generator err :",err);
}
}
//步驟二:定義 promise 函數
function myPromise(){
return axios.get("/api/info");
}

//步驟三:創建出迭代器

let mainIterator = main();

//步驟四:使用 promise 來控制迭代器的工作過程
let p = mainIterator.next().value;

p.then(
function(res){
let data = res.data;
console.log(" resolved : ", data);
//! promise 決議(完成)來控制迭代器
mainIterator.next(data);
},
function(error){
console.log(" rejected : ", error);
//! promise 決議(拒絕)來控制迭代器
mainIterator.throw(error);
}
);


//output
$ resolved : {name : frank}
\tgenerator result : {name : frank}/<code>

這樣,我們瞭解到了在生成器當中如何使用 Promise。並且能夠很好工作。只是,你會覺得,這個代碼甚至比之前的 Promise 寫法還要囉嗦。

假如有一個庫,它封裝好了所有與生成器、迭代器、Promise 結合的細節,你只需要簡單的調用(只需要寫上面代碼的步驟一與步驟二),就能夠將異步的寫法轉變為同步的寫法。你會想要麼?

ES7 的 async/await

上面的描述就是 ES7 中 async/await 語法的原理雛形。它的實現與考慮的情況遠比我們上面的這個 demo 版本更加複雜。它的寫法如下:

<code>function myPromise(){
return axios.get("/api/info");
}

async main(){
\ttry {
\t var text = await myPromise();
\t console.log(text);
\t}catch(err){
\t console.log(err);
\t}
}/<code>

如果你將和async/await 語法與生成器做一個對比,可以簡單地將 async 類比為 * ,而將 await 類比為 yield 。它就是那個在生成器與迭代器中融合了 Promise的一個官方版的實現。

更多的關於 ES7 中 async/await 語法知識,不是本文的重點。瞭解來龍去脈才是筆者關心的問題。所以這個章節就此打住啦!

從 Interator 講到 Async/Await

小結

這就是文章的主要內容了。我們從迭代器講到了生成器,並且最終結合 Promise 引出了ES7 中 async/await 語法。希望有所幫助!


分享到:


相關文章: