作为项目的技术负责人,前端并不是我的主业。目前前端团队无论是代码质量还是技术水平都不是很令人满意。我每周合并代码的时候,有时候会扫一眼,有的同学的代码真是让人揪心。目前前端团队都是技术差的初级工程师,做出来的东西也只是能用。他们不会总结经验,形成最佳实践;他们做事随性,总是机械地应付任务。我苦口婆心地劝他们多看书,思考如何把代码写得更好,但收效甚微。既然劝没有用,那我就逼着他们学!我设置的第一个主题是TypeScript,因为他们的代码质量很差,希望在引入TypeScript后代码的健壮性有所提高。前后端分享的频率是每周一次,但是我有点太忙了,所以改成了每周一次。在组织了几次TypeScript学习后,有同学提出分享自己感兴趣的研究课题,我没有反对。最近李老师在研究Promise,所以Promise成为了本周分享的话题。我来自后端。虽然自己也是前端项目的带头人,但是知识盲区还是很多。以前从未使用过承诺,这很荒谬。正好趁着这个机会梳理一下Promise的来龙去脉,才有了这篇文章。异步模型在计算机程序中,网络、IO等操作通常是比较耗时的。为此,计算机先驱们发明了两种不同的编程模型:同步模型,程序在操作发起后进入阻塞状态,等待操作完成;异步模型,操作启动后,程序会先做其他事情,操作完成后再通知程序处理;Javascript是一个单线程程序。为了避免网络和用户操作阻塞程序,只能使用异步编程模型。异步操作发起后,Javascript并不等待操作完成,而是继续做其他事情。异步编程模型通常需要一个回调函数(callbackfunction)来配合。在发起操作之前,注册一个回调函数,操作完成后自动执行回调函数。以浏览器中原始http请求为例://操作完成后要执行的回调函数functiononLoad(event){//输出响应内容console.log(event.target.response)}//创建requestobjectconstrequest=newXMLHttpRequest();//将回调函数注册为事件处理器//操作完成后自动执行request.addEventListener("load",onLoad);//发起操作请求。open("GET","https://cors.fasionchan.com/about.txt");request.send();CallbackHell基于回调函数的编程模型并不直观:因为回调函数会嵌套一层逐层,如果异步操作太多,就会掉入十八层地狱。为了解释回调地狱的由来,我们凭空创造了几个异步操作://将数据转为大写functionupperAsync(data,callback){setTimeout(function(){callback(data.toUpperCase());},1000);}//改变数据反转函数reverseAsync(data,callback){setTimeout(function(){callback(data.split("").reverse().join(""));},1000);}//重复数据几次functionrepeatAsync(data,n,callback){setTimeout(function(){callback(data.repeat(n))},1000);}这些都是简单的数据操作,但是我们暂时当成异步操作,需要一秒钟才能完成。在执行这些异步操作时,除了将数据作为参数传入外,还提供了一个回调函数:constdata="abc";upperAsync(data,function(uppered){console.log(`afterupper:${uppered}`);});数据处理完成后,upperAsync函数会调用回调函数,返回处理结果。因此,一秒钟后,我们将在屏幕上看到输出:afterupper:ABC。这看起来很直观,但如果将几个异步操作链接起来,那就是另一回事了。如果我们想先把数据转成大写,然后倒序,最后重复两次,回调函数就得一层层嵌套:constdata="abc";upperAsync(data,function(uppered){console.log(`afterupper:${uppered}`);reverseAsync(uppered,function(reversed){console.log(`afterreverse:${reversed}`);repeatAsync(reversed,2,function(repeated){console.log(`afterrepeat:${repeated)}`);});});});我们首先发起upperAsync操作,在回调函数中发起reverseAsync;而在reverseAsync的回调函数中,我们还需要发起repeatAsync操作;repeatAsync操作的回调函数将得到最终的处理结果。回调函数层层嵌套,代码缩进越来越深。查看console.log语句,一个比另一个更深。这个例子只串联了三个异步操作,看起来好费力。如果有很多异步操作怎么办?Promise为了解决嵌套回调地狱,前端先驱发明了Promise。Promise是一个特殊的对象,表示异步操作的执行结果(未来):成功或失败。要创建一个Promise对象,我们需要先准备一个执行器。executor是一个函数,接收两个参数:resolve函数,执行成功时调用,将执行结果告诉Promise;reject函数,执行失败时调用,告诉Promise错误原因;通过Promise对象的then方法,当注册成功时要执行的回调函数;失败时的回调函数可以通过catch方法注册。以upperAsync操作为例:constdata="abc";constpromise=newPromise(function(resolve){upperAsync(data,function(uppered){resolve(uppered);})});promise.then(function(uppered){console.log(`afterupper:${uppered}`);});创建一个Promise对象,executor执行upperAsync操作,结果通过resolve告诉Promise;调用Promise对象的then方法注册回调函数,当upperAsync操作完成时执行,并输出操作结果;这个例子乍一看好像把问题复杂化了,代码也繁琐了很多。如果将异步操作预先打包为Promise版本,那就更好了。以upperAsync操作为例,封装到Promise版本upperPromise中:所以我们只需要调用upperPromise就可以得到一个代表未来执行结果的Promise对象。然后我们执行Promise对象的then方法,注册操作成功的回调函数,并输出结果:constdata="abc";upperPromise(data).then(function(uppered){console.log(`afterupper:${uppered}`);});代码看起来干净了很多,但是感觉只是换一种写法?链式调用Promise真正的杀手级特性是链式调用:可以在then方法中发起另一个异步操作,返回Promise对象;在then方法之后,可以再次调用then方法为新的Promise对象注册回调函数;这样,通过Promise链调用,我们可以串联多个异步操作,无需嵌套层:constdata="abc";upperPromise(data).then(function(uppered){console.log(`afterupper:${uppered}`);returnreversePromise(uppered);}).then(function(reversed){console.log(`afterreverse:${reversed}`);returnrepeatPromise(reversed,2);}).then(function(repeated){console.log(`afterrepeat:${repeated}`);});执行upperPromise操作得到一个Promise对象;executethen方法注册回调函数,打印结果并在操作成功时发起reversePromise操作;then回调函数返回reversePromise操作的Promise,第二个then为其注册回调函数;当reversePromise成功完成后,执行第二个then回调,打印结果并发起repeatPromise操作;第三个然后为repeatPromise返回的Promise注册一个回调函数,并打印最终结果;通过链式调用,多个异步操作可以清晰串联,无需层层嵌套回调函数。注意每条console.log语句的缩进是一样的,都属于同一层。这种写法更符合人们的直观感受——程序是从上到下顺序执行的。异步函数后来ES引入了异步函数,彻底解决了这个问题。异步函数是指被async修饰的函数,在该函数内部可以使用await关键字来等待Promise对象的最终结果。当Promise对象被fulfilled时,await语句就可以完成等待并得到结果。我们将上面的例子改成了异步函数版本,看起来舒服多了:asyncfunctionprocess(data){constuppered=awaitupperPromise(data);console.log(`afterupper:${uppered}`);constreversed=awaitreversePromise(上);console.log(`afterreverse:${reversed}`);constrepeated=awaitrepeatPromise(reversed,2);console.log(`afterrepeat:${repeated}`);}constdata="abc";process(data);process函数用async关键字修饰,因此是异步函数。调用upperPromise操作后得到一个Promise对象,然后await等待Promise兑现操作的结果;最后将结果分配给变量uppered。然后,它以相同的方式顺序执行reversePromise和repeatPromise操作,完全忘记了回调函数。除了添加了async和await关键字外,编程模型与其他语言没有什么不同。除了能够等待一个Promise对象之外,await还可以等待另一个异步函数返回一个结果。实际上,异步函数是对Promise对象的重新封装,从语法层面进一步优化了Promise编程模型。我们执行一个异步函数并得到一个Promise对象,这就是为什么可以等待异步函数的原因。这篇文章是我作为后端研究员对Javascript异步编程模型和演进过程的初步认识,从最初的回调函数及其带来的回调地狱,到Promise及其链式调用,再到最终的解决方案——异步函数.发展背景是如此清晰和迷人。在学习一个新事物的时候,我一般不会一开始就啃那些抽象的定义,死记硬背那些繁琐的概念和用法。相反,我会从技术背景入手,看看它是如何解决什么问题的。理清技术设计思路和演进路径后,一切就水到渠成了。既然已经掌握了Promise和异步函数的基础知识,有机会写一下李老师分享的Promise的高级用法和实现原理,敬请期待!
