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