在网站开发中,异步事件是项目中不可避免的一部分,而由于前端框架的兴起,通过框架实现SPA已经是快速建站的标配。获取数据成为不可或缺的一环;本篇文章将讲述JavaScript中的异步处理方法。同步?异步?首先,当然要先了解什么是同步和异步。这两个名词总是让初学者感到困惑。毕竟中文的字面意思很容易被颠倒。从信息科学的角度来看,[同步](https://developer.mozilla.org/en-US/docs/Glossary/Synchronous)是指一次做一件事,而异步则是并行地做很多事情。比如我们去银行办理业务,在窗口前排队就是同步执行,先拿号去做其他事情就是异步执行;通过EventLoop的特性,JavaScript中的异步事件可以说是轻而易举,那么在JavaScript中处理异步事件的方法是什么呢?回调函数是我们最熟悉的回调函数。例如,网页与用户交互时注册的事件监听器需要接收回调函数;也可以通过回调函数在用户需要的时间触发setTimeout、xhr等其他WebAPI的各种功能。先看一个setTimeout的例子://callbackfunctionwithCallback(){console.log('start')setTimeout(()=>{console.log('callbackfunc')},1000)console.log('done')}withCallback()//start//done//callbackfunc在setTimeout之后执行,当指定的时间间隔过去后,回调函数会被放到队列的尾部,然后等待事件循环处理。注意:因为这种机制,开发者为setTimeout设置的时间间隔不会正好等于从执行到触发所经过的时间,使用时要特别注意!虽然回调函数在开发中很常见,但是也有很多无法回避的问题。例如,由于函数需要传递给其他函数,开发者很难控制其他函数中的处理逻辑;并且由于回调函数只能配合try...catch来捕获错误,异步错误发生时很难控制;此外,还有最著名的“回调地狱”。幸运的是,ES6之后出现了Promise,拯救了深陷地狱的开发者。它的基本用法也很简单:).then(()=>console.log('then2'))//promisefunc//then1//then2之前讨论EventLoop时没有提到的是,在HTML5WebAPI标准中,EventLoop增加了Micro任务队列(微任务队列),Promise由微任务队列驱动;栈清空时触发微任务队列,JavaScript引擎会先确认微任务队列中是否有任何东西。它会先被执行,直到清空入栈,才会有新的任务从队列中取出。如上例,当函数返回一个Promise时,JavaScript引擎会将传入的函数放入microtask队列,重复循环,输出上述结果。后面的.then语法会返回一个新的Promise,参数函数会接收到前面Promise.resolve的结果。通过传递此函数参数,开发人员可以在管道中顺序处理异步事件。如果在例子中加上setTimeout,可以更清楚地理解微任务和一般任务的区别:(()=>console.log('then1')).then(()=>setTimeout(()=>console.log('setTimeout'),0)).then(()=>console.log('then2'))//promisefunc//then1//then2->microtask优先执行//setTimeout另外上面提到的回调函数难以处理的异步错误也可以通过.catch语法捕获。functionwithPromise(){returnnewPromise(resolve=>{console.log('promisefunc')resolve()})}withPromise().then(()=>console.log('then1')).then(()=>{thrownewError('error')}).then(()=>console.log('then2')).catch((err)=>console.log('catch:',err))//promisefunc//then1//catch:error//...errorcallstackasyncawaitES6Promise出来后,异步代码逐渐从回调地狱变成了优雅的函数式管道处理,但是对于不熟悉的开发者来说,只是从回调地狱到答应地狱。新的async/await在ES8中被标准化。虽然只是Promise和GeneratorFunction的语法糖,但是通过async/await可以用同步语法来处理异步事件,就像老树开新花一样。写法和Promise完全不同:functionwait(time,fn){returnnewPromise(resolve=>{setTimeout(()=>{console.log('wait:',time)resolve(fn?fn():time)},time)})}awaitwait(500,()=>console.log('bar'))console.log('foo')//wait:500//bar//foo将setTimeout包装到Promise中,然后使用await关键字去调用,可以看到同步执行的结果会先出现bar,然后是foo,就是把异步事件写成开头说的同步处理。再看一个例子:asyncfunctionwithAsyncAwait(){for(leti=0;i<5;i++){awaitwait(i*500,()=>console.log(i))}}awaitwithAsyncAwait()//wait:0//0//wait:500//1//wait:1000//2//wait:1500//3//wait:2000//4代码实现了withAsyncAwait函数,配合for循环反复执行,await关键字等待函数;在这里执行时,循环会在执行下一个循环之前每次依次等待不同的秒数。在使用async/await时,由于await关键字只能在async函数中执行,所以一定要记得同时使用。另外,在使用循环处理异步事件时,需要注意的是ES6之后提供的很多Array方法都不支持async/await语法。如果用forEach代替for,结果会变成同步执行,每0.5秒打印一次数字。总结本文简要介绍了JavaScript处理异步的三种方式,并通过一些简单的例子说明了代码的执行顺序;呼应上面提到的事件循环,在其中加入了微任务队列的概念。希望能帮助大家理解同步和异步的应用。
