草庐IT

JS如何返回异步调用的结果?

我的技术园地 2023-03-28 原文

这个问题作者认为是所有从后端转向前端开发的程序员,都会遇到的第一问题。JS前端编程与后端编程最大的不同,就是它的异步机制,同时这也是它的核心机制。

为了更好地说明如何返回异步调用的结果,先看三个尝试异步调用的示例吧。

示例一:调用一个后端接口,返回接口返回的内容

function foo() {
  var result
  $.ajax({
    url: "...",
    success: function(response) {
      result = response
    }
  });
  return result // 返回:undefined
}

函数foo尝试调用一个接口并返回其内容,但每次执行都只会返回undefiend。

示例二:使用Promise的then方法,同样是调用接口然后返回内容

function foo() {
  var result
  fetch(url).then(function(response) {
    result = response
  })
  return result // 返回:undefined
}

与上一个示例的调用一样,也只会返回undefined。

示例三:读取本地文件,然后返回其内容

function foo() {
  var result
  fs.readFile("path/to/file", function(err, response) {
    result = response
  })
  return result // 返回:undefined
}

毫无意外这个示例的调用结果也是undefined。

为什么?

因为这三个示例涉及的三个操作————ajax、fetch、readFile都是异步操作,从操作指令发出,到拿到结果,这中间有一个时间间隔。无论你的机器性能多么强劲,这个间隔也无法完全抹掉。这是由JS的主线程是单线程而决定的,JS代码执行到一定位置的时候,它不能等待,等待意味着用户界面的卡顿,这是用户不能容忍的。JS采用异步线程优化该场景,当主线程中有异步操作发起时,主线程不会阻塞,会继续向下执行;当异步操作有数据返回时,异步线程会主动通知主线程:“Hi,老大,数据来了,现在要用吗?”

“好的!马上给我。”

这样异步线程把异步代码推给主线程,异步代码才得以执行。对于上面三个示例而言,result = response就是它们的异步代码。

下面作者画一张辅助理解这种机制吧:

当异步线程准备好数据的时候,主线程也不是马上就能处理,只有当主线程有空闲了,并且前面没有排队等待处理的数据了,新的异步数据才能得以处理。

在了解了JS的异步机制以后,下面看前面三个示例如何正确改写。

回调函数:最古老的异步结果返回方式

先看示例一,使用回调函数改写:

function foo(callback) {
  $.ajax({
    url: "...",
    success: function(response) {
      callback(response)
    }
  });
  // return result // 返回:undefined
}

在调用函数foo的时候,事先传递进来一个callback,当ajax操作取到接口数据的时候,将数据传递给callback,由callback自行处理。

这种基于回调的解决方案,虽然“巧妙”地解决了问题,但在存在多层异步回调的复杂项目中,往往由于一个操作依赖于多个异步数据而造成“回调噩梦”。

ES2015:使用Promise对象与then方法链式调用

第二种改进的方案,不使用回调函数,而是使用ES2015中新增的Promise及其then方法,下面以示例二进行改造:

function foo() {
  return new Promise(function(resolve, reject) {
    fetch(url).then(function(response) {
        resolve(response)
    })
  })
}
foo().then(function(res){
  console.log(res)
})..catch(function(err) {
  //
})

foo返回一个Promise对象,注意,Promise仅是一个可能承载正确数据的容器,它并不是数据。在使用它的,需要调用它的then方法才能取得数据(在有数据返回的时候)。与then同时存在的另一个有用的方法是catch,它用于捕捉异步操作可能出现的异常,处理可能的错误对加强鲁棒性至关重要,这个catch方法不容忽视。

注意:示例中的fetch方法作者没有给出具体实现,它在这里是作为一个返回Promise对象的异步操作被对待的,也因此我们看到了,在这个方法被调用后返回的对象上,也可以紧跟着调用then方法(第3行)。

但是,这种使用Promise的解决方案就完美了吗,就没有问题了吗?显然不是的。

ES2017:使用async/await语法关键字

过多的“紧随”风格的then方法调用及catch方法调用,让代码的前后逻辑不清晰;当我们阅读这样的代码时,并不是从上向下瀑布式阅读的,而是时而上、时而下跳动着阅读的,这很不舒服。不仅阅读时不舒服,编写时也很难以用一种像后端编程那样的从上向下的简洁的逻辑组织代码。

下面开始开始使用ES2017标准中提供async/await语法关键字,对示例三进行改写:

function foo() {
  return new Promise(function(resolve, reject) {
    fs.readFile("path/to/file", function(err, response) {
        resolve(response)
    })
  })
}
(async function(){
  const res = await foo().catch(console.log)
  console.log(res)
})()

基于async/await语法关键字的方案,是使用Promise的方案的升级版,在这个方案中也使用了Promise。第8行第11行,这是一个IIFE(立即调用函数表达式),之所以要用一个只使用一次的临时匿名函数将第9行第10行的代码包裹起来,是因为await必须用在一个被async关键字修饰的函数或方法中,只能直接用到顶层的文件作用域或模块作用域下。

使用这种方案的优化是,代码可以像后端编程那样从上向下写,结构可以很清晰。这也是一种被称为“异步转同步”的JS编程范式,在前端开发中已被普遍接受。

注意,“异步转同步”并没有真正改变异步代码,异步代码仍然是异步代码,它们仍然会在异步线程中先默默地执行,等有数据返回了再通知主线程处理。当我们使用这种编程模式的时候,一定不要在主线程上去await一个Promise,可以发起异步操作,让异步操作像葡萄一样挂在主线程上,但不能等待它们返回了再往下执行。

jQuery的Deferred Object(延迟对象)

先看一段Promise+then方法风格的jQuery代码:

$.ajax({
  url: "test.html",
  context: document.body
}).done(function() {
  $(this).addClass("done")
});

第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,结局要么等到有价值的数据,要么异步出现异步,什么也没有等到。为了避免出现异常,影响主线程的正常运行,一般要用catch规避异常。


著作权归LIYI所有 基于CC BY-SA 4.0协议 原文链接:https://yishulun.com/posts/2022/33.html

有关JS如何返回异步调用的结果?的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  8. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  9. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

    为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

  10. ruby-on-rails - 如何从 format.xml 中删除 <hash></hash> - 2

    我有一个对象has_many应呈现为xml的子对象。这不是问题。我的问题是我创建了一个Hash包含此数据,就像解析器需要它一样。但是rails自动将整个文件包含在.........我需要摆脱type="array"和我该如何处理?我没有在文档中找到任何内容。 最佳答案 我遇到了同样的问题;这是我的XML:我在用这个:entries.to_xml将散列数据转换为XML,但这会将条目的数据包装到中所以我修改了:entries.to_xml(root:"Contacts")但这仍然将转换后的XML包装在“联系人”中,将我的XML代码修改为

随机推荐