学习Promise

Promise是异步编程的一种解决方法,相比起传统的回调函数以及事件,有了更好的可读性

Promise有3种状态,分别是pendingresolved(准确来说应该叫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/)

推荐的资料: