【引自老贴博文】什么是异步(Asynchrony)根据维基百科上的解释:独立于主控制流而发生的事件称为异步。比如有一段顺序执行的代码voidfunctionmain(){fA();fB();}();fA=>fB是顺序执行的,fA总是在fB的前面执行,他们在一个同步关系。此时使用setTimeout延时fBvoidfunctionmain(){setTimeout(fA,1000);fB();}();此时fA与fB是异步的。main函数只是声明fA会在一秒后执行,并没有立即执行。此时fA的控制流程是独立于main的。JavaScript——天生就是异步的语言因为setTimeout的存在,JavaScript至少从被ECMA标准化的那一刻起就已经支持异步编程了。与其他语言中的sleep不同,setTimeout是异步的——它不会阻止当前程序继续执行。然而,由于Ajax的流行,异步编程才真正起飞。Ajax中的A(Asynchronous)真正指向了异步的概念——这就是IE5、IE6的时代。回调函数——异步编程的痛点异步任务执行后如何通知开发者?回调函数是最简单易想的实现。所以从异步编程诞生的那一刻起,就和回调函数绑在了一起。例如设置超时。该函数将启动一个定时器并在指定的时间后执行指定的函数。比如1秒后输出数字1,代码如下:setTimeout(()=>{console.log(1);},1000);正常使用。如果需求发生变化,需要每秒输出一个数字(当然不用setInterval),JavaScript初学者可能会这样写代码:for(leti=1;i<10;++i){setTimeout(()=>{//Error!console.log(i);},1000);}执行结果是等待1秒后一次性输出所有结果。因为这里的循环是同时启动10个定时器,每个定时器等待1秒。结果当然是1秒后所有定时器同时超时,触发回调函数。解决方法也很简单,在上一个定时器超时后再启动一个定时器即可,代码如下:setTimeout(()=>{console.log(1);setTimeout(()=>{console.log(2);setTimeout(()=>{console.log(3);setTimeout(()=>{console.log(4);setTimeout(()=>{console.log(5);setTimeout(()=>{//...},1000);},1000);},1000)},1000)},1000)},1000);一层层嵌套,结果就是这样一个漏斗形的代码。有人可能会想到新标准中的Promise,可以改写如下:functiontimeout(delay){returnnewPromise(resolve=>{setTimeout(resolve,delay);});}timeout(1000).then(()=>{console.log(1);returntimeout(1000);}).then(()=>{console.log(2);returntimeout(1000);}).then(()=>{console.log(3);returntimeout(1000);}).then(()=>{console.log(4);returntimeout(1000);}).then(()=>{console.log(5);returntimeout(1000);}).then(()=>{//..});漏斗形的代码没有了,但是代码量本身并没有减少多少。承诺不会杀死回调。因为回调函数的存在,所以不能使用循环。如果不能循环,那就只能考虑递归了。解决方法如下:leti=1;functionnext(){console.log(i);if(++i<10){setTimeout(next,1000);}}setTimeout(next,1000);注意,虽然写法是递归的,但是由于下一个函数是浏览器调用的,所以实际上并没有递归函数的调用栈结构。Generator——JavaScript中的半协程许多语言都引入了协程来简化异步编程,而JavaScript也有一个类似的概念,叫做Generator。MDN上的解释:Generator是一个中途退出可以重新进入的函数。它们的函数上下文在每次重入后都会被保留。简而言之,Generator与普通Function***的区别在于Generator本身保留了上次调用时的状态。一个简单的例子:function*gen(){yield1;yield2;return3;}voidfunctionmain(){variter=gen();console.log(iter.next().value);console.log(iter.next()。价值);console.log(iter.next().value);}();代码的执行顺序如下:请求gen,得到一个迭代器iter。注意此时gen的函数体并没有真正执行。调用iter.next()执行gen的函数体。遇到yield1,return1,iter.next()的返回值为{done:false,value:1},输出1,调用iter.next()。gen的执行从最后一个yield停止的地方继续。遇到yield2,return2,iter.next()的返回值为{done:false,value:2},输出2,调用iter.next()。gen的执行从最后一个yield停止的地方继续。遇到return3就返回3,return表示整个函数执行完毕。iter.next()的返回值为{done:true,value:3},调用Generator函数的输出3只会返回一个迭代器,当用户主动调用iter.next()时,Generator函数才是真正的实施。可以使用for...of来遍历一个迭代器,比如for(variofgen()){console.log(i);}输出12,***return3的结果不算。使用Generator的项生成数组也很简单,直接用Array.from(gen())或者直接用[...gen()]生成同样不包含的[1,2]***返回3。生成器是异步的吗?Generator也叫semicoroutine,自然和异步有很大关系。那么生成器是异步的吗?他们也不是。前面说过异步是相对的,比如上面的例子.log(iter.next().value);console.log(iter.next().value);}();我们可以直观的看出gen的方法体和main的方法体是交替执行的,所以可以肯定的说gen相对于main是异步执行的。但是在这个过程中,并没有将整个控制流返回给浏览器,所以gen和main相对于浏览器是同步执行的。使用Generator简化异步代码回到原题:for(leti=0;i<10;++i){setTimeout(()=>{console.log(i);},1000);//等待上面的setTimeoutExecutioncompleted}关键是如何等待前面的setTimeout触发回调,然后再执行下一个循环。如果使用Generator,我们可以考虑在setTimeout之后yieldout(控制流返回给浏览器),然后在setTimeout触发的回调函数中next,将控制流返回给代码,执行下一个循环。letiter;function*run(){for(leti=1;i<10;++i){setTimeout(()=>iter.next(),1000);yield;//等待上面setTimeout完成console.log(i);}}iter=run();iter.next();代码的执行顺序如下:请求运行,得到一个迭代器iter。注意此时run的函数体并没有真正执行。调用iter.next()执行run的函数体。循环开始,i初始化为1,执行setTimeout启动一个定时器,1秒后执行回调函数。遇到yield(即yieldundefined),控制流返回到最新的iter.next()。因为后面没有其他代码,浏览器获得控制权,响应用户事件,执行其他异步代码等。1秒后setTimeout超时,执行回调函数()=>iter.next()。调用迭代器。下一个()。从上次yield出去的地方继续执行,也就是console.log(i),输出i的值。在一个循环结束时,i自增2,然后回到第4步继续执行……这样就实现了类似同步睡眠的需求。async,await——用同步语法写异步代码。毕竟上面的代码需要手动定义iterator变量和手动next;更重要的是,它与setTimeout紧耦合,不能通用。我们知道Promises是异步编程的未来。Promise和Generator可以一起使用吗?这种考虑的结果是异步功能。使用async获取代码如下(1000);console.log(i);}}run();根据Chrome的设计文档,异步函数被编译成Generators执行。run函数本身返回一个Promise,用于让调用函数知道run函数何时执行完毕。所以run()后面也可以跟.then(xxx),甚至直接awaitrun()。注意,有时候我们确实需要多个异步事件并行执行(比如调用两个接口,两个接口都返回后再执行后续代码),那么不要过度使用await,例如:consta=awaitqueryA();//waitqueryA执行后,constb=awaitqueryB();//执行queryBdoSomething(a,b);此时queryA和queryB是串行执行的。可以稍微修改一下:constpromiseA=queryA();//执行queryAconstb=awaitqueryB();//执行queryB,等待其执行结束。这时queryA也在同时执行。consta=awaitpromiseA();//此时queryB已经执行完毕。继续等待queryA执行完doSomething(a,b);我个人更喜欢以下内容:const[a,b]=awaitPromise.all([queryA(),queryB()]);doSomething(a,b);将await与Promise结合使用,效果更佳!结论如今,所有主流浏览器(IE除外)都实现了异步功能。如果你想兼容旧的浏览器,你可以使用babel将其编译成Generator。如果想兼容只支持ES5的浏览器,可以继续将Generator编译成ES5。编译后的代码量比较大,注意代码扩展。如果你用node写Server,那你就不用管了,直接用就行了。koa使用async是你的好帮手。
