Description
先简单说说Promise是什么
在传统的js异步处理中,嵌套回调函数是最常规的做法,比如延迟一秒后执行fn
:
setTimeout(() => {
fn();
}, 1000);
在浏览器环境的js中,可能异步处理场景较少,嵌套回调的弊端还不会十分明显,但是对于node服务器这种I/O密集的场景下,当回调嵌套得足够多时,代码就很恶心了。比如按顺序异步读取4个文件后再调用fn
处理文件数据:
fs.readFile(path, (err, data) => {
fs.readFile(path, (err, data) => {
fs.readFile(path, (err, data) => {
fs.readFile(path, (err, data) => {
fn(data);
});
});
});
});
这种俄罗斯套娃式的写法,当嵌套达到一定数量级时,就会掉进著名的回调地狱(callback hell),此时代码可维护性基本为零。
Promise的出现就是用来解决这个问题的
实现Promise的库有很多,Promise的规范也有很多很多,但是这些都不在这篇文章的讨论范围之内,就不细说了,我们先直接看看怎么用Promise来解决上面读取文件的例子:
var promise = new Promise(function(resolve, reject) {
fs.readFile(path, (err, data) => {
resolve();
});
}).then(() => {
new Promise(function(resolve, reject){
fs.readFile(path, (err, data) => {
resolve();
});
});
})).then(() => {
new Promise(function(resolve, reject){
fs.readFile(path, (err, data) => {
resolve();
});
});
})).then(() => {
new Promise(function(resolve, reject){
fs.readFile(path, (err, data) => {
resolve();
});
});
})).then((data) => {
fn(data);
});
很棒的链式写法,直接将嵌套拆分成链式调用了。
尝试实现一个乞丐版的Promise
标准的Promise
对象其实是十分强大的,在拆分嵌套的同时,还支持异常捕获,参数传递,状态的维护和传递等。所谓的乞丐版就是都把这些功能去了(其实是我渣),单单支持拆分嵌套,所以写起来会比标准Promise要简单点。
先来预览一下完成版的调用方式是怎么样的:
new Promise((next) => {
console.log('1');
setTimeout(() => {
next();
}, 1000);
}).then((next) => {
console.log('2');
setTimeout(() => {
next();
}, 1000);
}).then((next) => {
console.log('3');
setTimeout(() => {
next();
}, 1000);
}).then((next) => {
console.log('4');
});
// 运行结果:
// 输出1
//(等待一秒)输出2
//(等待一秒)输出3
//(等待一秒)输出4
动手实现
首先把Promise对象的大致框架写出来:
class Promise {
constructor() {
}
then() { //先把最明显的then方法写出来
}
}
我们先把最明显的then
方法写到Promise
类里面,之后,我们要考虑Promise
应该有哪些属性。其实很明显的,每一次调用then
方法,都记录了一个即将会执行的任务函数(不一定是异步函数),也就是说我们应该用一个队列来将所有将要处理的事件储存起来:
constructor(fn) {
this.taskQueue = []; //用作储存任务的队列
this.taskQueue.push(fn); //马上将第一个任务压到队列
}
之后我们改写then
方法,将接收到的任务函数压入任务队列:
then(fn) {
this.taskQueue.push(fn); //任务函数fn就是then的参数,将其压进队列
}
然后很自然得,我们需要一个标记函数来标记异步函数在什么时候执行下一个任务,也就是说这个函数就是进入下一个任务函数的入口。结合上面的预览,很明显这个标记函数就是next
函数。
每一次执行next
函数,都会取出任务队列的第一个元素并且执行他(因为任务队列储存的就是函数,这一点一定要理解),同时把next
函数的本身作为参数传进这个任务函数,供这个任务函数调用下一个任务函数调用。这里有点绕,是整个实现的难点,来看代码或许会好理解点:
next() {
this.taskQueue.shift()(this.next.bind(this));
//执行next函数,将taskQueue的第一个元素shift出来执行,并且把next本身作为参数传进去
}
这里传递next
的时候为什么要bind(this)
呢?因为当任务函数执行next
时,函数上下文早已不是Promise
了,是不确定的,bind(this)
是把next
函数的上下文锁定为Promise
对象,保证next
在调用时能访问到Promise
里面的属性和方法(这里同样也是一个小小的难点,当时也坑了我一把)。
然后我们把链式调用支持加上,其实很简单的事情,就是在每次调用then
都返回Promise
自身.我们改写一下then
方法:
then(fn) {
this.taskQueue.push(fn); //任务函数fn就是then的参数,将其压进队列
return this; //返回Promise自身
}
最后,我们将任务队列的的第一个元素出列,异步执行,这也是整个乞丐版Promise
的入口。
setTimeout(() => {
this.taskQueue.shift()(this.next.bind(this)); //从第一个任务开始执行
}, 0);
到此为止,我们的‘乞丐版Promise’就完成了,代码很少,很精悍,但是里面包含了很多的知识点。在要求不是很高的项目里面,用起来应该也挺爽的(滑稽)。当然想要增加更多功能比如异常捕获参数传递也不是不可以,但是难度肯定会更大了。
下面是完整代码:
class Promise {
constructor(fn) {
this.taskQueue = []; //用作储存任务的队列
this.taskQueue.push(fn); //马上将第一个任务压到队列
setTimeout(() => {
this.taskQueue.shift()(this.next.bind(this)); //从第一个任务开始执行
}, 0);
}
next() {
this.taskQueue.shift()(this.next.bind(this));
//执行next函数,将taskQueue的第一个元素shift出来执行,并且把next本身作为参数传进去
}
then(fn) {
this.taskQueue.push(fn); //任务函数fn就是then的参数,将其压进队列
return this; //返回Promise自身
}
}