Promise作为JS中异步编程最为强大的解决方案之一,ES6将其写进了语言标准,统一了用法。
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
- 下面我们自己实现一个Promise:
Promis的手写实现我们用到用到ES6中的Class类,当然用构造函数也是一样的,基本骨架如下:
class ZhouPromise {
constructor(executor) {
const resolve = (res) => {
console.log(res);
};
const reject = (err) => {};
executor && executor(resolve, reject);
}
}
const promise = new ZhouPromise((resolve, reject) => {
resolve('zhoubaba');
})
当我们new ZhouPromise的时候,执行类的constructor构造器,接收一个函数执行体,同时并且 执行将自定义的resolve,reject传递进去当做resolve,reject的参数变量接收。
- resolve作用是将Promise对象状态由“未完成”变为“成功”,也就是Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
const PROMISE_STATUS_PENDING = "pending";
const PROMISE_STATUS_FULFILLED = "fulfilled";
const PROMISE_STATUS_REJECTED = "rejected";
constructor构造器内部状态,默认为pending
this.status = PROMISE_STATUS_PENDING;
this.value = undefined;
this.reason = undefined;
改变resolve和rejected,当执行的时候修改status状态
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING)
{
this.status = PROMISE_STATUS_FULFILLED;
this.value = value;
console.log("status", this.status, "val ue", this.value);
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED;
this.reason = reason;
console.log("status", this.status, "reason", this.reason);
}
}
const p = new ZhouPromise((resolve, reject) => {
resolve("fulfilled状态"); // status fulfilled value fulfilled状态
//reject("rejected状态"); // status rejectedvalue rejected状态
});
then方法的实现:
- Promise实例生成后,可用then方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
- Promise对象状态改为Resolved时调用 (必选)
- Promise对象状态改为Rejected时调用 (可选)
简单实现:
constructor(executor) {
...
this.onFulfilled = undefined; // !新增
this.onRejected = undefined; // !新增
const resolve = (value) => {
...
this.onFulfilled(this.value); // !新增
}
};
const reject = (reason) => {
...
this.onRejected(this.reason); // !新增
}
};
}
then(onFulfilled, onRejected) { // !新增
this.onFulfilled = onFulfilled;
this.onRejected = onRejected;
}
...
p.then(
(res) => {
console.log(res);
},
(err) => {
console.log(err);
}
);
如上代码所示(部分重复代码用…展示),constructor添加两个onFulfilled,onRejected用来存储then函数的回调。定义类的then方法,接收两个回调,同时赋值。
会发现resolve执行后会报错,那是因为new promise里的回调函数是同步执行的,当resolve的时候,p.then还没有执行,所以onFulfilled 为undefined。我们需要给resolve内部添加setTimeout或者queueMicrotask异步任务。异步任务不会阻塞代码,会进入到宏任务中。等到同步代码p.then执行完后再去执行resolve。
所以如下修改resolve和reject函数:
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDI NG) {
this.status = PROMISE_STATUS_FULFILLED;
queueMicrotask(() => { // !新增
this.value = value;
this.onFulfilled(this.value);
});
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED;
queueMicrotask(() => { // !新增
this.reason = reason;
this.onRejected(this.reason);
});
}
};
至此,一个then回调完成了。
这就是promise简单的基本手写思路。在面试中写到这里已经基本差不多了。但是可以更加完全接近promise的所有功能。
优化 .then
原生Promise的then方法可以被多次调用,这有点像Vue内的响应式收集。当我们调用多个then方法,会被收集到桶里面。
例如:
p.then(
(res) => {
console.log("res1", res);
},
(err) => {
console.log("err1", err);
}
);
p.then(
(res) => {
console.log("res2", res);
},
(err) => {
console.log("err2", err);
}
);
setTimeout(() => {
p.then(
(res3) => {
console.log(res3 + "res3");
},
(err3) => {
console.log(err3 + "err3");
}
);
}, 1000);
所以收集then回调的得是个可迭代的对象或者数组,在这里用数组代替。在resolve或者reject执行时对收集的数组内遍历函数。
this.onFulfilled = [];//修改
this.onRejected = [];//修改
----------------------------
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
//this.status = PROMISE_STATUS_FULFILLED; // !变动到微任务内
queueMicrotask(() => {
this.status = PROMISE_STATUS_FULFILLED;
this.value = value;
this.onFulfilled.forEach((fn) => { // !修改
fn && fn(this.value);
});
});
}
};
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
//this.status = PROMISE_STATUS_REJECTED; // !变动到微任务内
queueMicrotask(() => {
this.status = PROMISE_STATUS_REJECTED;
this.reason = reason;
this.onRejected.forEach((fn) => { // !修改
fn && fn(this.reason);
});
});
}
};
同时then方法内也要修改,防止在全局异步下添加的then函数没有被执行,如果当前状态已经被修改了,说明queueMicrotask已经执行了,是全局setTimeout添加的then方法,就直接执行函数;如果仍然是pending,说明仍然在收集then回调。
then(onFulfilled, onRejected) {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {//新增
onFulfilled(this.value);
return
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {//新增
onRejected(this.reason);
return
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(onFulfilled);
this.onRejected.push(onRejected);
}
这里特别注意一点: resolve和reject函数内的queueMicrotask微任务前的this.status状态改变需要移入queueMicrotask内部判断;不然会出现如下undefined效果:
原因是在新增的then方法内做了状态判断,在改变状态后执行为微任务队列,p.then方法会直接执行当前回调,所以this.value是undefined,我们需要将状态改变放到微任务队列中。
- 如何实现then的链式调用?我们知道then会返回一个promise对象,所以then需要return一个出去
then(onFulfilled, onRejected) {
return new MjcPromise((resolve, reject) => {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
onFulfilled(this.value);
return;
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
onRejected(this.reason);
return;
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(onFulfilled);
this.onRejected.push(onRejected);
}
});
}
最麻烦的问题来了,当前return 出去的promise是需要resolve或者reject的,而我们必须得拿到上一次结果的返回值,在新promise内resolve()出去。我们需要改变this.onFulfilled//onRejected的push对象,拿到返回值在这里resolve。
then(onFulfilled, onRejected) {
return new ZhouPromise((resolve, reject) => {
...
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilled.push(() => {
let value = onFulfilled(this.value);
resolve(value);
});
this.onRejected.push(() => {
let value = onRejected(this.reason);
resolve(value);
});
}
});
}
当用箭头函数的作用可以通过this的不绑定原则能拿到实例上的值。
p.then(
(res) => {
console.log("res1", res);
return "mjc";
},
(err) => {
console.log("err1", err);
return "mjcerr";
}
).then(
(res) => {
console.log("res2", res);
},
(err) => {
console.log("err2", err);
}
);
一开始这种处理确实难理解,因为没看懂this和resolve代表哪个promise。我们只要清楚this指向就知道如何执行的。如图:
我们在promise内传递的函数是箭头函数,那就意味着this指向上下文,也就是说this是指向上一个promise实例,而resolve的调用是处理当前new ZhouPromise对象的状态。
至此已经基本实现链式调用了。事实上剩下的是错误不断处理,判断类型正确,因为篇幅有限不展开了。 以上部分思路参考promiseA+以及coderwhy大神技术参考。
本篇文章内部分示例引用:
作者:麦当当
链接:https://juejin.cn/post/7102703565566640136
来源:稀土掘金
感谢阅读,有什么问题可以在评论区或后台留言讨论~
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。