1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-25 11:12:21 +00:00
es6tutorial/docs/promise.md
2015-05-21 15:56:39 +08:00

794 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Promise对象
## 基本用法
ES6原生提供了Promise对象。所谓Promise对象就是代表了某个未来才会知道结果的事件通常是一个异步操作并且这个事件提供统一的API可供进一步处理。
有了Promise对象就可以将异步操作以同步操作的流程表达出来避免了层层嵌套的回调函数。此外Promise对象提供的接口使得控制异步操作更加容易。Promise对象的概念的详细解释请参考[《JavaScript标准参考教程》](http://javascript.ruanyifeng.com/)。
ES6的Promise对象是一个构造函数用来生成Promise实例。
```javascript
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(value) {
// failure
});
```
上面代码中Promise构造函数接受一个函数作为参数该函数的两个参数分别是resolve方法和reject方法。如果异步操作成功则用resolve方法将Promise对象的状态从“未完成”变为“成功”即从pending变为resolved如果异步操作失败则用reject方法将Promise对象的状态从“未完成”变为“失败”即从pending变为rejected
Promise实例生成以后可以用then方法分别指定resolve方法和reject方法的回调函数。
下面是一个使用Promise对象的简单例子。
```javascript
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
timeout(100).then(() => {
console.log('done');
});
```
上面代码中timeout方法返回一个Promise实例表示一段时间以后才会发生的结果。一旦Promise对象的状态变为resolved就会触发then方法绑定的回调函数。
下面是一个用Promise对象实现的Ajax操作的例子。
```javascript
var getJSON = function(url) {
var promise = new Promise(function(resolve, reject){
var client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
function handler() {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
```
上面代码中getJSON是对XMLHttpRequest对象的封装用于发出一个针对JSON数据的HTTP请求并且返回一个Promise对象。需要注意的是在getJSON内部resolve方法和reject方法调用时都带有参数。
如果调用resolve方法和reject方法时带有参数那么它们的参数会被传递给回调函数。reject方法的参数通常是Error对象的实例表示抛出的错误resolve方法的参数除了正常的值以外还可能是另一个Promise实例表示异步操作的结果有可能是一个值也有可能是另一个异步操作比如像下面这样。
```javascript
var p1 = new Promise(function(resolve, reject){
// ...
});
var p2 = new Promise(function(resolve, reject){
// ...
resolve(p1);
})
```
上面代码中p1和p2都是Promise的实例但是p2的resolve方法将p1作为参数p1的状态就会传递给p2。
注意这时p1的状态决定了p2的状态。如果p1的状态是pending那么p2的回调函数就会等待p1的状态改变如果p1的状态已经是fulfilled或者rejected那么p2的回调函数将会立刻执行。
## Promise.prototype.then()
Promise.prototype.then方法返回的是一个新的Promise对象因此可以采用链式写法即then方法后面再调用另一个then方法。
```javascript
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
```
上面的代码使用then方法依次指定了两个回调函数。第一个回调函数完成以后会将返回结果作为参数传入第二个回调函数。
如果前一个回调函数返回的是Promise对象这时后一个回调函数就会等待该Promise对象有了运行结果才会进一步调用。
```javascript
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// ...
});
```
then方法还可以接受第二个参数表示Promise对象的状态变为rejected时的回调函数。
## Promise.prototype.catch()
Promise.prototype.catch方法是`Promise.prototype.then(null, rejection)`的别名,用于指定发生错误时的回调函数。
```javascript
getJSON("/posts.json").then(function(posts) {
// ...
}).catch(function(error) {
// 处理前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
```
上面代码中getJSON方法返回一个Promise对象如果该对象运行正常则会调用then方法指定的回调函数如果该方法抛出错误则会调用catch方法指定的回调函数处理这个错误。
下面是一个例子。
```javascript
var promise = new Promise(function(resolve, reject) {
throw new Error('test')
});
promise.catch(function(error) { console.log(error) });
// Error: test
```
上面代码中Promise抛出一个错误就被catch方法指定的回调函数捕获。
如果Promise状态已经变成resolved再抛出错误是无效的。
```javascript
var promise = new Promise(function(resolve, reject) {
resolve("ok");
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
```
上面代码中Promise在resolve语句后面再抛出错误不会被捕获等于没有抛出。
Promise对象的错误具有“冒泡”性质会一直向后传递直到被捕获为止。也就是说错误总是会被下一个catch语句捕获。
```javascript
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function(comments) {
// some code
}).catch(function(error) {
// 处理前面三个Promise产生的错误
});
```
上面代码中一共有三个Promise对象一个由getJSON产生两个由then产生。它们之中任何一个抛出的错误都会被最后一个catch捕获。
跟传统的try/catch代码块不同的是如果没有使用catch方法指定错误处理的回调函数Promise对象抛出的错误不会传递到外层代码即不会有任何反应。
```javascript
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
```
上面代码中someAsyncThing函数产生的Promise对象会报错但是由于没有调用catch方法这个错误不会被捕获也不会传递到外层代码导致运行后没有任何输出。
```javascript
var promise = new Promise(function(resolve, reject) {
resolve("ok");
setTimeout(function() { throw new Error('test') }, 0)
});
promise.then(function(value) { console.log(value) });
// ok
// Uncaught Error: test
```
上面代码中Promise指定在下一轮“事件循环”再抛出错误结果由于没有指定catch语句就冒泡到最外层成了未捕获的错误。
Node.js有一个unhandledRejection事件专门监听未捕获的reject错误。
```javascript
process.on('unhandledRejection', function (err, p) {
console.error(err.stack)
});
```
上面代码中unhandledRejection事件的监听函数有两个参数第一个是错误对象第二个是报错的Promise实例它可以用来了解发生错误的环境信息。。
需要注意的是catch方法返回的还是一个Promise对象因此后面还可以接着调用then方法。
```javascript
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
```
上面代码运行完catch方法指定的回调函数会接着运行后面那个then方法指定的回调函数。
catch方法之中还能再抛出错误。
```javascript
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错因为y没有声明
y + 2;
}).then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
```
上面代码中catch方法抛出一个错误因为后面没有别的catch方法了导致这个错误不会被捕获也不会到传递到外层。如果改写一下结果就不一样了。
```javascript
someAsyncThing().then(function() {
return someOtherAsyncThing();
}).catch(function(error) {
console.log('oh no', error);
// 下面一行会报错因为y没有声明
y + 2;
}).catch(function(error) {
console.log('carry on', error);
});
// oh no [ReferenceError: x is not defined]
// carry on [ReferenceError: y is not defined]
```
上面代码中第二个catch方法用来捕获前一个catch方法抛出的错误。
## Promise.all()Promise.race()
Promise.all方法用于将多个Promise实例包装成一个新的Promise实例。
```javascript
var p = Promise.all([p1,p2,p3]);
```
上面代码中Promise.all方法接受一个数组作为参数p1、p2、p3都是Promise对象的实例。Promise.all方法的参数不一定是数组但是必须具有iterator接口且返回的每个成员都是Promise实例。
p的状态由p1、p2、p3决定分成两种情况。
1只有p1、p2、p3的状态都变成fulfilledp的状态才会变成fulfilled此时p1、p2、p3的返回值组成一个数组传递给p的回调函数。
2只要p1、p2、p3之中有一个被rejectedp的状态就变成rejected此时第一个被reject的实例的返回值会传递给p的回调函数。
下面是一个具体的例子。
```javascript
// 生成一个Promise对象的数组
var promises = [2, 3, 5, 7, 11, 13].map(function(id){
return getJSON("/post/" + id + ".json");
});
Promise.all(promises).then(function(posts) {
// ...
}).catch(function(reason){
// ...
});
```
Promise.race方法同样是将多个Promise实例包装成一个新的Promise实例。
```javascript
var p = Promise.race([p1,p2,p3]);
```
上面代码中只要p1、p2、p3之中有一个实例率先改变状态p的状态就跟着改变。那个率先改变的Promise实例的返回值就传递给p的返回值。
如果Promise.all方法和Promise.race方法的参数不是Promise实例就会先调用下面讲到的Promise.resolve方法将参数转为Promise实例再进一步处理。
## Promise.resolve()Promise.reject()
有时需要将现有对象转为Promise对象Promise.resolve方法就起到这个作用。
```javascript
var jsPromise = Promise.resolve($.ajax('/whatever.json'));
```
上面代码将jQuery生成deferred对象转为一个新的ES6的Promise对象。
如果Promise.resolve方法的参数不是具有then方法的对象又称thenable对象则返回一个新的Promise对象且它的状态为fulfilled。
```javascript
var p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
```
上面代码生成一个新的Promise对象的实例p它的状态为fulfilled所以回调函数会立即执行Promise.resolve方法的参数就是回调函数的参数。
所以如果希望得到一个Promise对象比较方便的方法就是直接调用Promise.resolve方法。
```javascript
var p = Promise.resolve();
p.then(function () {
// ...
});
```
上面代码的变量p就是一个Promise对象。
如果Promise.resolve方法的参数是一个Promise对象的实例则会被原封不动地返回。
Promise.reject(reason)方法也会返回一个新的Promise实例该实例的状态为rejected。Promise.reject方法的参数reason会被传递给实例的回调函数。
```javascript
var p = Promise.reject('出错了');
p.then(null, function (s){
console.log(s)
});
// 出错了
```
上面代码生成一个Promise对象的实例p状态为rejected回调函数会立即执行。
## Generator函数与Promise的结合
使用Generator函数管理流程遇到异步操作的时候通常返回一个Promise对象。
```javascript
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
var g = function* () {
try {
var foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
var it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(value));
});
}
go(it.next());
}
run(g);
```
上面代码的Generator函数g之中有一个异步操作getFoo它返回的就是一个Promise对象。函数run用来处理这个Promise对象并调用下一个next方法。
## async函数
### 概述
async函数与Promise、Generator函数一样是用来取代回调函数、解决异步操作的一种方法。它本质上是Generator函数的语法糖。async函数并不属于ES6而是被列入了ES7但是traceur、Babel.js、regenerator等转码器已经支持这个功能转码后立刻就能使用。
下面是一个Generator函数依次读取两个文件。
```javascript
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());
};
```
上面代码中readFile函数是`fs.readFile`的Promise版本。
写成async函数就是下面这样。
```javascript
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
```
一比较就会发现async函数就是将Generator函数的星号*替换成async将yield替换成await仅此而已。
async函数对Generator函数的改进体现在以下三点。
1内置执行器。Generator函数的执行必须靠执行器而async函数自带执行器。也就是说async函数的执行与普通函数一模一样只要一行。
```javascript
var result = asyncReadFile();
```
2更好的语义。async和await比起星号和yield语义更清楚了。async表示函数里有异步操作await表示紧跟在后面的表达式需要等待结果。
3更广的适用性。co函数库约定yield命令后面只能是Thunk函数或Promise对象而async函数的await命令后面可以跟Promise对象和原始类型的值数值、字符串和布尔值但这时等同于同步操作
### 实现
async函数的实现就是将Generator函数和自动执行器包装在一个函数里。
```javascript
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
```
所有的async函数都可以写成上面的第二种形式其中的spawn函数就是自动执行器。
下面给出spawn函数的实现基本就是前文自动执行器的翻版。
```javascript
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
```
### 用法
同Generator函数一样async函数返回一个Promise对象可以使用then方法添加回调函数。当函数执行的时候一旦遇到await就会先返回等到触发的异步操作完成再接着执行函数体内后面的语句。
下面是一个例子。
```javascript
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
```
上面代码是一个获取股票报价的函数函数前面的async关键字表明该函数内部有异步操作。调用该函数时会立即返回一个Promise对象。
上面的例子用Generator函数表达就是下面这样。
```javascript
function getStockPriceByName(name) {
return spawn(function*(name) {
var symbol = yield getStockSymbol(name);
var stockPrice = yield getStockPrice(symbol);
return stockPrice;
});
}
```
上面的例子中spawn函数是一个自动执行器由JavaScript引擎内置。它的参数是一个Generator函数。async...await结构本质上是在语言层面提供的异步任务的自动执行器。
下面是一个更一般性的例子,指定多少毫秒后输出一个值。
```javascript
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
```
上面代码指定50毫秒以后输出“hello world”。
### 注意点
await命令后面的Promise对象运行结果可能是rejected所以最好把await命令放在try...catch代码块中。
```javascript
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
};
}
```
await命令只能用在async函数之中如果用在普通函数就会报错。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
```
上面代码会报错因为await用在普通函数之中了。但是如果将forEach方法的参数改成async函数也有问题。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
```
上面代码可能不会正常工作原因是这时三个db.post操作将是并发执行也就是同时执行而不是继发执行。正确的写法是采用for循环。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
```
如果确实希望多个请求并发执行可以使用Promise.all方法。
```javascript
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
```
ES6将await增加为保留字。使用这个词作为标识符在ES5是合法的在ES6将抛出SyntaxError。
### 与Promise、Generator的比较
我们通过一个例子来看Async函数与Promise、Generator函数的区别。
假定某个DOM元素上面部署了一系列的动画前一个动画结束才能开始后一个。如果当中有一个动画出错就不再往下执行返回上一个成功执行的动画的返回值。
首先是Promise的写法。
```javascript
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
var ret = null;
// 新建一个空的Promise
var p = Promise.resolve();
// 使用then方法添加所有动画
for(var anim in animations) {
p = p.then(function(val) {
ret = val;
return anim(elem);
})
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
/* 忽略错误,继续执行 */
}).then(function() {
return ret;
});
}
```
虽然Promise的写法比回调函数的写法大大改进但是一眼看上去代码完全都是Promise的APIthen、catch等等操作本身的语义反而不容易看出来。
接着是Generator函数的写法。
```javascript
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
var ret = null;
try {
for(var anim of animations) {
ret = yield anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
});
}
```
上面代码使用Generator函数遍历了每个动画语义比Promise写法更清晰用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于必须有一个任务运行器自动执行Generator函数上面代码的spawn函数就是自动执行器它返回一个Promise对象而且必须保证yield语句后面的表达式必须返回一个Promise。
最后是Async函数的写法。
```javascript
async function chainAnimationsAsync(elem, animations) {
var ret = null;
try {
for(var anim of animations) {
ret = await anim(elem);
}
} catch(e) {
/* 忽略错误,继续执行 */
}
return ret;
}
```
可以看到Async函数的实现最简洁最符合语义几乎没有语义不相关的代码。它将Generator写法中的自动执行器改在语言层面提供不暴露给用户因此代码量最少。如果使用Generator写法自动执行器需要用户自己提供。