当前位置: 首页 > 科技观察

说说JavaScript异步编程的历史

时间:2023-03-18 20:57:20 科技观察

前言在早期的web应用中,与后台交互时,需要提交form表单,然后在页面刷新后给用户反馈结果。在页面刷新过程中,后台会返回一段HTML代码。这段HTML中的大部分内容与之前的页面基本相同,必然会造成流量的浪费,同时也会延长页面的响应时间。会让人觉得Web应用的体验不如客户端应用。2004年,AJAX,即“异步JavaScript和XML”技术诞生,它改善了Web应用程序的体验。随后在2006年,jQuery问世,将Web应用的开发体验提升到了一个新的高度。由于JavaScript语言的单线程特性,无论是事件触发还是AJAX,都是通过回调来触发异步任务。如果我们要线性处理多个异步任务,代码中会出现如下情况:name)})})})我们经常将这种代码称为:“回调地狱”。事件和回调众所周知,JavaScript运行时运行在单线程上,并根据事件模型触发异步任务。无需考虑共享内存加锁的问题,绑定的事件会整齐有序的触发。要理解JavaScript的异步任务,首先要理解JavaScript的事件模型。因为是异步任务,我们需要组织一段代码在以后运行(当指定时间结束或者事件触发时),我们通常把这段代码放在一个匿名函数中,通常称为回调功能。setTimeout(function(){//指定时间结束时触发的回调},800)window.addEventListener("resize",function(){//浏览器窗口变化时触发的回调})以后会运行上面说的,回调函数的操作是在未来,这意味着回调中使用的变量在回调声明阶段并不固定。for(vari=0;i<3;i++){setTimeout(function(){console.log("i=",i)},100)}这里连续声明了三个异步任务,会输出变量100毫秒后i的结果,按照正常逻辑,应该输出0、1、2三个结果。然而,事实并非如此。这也是我们刚接触JavaScript时会遇到的问题,因为回调函数的实际执行时间是在未来,所以i的输出值就是循环结束时的值,三者asynchronoustasks结果是一致的,会输出三个i=3。遇到过这个问题的同学一般都知道我们可以通过关闭或者重新声明局部变量来解决这个问题。事件队列事件绑定后,会存储所有的回调函数,然后在运行过程中,由另一个线程调度处理这些异步调用的回调。一旦满足“触发”条件,回调函数就会被放入对应的事件队列(这里简单理解为一个队列,其实有两个事件队列:宏任务和微任务)。一般有以下几种情况满足触发条件:DOM相关操作触发事件,如点击、移动、离焦等行为;IO相关操作,文件读取完成,网络请求结束等;与时间相关的操作,到达定时任务的约定时间;当出现上述行为时,代码中之前指定的回调函数会被放入一个任务队列中,一旦主线程空闲,其中的任务就会按照先进先出的顺序一个一个执行过程。当有新的事件被触发时,会重新进入回调,以此类推🔄,所以JavaScript的这种机制通常被称为“事件循环机制”。for(vari=1;i<=3;i++){constx=isetTimeout(function(){console.log(`第${x}个setTimout被执行`)},100)}可以看出,其运行顺序它满足了队列先进先出的特点,最先执行的语句先执行。线程阻塞由于JavaScript的单线程特性,定时器实际上是不可靠的。当代码遇到阻塞时,即使事件到了触发时间,也会等到主线程空闲后再运行。conststart=Date.now()setTimeout(function(){console.log(`实际等待时间:${Date.now()-start}ms`)},300)//while循环让线程阻塞800mswhile(Date.now()-start<800){}上面代码中,定时器设置为300ms后触发回调函数。如果代码没有被阻塞,正常情况下会在300ms后输出等待时间。但是我们没有加while循环,这个循环会在800ms后结束。主线程已经被这个循环阻塞在这里,导致回调函数在时间到的时候不能正常运行。Promise事件回调的方法在编码过程中特别容易造成回调地狱。而Promise提供了一种更线性的方式来编写异步代码,有点类似于流水线机制。//回调地狱然后(函数(用户){returngetClassID(用户)})。然后(函数(id){returngetClassName(id)})。然后(函数(名称){console.log(名称)})。捕获(函数(错误){console.error('requestexception',err)})Promise在许多语言中都有类似的实现。在JavaScript的发展过程中,著名的框架jQuery和Dojo也有类似的实现。2009年,CommonJS规范推出,基于Dojo.Deffered的实现,提出了Promise/A规范。也是这一年,Node.js诞生了。Node.js的很多实现都是基于CommonJS规范的,比较熟悉的是它的模块化方案。Promise对象在早期的Node.js中也有实现,但是在2010年,Ry(Node.js的作者)认为Promise是一个比较高级的实现,Node.js的开发本来就依赖于V8引擎,V8引擎Promise支持不是原生提供的,所以后来的Node.js模块使用了错误优先的回调风格(cb(error,result))。constfs=require('fs')//第一个参数是Error对象,如果不为空,表示发生了异常fs.readFile('./README.txt',function(err,buffer){if(err!==null){return}console.log(buffer.toString())})这个决定也导致了Node.js中各种Promise库的出现,比较有名的有Q.js和Bluebird.关于Promise的实现,之前写过一篇文章,有兴趣的可以看看:《手把手教你实现 Promise》。在Node.js@8之前,V8原生的Promise实现存在一些性能问题,导致原生Promise的性能甚至不如一些第三方的Promise库。因此,在低版本的Node.js项目中,Promise经常被全局替换:constBulebird=require('bluebird')global.Promise=BulebirdGenerator&coGenerator(generator)是ES6提供的新函数类型。它主要用来定义一个可以自我迭代的函数。可以通过function*的语法构造生成器函数。函数执行后,会返回一个迭代器(iterator)对象。这个对象有一个next()方法。每次调用next()方法时,都会在yield关键字前暂停,直到再次调用next()方法。function*forEach(array){constlen=array.lengthfor(leti=0;i{//第二个next方法传入的值会是第一个yield关键字前面的变量generatorAccept//推回也是一样,第三个next方法的值会在第二个yield之前被变量接受//只会丢弃第一个next方法的值const{value:promise2}=gen.next(user).valuepromise2.then(cId=>{const{value:promise3,done}=gen.next(cId).value//依次传递直到next方法返回的done为true})})我们将上面的逻辑进行抽象,使得每个Promise对象正常返回后,会自动调用next,迭代器会自行执行,直到执行完成(即done为true)。functionco(gen,...args){constg=gen(...args)functionnext(data){const{value:promise,done}=g.next(data)if(done)returnpromisepromise.then(res=>{next(res)//将promise的结果传递给下一个yield})}next()//开始自执行}co(gen,'xxxx-token')这是koa早期核心库的实现逻辑co,但是co进行了一些参数校验和错误处理。在生成器中加入co可以让异步过程更加简单易读,这对于开发者来说绝对是一件开心的事情。async/awaitasync/await可以说是JavaScript异步改造的解决方案。其实本质上就是Generator&co的一个语法糖。只需要在异步生成器函数前加上async,然后将生成器函数中的yield替换为await即可。asyncfunctionfun(token){constuser=awaitgetUser(token)constcId=awaitgetClassID(user)constname=awaitgetClassName(cId)console.log(name)}fun()asyncfunction内置自执行器且不限于Promiseafterawait对象可以是任意值,async/await在语义上比generator的yield更清晰,一眼就能明白这是一个异步操作。