1. 前言
之前有一篇文章是关于详细聊事件循环机制的(eventLoop), 本文将深入到promise中,详细研究promise的实现,从根本上解决对promise的一些认识误区。关于promise的面试题也基本是必问考点,如果面试官对promise的认识非常深刻,那么他一定不会只问你promse.all
是做什么的,他一定会结合setTimeout、 promise的各种写法来迷惑你,看你到底是不是从根本上掌握了。
我在面试别人的时候,也经常会问一些promise的问题。 所以,今天跟随我的问题,一起揭开promise的底层实现。要读懂这篇文章,需要对事件循环有一个相对清晰的认识,建议看一下我的这篇文章: # 从setTimeout说到事件循环机制(event-loop)
2.从一道可以当面试题的小题目开始
setTimeout(() => {console.log(1)}, 0);
new Promise((resolve, reject) => {
console.log(2);
resolve('res1');
console.log(3);
}).then(res => console.log(4))
.then(data => console.log(5))
setTimeout(() => {
console.log(6);
Promise.resolve().then(res => console.log(7));
}, 0);
setTimeout(() => {
console.log(8)
})
console.log(9);
大家可以默念一下结果,然后跑一下看看结果。 如果使都对的,那么恭喜你,对promise的基础还算掌握的不错。
那么我们继续, 进入使用Promise的真正场景。
3.Promise的真实使用场景举例
下面再来一个题:
要实现一个小功能,实现第一个方法完成 -> sleep3秒钟 -> 第二个方法执行完成
要实现一个sleep的功能,就说明逻辑是需要有执行顺序的。大家可以尝试实现一下。
目前我们可以使用 async await
或 promise
实现。因为我们这篇文章介绍的使promise,那么就使用promise来实现。
// 创建一个队列
const queue = [];
const fn1 = () => new Promise((resolve, reject) => {
console.log('fn1 executed');
resolve();
});
const sleep = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log('sleep executed');
resolve();
}, 3000)
});
const fn2 = () => new Promise((resolve, reject) => {
console.log('fn2 executed');
resolve();
});
queue.push(fn1, sleep, fn2);
const execute = () => {
// 执行
let queueExe = Promise.resolve();
for(let i=0; i< queue.length; i++) {
queueExe = queueExe.then(queue[i]);
}
}
// 执行
execute();
promise的功能如此强大,不止只处理一个异步请求这么简单。通过上面两道题,其中比较容易令人困惑的是:
- 都说使promise是微任务,但是为什么例子1中的
new Promise()中的console.log会马上执行
? 所说的微任务是体现在哪里? - promise是如何通过resolve等方法做到可以顺序执行的?
所以有必要自己实现一个promise了。
4.代码实现一个Promise
针对上面提的两个问题,要从底层掌握,最好的方法还是实现一遍。 所以我通过写一个promise来帮助大家理解,也帮助自己加深记忆,毕竟时间长了不拿出来熟悉熟悉,也会忘记一些细节。
4.1 核心思想
先把思维框架搭建起来
const MyPromise = (fn) => {
let status = 'pending';
let newValue = null;
this.then = (onFulfilled) => {
// 因为then是可以链式调用的,并且我们也知道then会返回一个新的promise
return new MyPromise((resolve, reject) => {
// ...
})
}
const resolve = (value) => {
}
const reject = (value) => {
}
// 把resolve和reject函数传入
fn(resolve, reject);
}
new MyPromise((resolve, reject) => {
// ...
resolve('step1 done');
}).then((res) => {
// ...
}).then(res => {
// ...
})
目前只是把框架简单的搭了一下,但是这是核心的。 在这里我们会关注到resolve和reject到底是怎么来的,并不局限在只知道调用,也会关注到then()内部是怎么实现的,同样会关注到'pending', 'fulfilled' 的作用是什么等等。
4.2 对框架进行填充
下面开始对立面的逻辑进行填充。为了主体逻辑的尽量明了,会先不去实现reject的逻辑。
function MyPromise(cb) {
const callbacks = [];
let status = 'pending';
let newValue = null;
const handle = (fnObj) => {
// 处理执行问题
if(status === 'pending') {
callbacks.push(fnObj);
return;
}
// 执行
if(!fnObj.onFulfilled) {
resolve(newValue);
return;
}
const res = fnObj.onFulfilled(newValue);
fnObj.resolve(res);
}
this.then = (onFulfilled) => {
// 这里注意此回调的执行环境
return new MyPromise((resolve) => {
handle({
onFulfilled,
resolve
})
})
}
const resolve = (value) => {
const handleCb = () => {
while(callbacks.length) {
let retCb = callbacks.shift();
handle(retCb);
}
}
// 先放到任务队列中,等待下个事件循环时执行
const fn = () => {
if(status != 'pending') return;
status = 'fulfilled';
newValue = value;
// 去callbacks中取callback执行
handleCb();
}
// 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
// 但具体的微任务因为是v8中处理的,所以
// 目前只能先用setTimeout来模拟
setTimeout(fn, 0);
}
cb(resolve);
}
new MyPromise((resolve) => {
console.log(1);
resolve('init');
resolve('new data') // 这里就不会执行了
}).then((res) => {
console.log(res);
}).then((data) => {
console.log(3);
})
这样,就能够做到基本的promise。 其中的两个难点:
- resolve的作用和实现。也就是说,当微任务(例子中使用setTimeout模拟)执行时机到了以后,就从
callbacks
中获取cb执行。 - then的作用。 then中又调用并实例化了
MyPromise
函数,并且把then中的回调当做onFulfilled
函数存储到callbacks中。
这两步其实有些绕,特别是then方法中又调用MyPromise函数。
4.3 更复杂一些的场景
如果看懂了上面的代码,那么我们看一些更复杂的场景。我们要实现reject
和对应的catch
逻辑。
这里其实也对应了一个面试中的坑。当把这一章看完后,相信对reject就有了更深刻的认识。
function MyPromise(cb) {
const callbacks = [];
let status = 'pending';
let newValue = null;
const handle = (fnObj) => {
// 处理执行问题
if(status === 'pending') {
callbacks.push(fnObj);
return;
}
const callback = status === 'fulfilled' ? fnObj.onFulfilled : fnObj.onRejected;
const execute = status === 'fulfilled' ? fnObj.resolve : fnObj.reject;
if(!callback) {
execute(newValue);
return;
}
try{
const result = callback(newValue);
// 把页面代码中的回调函数返回值给到对应的resolve或reject函数
// 如果没有返回值,那就是undefined
execute(result);
} catch(e) {
// 直接进reject
fnObj.reject(e)
}
}
// 在promise API中,then中也是可以通过第二个回调函数捕获reject的
this.then = (onFulfilled, onRejected) => {
// 这里注意此回调的执行环境
return new MyPromise((resolve, reject) => {
handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
// 更多情况用的是catch方法
this.catch = (onReject) => {
// 同样需要返回一个新的promise
// 那么可以直接调用this.then
this.then(null, onReject);
}
const handleCb = () => {
while(callbacks.length) {
let retCb = callbacks.shift();
handle(retCb);
}
}
const resolve = (value) => {
// 先放到任务队列中,等待下个事件循环时执行
const fn = () => {
if(status != 'pending') return;
status = 'fulfilled';
newValue = value;
// 去callbacks中取callback执行
handleCb();
}
// 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
// 但具体的微任务因为是v8中处理的,所以
// 目前只能先用setTimeout来模拟
setTimeout(fn, 0);
}
const reject = (value) => {
// 原理上跟resolve是相同的
const fn = () => {
if(status !== 'pending') return;
status = 'rejected';
newValue = value;
handleCb();
}
setTimeout(fn, 0);
}
cb(resolve, reject);
}
new MyPromise((resolve, reject) => {
console.log(1);
reject('init error');
}).then((res) => {
console.log(res);
}).then((data) => {
console.log(3);
}).catch((e) => {
console.log('捕获到了错误', e);
})
// 结果是:
// 1
// 捕获到了错误 init error
4.4 更更复杂的场景
通过上面的操作,我们已经完成了80%的Promise功能。剩下的是 then
或catch
中,有异步数据获取的场景。 这种场景下,我们需要等待数据获取成功后才继续走下面的then或catch。 继续举个例子:
new MyPromise((resolve, reject) => {
console.log(1);
resolve('https://get/asncyData/api');
}).then((res) => {
let resData = null;
axios.get(res).then(data => {
// 把数据处理好
resData = data;
})
}).then((data) => {
// 这时应该怎么处理,我们才能在这里面获取到上面的真实输出??
console.log(data);
}).catch((e) => {
console.log('捕获到了错误', e);
})
要在第二个then中把axios中的请求结果获取到,那么最好的方法还是需要通过一个Promise
来实现,或者通过async await
来实现。 这里我们既然研究Promise,那就把Promise研究透,用Promise来实现。
new MyPromise((resolve, reject) => {
console.log(1);
resolve('https://get/asncyData/api');
}).then((res) => {
return new MyPromise((resolve, reject) => {
let resData = null;
axios.get(res).then(data => {
// 把数据处理好
resData = data;
resolve(resData);
})
})
}).then((data) => {
// 这时就能获取到上一个then中的数据了
console.log(data);
}).catch((e) => {
console.log('捕获到了错误', e);
})
一般来说,这就是我们遇到的比较复杂的场景了:Promise中套Promise
。
主要思路是: 在then中的回调方法执行时,需要判断一下这个回调是不是MyPromise的实例
, 如果是的话,就等执行完成后,再继续下面的操作。 这其中涉及的逻辑比较烧脑,涉及到递归处理一些事务,建议大家慢慢看,不要一知半解就翻篇。
下面看一下代码:
let flag = 0; // 用作辅助查看之间的关系,当然也可以不使用
function MyPromise(cb) {
debugger;
this.callbacks = [];
let status = 'pending';
let newValue = null;
this.flag = flag++;
const handle = (fnObj) => {
// 处理执行问题
if(status === 'pending') {
this.callbacks.push(fnObj);
return;
}
const callback = status === 'fulfilled' ? fnObj.onFulfilled : fnObj.onRejected;
const execute = status === 'fulfilled' ? fnObj.resolve : fnObj.reject;
if(!callback) {
execute(newValue);
return;
}
try{
const result = callback(newValue);
// 把页面代码中的回调函数返回值给到对应的resolve或reject函数
// 如果没有返回值,那就是undefined
execute(result);
} catch(e) {
// 直接进reject
fnObj.reject(e)
}
}
// 在promise API中,then中也是可以通过第二个回调函数捕获reject的
this.then = (onFulfilled, onRejected) => {
// 这里注意此回调的执行环境
return new MyPromise((resolve, reject) => {
//
handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
// 更多情况用的是catch方法
this.catch = (onReject) => {
// 同样需要返回一个新的promise
// 那么可以直接调用this.then
this.then(null, onReject);
}
const handleCb = () => {
// console.log('callbacks:::', this);
while(this.callbacks.length) {
let retCb = this.callbacks.shift();
handle(retCb);
}
}
const resolve = (value) => {
// 先放到任务队列中,等待下个事件循环时执行
const fn = () => {
if(status != 'pending') return;
// 当执行完上一个context的handle,执行当前context的resolve
if (typeof value === 'object' && value instanceof MyPromise) {
// 需要把promise插入到执行的队列中
const {then} = value;
then.call(value, resolve, reject);
return;
}
status = 'fulfilled';
newValue = value;
// 去callbacks中取callback执行
handleCb();
}
// 这样模拟其实是不合适的,因为设置成了宏任务,其实是微任务
// 但具体的微任务因为是v8中处理的,所以
// 目前只能先用setTimeout来模拟
setTimeout(fn, 0);
}
const reject = (value) => {
// 原理上跟resolve是相同的
const fn = () => {
if(status !== 'pending') return;
status = 'rejected';
newValue = value;
handleCb();
}
setTimeout(fn, 0);
}
cb(resolve, reject);
}
//
new MyPromise((resolve, reject) => {
console.log(1);
resolve('https://get/asncyData/api');
}).then((res) => {
return new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve({code: 0, msg: '模拟ajax获取数据返回的结果'})
}, 2000);
})
}).then((data) => {
// 这时就能获取到上一个then中的数据了
console.log('获取到的数据::', data);
}).catch((e) => {
console.log('捕获到了错误', e);
})
前几步基本上是线性的逻辑,相对比较好理解一些, 最后这种复杂场景,需要细细的理解。 我自己在想最后一部分的时候就绕了挺长时间。
纯ES6版本
/**
* 用class实现一个MyPromise
* 在promise-base的基础上,实现更复杂的功能:当then方法中需要处理另一个Promise时,应该怎么做
*/
const _status = new WeakMap();
const _resolveValue = new WeakMap();
class MyPromise {
callbacks = [];
constructor(cb) {
// 可以使用weakmap定义私有变量
_status.set(this, 'pending');
_resolveValue.set(this, void 0);
// 定义局部变量
const handle = (fnField) => {
const status = _status.get(this);
if(status === 'pending') {
this.callbacks.push(fnField);
return;
}
// 否则就要执行
const callback = status === 'fulfilled' ? fnField.onFulfilled : fnField.onRejected;
const execute = status === 'fulfilled'? fnField.resolve : fnField.reject;
if(!callback) {
execute(_resolveValue.get(this));
return;
}
try {
const result = callback(_resolveValue.get(this));
execute(result);
} catch (error) {
fnField.reject(error);
}
}
const handleCb = () => {
while(this.callbacks.length) {
const targetCb = this.callbacks.shift();
handle(targetCb);
}
}
const resolve = (value) => {
const fn = () => {
// 目的是需要去取出对应的callback并执行
if(_status.get(this) !== 'pending') {
return;
}
// 如果返回的value是个新的promise,那么需要把这个新的promise插入到下一个then中的回调
// 执行前,否则时序上就会出问题
if(typeof value === 'object' && value instanceof MyPromise) {
value.then(resolve, reject);
return;
}
_status.set(this, 'fulfilled');
_resolveValue.set(this, value);
handleCb();
}
setTimeout(fn, 0);
}
const reject = (errorMsg) => {
const fn = () => {
if(_status.get(this) !== 'pending') {
return;
}
if(typeof value === 'object' && value instanceof MyPromise) {
value.then(resolve, reject);
return;
}
_status.set(this, 'rejected');
_resolveValue.set(this, value);
handleCb();
}
setTimeout(fn, 0);
}
// 定义实例方法和属性
this.then = (onFulfilled, onRejected) => {
return new MyPromise((resolve, reject) => {
handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
cb(resolve, reject);
}
}
new MyPromise((resolve, reject) => {
console.log(1);
// 模拟异步ajax操作
setTimeout(() => {
resolve('init promise executed');
}, 1000);
}).then((res)=> {
console.log(2, res);
return new MyPromise((resolve, reject) => {
setTimeout(() => {
// 这时候的resolve是外层then方法中的resolve
resolve('目标promise也执行完了');
}, 2000);
})
}).then((res) => {
console.log(3, res);
})
后记
这篇文章并不是完整的promise实现,还少了一些方法,也不那么健壮。 但从深入分析原理来说,应该是足够使用了。
由于工作比较忙,从8月30号开始开头,到今天9月7号正式写完,用了一周的时间, 有点慢,但是是在工作外时间一个字一个字的敲出来,代码也是从 0到1,从简到难一点一点的实现出来,所以说,自己认为,这是一篇诚意之作,欢迎大家浏览和交流~~