phenomLi / Blog

Comments, Thoughts, Conclusions, Ideas, and the progress.
219 stars 17 forks source link

实现一个乞丐版的Promise #9

Open phenomLi opened 6 years ago

phenomLi commented 6 years ago

先简单说说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自身
        }
    }