为什么需要引入 Promise
我们都知道web 单线程 有很多异步回调,这短短的一段代码里面竟然出现了五次回调,这么多的回调会导致代码的逻辑不连贯、不线性,非常不符合人的直觉,这就是异步回调影响到我们的编码方式。
//执行状态
function onResolve(response){console.log(response) }
function onReject(error){console.log(error) }
let xhr = new XMLHttpRequest()
xhr.ontimeout = function(e) { onReject(e)}
xhr.onerror = function(e) { onReject(e) }
xhr.onreadystatechange = function () { onResolve(xhr.response) }
//设置请求类型,请求URL,是否同步信息
let URL = 'https://time.geekbang.com'
xhr.open('Get', URL, true);
//设置参数
xhr.timeout = 3000 //设置xhr请求的超时时间
xhr.responseType = "text" //设置响应返回的数据格式
xhr.setRequestHeader("X_TEST","time.geekbang")
//发出请求
xhr.send();
那么怎么才能 变的更加的线性,我们开始封装异步回调,只在乎输入和输出的结果。
引入promise 是如何解决消灭嵌套调用和多次错误处理?
Promise 如何实现了回调函数的延时绑定?
如何将回调函数 onResolve 的返回值穿透到最外层,摆脱嵌套循环的?
Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?
Promise 中为什么要引入微任务?
promise
- 1、对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)
- 2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
- Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
- resolved 函数是,promise 对象从pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
- rejected 函数是,promise 对象从pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
- Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
第一个回调函数是Promise对象的状态变为resolved时调用,
第二个回调函数是Promise对象的状态变为rejected时调用。
其中,第二个函数是可选的,不一定要提供。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
// promise 是立即执行的,promise状态进入resolve 状态,
//则直接将回调放入微任务队列中,执行then 方法是同步的,
//但是then 中的回调是异步的
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
// 调用resolve或reject并不会终结 Promise 的参数函数的执行。
//调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。
Promise.prototype.then()
Promise 实例具有then方法,也就是说,then方法是定义在原型对象。
- then方法的第一个参数是resolved状态的回调函数,
- 第二个参数(可选)是rejected状态的回调函数。
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
const promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
// Error: test
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
- catch 返回的仍然是一个promise,后面还可以调用then ,如果catch 中间不抛出错误,就直接跳过。
Promise.prototype.finally()
不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
Promise.all()
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
实现一个promise.all
有时候面试会遇到这样的问题
首先我们要知道 promise.all 返回的是一个promise 实例
如果传入的参数中的 promise 都变成完成状态,Promise.all 返回的 promise 异步地变为完成。
如果传入的参数中,有一个 promise 失败,Promise.all 异步地将失败的那个结果给失败状态的回调函数,而不管其它 promise 是否完成
在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function (promises){
return new Promise((resolve,reject) => {
// 将迭代对象转化为数组
promises = Array.from(promises)
if(promises.length === 0){
resolve([])
}else{
let result = [];
let index = 0;
for( let i = 0; i < promises.length; i++){
Promise.resolve(promises[i]).then(data=>{
result[i] = data;
if(++index === promises.length){
resolve(result)
}
},err =>{
reject(err)
return
})
}
}
})
}
// 封装 Promise.all方法
Promise.all = function (values) {
return new Promise((resolve, reject) => {
let result = []; // 存放返回值
let counter = 0; // 计数器,用于判断异步完成
function processData(key, value) {
result[key] = value;
// 每成功一次计数器就会加1,直到所有都成功的时候会与values长度一致,则认定为都成功了,所以能避免异步问题
if (++counter === values.length) {
resolve(result);
}
}
// 遍历 数组中的每一项,判断传入的是否是promise
for (let i = 0; i < values.length; i++) {
let current = values[i];
// 如果是promise则调用获取data值,然后再处理data
if (isPromise(current)) {
current.then(data => {
processData(i, data);
}, reject);
} else {
// 如果不是promise,传入的是普通值,则直接返回
processData(i, current);
}
}
});
}
promise 核心原理解析:
Promise函数参数可以作为输入信息,而后经过Promise的内部处理
// 实例化 Promise
new Promise((resolve, reject)=> {
// 输入
AjaxRequest.post({
url: 'url',
data: {},
sueccess: ()=> {
// resolve
resolve(res)
},
fail: (err)=> {
// reject
reject(err)
}
})
}).then((res)=> {
// res 输出
// ...操作
}).catch((err)=> {
// err 输出
// ...操作
})
内部进行了哪些操作呢?
pending状态下会运行的函数
1、实例化构造函数
// 首先运行,Promise构造函数
function Promise(fn) {
if (typeof this !== 'object') {
throw new TypeError('Promises must be constructed via new');
}
if (typeof fn !== 'function') {
throw new TypeError('Promise constructor\'s argument is not a function');
}
// _deferreds的类型,1是 single,2是 array
this._deferredState = 0;
// 0 - pending
// 1 - fulfilled(resolved)
// 2 - rejected
// 3 - 另一个Promise的状态
this._state = 0;
// promise 执行结果
this._value = null;
// then注册回调数组
this._deferreds = null;
// fn等于noop 即return
if (fn === noop) return;
// 接受Promise回调函数 和 this 作为参数
doResolve(fn, this);
}
Promise构造函数,会初始化属性,fn就是我们传入的函数。
doResolve(fn, this);,this指向它自己,负责执行fn函数。
等下面的then函数和catch函数的回调函数注册完之后,doResolve函数将立即执行
2、then 方法注册回调函数
Promise.prototype.then = function(onFulfilled, onRejected) {
if (this.constructor !== Promise) {
// safeThen函数也是通过调用handle函数,return 新的Promise对象
return safeThen(this, onFulfilled, onRejected);
}
// 生成新的Promise对象
var res = new Promise(noop);
handle(this, new Handler(onFulfilled, onRejected, res));
return res;
};
function safeThen(self, onFulfilled, onRejected) {
return new self.constructor(function (resolve, reject) {
var res = new Promise(noop);
res.then(resolve, reject);
handle(self, new Handler(onFulfilled, onRejected, res));
});
}
// Handler构造函数
// 它的作用是挂载 then中的回调函数 和 一个空的Promise对象
function Handler(onFulfilled, onRejected, promise){
// then中的Fulfilled回调函数
this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
// then中的Rejected回调函数
this.onRejected = typeof onRejected === 'function' ? onRejected : null;
// 保存新的Promise
this.promise = promise;
}
// 保存then注册回调函数,更新回调函数状态
function handle(self, deferred) {
while (self._state === 3) {
self = self._value;
}
if (Promise._onHandle) {
Promise._onHandle(self);
}
// pedding 状态
if (self._state === 0) {
// deferred == new Handler(onFulfilled, onRejected, res)
if (self._deferredState === 0) {
self._deferredState = 1;
// 存储then回调deferred对象
self._deferreds = deferred;
return;
}
if (self._deferredState === 1) {
self._deferredState = 2;
// 存储then回调deferred对象
self._deferreds = [self._deferreds, deferred];
return;
}
// 存储then回调函数对象
self._deferreds.push(deferred);
return;
}
// 只有当进入到非pedding状态,handleResolved才会运行
handleResolved(self, deferred);
}
Handler函数生成一个deffer对象,用于保存then函数中的onFulfilled和onRejected回调.以及返回的新的promise实例。
then方法中的核心函数就是handle函数,它负责接收this和new Handler对象。
若在pedding状态下,handle函数只负责注册回调函数,更新回调函数状态。在非pedding状态下,则会执行handleResolved函数。
3、 catch方法注册回调函数
Promise.prototype['catch'] = function (onRejected) {
return this.then(null, onRejected);
};
// catch方法的回调函数实际是通过then方法来完成保存的。
4、调用doResolve函数执行fn
// 调用doResolve函数
function doResolve(fn, promise) {
var done = false;
// tryCallTwo函数执行 类似于
// (resolve, reject) => {if(err){reject(err);return};resolve(res)}执行;
var res = tryCallTwo(fn, function (value) {
if (done) return;
done = true;
resolve(promise, value);
}, function (reason) {
if (done) return;
done = true;
reject(promise, reason);
});
// fn函数调用失败,手动运行reject函数
if (!done && res === IS_ERROR) {
done = true;
reject(promise, LAST_ERROR);
}
}
doResolve是同步直接调用传入的函数。
其中tryCallTwo函数作用是调用函数fn,它接受三个参数。先执行fn函数,根据结果,再执行resolve函数或reject函数。
在resolve函数或reject函数被调用之前,Promise对象的状态依然是pending。
进入resolve或reject状态时会运行的函数:
- 调用resolve
- 调用finale
- 调用handleResolved函数
调用resolve
若Promise对象的fn函数执行正常,之后就会调用resolve函数
function resolve(self, newValue) {
// 。。。省略
// newValue存在 & (newValue是一个对象 || newValue是一个函数)
if (
newValue &&
(typeof newValue === 'object' || typeof newValue === 'function')
) {
// 获取then函数
var then = getThen(newValue);
// 。。。省略
if (
then === self.then &&
newValue instanceof Promise
) {
// 如果newValue 是一个Promise对象,那么调用finale函数
self._state = 3;
self._value = newValue;
finale(self);
return;
} else if (typeof then === 'function') {
// 如果newValue 是一个函数,就继续调用doResolve函数
doResolve(then.bind(newValue), self);
return;
}
}
// 标记完成,进入结束流程
self._state = 1;
self._value = newValue;
finale(self);
}
确认newValue的值,如果newValue是一个函数,就继续循环调用doResolve函数;
如果newValue 是一个Promise对象,那么就直接调用finale函数。
都不是,则直接调用finale函数。
调用finale函数
function finale(self) {
// 单个回调
if (self._deferredState === 1) {
// 执行handle函数,实际是执行handleResolved
handle(self, self._deferreds);
self._deferreds = null;
}
// 回调数组
if (self._deferredState === 2) {
for (var i = 0; i < self._deferreds.length; i++) {
// 执行handle函数,实际是执行handleResolved
handle(self, self._deferreds[i]);
}
self._deferreds = null;
}
}
finale函数表示进入结束流程,执行handle函数。
同时在上面已经说到,在非pedding状态下,执行handle函数,实际会是执行handleResolved函数。
调用handleResolved函数
var asap = require('asap/raw');
function handleResolved(self, deferred) {
asap(function() {
var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
// 不存在 onFulfilled & onRejected
// deferred.promise 只是一个空的Promise对象
if (cb === null) {
// 1 - fulfilled(resolved)
if (self._state === 1) {
resolve(deferred.promise, self._value);
} else {
reject(deferred.promise, self._value);
}
return;
}
// 执行cb回调函数
var ret = tryCallOne(cb, self._value);
if (ret === IS_ERROR) {
// 错误,报reject
reject(deferred.promise, LAST_ERROR);
} else {
resolve(deferred.promise, ret);
}
});
}
通过异步
asap
调用,若不存在onFulfilled
和onRejected
,直接调用resolve
或reject
。若存在,则tryCallOne
回调的结果,直接调用resolve
或reject
。
Promise 的注册和执行过程
第一道题
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
return new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
});
// 当执行 then 方法时,如果前面的 promise 已经是 resolved 状态,
// 则直接将回调放入微任务队列中
output:
外部promise
外部第一个then
内部promise
内部第一个then
内部第二个then
外部第二个then
外部第一个 new Promise 执行,执行完 resolve ,
所以立即将回调放入微任务队列
。所以此时 外部第一个then 放入微任务外部第一个 then 方法里面 return 一个 Promise,这个 return ,代表 外部的第二个 then 的执行需要等待 return 之后的结果
当然会先执行完内部两个 then 之后,再执行 外部的第二个 then
如果前面的 promise 已经是 resolved 状态,则会立即将回调推入微任务队列(但是执行回调还是要等到所有同步任务都结束后)
如果 then 中的回调返回了一个 promise,那么 then 返回的 promise 会等待这个 promise 被 resolve 后再 resolve
第二道题
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
});
// 比上面的代码少一个return
output:
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
}) .then(() => {
console.log("内部第3个then");
})
})
.then(() => {
console.log("外部第二个then");
})
.then(() => {
console.log("外部第3个then");
})
外部promise
外部第一个then
内部promise
内部第一个then
外部第二个then
内部第二个then
外部第3个then
内部第3个then
事件执行机制: 先注册后执行
*1、 当new Promise 执行碰到resolve 状态确定之后,开始第一个then 的微任务注册。
2、外1then 的回调还没有执行,是pending 状态。
所以外2then 的回调也不会被推入微任务队列也不会执行
。(外部的第二个 then 的注册,需要等待 外部的第一个 then 的同步代码执行完成
)3、在实例化时执行函数,打印 log: 内部promise,然后执行 resolve 函数,接着执行到内部的第一个 then(内1then),由于前面的 promise 已被 resolve,所以将回调放入微任务队列中。
4、内1then 返回的 promise 是 pending 状态 时,内2then和外2 then 不注册不执行。
5、外部1then 执行完,外1then 返回的 promise 的状态由 pending 变为 resolved。
同时遍历之前通过 then 给这个 promise 注册的所有回调,将它们的回调放入微任务队列中,也就是外2then 的回调
外部的第一个 then 的同步操作已经完成了,然后开始注册外部的第二个 then,此时外部的同步任务也都完成了。
同步操作完成之后,那么开始执行微任务:
内部的第一个 then 是优先于外部的第二个 then 的注册,所以会执行完内部的第一个 then 之后;
然后注册内部的第二个 then ;
然后执行外部的第二个 then ;
,然后再执行内部的第二个 then。
第三道
如果调用resolve函数和reject函数时带有参数, 参数会被传递给回调函数,
resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例
const p1 = new Promise(function (resolve, reject) {
// ...
});
const p2 = new Promise(function (resolve, reject) {
// ...
resolve(p1);
})
这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。
如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
const p111 = new Promise(function (resolve, reject) {
// ...
resolve()
}).then(()=>{
console.log('p1 then')
return 11
});
const p12 = new Promise(function (resolve, reject) {
// ...
resolve(p111);
}).then((p3)=>{
console.log('p2 then',p111,p3)
})
// p1 then > p2 then Promise {<resolved>: 11} 11
改写函数成promise 形式:
var fs = require("fs");
function myReadFile(){
return new Promise(function(resolve,reject){
fs.readFile("./index.html",function(err,data){
if (!err) {
resolve(data);
}else{
reject(err);
}
});
});
}
myReadFile().then(function(d){
console.log(d.toString());
}).catch();
红灯3秒亮一次,绿灯1秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用Promise实现)三个亮灯函数已经存在:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
红灯3秒亮一次,绿灯1秒亮一次 ,黄灯2秒亮一次,意思就是3秒执行一次red函数,2秒执行一次green函数,1秒执行一次yellow函数,不断交替重复亮灯,意思就是按照这个顺序一直执行这3个函数,这步可以利用递归来实现。
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
var light = function (timmer, cb) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
cb();
resolve();
}, timmer);
});
};
var step = function () {
Promise.resolve().then(function () {
return light(3000, red);
}).then(function () {
return light(2000, green);
}).then(function () {
return light(1000, yellow);
}).then(function () {
step();
});
}
step();