学习Promise
Promise是异步编程的一种解决方法,相比起传统的回调函数以及事件,有了更好的可读性
Promise有3种状态,分别是pending
、resolved
(准确来说应该叫fulfilled)、rejected
,状态的转换不可逆转,也就是说,一旦状态从pending转变为其他两个状态中的任意一个,那么状态就是固定不变的。
有了promise对象,我们可以通过then
的操作链来传入回调,在一个操作链中,前面的then回调执行完毕之后会将执行结果传递到下一个then,我们就可以将异步编程以同步编程的编程习惯表达(generator和async在这方面可以更彻底)。
promise一旦创建后立即执行,无法中途取消,并且内部抛出的错误无法传递到外部,因此无法在外部捕获
基本使用
创建一个promise实例:
var promise = new Promise(function(resolve, reject) {
// 执行的代码
if(success) {
resolve(params);
} else {
reject(error);
}
});
在promise实例中,可以传入两个参数resolve和reject,都是异步操作的函数,分别对应成功和失败的回调
在调用promise实例的时候,可以用then方法接受异步回调:
promise.then((val) => {
console.log(val);
}, (err) => {
console.error(err);
})
举一个栗子来说明promise的基本运行机制:
let promise = new Promise(function(resolve) {
console.log('start');
resolve();
});
promise.then(() => {
console.log('resolved');
});
console.log('outter');
// start
// outer
// resolved
在上面的代码中,promise一旦创建就立即执行了,输出start,然后执行同步任务打印outer,在同步任务执行完毕后执行指定的回调
then方法的回调函数都可以传入参数,reject状态传入的通常是Error对象的实例,表示抛出的错误;而resolve状态的参数除了一些普通的正常值外,还可以传入另外一个promise实例:
var p1 = new Promise((resolve, rejecte) => {
console.log('p1');
setTimeout(() => {
resolve();
}, 2000);
});
var p2 = new Promise((resolve, rejecte) => {
resolve(p1);
});
p2.then(() => {
console.log('p2-then');
});
p1.then(() => {
console.log('p1-then');
});
//p1
//p1-then
//p2-then
上面创建了两个promise实例p1、p2,其中p2回调的参数是p1,那么p2的状态取决于p1的状态,若p1是peding,那么p2的回调会等待p1的状态改变。
注意,在创建promise的实例的时候有个小坑,比如下面:
new Promise((resolve, reject) => {
resolve(1);
console.log(11);
}).then(p => {
console.log(p);
});
// 11
// 1
在上面的代码中,由于传入的resolve回调是异步操作,所以resolve后的同步任务会优先运行,所以,立即转为resolved的promise是在本轮事件循环的最后执行的,并且晚于本轮的同步事件
一般来说,一旦调用了resolve或reject后,promise就完成了自己的任务,后续的操作要放在then方法内,或者也可以在回调函数前面加上return,后面的同步任务就不会运行了
用promise封装ajax:
function makeAjaxCall(url, methodType){
var promiseObj = new Promise(function(resolve, reject){
var xhr = new XMLHttpRequest();
xhr.open(methodType, url, true);
xhr.send();
xhr.onreadystatechange = function(){
if (xhr.readyState === 4){
if (xhr.status === 200){
console.log("xhr done successfully");
var resp = xhr.responseText;
var respJson = JSON.parse(resp);
resolve(respJson);
} else {
reject(xhr.status);
console.log("xhr failed");
}
} else {
console.log("xhr processing going on");
}
}
console.log("request sent succesfully");
});
return promiseObj;
}
document.getElementById("userDetails").addEventListener("click", function(){
// git hub url to get btford details
var userId = document.getElementById("userId").value;
var URL = "https://api.github.com/users/"+userId;
makeAjaxCall(URL, "GET").then(processUserDetailsResponse, errorHandler);
});
document.getElementById("repoList").addEventListener("click", function(){
// git hub url to get btford details
var userId = document.getElementById("userId").value;
var URL = "https://api.github.com/users/"+userId+"/repos";
makeAjaxCall(URL, "GET").then(processRepoListResponse, errorHandler);
});
function processUserDetailsResponse(userData){
console.log("render user details", userData);
}
function processRepoListResponse(repoList){
console.log("render repo list", repoList);
}
function errorHandler(statusCode){
console.log("failed with status", status);
}
Promise.prototype.then
前面提到了then方法,跟在promise实例后面,里面可以传入异步的回调函数
它可以采用链式操作,前一个回调运行完毕后会将运行结果传递给下一个then:
var a = 1;
var t=new Promise((resolve) => {resolve();}).then(()=>{console.log(1);return a++;}).then(() => {console.log(a);});
//1
//2
前一个回调也可能返回一个promise,那么只有当该promise的状态确定了,后面的那个then方法才会执行
Promise.prototype.catch
事实上,Promise.prototype.catch
是.then(null,reject)
的别名,是用于处理错误的回调
但是,使用catch比起传入reject回调有优势:如果在同步任务中出现了错误,状态变为rejected,就会调用catch指定的回调;如果在then方法指定的回调运行时抛出错误,也可以被catch捕获。而如果只是简单传入reject,无法做到第二点,除非传入第二个then来捕获错误:
p.then(() => console.log('resolved'))
.catch((err) => console.log(err));
// 等同下面
p.then(() => console.log('resolved'))
.then(null, (err) => console.log(err));
注意,不要在resolve语句后抛出错误,因为promise状态一旦改变就是确定的。
promise产生的错误的冒泡特性
promise对象的错误会向后传递,在没有没捕获的情况下会一直传递下去,比如:
promise().then(() => func()).then(() => func()).catch((err) => console.error(err));
在catch前面产生的错误,最后都会被最后的catch捕获。
一般地说,不要在then方法内传入reject的回调,加入catch比较合适:
//bad
promise
.then(function(data){
//success
},function(err) {
//error
});
//good
promise
.then(function(data){
//success
})
.catch(function(err){
//error
});
如果要在promise内部抛出错误,最好的方式是用reject抛出而不是throw抛出:
//bad
var promise = new Promise(function(resolve, reject){
throw new Error("message");
});
promise.catch(function(error){
console.error(error);// => "message"
});
//good
var promise = new Promise(function(resolve, reject){
reject(new Error("message"));
});
promise.catch(function(error){
console.error(error);// => "message"
})
如果在resolve后抛出错误是无效的,因为promise状态已经确定,比如这样:
var promise = new Promise((resolve, reject) => {
resolve('resolved');
throw new Error('out');
});
promise
.then((val) => console.log(val))
.catch((err) => console.log(err));
但是,假如像下面那样加入了定时器,由于定时器的调用栈是独立的,错误会在promise函数体外抛出,冒泡到最外,但是无法被捕获
new Promise((resolve, reject) => {
resolve('resolved');
setTimeout(() => { throw new Error('out') }, 0);
}).then((val) => console.log(val)).catch((err) => console.log(err));
如果你用的是Node,它有一个unhandleRejection
事件,专门监听未捕获的reject错误。
如果是在多个then操作链中间发生了错误,没有被catch,那么后续的then就不会运行,比如这样:
function taskA() {
console.log("Task A");
throw new Error("throw Error @ Task A")
}
function taskB() {
console.log("Task B");// 不会被调用
}
function onRejected(error) {
console.log(error);// => "throw Error @ Task A"
}
function finalTask() {
console.log("Final Task");
}
var promise = Promise.resolve();
promise
.then(taskA)
.then(taskB)
.catch(onRejected)
.then(finalTask);
/*
Task A
Error: throw Error @ Task A
Final Task
*/
Promise.resolve()
这个方法将普通对象转换为Promise对象,其返回值也是一个promise对象,比如:
Promise.resolve(1).then(function(value){
console.log(value);
});
直接调用Promise.resolve()
就可以得到一个promise对象;如果是传入一个普通的对象,那么这个对象就会传递给then方法表示resolved的回调函数;如果传入的是一个thenable对象,那么这个thenable对象就会被转换为promise对象。
要注意,立即resolve的对象是在本轮的事件循环结束时开始的,demo:
setTimeout(() => {
console.log(3);
},0);
Promise.resolve().then(() => console.log(2));
console.log(1);
//1
//2
//3
补充:
Promise对象的状态只能由执行器内部的函数来改变,不随外部改变,一个简单的demo:
Promise.resolve()
.then( (val) => {
console.log('Step 1', val);
return Promise.resolve('Hello');
})
.then( value => {
console.log(value, 'World');
return Promise.resolve(new Promise( resolve => {
setTimeout(() => {
resolve('Good');
}, 2000);
}));
})
.then( value => {
console.log(value, ' evening');
return Promise.resolve({
then() {
console.log('everyone');
}
})
})
/*
Step 1 undefined
Hello World
Good evening
everyone
*/
开始Promise.resolve返回一个fulfilled的promise,立刻执行then,然后返回一个新的promise实例,第二个then在接收到前面then传递的实例后输出Hello World。第二个then里面,Promise.resolve内的promise对象不会被resolve改变,只有当该promise对象内部代码执行状态确定后才会将自身状态变为fulfilled,所以第三个then需要等待第二个then fulfilled返回的结果。
Promise.reject()
类似于前面的Promise.resolve
,会返回一个新的promise实例,状态为rejected。和resolve不同的是,即使Promise.reject接收到的参数是一个promise对象,该函数也还是会返回一个全新的promise对象。
var r = Promise.reject(new Error("error"));
console.log(r === Promise.reject(r));// false
Promise.all()
可以接收多个promise实例,并且返回一个新的promise对象。它接受的是具有iterator接口的数据结构,且内部每个成员都是promise实例。返回的新promise实例的状态由成员决定:
- 当所有成员的状态都变为resolved,新实例状态才会变为resolved,此时每个成员的返回值组成为一个数组传递给新实例的回调
- 只要所有成员中的任意一个状态变为rejected,新实例的状态就会变为rejected,第一个变为rejected的实例的返回值会传递给新实例的回调
一个简单的demo:
var p1 = Promise.resolve(1),
p2 = Promise.resolve(2),
p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function (results) {
console.log(results); // [1, 2, 3]
});
并且,每个成员都同时、并行运行的:
function timeDelay(ms) {
return new Promise((resolve) => {
setTimeout(() => {resolve(ms)}, ms);
});
}
var startTime = Date.now();
Promise.all([
timeDelay(1),
timeDelay(50),
timeDelay(100)
]).then((val) => {
console.log(Date.now() - startTime + 'ms');
console.log(val);
});
//1,50,100
上面的代码大概在过了100ms后执行完毕,如果是串行执行,那么就需要1+50+100ms
注意,如果成员内部有了catch捕获异常,那么最后传递给Promise.all()的实例的状态都是resolved的,比如:
var p1 = new Promise((resolve) => {
throw new Error('p1');
}).then(val => val)
.catch(err => err);
var p2 = new Promise((resolve) => {
throw new Error('p2');
}).then(val => val)
.catch(err => err);
Promise.all([p1, p2])
.then(val => console.log(val))
.catch(err => console.log('catch: ', err));
上面的代码,两个成员实例最后都抛出了错误,但是最后Promise.all后面的catch不会捕获到它们抛出的错误。如果上面的任意一个成员没有自己的catch方法,那么该成员就会调用Promise.all的catch方法,捕获自己的错误
传入可迭代数据
事实上,promise.all方法可以传入的不仅是数组,其他部署了iterator的数据结构也可以传入,比如下面:
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
}),
2,
3
]).then(val => console.log(val));
//[1,2,3]
和.map()一起使用
let urls = [
'https://api.github.com/users/tony',
'https://api.github.com/users/lily',
'https://api.github.com/users/parker'
];
let requests = urls.map(url => fetch(url));
Promise.all(requests)
.then(res => res.forEach(res => console.log(`${res.url}: ${res.status}`)
});
Promise.race()
该方法也是包装多个promise实例并且返回一个新的promise实例,但是传入的成员只要其中一个率先改变状态,新实例的状态就会改变,且它会接收到率先改变的成员的返回值
异步并行
如果使用的是同一个promise实例,那么相对应的代码块是并行执行的:
let promise = new Promise(resolve => {
setTimeout(() => {
console.log('the promise fulfilled');
resolve('hello, world');
}, 1000);
});
setTimeout(() => {
promise.then( value => {
console.log(value);
});
}, 3000);
在上面的代码中,在等待1秒后输出the promise fulfilled,再过2s后输出hello, world。因为前后两个定时器的对象都是同一个promise实例,所以它们是并行执行的
异步队列中的栈
console.log('go');
new Promise(resolve => {
setTimeout( () => {
resolve('hello');
}, 2000);
})
.then( value => {
console.log(value);
console.log('world');
(function () {
return new Promise(resolve => {
setTimeout(() => {
console.log('Func');
resolve('Func-resolve');
}, 2000);
});
}());
return false;
})
.then( value => {
console.log(value + ' lastThen');
});
//输出:
/*
go
hello
world
false lastThen
*/
在第一个then方法内有一个匿名函数,里面返回一个新promise实例,但是这个实例不是返回给第一个then方法的,而且给了匿名函数,所以第二个then方法不会等待前面返回promise实例,直接就运行了,而且里面的resolve也无法传递给最后的then方法。由于匿名函数内的定时器仍然有效,所以在2s后会输出Func。如果将匿名函数的return false去掉,会得到undefined lastThen,可以看出,无论then方法是否有返回值都不会影响到后面的then方法的运行。
使用上一些常见的错误
思考下面代码的运行
- 代码1
doSomething()
.then(function () {
return doSomethingElse();
})
.then(finalHandler);
答案:
// doSomething
// |-----------|
// doSomethingElse(undefined)
// |------------|
// finalHandler(resultOfDoSomethingElse)
// |------------|
这段代码是常见的then链式调用
- 代码2
doSomething()
.then(function () {
doSomethingElse();
})
.then(finalHandler);
答案:
// doSomething
// |------------------|
// doSomethingElse(undefined)
// |------------------|
// finalHandler(undefined)
// |------------------|
由于第一个then在执行后的promise实例没有返还给then方法而是给了匿名函数,所以下一个then就几乎是和第一个then同时一起运行
- 问题3
doSomething()
.then(doSomethingElse())
.then(finalHandler);
答案:
// doSomething
// |------------------|
// doSomethingElse(undefined)
// |----------------------------------|
// finalHandler(resultOfDoSomething)
// |------------------|
第一个then传入的函数是以执行的方式传递进去的,实际上就是传入了一个promise实例,所以它和前面的doSomething处于同一个栈,可以看做是同时执行。由于第一个then内部没有返回promise,所以第二个then的执行时机在doSomething之后
关于链式调用的返回值,可以参照MDN的文档:
MDN
实现事件队列
有时我们不希望所有事件动作一起发生,而且要按照一定顺序逐个进行,那么我们可以用promise来解决。
以下提供伪代码实现方式
- 用forEach来实现
function queue(things) {
let promise = Promise.resolve();
things.forEach(thing => {
promise = promise.then(() => {
return new Promise(resolve => {
doThing(thing, () => {
resolve();
});
});
});
});
return promise;
}
queue('a','b','c');
在使用的时候要注意不要忘记将then产生的新promise实例返回,这样会导致then方法同时触发:
// Error
function queue(things) {
let promise = Promise.resolve();
things.forEach(thing => {
promise = promise.then(() => {
return new Promise(resolve => {
doThing(thing, () => {
resolve();
});
});
});
});
}
- 用reduce实现
function queue(things) {
return things.reduce((promise, thing) => {
return promise.then(() => {
return new Promise(resolve => {
doThing(thing, () => {
resolve();
});
});
});
}, Promise.resolve());
}
queue('a','b','c');
用reduce实现的时候要注意,一定是在then方法内部实例化promise,否则promise实例一旦创建就马上运行:
// Error
function queue(things) {
return things.reduce((promise, thing) => {
let step = new Promise(resolve => {
doThing(thing, () => {
resolve();
});
});
return promise.then(step);
}, Promise.resolve());
}
参考:
ES6标准入门(第三版)
[Promise迷你书](http://liubin.org/promises-book/)
[Ajax — Async, Callback & Promise](https://medium.com/front-end-hacking/ajax-async-callback-promise-e98f8074ebd7)
[[翻译] We have a problem with promises](http://fex.baidu.com/blog/2015/07/we-have-a-problem-with-promises/)
推荐的资料: