前言我们知道Javascript语言的执行环境是“单线程”的。也就是说,一次只能完成一项任务。如果有多个任务,则必须排队,上一个任务完成,然后执行下一个任务。这种模式虽然实现起来比较简单,执行环境也比较简单,但是只要一个任务耗时长,后面的任务就必须排队,这样会延迟整个程序的执行。常见的浏览器无响应(假死),往往是因为某段Javascript代码运行时间过长(比如死循环),导致整个页面卡在这个地方,无法执行其他任务。为了解决这个问题,Javascript语言将任务执行方式分为同步和异步两种。本文主要介绍异步编程的几种方法,并通过比较,得出异步编程的最佳方案!想阅读更多优质文章,请戳GitHub博客。1.同步与异步我们可以笼统的理解异步就是一个任务被分成了两部分。首先执行第一部分,然后执行其他任务。准备好后,返回Overdo第二段。异步任务背后的代码会立即运行,无需等待异步任务结束,也就是说,异步任务没有“阻塞”的效果。例如,有一个读取文件进行处理的任务。异步执行过程就是下面的不连续执行,称为异步。相应地,被称为同步“异步模式”的持续执行非常重要。在浏览器端,应该异步执行耗时较长的操作,以避免浏览器无响应。最好的例子是Ajax操作。在服务器端,“异步模式”甚至是唯一的模式,因为执行环境是单线程的,如果让所有的http请求都同步执行,服务器性能会急剧下降,很快就会无响应。接下来,我们将介绍异步编程的六种方法。二、回调函数(Callback)回调函数是异步操作最基本的方法。下面的代码是一个回调函数的例子:ajax(url,()=>{//处理逻辑})但是回调函数有一个致命的弱点,就是很容易写出回调地狱(Callbackhell).假设多个请求有依赖关系,你可以写如下代码:ajax(url,()=>{//处理逻辑ajax(url1,()=>{//处理逻辑ajax(url2,()=>{//处理逻辑})})})回调函数的优点是简单,易于理解和实现,缺点是不利于代码的阅读和维护,相互之间耦合度高繁多的部分使得程序结构混乱,流程难以追踪(尤其是嵌套多个回调函数时),而且每个任务只能指定一个回调函数。另外,它不能用trycatch来捕捉错误,不能直接返回。3、在这种事件监听方式下,异步任务的执行不依赖于代码的顺序,而是依赖于事件是否发生。下面是两个函数f1和f2。编程的本意是f2必须等到f1执行完毕后才能执行。首先,给f1绑定一个事件(这里使用的jQuery方法)f1.on('done',f2);上面这行代码的意思是当f1中done事件发生时,f2会被执行。然后,重写f1:functionf1(){setTimeout(function(){//...f1.trigger('done');},1000);}上面代码中,f1.trigger('done')表示即执行完成后立即触发done事件,从而开始执行f2。这种方式的好处是比较容易理解,可以绑定多个事件,可以为每个事件指定多个回调函数,可以“解耦”,有利于模块化。缺点是整个程序必须变成事件驱动,运行过程会变得很不清晰。阅读代码时,很难看清主要流程。4.发布和订阅我们假设有一个“信号中心”。某个任务完成后,它会向信号中心“发布”一个信号,其他任务可以“订阅”(subscribe)到信号中心知道这个信号。我什么时候可以开始自己做。这称为“发布/订阅模式”(publish-subscribepattern),也称为“观察者模式”(observerpattern)。首先,f2将done信号订阅到信号中心jQuery。jQuery.subscribe('完成',f2);然后,f1改写如下:')表示f1执行完成后,向信号中心jQuery发出done信号,从而触发f2的执行。f2执行完后,可以取消订阅(unsubscribe)jQuery.unsubscribe('done',f2);这种方式本质上类似于“事件监听”,但明显优于后者。因为你可以查看“消息中心”,了解存在多少个信号,每个信号有多少订阅者,从而监控程序的运行情况。5.Promise/A+Promise意在成为承诺。在节目中,就是承诺过一段时间我给你一个结果。什么时候使用一段时间?答案是异步操作,异步是指可能需要很长时间才能得到结果,比如网络请求,读取本地文件等---可以理解为成功状态Rejected----可以理解作为一个失败的国家。一旦promise从等待状态变为另一个状态,它就永远不能改变状态。例如,一旦状态变为已解决,就不能再更改。改为Fulfilledletp=newPromise((resolve,reject)=>{reject('reject')resolve('success')//不执行无效代码})p.then(value=>{console.log(value)},reason=>{console.log(reason)//reject})当我们构造Promise时,构造函数里面的代码会立即执行newPromise((resolve,reject)=>{console.log('newPromise')resolve('success')})console.log('end')//newPromise的链式调用=>end2.promise每次调用返回一个新的Promise实例(这就是then可用链式调用的原因)如果then中return是一个result,会把result传递给下一个then中的success回调。如果then发生了异常,就会去下一个then的失败回调。如果then中使用了return,那么return的值会被Promise.resolve()包裹起来(见例1、2)then中不能传入任何参数,不传则传给下一个then(见例1、2)example3)catch会捕获未捕获的异常接下来我们看几个例子://Example1Promise.resolve(1).then(res=>{console.log(res)return2//打包到Promise.resolve(2)}).catch(err=>3).then(res=>控制台.log(res))//示例2Promise.resolve(1).then(x=>x+1).then(x=>{thrownewError('MyError')}).catch(()=>1).then(x=>x+1).then(x=>console.log(x))//2.catch(console.error)//例子3letfs=require('fs')functionread(url){returnnewPromise((resolve,reject)=>{fs.readFile(url,'utf8',(err,data)=>{if(err)reject(err)resolve(data)})})}read('./name.txt').then(function(data){thrownewError()//如果then中有异常,会去下一个then的失败回调})//因为有接下来的then没有失败回调,会继续往下看,如果没有就被catch.then(function(data){console.log('data')}).then()捕获。then(null,function(err){console.log('then',err)//thenerror}).catch(function(err){console.log('error')})Promise不仅可以捕获错误,同时也解决了回调地狱的问题,可以将之前的回调地狱例子重写为如下代码:ajax(url).then(res=>{console.log(res)returnajax(url1)}).then(res=>{console.log(res)returnajax(url2)}).then(res=>console.log(res))它也缺点是Promise不能取消,需要通过回调函数捕获错误。函数执行。从语法上,首先可以理解为Generator函数是一个封装了多个内部状态的状态机。除了状态机,Generator函数也是遍历器对象生成函数。function可以挂起,yield可以挂起,启动next方法,每次返回yield之后的表达式结果。yield表达式本身没有返回值,或者总是返回undefined。next方法可以接受一个参数,该参数将用作前一个yield表达式的返回值。我们先来看一个例子:function*foo(x){lety=2*(yield(x+1))letz=yield(y/3)return(x+y+z)}letit=foo(5)console.log(it.next())//=>{value:6,done:false}console.log(it.next(12))//=>{value:8,done:false}console.log(it.next(13))//=>{value:42,done:true}结果可能与你的想象不符。接下来我们逐行分析代码:首先,Generator函数调用与普通函数不同,它会返回当一个迭代器执行到第一个next时,传入的参数会被忽略,函数暂停在yield(x+1),所以它返回5+1=6。当执行第二个next时,传入的参数12刚好会被当作上一个yield表达式的返回值。如果不传递参数,yield将始终返回undefined。此时令y=212,所以第二个yield等于212/3=8next执行第三个时,传入的参数13会被视为上一个yield表达式的返回值,所以z=13,x=5,y=24,总和等于42。再看一个例子:本地文件有1.txt、2.txt、3.txt三个,内容只有一句话。下一个请求依赖于上一个请求的结果,我想通过Generator函数依次调用三个文件//1.txt文件2.txt//2.txt文件3.txt//3.txtfileendletfs=require('fs')functionread(file){returnnewPromise(function(resolve,reject){fs.readFile(file,'utf8',function(err,data){if(err)reject(err)resolve(data)})})}function*r(){letr1=yieldread('./1.txt')letr2=yieldread(r1)letr3=yieldread(r2)控制台。log(r1)console.log(r2)console.log(r3)}letit=r()let{value,done}=it.next()value.then(function(data){//value是一个承诺console.log(data)//data=>2.txtlet{value,done}=it.next(data)value.then(function(data){console.log(data)//data=>3.txtlet{value,done}=it.next(data)value.then(function(data){console.log(data)//data=>end})})})//2.txt=>3.txt=>结论从上面的例子可以看出,手动迭代Generator函数比较麻烦,实现逻辑也有点绕,但实际开发中通常使用co库。co是用于Node.js和浏览器的基于生成器的进程控制工具。使用Promises,您可以以更优雅的方式编写非阻塞代码。只需要安装co库:npminstallco上面的例子只需要两句就可以轻松实现function*r(){letr1=yieldread('./1.txt')letr2=yieldread(r1)letr3=yieldread(r2)console.log(r1)console.log(r2)console.log(r3)}letco=require('co')co(r()).then(函数(数据){console.log(data)})//2.txt=>3.txt=>end=>undefined我们可以使用Generator函数来解决回调地狱的问题,我们可以将之前回调地狱的例子改写成以下代码:function*fetch(){yieldajax(url,()=>{})yieldajax(url1,()=>{})yieldajax(url2,()=>{})}letit=fetch()letresult1=it.next()letresult2=it.next()letresult3=it.next()7.async/await简介1.Async/Await使用async/await,你可以轻松实现你所做的事情generators和cofunctions之前,它有以下特点:async/await是基于Promise实现的,不能用于普通的回调函数。async/await和Promise一样,是非阻塞的。async/await使异步代码看起来像同步代码,这就是它神奇的地方。如果一个函数添加了async,那么该函数将返回一个Promiseasyncfunctionasync1(){return"1"}console.log(async1())//->Promise{
