在JavaScript的世界里,所有的代码都是在单线程中执行的。由于这个“缺陷”,JavaScript中的所有网络操作和浏览器事件都必须异步执行。异步执行可以用回调函数来实现。异步操作会在未来某个时刻触发函数调用。主流的异步处理方案主要包括:回调函数(CallBack)、Promise、Generator函数、async/await。1.回调函数(CallBack)这是异步编程最基本的方法。假设我们有一个用于异步获取数据的getData方法。第一个参数是请求的url地址,第二个参数是回调函数,如下:functiongetData(url,callBack){//模拟发送网络请求setTimeout(()=>{//假设返回的是resdatavarres={url:url,data:Math.random()}//执行回调,将数据作为参数传递callBack(res)},1000)}我们预先设置了一个场景,假设我们要请求server三次,每次请求都依赖于上一次请求的结果,如下:getData('/page/1?param=123',(res1)=>{console.log(res1)getData(`/page/2?param=${res1.data}`,(res2)=>{console.log(res2)getData(`/page/3?param=${res2.data}`,(res3)=>{console.log(res3)})})})从上面代码可以看出,第一次请求的url地址为:/page/1?param=123,返回结果为res1。第二次请求的url地址为:/page/2?param=${res1.data},依赖于第一次请求的res1.data,返回结果为res2`。第三次请求的url地址为:/page/3?param=${res2.data},根据第二次请求的res2.data,返回结果为res3。由于后面的请求依赖于前面请求的结果,所以我们只能把后面的请求写到前面请求的回调函数中,这样就形成了常说的:回调地狱。2.发布/订阅我们假设有一个“信号中心”。当一个任务完成后,它会向信号中心“发布”一个信号,其他任务可以“订阅”(subscribe)到信号中心。知道什么时候可以开始。这称为“发布-订阅模式”,也称为“观察者模式”。这种模式有很多实现。下面是BenAlman的TinyPub/Sub,是jQuery的一个插件,首先f2订阅“信号中心”jQuery“done”信号jQuery.subscribe("done",f2);f1改写如下functionf1(){ setTimeout(function(){ //f1 jQuery.publish("done"); },1000);}jQuery的任务代码.publish("done")表示f1执行完成后,将"done"发布到"信号中心"jQuery"信号,触发f2的执行。另外,f2执行完成后,还可以取消订阅(unsubscribe)jQuery.unsubscribe("done",f2);这种方法本质上类似于“事件监听”,但明显优于后者。因为我们可以通过查看“消息中心”以了解存在多少个信号以及每个信号有多少订阅者。3.PromisePromise是一种异步编程的解决方案,比传统的解决方案——回调函数和事件更合理和强大——所谓Promise只是一个容器,里面装着一个将在未来结束的事件(通常是某个事件的结果)异步操作)。从语法上讲,Promise是一个对象,可以从中获取异步操作的消息。Promise提供了统一的API,各种异步操作可以统一处理。简单的说,它的思想就是每个异步任务返回一个Promise对象,这个对象有一个then方法可以让你指定一个回调函数。现在我们使用Promise重新实现上面的案例。首先,我们需要将异步请求数据的方法封装到Promise函数getDataAsync(url){returnnewPromise((resolve,reject)=>{setTimeout(()=>{varres={url:url),data:Math.random()}resolve(res)},1000)})}那么请求的代码应该这样写getDataAsync('/page/1?param=123').then(res1=>{console.log(res1)returngetDataAsync(`/page/2?param=${res1.data}`)}).then(res2=>{console.log(res2)returngetDataAsync(`/page/3?param=${res2.data}`)}).then(res3=>{console.log(res3)})then方法返回一个新的Promise对象,then方法的链式调用避免了CallBack回调地狱但不是***,例如我们需要添加很多的then语句,每个then仍然需要编写一个回调。如果场景比较复杂,比如后面的每一次请求都依赖于前面所有请求的结果,而不仅仅是前面请求的结果,那就更复杂了。为了做得更好,async/await应运而生,下面看看如何使用async/await来实现4.async/await的getDataAsync方法不变,如下functiongetDataAsync(url){returnnewPromise((resolve,reject)=>{setTimeout(()=>{varres={url:url,data:Math.random()}resolve(res)},1000)})}业务代码如下asyncfunctiongetData(){varres1=awaitgetDataAsync('/page/1?param=123')console.log(res1)varres2=awaitgetDataAsync(`/page/2?param=${res1.data}`)console.log(res2)varres3=awaitgetDataAsync(`/page/2?param=${res2.data}`)console.log(res3)}可以看到使用async\await就像写同步代码一样。和Promise相比感觉如何?是不是很清楚了,但是async/await是基于Promise的,因为使用async修饰的方法最终返回的是一个Promise。其实async/await可以看作是利用Generator函数来处理异步的语法糖。下面我们就来看看如何使用Generator函数来处理异步。returnnewPromise((resolve,reject)=>{setTimeout(()=>{varres={url:url,data:Math.random()}resolve(res)},1000)})}使用Generator函数写函数像这样*getData(){varres1=yieldgetDataAsync('/page/1?param=123')console.log(res1)varres2=yieldgetDataAsync(`/page/2?param=${res1.data}`)console.log(res2)varres3=yieldgetDataAsync(`/page/2?param=${res2.data}`)console.log(res3))}然后我们逐步执行varg=getData()像这样g.next().value.then(res1=>{g.next(res1).value.then(res2=>{g.next(res2).value.then(()=>{g.next()})})})在上面的代码中,我们一步步调用遍历器的next()方法。由于每个next()方法的返回值的value属性都是一个Promise对象,我们为其添加一个then方法,然后在then方法中运行next方法移动遍历器的指针,直到Generator函数完成.事实上,我们不必手动执行此过程。可以封装成一个简单的执行函数run(gen){varg=gen()functionnext(data){varres=g.next(data)if(res.done)returnres.valueres.value.then((data)=>{next(data)})}next()}run方法用于自动运行异步Generator函数,其实就是一个递归过程调用过程,这样我们就不用手动去执行Generator函数了。有了run方法,我们只需要这样运行getData方法run(getData),这样就可以把异步操作封装在Generator函数内部,把run方法作为Generator函数的自执行器来处理异步。其实我们不难发现,async/await方法与Generator处理异步的方式相比有很多相似之处,只是async/await在语义上更加明显,async/await不需要我们写执行人。它的内部已经帮我们封装好了,这也是为什么async/await是Generator函数处理异步的语法糖。
