JS中的几种异步编程方式以及promise基本功能的实现

空谷空谷

参考文献

Javascript异步编程的4种方法

阮一峰ES6教程---Promise


相关技术

Promis, class, 面向对象


JS中的同步和异步

  • 同步任务模式

在JS中,因为它的执行时单线程的,也就是说一次只能执行一个任务,当有多个任务需要执行的时候,就需要排队,一个一个的进行执行,就像下面的数组一样,从左到右一个一个的执行:

[任务一,任务二,任务三]

这样的任务执行模式是同步执行的,有可能会因为其中的某一个任务执行时间非常长而阻塞后续任务,导致页面卡死;

  • 异步任务模式

而异步任务模式则解决了上述问题,例如数组中的任务一执行完毕后,执行的不是数组第二位的任务二,而是一个任务二回调函数(需要将任务二做成回调函数放入任务一中),而且任务二可以不等待任务一执行完毕就进行执行,这种模式的任务执行是与任务队列(上述数组)的排列顺序是不一样的,比如下面的代码:

// 任务一
        function fn1() {
            console.log(3)
            setTimeout(function() {
                console.log(1)
            }, 0)
        }
// 任务二
        function fn2() {
            console.log(2)
        }
        fn1()
        fn2()

在该例子中,`setTimeout`函数是异步的,在任务一执行过程中遇到了`setTimeout`,于是将其内的任务操作放到了事件队列的队尾,先去执行任务二,执行完毕后再回过头来执行队尾的任务`console.log(1)`,在此过程中,任务一内的执行没有阻塞任务二的执行

输出结果:



几种异步任务模式

  • 使用setTimeout

JS中的定时器setTimeout是实现异步任务执行最简单也最常见的方式,通过使用setTimeout(callback, 0)可以让执行上一个任务执行时间过长的操作放到下一个任务执行完之后再执行,比如上一小节举的例子,而关于定时器setTimeout和setInterval相关可以参考下面链接:

setTimeout

setInterval

优点:简单明了;

缺点:①、耦合严重;②、容易陷入回调地狱

PS:回调地狱例子(该操作只有三步,如果是十步,嵌套将会非常严重)

        function fn(callback1, callback2) {
            // 耗时操作
            let a = 0
            for (let i = 0; i < 100; i++) {
                a++
            }
            setTimeout(function() {
                callback1(++a)
                setTimeout(function() {
                    callback2(++a)
                }, 0)
            }, 0)
        }

        function fn1(a) {
            console.log(a)
        }

        function fn2(a) {
            console.log(a)
        }
        fn(fn1, fn2)


  • 发布/订阅

发布/订阅模式是通过信号的发送时机来决定什么时候执行其内相应的任务,下面是一个发布订阅模式的代码实现:

        class EventCenter {
            // 定义事件中心
            constructor() {
                this.events = {}
            }

            // 发布器
            on(evt, handler) {
                // 检测事件信号是否存在,当存在时不做操作,不存在时创建给予这个信号的方法存储器(数组)
                this.events[evt] = this.events[evt] || []
                    // 将传入的方法放入数组中
                this.events[evt].push({
                    handler: handler
                })
            }

            // 订阅器
            fire(evt, params) {
                // 检测当前被订阅的信号是否存在,存在则执行其内的所有方法
                if (!this.events[evt]) {
                    return
                }
                for (let i = 0; i < this.events[evt].length; i++) {
                    this.events[evt][i].handler(params)
                }
            }
        }

        let center = new EventCenter()

        center.on('event', function(data) {
            console.log('event执行了第一个任务')
        })

        center.on('event', function(data) {
            console.log('event执行了第二个任务')
        })

        center.fire('event')

输出结果:


  • Promise

Promise是ES6中新增的内置对象,专门用于解决异步相关的问题,其内最重要的两个方法是`then`和`catch`,`then`方法第一个参数是resolve状态时执行的回调,第二个参数则是reject状态时执行的回调,而catch则是`then`中有一环是`reject`就执行的回调函数

通常使用的姿势是这样的

        function getData(){
            let promise = new Promise((resolve, reject) => {
                //  AJAX获取数据。。。。
                if(success){
                    // 成功时执行
                    resolve(fn1)
                }else{
                    // 失败时执行
                    reject(fn2)
                }
            })

            return promise
        }

        getData().then(fn1).catch(fn2)

可以从中发现,除了`then`和`catch`两个方法外,Promise还有链式调用的功能,那么下面就实现这样的一个Promise。

Promise基本功能的实现

  • 首先写一段测试代码,使用Promise的调用方式
        let p = new Promise()

        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.resolve('2')
            }, 1000)
        }

        function f3(result) {
            console.log('f3', result)
            setTimeout(function() {
                p.resolve('3')
            }, 1000)
        }

        function f4(result) {
            console.log('f4', result)
        }

        f1().then(f2).then(f3).catch(f4)
// 或者 f1().then(f2, f4).then(f3, f4)
  • 第一步、我们创建这么一个类,里面有`then`和`catch`两个方法,并且能够链式调用,为了防止重复,这里用小写开头的promise
        class promise {
            constructor(){

            }

            then(success, fail) {
                // 链式调用
                return this
            }

            catch (fail) {
                // 链式调用
                return this
            }
        }
  • 第二步、因为有`resolve`和`reject`两种状态,那么另外再设立两个函数`resolve`和`reject`分别进行不同的状态管理
        class promise {
            constructor() {

            }

            then(success, fail) {
                // 链式调用
                return this
            }

            catch (fail) {
                // 链式调用
                return this
            }

            // 成功状态的管理
            resolve(result) {

            }

            // 失败状态的管理
            reject(result) {

            }
        }
  • 第三步、设定一个数组对需要执行的方法进行暂存,以及一个方法的执行器,该执行器依赖于`resolve`和`reject`传入的状态进行相应的执行
        class promise {
            constructor() {
                this.callbacks = []
            }

            then(success, fail) {
                // 链式调用
                return this
            }

            catch (fail) {
                // 链式调用
                return this
            }

            // 成功状态的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

            // 失败状态的管理
            reject(result) {
                this.actuator('reject', result)
            }

            // 执行器
            actuator(status, result) {

            }
        }
  • 第四步、编写`then`函数与执行器中的逻辑
// 为了方便查看放到了最上面
        let p = new promise()
        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }
// ①、在写then函数之前,先看看最开始Promise的调用方式是怎么样的:f1().then(f2).then(f3).catch(f4),
然后在f1中嵌套回调f2并且返回这个Promise对象。
此外,then函数可以接受两个参数,一个成功回调一个失败回调,
所以思路就是创建一个对象里面有'resolve'和'reject'对应这两个回调然后放入callbacks数组中进行管理;
            then(success, fail) {
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 链式调用
                return this
            }


// ②、这时候在调用f1时他会先返回Promise对象,然后再调用setTimeout里面的resolve回调并传入参数,而在resolve函数中调用了执行器actuator,并且传入了resolve这个状态和在f1中传入的参数;
            // 成功状态的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

// ③、执行actuator函数,其实分析到了这一步就很简单了,不过是将先前传入callbaks中的函数取出来,然后执行其中的成功回调就是了
            actuator(status, result) {
                // 取出之前传入的回调函数对象(包含成功和失败回调),然后执行
                let handlerObj = this.callbacks.shift()
                handlerObj[type](result)
            }

// ④、整体代码

        class promise {
            constructor() {
                this.callbacks = []
            }

            then(success, fail) {
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 链式调用
                return this
            }

            catch (fail) {
                // 链式调用
                return this
            }

            // 成功状态的管理
            resolve(result) {
                this.actuator('resolve', result)
            }

            // 失败状态的管理
            reject(result) {
                this.actuator('reject', result)
            }

            // 执行器
            actuator(status, result) {
                // 取出之前传入的回调函数对象(包含成功和失败回调),然后执行
                let handlerObj = this.callbacks.shift()
                handlerObj[status](result)
            }
        }
  • 其实到了这一步,Promise的基本功能(resolve和reject)已经实现了,下面来看看`f1().then(f2, f4).then(f3, f4).then(f4)`的执行结果吧

①、全部resolve状态执行结果


②、f2为reject时候执行结果

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.reject('2')
            }, 1000)
        }


  • 最后、我们再添加`catch`方法进去

再上述代码的基础上,`catch`方法的实现其实已经变得很简单了,只需要在constructor里设立一个oncatch用以保存传入catch的回调,然后当中间某个环节reject的时候调用这个oncatch方法就好了,全部实现代码以及调用实例:

        class promise {
            constructor() {
                    // 定义回调函数管理器和catch
                    this.callbacks = []
                    this.oncatch
                }
                // 当状态为reject时候,传入reject状态给执行器
            reject(result) {
                    this.actuator('reject', result)
                }
                // 当状态为resolve时候,传入resolve状态给执行器
            resolve(result) {
                    this.actuator('resolve', result)
                }
                // 执行器
            actuator(status, result) {
                // 检测当状态为reject并且oncatch不为空时,执行oncatch保存的失败回调, 适用于f1().then(f2).then(f3).catch(f4)
                if (status === 'reject' && this.catch) {
                    this.callbacks = []
                    this.oncatch(result)
                // 检测当callbacks第一位有方法时,执行相应状态的方法,适用于f1().then(f2, f4).then(f3, f4)
                } else if (this.callbacks[0]) {
                    let handlerObj = this.callbacks.shift()
                    if (handlerObj[status]) {
                        handlerObj[status](result)
                    }
                }
            }

            then(success, fail) {
                // 将传入的成功和失败回调组成对象,放入回调数组中进行管理
                this.callbacks.push({
                    resolve: success,
                    reject: fail
                })
                // 用于链式调用
                return this
            }

            catch (fail) {
                // 保存传入的失败回调
                this.oncatch = fail
                // 用于链式调用
                return this
            }
        }


        let p = new promise()

        function f1() {
            console.log('f1')
            setTimeout(function() {
                p.resolve('1')
            }, 1000)
            return p
        }

        function f2(result) {
            console.log('f2', result)
            setTimeout(function() {
                p.resolve('2')
            }, 1000)
        }

        function f3(result) {
            console.log('f3', result)
            setTimeout(function() {
                p.resolve('3')
            }, 1000)
        }

        function f4(result) {
            console.log('f4', result)
        }
        // 第一种调用
        f1().then(f2).then(f3).catch(f4)
        // 第二种调用
        f1().then(f2, f4).then(f3, f4)


附录,ES7中新增了await async关键字用于实现更简单更直观的异步代码,甚至写出来后和同步执行的代码看起来没什么区别,但在本质上还是使用了Promise,想要了解的同学也可以点击下面的链接一起学习,在此就不再多做介绍了.

体验异步的终极解决方案-ES7的Async/Await


吐槽:知乎编辑器不支持markdown语法,搞得我要一段一段的从简书上拉下来改格式真心痛苦。

顺便推一波我的简书地址:

大春春



文章被以下专栏收录
3 条评论
推荐阅读