1、萬惡的回調 對前端工程師來說,非同步回調是再熟悉不過了,瀏覽器中的各種交互邏輯都是通過事件回調實現的,前端邏輯越來越複雜,導致回調函數越來越多,同時 nodejs 的流行也讓 javascript 在後端的複雜場景中得到應用,在 nodejs 代碼中更是經常看到層層嵌套。非同步操作的回調一旦嵌套很多 ...
1、萬惡的回調
對前端工程師來說,非同步回調是再熟悉不過了,瀏覽器中的各種交互邏輯都是通過事件回調實現的,前端邏輯越來越複雜,導致回調函數越來越多,同時 nodejs 的流行也讓 javascript 在後端的複雜場景中得到應用,在 nodejs 代碼中更是經常看到層層嵌套。非同步操作的回調一旦嵌套很多,不僅代碼會變的臃腫,還很容易出錯。
以下是一個典型的非同步場景:先通過非同步請求獲取頁面數據,然後根據頁面數據請求用戶信息,最後根據用戶信息請求用戶的產品列表。過多的回調函數嵌套,使得程式難以維護,發展成萬惡的回調。
$.get('/api/data', function(data) {
console.log(data);
$.get('/api/user', function(user) {
console.log(user);
$.get('/api/products', function(products) {
console.log(products)
});
});
});
2、非同步流程式控制制
-
最原始非同步流程的寫法,就是類似上面例子里的回調函數嵌套法,用過的人都知道,那叫一個酸爽。
-
後來出現了 Promise ,它極大提高了代碼的可維護性,消除了萬惡的回調嵌套問題,並且現在已經成為 ES6 標準的一部分。
$.get('/api/data')
.then(function(data) {
console.log(data);
return $.get('/api/user');
})
.then(function(user) {
console.log(user);
return $.get('/api/products');
})
.then(function(products) {
console.log(products);
});
- 之後在 nodejs 圈出現了 co 模塊,它基於 ES6 的 generator 和 yield ,讓我們能用同步的形式編寫非同步代碼。
co(function *() {
var data = yield $.get('/api/data');
console.log(data);
var user = yield $.get('/api/user');
console.log(user);
var products = yield $.get('/api/products');
console.log(products);
});
- 以上的 Promise 和 generator 最初創造它的本意都不是為瞭解決非同步流程式控制制。其中 Promise 是一種編程思想,用於“當xx數據準備完畢,then執行xx動作”這樣的場景,不只是非同步,同步代碼也可以用 Promise。而 generator 在 ES6 中是迭代器生成器,被 TJ 創造性的拿來做非同步流程式控制制了。真正的非同步解決方案請大家期待 ES7 的 async 吧!本文以下主要介紹 co 模塊。
3、co 模塊
上文已經簡單介紹了co 模塊是能讓我們以同步的形式編寫非同步代碼的 nodejs 模塊,主要得益於 ES6 的 generator。nodejs >= 0.11 版本可以加 --harmony
參數來體驗 ES6 的 generator 特性,iojs 則已經預設開啟了 generator 的支持。
要瞭解 co ,就不得不先簡單瞭解下 ES6 的 generator 和 iterator。
-
Iterator
Iterator 迭代器是一個對象,知道如何從一個集合一次取出一項,而跟蹤它的當前序列所在的位置,
它的介面很簡單,一般擁有以下三個方法就可以了。
hasNext() //集合中是否還有下一個元素
next() //迭代到下一個元素
reset()//重置,我見到的代碼一般是拋出異常,即一般不支持多次迭代
它提供了一個next()方法返回序列中的下一個項目。
var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
var pair = it.next();
console.log(pair); // ["name", "JavaScript"]
pair = it.next();
console.log(pair); // ["birthYear", 1995]
pair = it.next(); // A StopIteration exception is thrown
-
可以沒有真正的集合(像Array),只要有相應的生成規則就行。這種情況下,沒有記憶體的限制,因此可以表示無限序列
-
不調用next(),迭代器不進行迭代的,因此有延遲載入的特性。
-
迭代器,本質上是一個狀態機,每個狀態下,進行一些操作,然後進入下一個狀態或維持狀態不變。
乍一看好像沒什麼奇特的,不就是一步步的取對象中的 key 和 value 嗎,for ... in
也能做到,但是把它跟 generator 結合起來就大有用途了。
-
Generator
迭代器模式是很常用的設計模式,但是實現起來,很多東西是程式化的;當迭代規則比較複雜時,維護迭代器內的狀態,是比較麻煩的。 於是有了generator。Generator 生成器允許你通過寫一個可以保存自己狀態的的簡單函數來定義一個迭代演算法。 Generator 是一種可以停止併在之後重新進入的函數。生成器的環境(綁定的變數)會在每次執行後被保存,下次進入時可繼續使用。generator 字面上是“生成器”的意思,在 ES6 里是迭代器生成器,用於生成一個迭代器對象,最大特點就是可以交出函數的執行權(即暫停執行)。只有在調用了 next()
函數之後,函數才會開始執行。
function *gen() {
yield 'hello';
yield 'world';
return true;
}
以上代碼定義了一個簡單的 generator,看起來就像一個普通的函數,區別是function
關鍵字後面有個*
號,函數體內可以使用yield
語句進行流程式控制制。
var iter = gen();
var a = iter.next();
console.log(a); // {value:'hello', done:false}
var b = iter.next();
console.log(b); // {value:'world', done:false}
var c = iter.next();
console.log(c); // {value:true, done:true}
當執行gen()
的時候,並不執行 generator 函數體,而是返回一個迭代器。迭代器具有next()
方法,每次調用 next() 方法,函數就執行到yield
語句的地方。next() 方法返回一個對象,其中value屬性表示 yield 關鍵詞後面表達式的值,done 屬性表示是否遍歷結束。generator 生成器通過next
和yield
的配合實現流程式控制制,上面的代碼執行了三次 next() ,generator 函數體才執行完畢。
-
co 模塊思路
從上面的例子可以看出,generator 函數體可以停在 yield 語句處,直到下一次執行 next()。co 模塊的思路就是利用 generator 的這個特性,將非同步操作跟在 yield 後面,當非同步操作完成並返回結果後,再觸發下一次 next() 。當然,跟在 yield 後面的非同步操作需要遵循一定的規範 thunks 和 promises。
4、7行代碼
再看看文章開頭的7行代碼:
function co(gen) {
var it = gen();
var ret = it.next();
ret.value.then(function(res) {
it.next(res);
});
}
首先生成一個迭代器,然後執行一遍 next(),得到的 value 是一個 Promise 對象,Promise.then() 裡面再執行 next()。當然這隻是一個原理性的演示,很多錯誤處理和迴圈調用 next() 的邏輯都沒有寫出來。
下麵做個簡單對比: 傳統方式,sayhello
是一個非同步函數,執行helloworld
會先輸出"world"
再輸出"hello"
。
function sayhello() {
return Promise.resolve('hello').then(function(hello) {
console.log(hello);
});
}
function helloworld() {
sayhello();
console.log('world');
}
helloworld();
輸出
> "world"
> "hello"
co 的方式,會先輸出"hello"
再輸出"world"
。
function co(gen) {
var it = gen();
var ret = it.next();
ret.value.then(function(res) {
it.next(res);
});
}
function sayhello() {
return Promise.resolve('hello').then(function(hello) {
console.log(hello);
});
}
co(function *helloworld() {
yield sayhello();
console.log('world');
});
輸出
> "hello"
> "world"
5、消除回調金字塔
假設sayhello
/sayworld
/saybye
是三個非同步函數,用真正的 co 模塊就可以這麼寫:
var co = require('co');
co(function *() {
yield sayhello();
yield sayworld();
yield saybye();
});
輸出
> "hello"
> "world"
> "bye
通過上面的分析,yield
之後,實際上本次調用就結束了,控制權實際上已經轉到了外部調用了generator的next方法的函數,調用的過程中伴隨著狀態的改變。那麼如果外部函數不繼續調用next方法,那麽yield
所在函數就相當於停在yield
那裡了。所以把非同步的東西做完,要函數繼續執行,只要在合適的地方再次調用generator 的next就行,就好像函數在暫停後,繼續執行。
-
yield與return的區別:
(1)yield僅代表本次迭代完成,並且還必有下一次迭代;
(2) return則代表生成器函數完成;
利用生成器函數可以進行惰性求值,但無法獲取到第一次next函數傳入的值,而且只要執行了yield的返回操作,那麼構造函數一定沒有執行完成,除非遇到了顯式的return語句。
6、代理yeild
yield* 後面接受一個 iterable object 作為參數,然後去迭代(iterate)這個迭代器(iterable object),同時 yield* 本身這個表達式的值就是迭代器迭代完成時(done: true)的返回值。調用 generator function 會返回一個 generator object,這個對象本身也是一種 iterable object,所以,我們可以使用 yield* generator_function() 這種寫法。
➜ qiantou cat yield05.js
function* outer(){
yield 'begin';
var ret = yield* inner();
console.log(ret);
yield 'end';
}
function * inner(){
yield 'inner';
r
return 'return from inner';
}
var it = outer(),v;
v = it.next().value;
console.log(v);
v = it.next().value;
console.log(v);
v = it.next().value;
console.log(v)
輸出:
➜ qiantou node yield05.js
begin
inner
r
eturn from inner
end
yield* 後面接受一個 iterable object 作為參數,然後去迭代(iterate)這個迭代器(iterable object),同時 yield* 本身這個表達式的值就是迭代器迭代完成時(done: true)的返回值。調用 generator function 會返回一個 generator object,這個對象本身也是一種 iterable object,所以,我們可以使用 yield* generator_function() 這種寫法。
-
yield* 的作用
(1)用原生語法,拋棄 co 的黑魔法,換取一點點點點性能提升
(2)明確表明自己的意圖,避免混淆
(3)調用時保證正確的 this 指向
7、總結
對於ES6的生成器函數總結有四點:
(1) yield必須放置在*函數中;
(2) 每次執行到yield時都會暫停函數中剩餘代碼的執行;
(3) *函數必須通過函數調用的方式(new方式會報錯)才能產生自身的實例,並且每個實例都互相獨立;
(4)一個生成器函數一旦迭代完成,則再也無法還原,一直停留在最後一個位置;
尤其是第二點,是非常強大的功能,暫停代碼執行,以前只有在瀏覽器環境中,alert、comfirm等系統內置函數才具有類似的能力,所以如果熟悉多線程的語言,你會找到類似的感覺,於是也有人說,有了yield,NodeJS就有協程的能力,完全可以處理多個需要協作的任務。