阿里巴巴前端工程师逸翾对JavaScript中的异步编程进行了详细讲解。JavaScript的特点就是单线程,本文首先对单线程异步的原理进行了解读,接着着重分析了JavaScript异步解决方案,详述了Callback、Promise、Generator、Async/Await的特性和使用原理。
以下是精彩视频内容整理:
单线程异步
JavaScript语言的一大特点就是单线程,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码,也就是说,同一个时间只能做一件事。
常用的异步操作最主要有网络请求,请求会有响应时间,在响应结果回来之前要处理其它事情;IO操作,比如读取某个文件,在此过程中做其它事情;定时函数。
异步是由浏览器的两个或以上常驻线程共同完成,列如异步请求:JS执行线程发起异步请求,浏览器开一条新的http请求来执行请求,当监视到请求已完成,它会把函数插入到JS执行队列的尾部等待处理。
通过如图所示定时函数实例,可以看出异步处理过程。先是打印一个1,然后执行setTimeout函数,约定在0秒后再打印一个2,然后打印一个3,执行结果为先打印1,再打印3,***再打印2,说明整个过程已经发生异步过程。
Javascript有一个主要的执行主线程,但是setTimeout、IO操作、网络异步请求等都会有一个回调函数,这些操作都是通过浏览器API实现的,当定时器时间到了或者请求结果回来时,会将回调函数push到一个javascript任务队列中,当javascript主线程任务执行完毕后,才会从任务队列中继续拿等待代码再执行。具体流程总结如下:
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
异步解决方案
了解了Javascript单线程与异步关系,如何书写日常工作中的工程代码呢?怎样能在异步请求结束后执行特定方法?目前,javascript异步编程经历了几个阶段,且在不断演进。
Callback
最开始是回调函数,当事件完成后执行回调函数,相当于完成异步操作。我们可以像使用变量一样使用函数,作为另一个函数的参数,在另一个函数中作为返回结果,在另一个函数中调用它。当我们作为参数传递一个回调函数给另一个函数时,我们只传递了这个函数的定义,并没有在参数中执行它。当函数拥有了在参数中定义的回调函数后,它可以在任何时候调用(也就是回调)它。
我们在工作中最长接触的就是发送Ajax异步请求,在***个请求发送完后,用它的参数处理第二个请求,再用第二个请求的参数请求第三个,三层嵌套代码如图所示。
如图所示为mongoDB在node.js中的实例,嵌套了六层。当多个异步事务多级依赖时,回调函数会形成多级的嵌套,代码变成金字塔型结构。虽然能解决异步问题,但这使得代码得看难懂,更使得调试、重构的过程充满风险。
Promise
Promise比传统解决方案更加合理和强大,是更加好的异步解决方案,promise对象有以下四个特点:
- promise 可能有三种状态:等待(pending)、已完成(resolved)、已拒绝(rejected)。
- promise 的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换。
- promise对象必须实现then方法,而且then必须返回一个promise ,同一个 promise 的then可以调用多次(链式),并且回调的执行顺序跟它们被定义时的顺序一致。
- then方法接受两个参数,***个参数是成功时的回调,在 promise 由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在 promise 由“等待”态转换到“拒绝”态时调用
如图所示,左侧创建promise对象,隔2000秒后执行,***返回promise对象。Promise有立即执行性,当创建完对象后,在已完成或已拒绝之前代码都会执行一遍。右侧为打印结果,可以看到创建promise对象被打印出来,并成功生成promise对象,当执行then时,此时已经返回成功状态,所以一直执行成功的回调函数。
那么,promise如何解决异步问题的呢?又有哪些特点呢?具体如下:
代码看起来更加符合逻辑、可读性更强。
Promise并没有改变JS异步执行的本质,从写法上甚至还能看到一点点callback的影子。
如图所示为链式调用方式,***个请求成功后,就去执行下一个,如果任何一个请求未成功,直接catch掉error,并打印出来。
Generator
我们希望以同步方式写异步代码,可以使逻辑更加清晰,代码量减少更多。为了实现这种目标,我们又演进出generator解决方案。
Generator函数很特殊,理解起来比promise和callback更加难,从语法上将,generator具备以下特质:
- 定义Generator时,需要使用function*。
- 使用时生成一个Generator对象。
- 执行.next()激活暂停状态,开始执行内部代码,直到遇到yield,返回此时执行的结果,并记住此时执行的上下文,暂停。
- 再次执行.next()时重复第三步。
如图所示,首先定义了test=add(5),函数并没有执行,只是生成了一个generator对象,函数属于暂停状态,只有当执行.next()时,才会激活暂停状态,开始执行内部代码,直到遇到***个yield才会返回执行结果,并记住上下文,暂停,交出控制权。再次执行.next()时,找到第二个yield,再次记住上下文,暂停,交出控制权,依此重复。
如图所示,封装一个异步任务,定义一个generator对象,执行请求后交出控制权,通过promise判断是否要继续执行。在异步事件发生后,首先将控制权交给别人,让程序执行其它代码,当异步事件完成后,将控制权抢回来,继续执行其它操作。Generator需要一个自动执行器配合使用,实现正常思维下的异步处理,有了自动执行器,异步请求可以用同步的方式写异步代码,直接将请求全部写在一起。
通过进一步的演进,async和await函数应运而生。它们的个性特征如下:
- async 表示这是一个async函数,而await只能在这个函数里面使用。
- await 表示在这里等待await后面的操作执行完毕,再执行下一句代码。
- await 后面紧跟着的***是一个耗时的操作或者是一个异步操作。
- await后面必须是一个Promise对象,如果不是会被转化为一个已完成状态的Promise
Async与generator的写法类似,本质上是generator的语法糖。其内置执行器,具备更好的语义,更广的适用性。并且返回值是Promise。