笔者认为这个问题是所有从后端转前端开发的程序员都会遇到的第一个问题。JS前端编程和后端编程最大的区别就是它的异步机制,这也是它的核心机制。为了更好地说明如何返回异步调用的结果,让我们看一下尝试异步调用的三个示例。示例1:调用后端接口,返回接口返回的内容functionfoo(){varresult$.ajax({url:"...",success:function(response){result=response}});returnresult//return:undefined}函数foo试图调用一个接口并返回它的内容,但每次执行它都只返回undefined。示例2:使用Promise的then方法,同样调用接口并返回内容}及以上示例调用只会返回未定义的。示例3:读取本地文件并返回其内容不出所料,这个例子的调用结果也是undefined。为什么?因为这三个例子中涉及的三个操作——ajax、fetch、readFile都是异步操作,所以从发出操作指令到得到结果是有时间间隔的。无论你的机器多么强大,这个差距都无法完全消除。这是由JS的主线程是单线程决定的。当JS代码执行到某个位置时,就不能等待了。等待意味着用户界面被卡住,这是用户无法忍受的。JS使用异步线程来优化这种场景。当在主线程发起异步操作时,主线程不会阻塞,继续向下执行;当异步操作返回数据时,异步线程会主动通知主线程:“老板您好,数据来了,您现在要用吗?”“好!马上给我。”这样异步线程就把异步代码推送给了主线程,这样异步代码就可以执行了。以上三个例子,result=response就是他们的异步代码。下面笔者画一张图来帮助理解这个机制:当异步线程准备数据时,主线程不能立即处理。可以处理数据。了解了JS的异步机制后,我们来看看如何正确改写前面的三个例子。回调函数:最古老的返回异步结果的方式先看例子1,使用回调函数重写:functionfoo(callback){$.ajax({url:"...",success:function(response){callback(回复)}});//returnresult//Return:undefined}调用函数foo时,提前传入一个回调,ajax操作获取接口数据时,将数据传递给回调,回调自己处理。虽然这种基于回调的方案“巧妙地”解决了问题,但在具有多层异步回调的复杂项目中,往往会因为一个操作依赖多个异步数据而造成“回调噩梦”。ES2015:使用Promise对象和then方法链式调用第二种改进方案。不使用回调函数,而是使用ES2015中新的Promise及其then方法。下面是对第二个例子的改造:.then(function(res){console.log(res)})..catch(function(err){//})foo返回一个Promise对象,注意Promise只是一个可能携带正确数据的容器,它是不是数据。使用时需要调用它的then方法获取数据(当有数据返回时)。与then同时存在的另一个有用的方法是catch,用于捕获异步操作中可能出现的异常。处理可能的错误对于增强鲁棒性至关重要。这个catch方法不能忽略。注:示例中fetch方法的作者并没有给出具体的实现。它在这里被视为返回Promise对象的异步操作。因此,我们可以看到调用这个方法后返回的对象也是可以按下的。接着调用then方法(第3行)。但是这个使用Promises的解决方案是完美的,没有问题吗?很明显不是。ES2017:使用async/await语法关键字,有太多“following”风格的then方法调用和catch方法调用,导致代码逻辑不清晰;我们在阅读这样的代码时,不是瀑布式的从上到下阅读难受,而是时不时跳上跳下的方式来阅读。不仅读起来不舒服,而且写的时候也很难像后端编程那样按照自上而下的简洁逻辑来组织代码。让我们开始使用ES2017标准中提供的async/await语法关键字来重写示例3:functionfoo(){returnnewPromise(function(resolve,reject){fs.readFile("path/to/file",function(err,response){resolve(response)})})}(asyncfunction(){constres=awaitfoo().catch(console.log)console.log(res)})()基于async/await语法的keywordscheme是使用Promise的scheme的升级版,本方案也使用了Promise。第8到11行,这是一个IIFE(立即调用的函数表达式)。之所以将第9~10行的代码用一个只使用一次的临时匿名函数包裹起来,是因为await必须用在用async关键字修饰的函数或方法中,只能在顶层文件中直接使用范围或模块范围。使用这种方案的优化是代码可以像后端编程一样从上往下写,结构可以非常清晰。这也是一种被称为“异步转同步”的JS编程范式,在前端开发中已经被普遍接受。注意,“异步转同步”并没有真正改变异步代码,异步代码仍然是异步代码,它们仍然会先在异步线程中静默执行,等有数据返回后再通知主线程处理。当我们使用这种编程模式时,我们一定不能在主线程上等待一个Promise。我们可以发起异步操作,让异步操作像葡萄一样挂在主线程上,但不能等到它们返回后再进行。jQuery的DeferredObject(延迟对象)先看一个Promise+then方法风格的jQuery代码:$.ajax({url:"test.html",context:document.body}).done(function(){$(this).addClass("完成")});第4行,这里的done方法是jQuery自己实现的,$.ajax方法返回一个DeferredObject(延迟对象)。这个对象上有个done方法,和Promise的then类似。jQuery成名之前,在ES2015标准诞生之前,jQuery的DeferredObject就已经定义好了。Promise本身并没有什么神奇之处。它可以发挥作用。主要依赖于在JS中,Object是一个引用对象,继承自Object原型的Promise也是一个引用对象。当发起异步操作时,只接受一个“空”的Promise。已创建,但保留其引用;当数据返回时,将数据“填充”到这个对象中,这样异步代码就可以通过之前持有的引用来访问对象上携带的数据。Promise的胜利更多的是编程思维的胜利,Promise的成功也是编程思维的成功。一种语言中所有成功的编程思想都可以在其他语言中学习和借鉴。事实上,在后端编程中,这种伪装成同步代码风格的异步编程思想也极为普遍。它们有一个共同的名字叫协程。总结JS处理异步调用的结果,最佳实践是“异步到同步”:使用Promise+async/await语法关键字。这里async总是和await配对,一个async函数总是返回一个Promise,一个await关键字总是试图“解开”一个Promise,最后要么等待有价值的数据,要么async来了async,没有什么没有等待。为了避免出现异常,影响主线程的正常运行,一般需要使用catch来避免异常。版权归LIYI所有,基于CCBY-SA4.0协议
