NodeJs 暢談異步
來源:程序員人生 發布時間:2016-11-17 09:42:25 閱讀次數:2645次
我們都知道NodeJs是基于事件回調解理事件的,那我們常常會有這樣的疑問,比如我們會使用db或redis或fs等模塊去存儲我們的信息,當我們需要實時返回1個數據并作出處理的時候,異步的通知顯得不是那末人性化了,此時我們需要將異步變成同步,在此進程中我們有這樣幾個知識點要掌握以下,包括JavaScript中的生成器generartor函數,配合它的是yield關鍵字的暫停,thunk和promise的使用,和co模塊和框架thunkify的配合使用。
生成器Generartor
生成器函數是寫成:function* functionName(){} 的情勢,其本質也是1個函數,所以它具有普通函數所具有的所有特性。除此以外,它還具有以下有用特性:
1. 履行生成器函數后返回1個生成器(Generator),且生成用具有throw()方法,可手動拋出1個異常,也常被用于判斷是不是是生成器;
2. 在生成器函數內部可使用yield(或yield*),函數履行到yield的時候都會暫停履行,并返回yield的右值(函數上下文,如變量的綁定等信息會保存),通過生成器的next()方法會返回1個對象,含當前yield右側表達式的值(value屬性),和generator函數是不是已履行完(done屬性)等的信息。每次履行next()方法,都會從上次履行的yield的地方往下,直到遇到下1個yield并返回包括相干履行信息的對象后暫停,然后等待下1個next()的履行;
3. 生成器的next()方法返回的是包括yield右側表達式值及是不是履行終了信息的對象;而next()方法中的參數是上1個暫停處yield的返回值。var a = yield ... 該參數的值將賦值給a變量。
實例1:
function test(){
return 'b';
}
function* func(){
var a = yield 'a';
console.log('gen:',a);// gen: undefined
var b = yield test();
console.log('gen:',b);// gen: undefined
}
var func1 = func();
var a = func1.next();
console.log('next:', a);// next: { value: 'a', done: false }
var b = func1.next();
console.log('next:', b);// next: { value: 'b', done: false }
var c = func1.next();
console.log('next:', c);// next: { value: undefined, done: true }
根據上面說過的第3條履行準則:“生成器的next()方法返回的是包括yield右側表達式值及是不是履行終了信息的對象;而next()方法的參數是上1個暫停處yield的返回值”,由于我們沒有往生成器的next()中傳入任何值,所以:var a = yield ‘a’;中a的值為undefined。
那我們可以將例子略微修改下:
function test(){
return 'b';
}
function* func(){
var a = yield 'a';
console.log('gen:',a);// gen:1
var b = yield test();
console.log('gen:',b);// gen:2
}
var func2 = func();
var a = func2.next();
console.log('next:', a);// next: { value: 'a', done: false }
var b = func2.next(1);
console.log('next:', b);// next: { value: 'b', done: false }
var c = func2.next(2);
console.log('next:', c);// next: { value: undefined, done: true }
關于yield
普通yield實例:
function* outer() {
yield 'begin';
yield inner();
yield 'end';
}
function* inner() {
yield 'inner';
}
var it = outer(), v;
v = it.next().value;
console.log(v); // -> 輸出:begin
v = it.next().value;
console.log(v); // -> 輸出:{}
console.log(v.toString()); // -> 輸出:[object Generator]
v = it.next().value;
console.log(v); // -> 輸出:end
代理yield實例:
function* outer() {
yield 'begin';
/*
* 這行等價于 yield 'inner';就是把inner里面的代碼替換過來。同時取得的rt恰好就是inner的返回值
*/
var rt = yield* inner();
console.log(rt); // -> 輸出:return from inner
yield 'end';
}
function* inner() {
yield 'inner';
return 'return from inner';
}
var it = outer(), v;
v = it.next().value;
console.log(v); // -> 輸出:begin
v = it.next().value;
console.log(v); // -> 輸出:inner
v = it.next().value;
console.log(v); // -> 輸出:end
根據文檔的描寫,yield* 后面接受1個 iterable object 作為參數,然后去迭代(iterate)這個迭代器(iterable object),同時 yield* 本身這個表達式的值就是迭代器迭代完成時(done: true)的返回值。調用 generator function 會返回1個 generator object,這個對象本身也是1種 iterable
object,所以,我們可使用 yield* generator_function() 這類寫法。
啊,好繞。在我實際的使用進程中,yield* 1般用來在1個 generator 函數里“履行”另外一個 generator 函數,并可以獲得其返回值。
關于yield和co
co(function* () {
// yield promise
var a = yield Promise.resolve(1);
console.log(a); // -> 輸出:1
// yield thunk
var b = yield later(10);
console.log(b); // -> 輸出:10
// yield generator function
var c = yield fn;
console.log(c); // -> 輸出:fn_1
// yield generator
var d = yield fn(5);
console.log(d); // -> 輸出:fn_5
// yield array
var e = yield [
Promise.resolve('a'),
later('b'),
fn,
fn(5)
];
console.log(e); // -> 輸出:['a', 'b', 'fn_1', 'fn_5']
// yield object
var f = yield {
'a': Promise.resolve('a'),
'b': later('b'),
'c': fn,
'd': fn(5)
};
console.log(f); // -> 輸出:{a: 'a', b: 'b', c: 'fn_1', d: 'fn_5'}
function* fn(n) {
n = n || 1;
var a = yield later(n);
return 'fn_' + a;
}
function later(n, t) {
t = t || 1000;
return function(done) {
setTimeout(function() { done(null, n); }, t);
};
}
}).catch(function(e) {
console.error(e);
});
通過上面的代碼,我們看清了1個事實,那就是co模塊中yield后面的只能是promise、generator、thunk函數等,并且他只是同步的,在之前我們討論過,如果我們想要這句代碼 var a = yield 'value' 的a變量賦值,那末我們需要在調用next的時候傳入參數值,但是在co模塊中,他會將promise的通知值(也就是resolve)、thunk函數的灰調函數的值賦值給a變量。
promise函數
Promise對象是CommonJS工作組提出的1種規范,目的是為異步操作提供統1接口。
那末,甚么是Promises?
首先,它是1個對象,也就是說與其他JavaScript對象的用法,沒有甚么兩樣;其次,它起到代理作用(proxy),充當異步操作與回調函數之間的中介。它使得異步操作具有同步操作的接口,使得程序具有正常的同步運行的流程,回調函數沒必要再1層層嵌套。
簡單說,它的思想是,每個異步任務立刻返回1個Promise對象,由因而立刻返回,所以可以采取同步操作的流程。這個Promises對象有1個then方法,允許指定回調函數,在異步任務完成后調用。
比如,異步操作f1返回1個Promise對象,它的回調函數f2寫法以下。(new Promise(f1)).then(f2);
// 傳統寫法
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// ...
});
});
});
});
// Promises的寫法
(new Promise(step1))
.then(step2)
.then(step3)
.then(step4);
從上面代碼可以看到,采取Promises接口以后,程序流程變得非常清楚,10分易讀。
注意,為了便于理解,上面代碼的Promise對象的生成格式,做了簡化,真實的語法請參照下文。
總的來講,傳統的回調函數寫法使得代碼混成1團,變得橫向發展而不是向下發展。Promises規范就是為了解決這個問題而提出的,目標是使用正常的程序流程(同步),來處理異步操作。它先返回1個Promise對象,后面的操作以同步的方式,寄存在這個對象上面。等到異步操作有了結果,再履行前期存放在它上面的其他操作。
Promises本來只是社區提出的1個構想,1些外部函數庫率先實現了這個功能。ECMAScript 6將其寫入語言標準,因此目前JavaScript語言原生支持Promise對象。
關于Promise接口,前面說過,Promise接口的基本思想是,異步任務返回1個Promise對象。他只有3種狀態pending(未完成)、resolved(已完成)、rejected(失敗),那末當它返回時,只有成功和失敗兩個概念了,履行成功Promise對象傳回1個值,狀態變成resolved,異步操作失敗,Promise對象拋出1個毛病,狀態變成rejected。
Promise對象使用then方法添加回調函數。then方法可以接受兩個回調函數,第1個是異步操作成功時(變成resolved狀態)時的回調函數,第2個是異步操作失敗(變成rejected)時的回調函數(可以省略)。1旦狀態改變,就調用相應的回調函數。
po.then(console.log, console.error);
上面代碼中,Promise對象po使用then方法綁定兩個回調函數:操作成功時的回調函數console.log,操作失敗時的回調函數console.error(可以省略)。這兩個函數都接受異步操作傳回的值作為參數。固然了,then可支持鏈式編程。
上面代碼中,po的狀態1旦變成resolved,就順次調用后面每個then指定的回調函數,每步都必須等到前1步完成,才會履行。最后1個then方法的回調函數console.log和console.error,用法上有1點重要的區分。console.log只顯示回調函數step3的返回值,而console.error可以顯示step1、step2、step3當中任意1個產生的毛病。也就是說,假定step1操作失敗,拋出1個毛病,這時候step2和step3都不會再履行了(由于它們是操作成功的回調函數,而不是操作失敗的回調函數)。Promises對象開始尋覓,接下來第1個操作失敗時的回調函數,在上面代碼中是console.error。這就是說,Promises對象的毛病有傳遞性。
ES6提供了原生的Promise構造函數,用來生成Promise實例。
var promise = new Promise(function(resolve, reject) {
// 異步操作的代碼
if (/* 異步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
這里我們可能有點和then的用法沖突了,其實沒有,下面我們給出1個ajax配合使用promise的用法:function search(term) {
var url = 'http://example.com/search?q=' + term;
var xhr = new XMLHttpRequest();
var result;
var p = new Promise(function (resolve, reject) {
xhr.open('GET', url, true);
xhr.onload = function (e) {
if (this.status === 200) {
result = JSON.parse(this.responseText);
resolve(result);
}
};
xhr.onerror = function (e) {
reject(e);
};
xhr.send();
});
return p;
}
search("paramValue").then(console.log, console.error);
此時很明白了吧。。。。。。
那我們說了這么多,我們也想promise配合co模塊使用1次,將其異步變成同步的使用方法就能夠了,實例以下:
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
到此,明白了吧,這個配合co模塊實現了同步的操作了吧
關于promise的更多詳細講授,參照 http://javascript.ruanyifeng.com/advanced/promise.html官方,http://www.ruanyifeng.com/blog/2015/05/co.html阮1峰
這其中有幾點要注意的也就是,首先promise是1個將異步金字塔管理成為then的調用,他會立即返回1個狀態,當其中1個履行成功后,then去履行下1個異步回調函數,請注意它的3個狀態,其中1個api Promise.resolve(value),這個api只是的立刻馬上返回1個value給return使用。
thunk函數
co模塊可以將我們的生成器自動的去履行,而不需要我們手動進行next履行,但是在co的利用中,為了能像寫同步代碼那樣書寫異步代碼,比較多的使用方式是使用thunk函數(但不是唯1方式,還可以是:Promise)。比如讀取文件內容的1步函數fs.readFile()方法,轉化為thunk函數的方式以下:
function readFile(path, encoding){
return function(cb){
fs.readFile(path, encoding, cb);
};
}
thunk函數具有以下兩個要素:
1. 有且只有1個參數是callback的函數;
2. callback的第1個參數是error。
其實去看看這兩點,在node中的回調函數都是可以寫成thunk函數去履行的。使用thunk函數,同時結合co我們就能夠像寫同步代碼那樣來寫書寫異步代碼,先來個例子感受下:
var co = require('co'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
function readFile(path, encoding){
return function(cb){ // thunk函數
fs.readFile(path, encoding, cb);
};
}
co(function* (){// 外面不可見,但在co內部其實已轉化成了promise.then().then()..鏈式調用的情勢
var a = yield readFile('a.txt', {encoding: 'utf8'});
console.log(a); // a
var b = yield readFile('b.txt', {encoding: 'utf8'});
console.log(b); // b
var c = yield readFile('c.txt', {encoding: 'utf8'});
console.log(c); // c
return yield Promise.resolve(a+b+c);
}).then(function(val){
console.log(val); // abc
}).catch(function(error){
console.log(error);
});
其實,對每次都去自己書寫1個thunk函數還是比較麻煩的,有1個框架thunkify可以幫我們輕松實現,修改后的代碼以下:var co = require('co'),
thunkify = require('thunkify'),
fs = require('fs'),
Promise = require('es6-promise').Promise;
var readFile = thunkify(fs.readFile);
co(function* (){// 外面不可見,但在co內部其實已轉化成了promise.then().then()..鏈式調用的情勢
var a = yield readFile('a.txt', {encoding: 'utf8'});
console.log(a); // a
var b = yield readFile('b.txt', {encoding: 'utf8'});
console.log(b); // b
var c = yield readFile('c.txt', {encoding: 'utf8'});
console.log(c); // c
return yield Promise.resolve(a+b+c);
}).then(function(val){
console.log(val); // abc
}).catch(function(error){
console.log(error);
});
辨別好thunkify的使用,它可以包括1個異步的具有回調函數的thunk函數,提供給co模塊使用。
那好,我們最后來1個NodeJs異步中的總結,首先由于生成器和yield可使程序暫停履行,但是其實不具有將異步函數的返回值返回給var a變量的能力,那末我們加入了co這個模塊來使用,co可以將yield后面的不管甚么回調異步的返回值return給變量a使用,但是有1個問題,yield后面如果有多個或更多的異步怎樣辦,我們就使用thunk和promise將所有的異步串行履行起來,最后加入co模塊,完善!再次推薦使用thunk,由于我們大多是情況下是需要1個異步函數的返回值,并且有thunkify框架的支持,而promise也是有1些異常的東西,比如,有時候你在拿去它的返回值的時候,不太容易,為何呢,then后如果寫了console.log,由于上1次的值傳入了log中,log并沒與傳出所以失去了結果,比如代碼好像是這樣的
var promise = new Promise(function(resolve, reject) {
if (true){
//成功時,將2作為返回值,傳入下1個then的參數中,如果沒有下1個then,會將值村吃在promise._result中
resolve(2);
} else {
reject(error);
}
});
var result = promise._result;
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈