浅谈异步发展史(一):回调地狱与promise

989 阅读5分钟

前言

众所周知,js在设计之初就是一门单线程的语言,虽然执行效率不比Java等多线程语言,但其诞生的目的在于实现浏览器与用户的交互,所以单线程足以。但是,由于单线程的特性导致在执行到部分需要消耗一定时长的代码的时候,就会造成实际执行顺序与预期执行顺序不一致的情况,这个时候就需要异步操作了。

正文

回调

在js这门语言诞生任何直接关于异步的操作之前,想要将异步强行捋成同步是一个非常痛苦的过程。什么是异步?先看一段代码

function a() {  
        setTimeout(() => {
            console.log('aaa');
            reject('a is ok')
        }, 1000);
  
}

function b() { 
        setTimeout(() => {
            console.log('bbb');
            resolve('b is ok')
        }, 1500)
    
}

如果说我希望运行的时候,先打印bbb,再打印aaa,该怎么做?先调用b再调用a?

01.png  

 

显然不行。这个时候你可能就会想,那我把b放在a里面执行不就好了?把代码写成这样

function a() {
           setTimeout(() => {
            console.log('aaa');            
        }, 1000);
b()
   }

恭喜你,你已经掌握了十年前处理异步的方法了。但问题是,如果我有十个,二十个,甚至一百个函数需要处理怎么办?全部套进去吗?那万一出了点问题,就好比让你去套了一百层的俄罗斯套娃里面找一个出了问题的娃,简直就是地狱,所以人们把这种情况亲切地称呼为“回调地狱”

Promise

为了拯救程序员日渐锃亮的脑门,js官方在es5版本中推出了promise。还是那两个函数,这次我们不再需要任何套娃的操作了,而是在函数内返回一个promise对象

function a() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('aaa');
            resolve('a is done')//res的值
        }, 1000)
    })
}

function b() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('bbb');
            resolve('b is done')//res的值
        }, 1500)
    })
}


如此一来我们就可以通过调用promise对象自带的then方法去实现同步了

a()
    .then((res) => {
        console.log(res);
        return b()
    }).then(res => {
        console.log(res);
    })

这样一来就优雅多了,倘若还有个c函数需要执行,继续往后面加一个then就OK

a()
    .then((res) => {
        console.log(res);
        return b()
    }).then(res => {
        console.log(res);
    }).then(res => {
       return c()
    })

这种方法既避免了回调地狱的出现,同时还使代码能够更加便于维护,简直不要太舒服。需要说明的是,在then中的回调函数会接受一个形参res,而这个形参是内部函数中resolve出来的值,比如刚刚的代码中的res的打印结果就是

01.png  

除了resolve,还有reject,reject相当于手动给代码报错。就比如将上面a函数中的resolve改成reject,那么得到的就只能是  01.png

而promise对象中也有接受reject出来的值的方法,那就是catch。例如我们把then改为catch去获取a函数中返回的promise对象的reject出来的值

01.png  

手撕promise

当然,在现在的面试中(尤其是对我这种在校实习生的面试)异步几乎是必考题,而面试也肯定不会就只问你“promise怎么用”这种小学生问题,而是更倾向于手撕一个异步出来。

在手撕之前我们要知道两个关键点:第一promise是一个构造函数,所以通常来说更倾向于用class这颗“糖”来写。第二,promise对象是有状态的,前面说的resolve和reject就是通过状态来调整的。 知道这两点,那就话不多说直接开干。

首先自然是有自己的类,我这里取的类名就是MyPromise

class MyPromise{

}

然后就是构造函数,在之前的例子中我们也能看到,promise构造函数接受resolve和reject两个参数,并且之前还提到了promise对象是有状态的,分别是

  • Pending(进行中):初始状态,表示操作还未完成或失败。Promise 对象刚被创建时的初始状态。

  • Fulfilled(也可以叫resolveed)(已完成):操作成功完成。表示异步操作已经成功完成,并且返回了一个值。返回值也就是resolve出来的。

  • Rejected(已失败):操作失败。表示异步操作失败,并且指定了失败的原因。这里的失败原因就是我们reject出来的值

特别注意,这些状态之间是相互转换的:Pending 可以转为 Resolveed或 Rejected,但一旦转变为 Resolveed或 Rejected,就不可再改变状态。

知道这些,我们就能基本上把promise的构造函数写出来了

constructor(excutor) {
        this.state = 'pending'
        // promise的状态
        this.value = undefined
        // resloved的参数
        this.reason = undefined
        // rejected的参数
        this.onResolvedCallbacks = []
        // resolved状态下调用的函数
        this.onRejectedCallbacks = []
        // rejected状态下调用的函数

        const resolve = (value) => {
            if (this.state === 'pending') {
                this.state = 'resolved'
                this.value = value
                this.onResolvedCallbacks.forEach(item => {
                    item(value)
                    // 将所有then接受的回调函数全部执行
                })
            }
        }

        const reject = (reason) => {
            if (this.state === 'pending') {
                this.state = 'rejected'
                this.reason = reason
                this.onRejectedCallbacks.forEach(item => {
                    item(reason)
                    // 将所有then接受的回调函数全部执行
                })
            }

        }

        excutor(resolve, reject)
    }

对于onResolvedCallbacks和onRejectedCallbacks两个数组可能会不太好理解,其实很简单,我们刚刚在then里面只调用了一个b函数,但是实际上可能会在同一个then中调用多个回调函数,所以我们需要将这些函数全部塞到数组里面,然后当resolve方法一调用,立即通过forEach遍历对应的数组并将里面所有函数全部同步执行掉。onRejectedCallbacks也是同样的道理。关键点在于对promise对象的理解。

总结

promise对象自身的构造方法其实非常简单,并不需要考虑太多复杂的东西,真正复杂的部分在promise实例对象的then方法等部分。但由于时间和篇幅限制,接下来的部分会在后续文章中详细写出,祝各位0 waring(s) 0 error(s)。