这里是修真院前端小课堂,每篇分享文从

【背景介绍】【知识剖析】【常见问题】【解决方案】【编码实战】【扩展思考】【更多讨论】【参考文献】

八个方面深度解析前端知识/技能,本篇分享的是:

【异步编程有哪几种方法来实现?】

大家好,我是IT修真院武汉分院web第16期的学员孟晨,一枚正直纯洁善良的web程序员 今天给大家分享一下,修真院官网js(职业)任务五,深度思考中的知识点——异步编程有哪几种方法来实现?

 

1.背景介绍
你可能知道,Javascript语言的执行环境是"单线程"(single thread)。
所谓"单线程",就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。

为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

"同步模式"就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;"异步模式"则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。
"异步模式"非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

2.知识剖析
常用的异步编程的几种方法?
首先我们来看一个基本的例子,在这个例子中输出的顺序是1,3,2,我们想让他按顺序1,2,3输出就需要用到异步编程的方法

function fn1() {                                                                                     

    console.log('Function 1')                                                                  

}                                                                                                            

function fn2() {                                                                                     

    setTimeout(() => {                                                                        

        console.log('Function 2')                                                            

    }, 2000)                                                                                       

}                                                                                                           

function fn3() {                                                                                   

    setTimeout(() => {                                                                      

        console.log('Function 3')                                                         

    }, 500)                                                                                          

}                                                                                                       

fn1()                                                                                                 

fn2()                                                                                                 

fn3()                                                                                                 

// output =>                                                                                     

// Function 1                                                                                       

// Function 3                                                                                    

// Function 2                                                                                     

 

壹.回调函数

回调函数是异步编程的方法中最简单也是最常用的一个方法

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。
回调函数的优点是简单、容易理解和部署,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(Coupling),流程会很混乱,而且每个任务只能指定一个回调函数。

如果再嵌套多几层,代码会变得多么难以理解 这个被称之为“回调函数噩梦”(callback hell)!!!

也是可以看看例子,如果按照咱们刚刚的写法的话输出顺序会是3,2,1,所以把每个函数中写成回调函数的形式

就可以让执行完了前面的才会执行后面的,然后标红的部分就是被称为回调函数噩梦的原因,

只是少数嵌套函数的话不明显但多层嵌套代码就会显得很混乱

 

function fn1(f) {

    setTimeout(() => {

        console.log('Function 1')

        f()

    }, 1000)

}

function fn2(f) {

    setTimeout(() => {

        console.log('Function 2')

        f()

    }, 2000)

}

function fn3() {

    setTimeout(() => {

        console.log('Function 3')

    }, 500)

}

fn1(function () {

    fn2(fn3)

})

// output =>

// Function 1

// Function 2

// Function 3

 

贰.事件发布/订阅

发布/订阅模式也是诸多设计模式当中的一种,恰好这种方式可以在es5下相当优雅地处理异步操作。什么是发布/订阅呢?以上一节的例子来说,fn1,fn2,fn3都可以视作一个事件的发布者,只要执行它,就会发布一个事件。这个时候,我们可以通过一个事件的订阅者去批量订阅并处理这些事件,包括它们的先后顺序。下面我们基于上一章节的例子,增加一个消息订阅者的方法(为了简单起见,代码使用了es6的写法):

class AsyncFunArr {

    constructor(...arr) {

        this.funcArr = [...arr]

    }

    next() {

        const fn = this.funcArr.shift()

        if (typeof fn === 'function') fn()

    }

 

    run() {

        this.next()

    }

}

const asyncFunArr = new AsyncFunArr(fn1, fn2, fn3)

function fn1() {

console.log('Function 1')

asyncFunArr.next()

}

function fn2() {

    setTimeout(() => {

        console.log('Function 2')

        asyncFunArr.next()

    }, 500)

}

function fn3() {

    console.log('Function 3')

    asyncFunArr.next()

}

// output =>

// Function 1

// Function 2

// Function 3

 

叁.PROMISE对象

romises对象是CommonJS工作组提出的一种规范,目的是为异步编程提供统一接口。 简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:   f1().then(f2); 这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。 比如,指定多个回调函数:   f1().then(f2).then(f3); 再比如,指定发生错误时的回调函数:   f1().then(f2).fail(f3); 而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。

标红处就是所谓的链式写法,这样写带来了结构清晰的好处

function fn1() {

    return new Promise((resolve, reject) => {

        setTimeout(() => {

            console.log('Function 1')

            resolve()

        }, 1000)

    })

}

function fn2() {

    return new Promise((resolve, reject) => {

        setTimeout(() => {

            console.log('Function 2')

            resolve()

        }, 2000)

    })

}

function fn3() {

    setTimeout(() => {

        console.log('Function 3')

    }, 500)

}

fn1()

.then(fn2)

.then(fn3)

// output =>

// Function 1

// Function 2

// Function 3

肆.GENERATOR

如果说Promise的使用能够化回调为链式,那么generator的办法则可以消灭那一大堆的Promise特征方法,比如一大堆的then()。
generator函数asyncFunArr()接受一个待执行函数列表fn,异步函数将会通过yield来执行。在异步函数内,通过af.next()激活generator函数的下一步操作。
这么粗略的看起来,其实和发布/订阅模式非常相似,都是通过在异步函数内部主动调用方法,告诉订阅者去执行下一步操作。但是这种方式还是不够优雅,比如说如果有多个异步函数,那么这个generator函数肯定得改写,而且在语义化的程度来说也有一点不太直观。

function fn1() {

    setTimeout(() => {

        console.log('Function 1')

    }, 1000)

}

 

function fn2() {

    setTimeout(() => {

        console.log('Function 2')

    }, 2000)

}

 

function fn3() {

    setTimeout(() => {

        console.log('Function 3')

    }, 500)

}

 

function* asyncFunArr(...fn) {

    fn[0]()

    yield fn[1]()

    fn[2]()

}

const af = asyncFunArr(fn1, fn2, fn3)

af.next()

// output =>

// Function 1

// Function 2

// Function 3

伍.优雅的ASYNC/AWAIT

使用最新版本的Node已经可以原生支持async/await写法了,通过各种pollyfill也能在旧的浏览器使用。那么为什么说async/await方法是最优雅的呢?
有没有发现,在定义异步函数fn2的时候,其内容和前文使用Promise的时候一模一样?再看执行函数asyncFunArr(),其执行的方式和使用generator的时候也非常类似。
异步的操作都返回Promise,需要顺序执行时只需要await相应的函数即可,这种方式在语义化方面非常友好,对于代码的维护也很简单——只需要返回Promise并await它就好,无需像generator那般需要自己去维护内部yield的执行。

 

 function fn1() {

            return new Promise((resolve, reject) => {

                setTimeout(() => {

                    console.log('Function 1')

                    resolve()

                }, 3000)

            })

        }

        function fn2() {

            return new Promise((resolve, reject) => {

                setTimeout(() => {

                    console.log('Function 2')

                    resolve()

                }, 2000)

            })

        }

        function fn3() {

            setTimeout(() => {

                console.log('Function 3')

            }, 500)

        }

        async function asyncFunArr() {

            await fn1()

            await fn2()

            await fn3()

        }

        asyncFunArr()

        // output =>

        // Function 1

        // Function 2

        // Function 3

3.常见问题
何时使用异步

4.解决方案
在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。在服务器端,"异步模式"甚至是唯一的模式,因为执行环境是单线程的,如果允许同步执行所有http请求,服务器性能会急剧下降,很快就会失去响应。

5.代码实战
6.拓展思考
异步的好处: 1、异步流程可以立即给调用方返回初步的结果。 
2、异步流程可以延迟给调用方最终的结果数据,在此期间可以做更多额外的工作,例如结果记录等等。 
3、异步流程在执行的过程中,可以释放占用的线程等资源,避免阻塞,等到结果产生再重新获取线程处理。 
4、异步流程可以等多次调用的结果出来后,再统一返回一次结果集合,提高响应效率。

 

 

7.参考文献
谈一谈几种处理JavaScript异步操作的办法
Javascript异步编程的4种方法

8.更多讨论
鸣谢

感谢大家观看

BY : 孟晨


用户bPbdDlb
422 声望36 粉丝