在Promise中你可以將?`then`?和?`catch`?等方法連在一起寫。這非常像DOM或者jQuery中的方法鏈。
一般的方法鏈都通過返回?`this`?將多個方法串聯起來。
> 關于如何創建方法鏈,可以從參考?[方法鏈的創建方法 - 余味(日語博客)](http://taiju.hatenablog.com/entry/20100307/1267962826)?等資料。
另一方面,由于Promise?[每次都會返回一個新的promise對象](http://liubin.github.io/promises-book/#then-return-new-promise)?,所以從表面上看和一般的方法鏈幾乎一模一樣。
在本小節里,我們會在不改變已有采用方法鏈編寫的代碼的外部接口的前提下,學習如何在內部使用Promise進行重寫。
## 4.7.1\. fs中的方法鏈
我們下面將會以?[Node.js中的fs](http://nodejs.org/api/fs.html)?為例進行說明。
此外,這里的例子我們更重視代碼的易理解性,因此從實際上來說這個例子可能并不算太實用。
fs-method-chain.js
~~~
"use strict";
var fs = require("fs");
function File() {
this.lastValue = null;
}
// Static method for File.prototype.read
File.read = function FileRead(filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.read = function (filePath) {
this.lastValue = fs.readFileSync(filePath, "utf-8");
return this;
};
File.prototype.transform = function (fn) {
this.lastValue = fn.call(this, this.lastValue);
return this;
};
File.prototype.write = function (filePath) {
this.lastValue = fs.writeFileSync(filePath, this.lastValue);
return this;
};
module.exports = File;
~~~
這個模塊可以將類似下面的 read → transform → write 這一系列處理,通過組成一個方法鏈來實現。
~~~
var File = require("./fs-method-chain");
var inputFilePath = "input.txt",
outputFilePath = "output.txt";
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
~~~
`transform`?接收一個方法作為參數,該方法對其輸入參數進行處理。在這個例子里,我們對通過read讀取的數據在前面加上了?`>>`?字符串。
## 4.7.2\. 基于Promise的fs方法鏈
下面我們就在不改變剛才的[方法鏈](http://liubin.github.io/promises-book/#fs-method-chain.js)對外接口的前提下,采用Promise對內部實現進行重寫。
fs-promise-chain.js
~~~
"use strict";
var fs = require("fs");
function File() {
this.promise = Promise.resolve();
}
// Static method for File.prototype.read
File.read = function (filePath) {
var file = new File();
return file.read(filePath);
};
File.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
File.prototype["catch"] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
File.prototype.read = function (filePath) {
return this.then(function () {
return fs.readFileSync(filePath, "utf-8");
});
};
File.prototype.transform = function (fn) {
return this.then(fn);
};
File.prototype.write = function (filePath) {
return this.then(function (data) {
return fs.writeFileSync(filePath, data)
});
};
module.exports = File;
~~~
新增加的`then`?和`catch`都可以看做是指向內部保存的promise對象的別名,而其它部分從對外接口的角度來說都沒有改變,使用方法也和原來一樣。
因此,在使用這個模塊的時候我們只需要修改?`require`?的模塊名即可。
~~~
var File = require("./fs-promise-chain");
var inputFilePath = "input.txt",
outputFilePath = "output.txt";
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
~~~
`File.prototype.then`?方法會調用?`this.promise.then`?方法,并將返回的promise對象賦值給了?`this.promise`?變量這個內部promise對象。
這究竟有什么奧妙么?通過以下的偽代碼,我們可以更容易理解這背后發生的事情。
~~~
var File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath);
// => 處理流程類似以下的偽代碼
promise.then(function read(){
return fs.readFileSync(filePath, "utf-8");
}).then(function transform(content) {
return ">>" + content;
}).then(function write(){
return fs.writeFileSync(filePath, data);
});
~~~
看到?`promise = promise.then(...)`?這種寫法,會讓人以為`promise`的值會被覆蓋,也許你會想是不是promise的chain被截斷了。
你可以想象為類似?`promise = addPromiseChain(promise, fn);`?這樣的感覺,我們為promise對象**增加**了新的處理,并返回了這個對象,因此即使自己不實現順序處理的話也不會帶來什么問題。
## 4.7.3\. 兩者的區別
### 同步和異步
要說[fs-method-chain.js](http://liubin.github.io/promises-book/#fs-method-chain.js)和[Promise版](http://liubin.github.io/promises-book/#fs-promise-chain.js)兩者之間的差別,最大的不同那就要算是同步和異步了。
如果在類似?[fs-method-chain.js](http://liubin.github.io/promises-book/#fs-method-chain.js)?的方法鏈中加入隊列等處理的話,就可以實現幾乎和異步方法鏈同樣的功能,但是實現將會變得非常復雜,所以我們選擇了簡單的同步方法鏈。
Promise版的話如同在?[專欄: Promise只能進行異步處理?](http://liubin.github.io/promises-book/#promise-is-always-async)里介紹過的一樣,只會進行異步操作,因此使用了promise的方法鏈也是異步的。
### 錯誤處理
雖然[fs-method-chain.js](http://liubin.github.io/promises-book/#fs-method-chain.js)里面并不包含錯誤處理的邏輯, 但是由于是同步操作,因此可以將整段代碼用?`try-catch`?包起來。
在?[Promise版](http://liubin.github.io/promises-book/#fs-promise-chain.js)?提供了指向內部promise對象的`then`?和?`catch`?別名,所以我們可以像其它promise對象一樣使用`catch`來進行錯誤處理。
fs-promise-chain中的錯誤處理
~~~
var File = require("./fs-promise-chain");
File.read(inputFilePath)
.transform(function (content) {
return ">>" + content;
})
.write(outputFilePath)
.catch(function(error){
console.error(error);
});
~~~
如果你想在[fs-method-chain.js](http://liubin.github.io/promises-book/#fs-method-chain.js)中自己實現異步處理的話,錯誤處理可能會成為比較大的問題;可以說在進行異步處理的時候,還是使用Promise實現起來比較簡單。
## 4.7.4\. Promise之外的異步處理
如果你很熟悉Node.js的話,那么看到方法鏈的話,你是不是會想起來?[Stream](http://nodejs.org/api/stream.html)?呢。
如果使用?[Stream](http://nodejs.org/api/stream.html)?的話,就可以免去了保存?`this.lastValue`?的麻煩,還能改善處理大文件時候的性能。 另外,使用Stream的話可能會比使用Promise在處理速度上會快些。
使用Stream進行read→transform→write
~~~
readableStream.pipe(transformStream).pipe(writableStream);
~~~
因此,在異步處理的時候并不是說Promise永遠都是最好的選擇,要根據自己的目的和實際情況選擇合適的實現方式。
> Node.js的Stream是一種基于Event的技術
關于Node.js中Stream的詳細信息可以參考以下網頁。
* [利用Node.js Stream API對數據進行流式處理 - Block Rockin’ Codes](http://jxck.hatenablog.com/entry/20111204/1322966453)
* [Stream2基礎](http://www.slideshare.net/shigeki_ohtsu/stream2-kihon)
* [關于Node-v0.12新功能](http://www.slideshare.net/shigeki_ohtsu/node-v012tng12)
## 4.7.5\. Promise wrapper
再回到?[fs-method-chain.js](http://liubin.github.io/promises-book/#fs-method-chain.js)?和?[Promise版](http://liubin.github.io/promises-book/#fs-promise-chain.js),這兩種方法相比較內部實現也非常相近,讓人覺得是不是同步版本的代碼可以直接就當做異步方式來使用呢?
由于JavaScript可以向對象動態添加方法,所以從理論上來說應該可以從非Promise版自動生成Promise版的代碼。(當然靜態定義的實現方式容易處理)
盡管?[ES6 Promises](http://liubin.github.io/promises-book/#es6-promises)?并沒有提供此功能,但是著名的第三方Promise實現類庫?[bluebird](https://github.com/petkaantonov/bluebird/)?等提供了被稱為?[Promisification](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisification)?的功能。
如果使用類似這樣的類庫,那么就可以動態給對象增加promise版的方法。
~~~
var fs = Promise.promisifyAll(require("fs"));
fs.readFileAsync("myfile.js", "utf8").then(function(contents){
console.log(contents);
}).catch(function(e){
console.error(e.stack);
});
~~~
### Array的Promise wrapper
前面的?[Promisification](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisification)?具體都干了些什么光憑想象恐怕不太容易理解,我們可以通過給原生的?`Array`?增加Promise版的方法為例來進行說明。
在JavaScript中原生DOM或String等也提供了很多創建方法鏈的功能。?`Array`?中就有諸如?`map`?和?`filter`?等方法,這些方法會返回一個數組類型,可以用這些方法方便的組建方法鏈。
array-promise-chain.js
~~~
"use strict";
function ArrayAsPromise(array) {
this.array = array;
this.promise = Promise.resolve();
}
ArrayAsPromise.prototype.then = function (onFulfilled, onRejected) {
this.promise = this.promise.then(onFulfilled, onRejected);
return this;
};
ArrayAsPromise.prototype["catch"] = function (onRejected) {
this.promise = this.promise.catch(onRejected);
return this;
};
Object.getOwnPropertyNames(Array.prototype).forEach(function (methodName) {
// Don't overwrite
if (typeof ArrayAsPromise[methodName] !== "undefined") {
return;
}
var arrayMethod = Array.prototype[methodName];
if (typeof arrayMethod !== "function") {
return;
}
ArrayAsPromise.prototype[methodName] = function () {
var that = this;
var args = arguments;
this.promise = this.promise.then(function () {
that.array = Array.prototype[methodName].apply(that.array, args);
return that.array;
});
return this;
};
});
module.exports = ArrayAsPromise;
module.exports.array = function newArrayAsPromise(array) {
return new ArrayAsPromise(array);
};
~~~
原生的 Array 和?`ArrayAsPromise`?在使用時有什么差異呢?我們可以通過對?[上面的代碼](http://liubin.github.io/promises-book/#array-promise-chain.js)?進行測試來了解它們之間的不同點。
array-promise-chain-test.js
~~~
"use strict";
var assert = require("power-assert");
var ArrayAsPromise = require("../src/promise-chain/array-promise-chain");
describe("array-promise-chain", function () {
function isEven(value) {
return value % 2 === 0;
}
function double(value) {
return value * 2;
}
beforeEach(function () {
this.array = [1, 2, 3, 4, 5];
});
describe("Native array", function () {
it("can method chain", function () {
var result = this.array.filter(isEven).map(double);
assert.deepEqual(result, [4, 8]);
});
});
describe("ArrayAsPromise", function () {
it("can promise chain", function (done) {
var array = new ArrayAsPromise(this.array);
array.filter(isEven).map(double).then(function (value) {
assert.deepEqual(value, [4, 8]);
}).then(done, done);
});
});
});
~~~
我們看到,在?`ArrayAsPromise`?中也能使用 Array的方法。而且也和前面的例子類似,原生的Array是同步處理,而?`ArrayAsPromise`?則是異步處理,這也是它們的不同之處。
仔細看一下?`ArrayAsPromise`?的實現,也許你已經注意到了,?`Array.prototype`?的所有方法都被實現了。 但是,`Array.prototype`?中也存在著類似`array.indexOf`?等并不會返回數組類型數據的方法,這些方法如果也要支持方法鏈的話就有些不自然了。
在這里非常重要的一點是,我們可以通過這種方式,為具有接收相同類型數據接口的API動態的創建Promise版的API。 如果我們能意識到這種API的規則性的話,那么就可能發現一些新的使用方法。
> 前面我們看到的?[Promisification](https://github.com/petkaantonov/bluebird/blob/master/API.md#promisification)?方法,借鑒了了 Node.js的Core模塊中在進行異步處理時將?`function(error,result){}`方法的第一個參數設為?`error`?這一規則,自動的創建由Promise包裝好的方法。
## 4.7.6\. 總結
在本小節我們主要學習了下面的這些內容。
* Promise版的方法鏈實現
* Promise并不是總是異步編程的最佳選擇
* Promisification
* 統一接口的重用
[ES6 Promises](http://liubin.github.io/promises-book/#es6-promises)只提供了一些Core級別的功能。 因此,我們也許需要對現有的方法用Promise方式重新包裝一下。
但是,類似Event等調用次數沒有限制的回調函數等在并不適合使用Promise,Promise也不能說什么時候都是最好的選擇。
至于什么情況下應該使用Promise,什么時候不該使用Promise,并不是本書要討論的目的, 我們需要牢記的是不要什么都用Promise去實現,我想最好根據自己的具體目的和情況,來考慮是應該使用Promise還是其它方法。
- 前言
- 第一章 - 什么是Promise
- 1.1. 什么是Promise
- 1.2. Promise簡介
- 1.3. 編寫Promise代碼
- 第二章 - 實戰Promise
- 2.1. Promise.resolve
- 2.2. Promise.reject
- 2.3. 專欄: Promise只能進行異步操作?
- 2.4. Promise#then
- 2.5. Promise#catch
- 2.6. 專欄: 每次調用then都會返回一個新創建的promise對象
- 2.7. Promise和數組
- 2.8. Promise.all
- 2.9. Promise.race
- 2.10. then or catch?
- 第三章 - Promise測試
- 3.1. 基本測試
- 3.2. Mocha對Promise的支持
- 3.3. 編寫可控測試(controllable tests)
- 第四章 - Advanced
- 4.1. Promise的實現類庫(Library)
- 4.2. Promise.resolve和Thenable
- 4.3. 使用reject而不是throw
- 4.4. Deferred和Promise
- 4.5. 使用Promise.race和delay取消XHR請求
- 4.6. 什么是 Promise.prototype.done ?
- 4.7. Promise和方法鏈(method chain)
- 4.8. 使用Promise進行順序(sequence)處理
- 第五章 - Promises API Reference
- 5.1. Promise#then
- 5.2. Promise#catch
- 5.3. Promise.resolve
- 5.4. Promise.reject
- 5.5. Promise.all
- 5.6. Promise.race
- 第六章 - 用語集
- 第七章 - 參考網站
- 第八章 - 關于作者
- 第九章 - 關于譯者