文章目录
- 前言
- 原理
- 使用场景
- Promise 的api
- EventLoop注意点
- 任务队列
- 同步任务
- 异步任务
- 事件循环
- 总结
- 练习
- 分析执行过程
- 总结
- 结果
- 参考资料
前言
Promise 规范有很多,目前主要的标准有
-
Promise/A
-
Promise/B
-
Promise/D
-
Promise/A
-
Promise/A+
ES6 中,采用了 Promise/A+ 规范,所以接下来是按照《Promise/A+规范》来介绍的。
Promise正如其单词意思一样,“承诺”,即一旦从初始状态(pending)转变成其他状态,那么它就不能改变了。
then
函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending
状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then
调用就失去意义了。
javascript">//不使用Promise
xhr.get('request_url', function (result) {
//do something
console.log(result.id);
});
javascript">//使用Promise
new Promise(function (resolve) {
//异步请求
http.get('request_url', function (result) {
resolve(result.id)
})
}).then(function (id) {
//do something
console.log(id);
})
传统的异步回调写法在多次请求的场景下会出现回调地狱的问题,这样在审计代码的时候非常不便。因此使用promise来编写异步代码,这样看起来就舒爽多了。
javascript">//不使用Promise
http.get('some_url', function (id) {
//do something
http.get('getNameById', id, function (name) {
//do something
http.get('getCourseByName', name, function (course) {
//dong something
http.get('getCourseDetailByCourse', function (courseDetail) {
//do something
})
})
})
});
//使用Promise
function getUserId(url) {
return new Promise(function (resolve) {
//异步请求
http.get(url, function (id) {
resolve(id)
})
})
}
getUserId('some_url').then(function (id) {
//do something
return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
//do something
return getCourseByName(name);
}).then(function (course) {
//do something
return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
//do something
});
原理
- Promise有三种状态,分别是pending、resolved(也可以叫fulfilled)、rejected。
- pending表示Promise对象实例创建的时候初始状态。
- resolved(fulfilled)表示为异步任务成功执行的状态,例如,请求状态码为200时。
- rejected表示异步任务失败的状态,例如,请求状态码为404时,也有可能是抛出异常,这个可以自己设置。
- 构造一个Promise实例需要给Promise构造函数传入一个函数,然后传入的函数需要有两个形参,两个参数都是函数类型的参数,分别是resolved和rejected。
- Promise有then方法,then方法就是用来指定Promise对象的状态改变时需要执行的操作。当状态变为resolved时需执行第一个函数onResolved;当状态变为rejected时需执行第二个函数onRejected。
在这里我们需要注意:pending状态只能变成resolved(fulfilled)成功状态或者pendin状态只能变成失败rejected状态这两种状态;不能resolved(fulfilled)成功状态变成pending状态或者失败状态变成pending状态或者resolved(fulfilled)成功状态和rejected失败状态之间各自相互转换。
可以把 Promise 看成一个有限状态机。初始是 pending
状态,可以通过函数 resolve
和 reject
,将状态转变为 resolved
或者 rejected
状态,状态一旦改变就不能再次变化。
then
函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending
状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then
调用就失去意义了。
下图是我个人的一个理解,仅供参考。
使用场景
例如使用promise来创建xhr请求,获得json数据。
javascript">function getJson(type = 'GET', url = './test.json', data = {}) { //设定默认值
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState === 1) {
this.responseType = 'json';
this.setRequestHeader("Content-type", "application/json; charset=UTF-8");
} else if (this.readyState === 4) {
if (this.status === 200 || this.status == 201 || this.status === 304) {
resolve({
data: this.response,
msg: 'success'
})
} else {
reject({
data: this.response,
msg: 'error'
})
}
}
}
xhr.open(type, url);
xhr.send(JSON.stringify(data));
})
}
// 模拟ajax
function ajax() {
getJson('POST', 'http://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: 'bar',
userId: 1
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err)
});
}
Promise 的api
具体使用方法请查阅官方资料。
- Promise.resolve()
- Promise.reject()
- Promise.prototype.then()
- Promise.prototype.catch()
- Promise.all([promise1,promise2…]) 列表中所有的promise都有完成,相当于 且
- Promise.race([promise1,promise2…]) 列表中promise完成一个即可,相当于 或
一些注意点
- Promise.resolve()的作用将现有对象转为Promise对象resolved。
相当于Promise.resolve(‘test’)==new Promise(resolve=>resolve(‘test’))
- Promise.reject()返回一个Promise对象,状态为rejected;
相当于Promise.reject(‘test’)==new Promise((resolve, reject)=>reject(‘test’))
-
then方法上边已经做介绍,这里就不再介绍。
-
catch():发生错误的回调函数。
-
Promise.race()的作用是同时执行多个实例,只要有一个实例改变状态,Promise就改为那个实例所改变的状态。
-
Promise.all()适合用于所有的结果都完成了才去执行then()成功的操作。例如:
javascript">// 有一个请求需要同时使用学号/姓名/班级三个参数,而这三个参数的请求分别不同,需要分别请求
let p1 =new Promise(function(resolve,reject){
resolve(1);
});
let p2 = new Promise(function(resolve,reject){
resolve(2);
});
let p3 = new Promise(function(resolve,reject){
resolve(3);
});
// 学号/姓名/班级三个参数都请求完成且成功后
Promise.all([p1, p2, p3]).then(function (results) {
console.log('success:'+results);
}).catch(function(r){
console.log("error");
console.log(r);
});
EventLoop注意点
要想真正弄明白Promise的运行机制,最好把EventLoop这块吃透。
任务队列
-
如果前一个任务耗时很长,后一个任务就不得不一直等着。
-
js中所有任务可以分成两种,一种是同步任务,另一种是异步任务。
同步任务
- 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 所有同步任务都在主线程上执行,形成一个执行栈
异步任务
-
异步任务是不进入主线程,而进入"任务队列"的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
-
当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。
事件循环
主线程在运行的时候,产生堆和栈。只有栈中的代码执行完毕,主线程才会去读取“任务队列”中的回调函数依次执行。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。
总结
macrotask
的执行:是在evenloop的每次循环过程,取出macrotask queue中可执行的第一个(注意不一定是第一个,例如setTimeout可以指定任务被执行的最少延迟时间,当前macrotask queue的首位保存的任务可能还没有到执行时间,所以queue只是代表callback
插入的顺序,不代表执行时也要按照这个顺序)。microtask
的执行:在evenloop的每次循环过程之后,如果当前的执行栈(call stack)为空,那么执行microtask queue
中所有可执行的任务。
练习
javascript">//使用while循环同步阻塞xx毫秒
function sleep(delay) {
var start = (new Date()).getTime();
while((new Date()).getTime() - start < delay) {
continue;
}
}
console.log((new Date()).getTime());
setTimeout(() => {
console.log((new Date()).getTime());
console.log('0');
}, 15000)
new Promise((resolve, reject) => {
// 异步代码
setTimeout(function(){
sleep(5000);
}, 3000)
sleep(10000);
console.log('1');
resolve();
}).then(() => {
console.log('2');
new Promise((resolve, reject) => {
console.log('3');
resolve()
}).then(() => {
console.log('4');
}).then(() => {
console.log((new Date()).getTime());
console.log('5');
})
}).then(() => {
console.log('6');
})
new Promise((resolve, reject) => {
console.log('7');
resolve();
}).then(() => {
console.log('8');
})
console.log('9');
分析执行过程
1、首先将定时器中的回调添加到宏任务队列中
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[]
** - 已打印
[]
2、执行到第一个Promise处,在异步任务table中注册了一个阻塞5秒的异步宏任务,然后再同步地阻塞10000毫秒,接下来执行同步代码打印**1
,然后状态立马变成resolve,其中的异步回调函数打印代码加入到微队列中[2]
**。
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[2]
** - 已打印
[1]
3、往下走,到下一个Promise中的代码,打印同步代码7
,然后立即变为resolve状态,并将异步回调打印8的代码放入微队列中。
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[2,8]
** - 已打印
[1,7]
4、往下走,遇到同步代码打印9,至此,主线程执行完了同步任务,此时任务队列为空,异步任务table开始计时,按照对应计时结束后往异步队列中注册任务。
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[2,8]
** - 已打印
[1,7,9]
5、先把微队列中的代码都取出执行完,才去执行后面的代码以及宏队列的代码。
所以先取出2,即打印2
, 所以现在的微队列只有一个任务[8]
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[8]
** - 已打印
[1,7,9,2]
6、打印完2后,往下走,又new了一个Promise对象,里面有同步代码打印3
,然后立即变为resolve状态,因此将4放入微队列[8,4]
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[8,4]
** - 已打印
[1,7,9,2,3]
7、接下来注意:未打印4的时候,是不会把后面then方法中的5放入微队列中的。因为then返回的还是一个promise对象。在本次EventLoop中,会先将外层Promise中的then中的6放入微队列,因为内层的Promise已经执行完最后一个then方法了,因此现在的微队列是[8,4,6]
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[8,4,6]
** - 已打印
[1,7,9,2,3]
8、取出微队列中的任务进行执行,将执行打印8
的代码。打印完8后面没有其余代码,因此继续取出打印4的任务再打印4
,当打印完4,之后将后面then中的打印5的异步任务放入微队列,因此现在的微队列是[6,5]
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[6, 5]
** - 已打印
[1,7,9,2,3, 8 , 4 ]
9、往下走,将微队列中剩余的任务中剩余的任务取出执行。
- 宏任务队列中的任务**
[0]
** - 微任务队列中的任务**
[]
** - 已打印
[1,7,9, 2,3,8,4,6,5]
10、直到微队列为空,宏队列中的任务才得以被取出执行。注意,不是现在才开始给宏任务计时,而是在第一次主线程栈空的时候,往子任务队列要任务的时候,就开始计时了。
- 宏任务队列中的任务**
[]
** - 微任务队列中的任务**
[]
** - 已打印
[1,7,9, 2,3,8,4,6,5,0]
总结
-
setTimeout是在第一次主线程栈空的时候,向子任务队列要任务的时候,就开始计时了。
-
new Promise(…).then()返回的也是一个promise对象,注意,是一个新的Promise实例。
-
在一次EventLoop中,宏队列要等微队列为空的时候才开始取任务。
结果
参考资料
《图解 Promise 实现原理》
《ES6入门 之 Promise 对象》
《Promise/A+规范》
《Promise/A+规范中文版》
《js promise看这篇就够了》