【译】异步JavaScript的演变史:从回调到Promises再到Async/Await
我最喜欢的网站之一是BerkshireHathaway.com--它简单,有效,并且自1997年推出以来一直正常运行。更值得注意的是,在过去的20年中,这个网站很有可能从未出现过错误。为什么?因为它都是静态的。它自20多年前推出以来几乎一样。如果你预先拥有所有数据,那么构建网站非常简单。不幸的是,现在大多数网站都没有。为了弥补这一点,我们发明了“模式”来处理为我们的应用程序提取外部数据。像大多数事情一样,这些模式都随着时间的推移而发生变化。在这篇文章中,我们将分析这三种最常见的模式的优缺点,模式分别是回调(Callbacks),Promises,和Async/Await 并从历史背景谈论它们的意义和进展。
让我们从这些数据获取的最初的模式开始,回调(Callbacks)
回调(Callbacks)
我假设你完全不知道什么是回调。如果我假设错了,只需向下滚动一下跳过。
当我第一次学习编程时,它帮助我将函数理解为机器。这些机器可以做任何你想要的东西。他们甚至可以接受输入并返回一个值。每台机器上都有一个按钮,你可以在需要机器运行时按下该按钮,即()。
function add (x, y) {
return x + y
}
add(2,3) // 5 - 按下按钮,执行机器
无论我按下按钮,你按下按钮,或者别人按下按钮无所谓。无论何时按下按钮,机器都将运行。
function add (x, y) {
return x + y
}
const me = add
const you = add
const someoneElse = add
me(2,3) // 5 - Press the button, run the machine.
you(2,3) // 5 - Press the button, run the machine.
someoneElse(2,3) // 5 - Press the button, run the machine.
在上面的代码,我们分配add函数,三个不同的变量,me,you,和someoneElse。重要的是要注意add我们创建的原始变量和每个变量都指向内存中的相同位置。它们在不同的名称下完全相同。所以,当我们调用me时you,或者someoneElse,就好像我们正在调用一样add函数。
现在如果我们把add机器送到另一台机器怎么办?请记住,按下()按钮并不重要,如果按下它,它就会运行。
function add (x, y) {
return x + y
}
function addFive (x, addReference) {
return addReference(x, 5) // 15 - Press the button, run the machine.
}
addFive(10, add) // 15
你的大脑可能在这一点上有点奇怪,但这里没有新的东西。我们不是“按下按钮” add,而是add作为参数传递addFive,重命名它addReference,然后我们“按下按钮”或调用它。
这突出了JavaScript语言的一些重要概念。首先,正如你可以将字符串或数字作为参数传递给函数一样,你也可以将函数的引用作为参数传递。当执行此操作时,作为参数传递的函数称为回调函数,并且将回调函数传递给的函数称为高阶函数。
因为词汇很重要,所以这里的代码与重新命名的变量相同,以匹配他们演示的概念。
function add (x,y) {
return x + y
}
function higherOrderFunction (x, callback) {
return callback(x, 5)
}
higherOrderFunction(10, add)
这种模式应该看起来很熟悉,无处不在。如果你曾经使用过任何JavaScript Array方法,那么你已经使用了回调。如果你曾经使用过lodash,那么你已经使用过回调。如果你曾经使用过jQuery,那么你已经使用了回调。
[1,2,3].map((i) => i + 5)
_.filter([1,2,3,4], (n) => n % 2 === 0 );
$('#btn').on('click', () =>
console.log('Callbacks are everywhere')
)
通常,回调有两种常见的用例。第一,我们看下.map和_.filter例子,是翻转一个值到另一个很好的抽象。我们说“嘿,这是一个数组和一个函数。来吧,根据我给你的函数给我一个新的值“。第二个,也就是我们在jQuery示例中看到的,是将函数的执行延迟到特定时间。“嘿,这是这个函数。每当btn点击具有id的元素时,请继续调用它。“这是我们将关注的第二个用例,”延迟执行函数直到特定时间“。
现在我们只看了同步的例子。正如我们在本文开头所讨论的那样,我们构建的大多数应用程序都没有预先获得所需的所有数据。相反,他们需要在用户与应用程序交互时获取外部数据。我们刚刚看到回调如何成为一个很好的用例,因为它们再次允许你“延迟执行函数直到特定时间”。看看我们如何使该句子适应数据提取并不需要太多想象力。我们可以延迟函数的执行,直到我们获得所需的数据,而不是将函数的执行延迟到特定时间。这可能是最流行的例子,jQuery的方法:getJSON。
// updateUI and showError are irrelevant.
// Pretend they do what they sound like.
const id = 'tylermcginnis'
$.getJSON({
url: `https://api.github.com/users/${id}`,
success: updateUI,
error: showError,
})
在获得用户数据之前,我们无法更新应用的UI。那么我们该怎么办?我们说,“嘿,这是一个对象。如果请求成功,请继续调用success并传递用户的数据。如果没有,请继续调用error并传递错误对象。你不需要担心每种方法的作用,只要确保在你应该的时候调用它们。这是使用异步请求回调的完美演示。
在这一点上,我们已经了解了回调是什么以及它们如何在同步和异步代码中都有用处的。我们还没有谈到的是回调的黑暗面。请看下面的代码。你能说出发生了什么吗?
// updateUI, showError, and getLocationURL are irrelevant.
// Pretend they do what they sound like.
const id = 'tylermcginnis'
$("#btn").on("click", () => {
$.getJSON({
url: `https://api.github.com/users/${id}`,
success: (user) => {
$.getJSON({
url: getLocationURL(user.location.split(',')),
success (weather) {
updateUI({
user,
weather: weather.query.results
})
},
error: showError,
})
},
error: showError
})
})
如果觉得有帮助,你可以在这里玩实时版本。
请注意,我们添加了一些回调层。首先,我们说在btn点击具有id的元素之前不要运行初始的AJAX请求。单击按钮后,我们会发出第一个请求。如果该请求成功,我们会发出第二个请求。如果该请求成功,我们将调用updateUI从两个请求获得的数据的方法。无论你是否乍一看是否理解了代码,客观地说它比以前的代码更难阅读。这将我们带到“回调地狱”的主题。
作为人类,我们很自然地会顺序思考。当你在嵌套回调中嵌套回调时,它会强迫你超出你自然的思维方式。当你的软件阅读方式与自然思考方式之间存在脱节时,就会发生错误。
像大多数软件问题的解决方案一样,一种使“回调地狱”更容易消费的常用方法是模块化你的代码。
function getUser(id, onSuccess, onFailure) {
$.getJSON({
url: `https://api.github.com/users/${id}`,
success: onSuccess,
error: onFailure
})
}
function getWeather(user, onSuccess, onFailure) {
$.getJSON({
url: getLocationURL(user.location.split(',')),
success: onSuccess,
error: onFailure,
})
}
$("#btn").on("click", () => {
getUser("tylermcginnis", (user) => {
getWeather(user, (weather) => {
updateUI({
user,
weather: weather.query.results
})
}, showError)
}, showError)
})
如果觉得有帮助,你可以在这里玩实时版本。
好的,函数名称可以帮助我们更加了解正在发生的事情,但客观上是“更好”吗?并不是很多。我们只是在回调地狱的可读性问题上加了一个创可贴。问题仍然存在,我们自然地按顺序思考,即使有额外的功能,嵌套的回调也会使我们摆脱顺序的思维方式。
下一期回调与控制反转有关。当你编写一个回调时,假设你给回调的程序是负责的,并且会在它应该的时候(并且只有当它)时调用它。实际上是将程序控制权转换为另一个程序。当您处理jQuery,lodash甚至vanilla JavaScript等库时,可以安全地假设使用正确的参数在正确的时间调用回调函数。但是,对于许多第三方库,回调函数是您与它们交互方式的接口。第三方库无论是故意的还是偶然的,都可以打破他们与你的回调互动的方式,这是完全合情合理的。
function criticalFunction () {
// It's critical that this function
// gets called and with the correct
// arguments.
}
thirdPartyLib(criticalFunction)
既然你不是那个调用者criticalFunction,你就可以控制调用它的时间和参数。大多数时候这不是问题,但是当它出现问题时,这是一个很大的问题。
Promises
你有没有预订去过一个繁忙的餐馆?当这种情况发生时,餐厅需要一种方法在桌子打开时与你联系。从历史上看,当你的桌子准备就绪时,他们只会取你的名字并大喊大叫。然后,自然而然地,他们决定开始变幻想。一个解决方案是,一旦桌子打开,他们就会取你的号码并给你发短信,而不是取你的名字。这使您可以超出大喊大叫的范围,但更重要的是,它允许他们随时根据需要定位你的手机广告。听起来有点熟?这应该!好吧,也许不应该。这是回调的隐喻!将你的号码提供给餐馆就像给第三方服务提供回拨功能一样。你希望餐厅在桌子打开时给您发短信,就像你一样期望第三方服务在何时以及如何表达时调用你的功能。一旦你的号码或回叫功能掌握在他们手中,您就失去了所有控制权。
值得庆幸的是,存在另一种解决方案。一个设计,允许您保持所有控制。你甚至可能以前都经历过 - 这是他们给你的小嗡嗡声。你知道,这个。
如果你之前从未使用过,那么这个想法很简单。他们没有取你的名字或号码,而是给你这个设备。当设备开始嗡嗡作响并发光时,你的桌子就准备好了。当你等待桌子打开时,你仍然可以做任何你想做的事,但现在你不必放弃任何东西。事实上,恰恰相反。他们必须给你一些东西。没有控制倒置。
蜂鸣器始终处于三种不同状态中的一种- pending,fulfilled或rejected。
pending是默认的初始状态。当他们给你蜂鸣器时,它处于这种状态。
fulfilled 当蜂鸣器闪烁并且你的桌子准备就绪时蜂鸣器所在的状态。
rejected当出现问题时,蜂鸣器处于状态。也许餐厅即将关闭,或者他们忘了有人在晚上出租餐厅。
同样,要记住的重要一点是,你,蜂鸣器的接收器,拥有所有的控制权。如果蜂鸣器进入fulfilled,你可以去你的桌子。如果它被放入fulfilled并且你想忽略它,那么很酷,你也可以这样做。如果它被放入rejected,那很糟糕,但你可以去别的地方吃。如果没有任何事情发生并且它留在pending,你永远不会吃,但你实际上并没有任何东西。
现在你已成为餐厅蜂鸣器的主人,让我们将这些知识应用到重要的事情上。
如果给餐厅你的号码就像给他们一个回调功能,接收这个小小的东西就像收到所谓的“Promise”。
一如既往,让我们从为什么开始吧。为什么Promises存在?它们的存在使得使异步请求更易于管理的复杂性。完全像蜂鸣器,一个 Promise可以处于三种状态之一pending,fulfilled或者rejected。与蜂鸣器不同,它们代表表示餐馆桌子状态的这些状态,它们代表异步请求的状态。
如果异步请求仍在进行中,则Promise状态为pending。如果异步请求成功完成,则Promise状态将更改为fulfilled。如果异步请求失败,Promise则将更改为状态rejected。蜂鸣器比喻很有意义,对吗?
既然你已经理解了Promise存在的原因以及它们可以存在的不同状态,那么我们还需要回答三个问题。
1、如何创造一个Promise?
2、如何改变Prommise的状态?
3、当Promise的状态发生变化时,如何监听?
如何创造一个Promise?
这个很直接。创建一个new实例Promise。
const promise = new Promise()
如何改变Prommise的状态?
该Promise构造函数接受一个参数,一个(回调)函数。这个函数将传递两个参数,resolve和reject。
resolve - 一个允许你更改Promise状态的功能 fulfilled
reject- 一个允许你更改Promise状态的功能rejected。
在下面的代码中,我们使用setTimeout等待2秒然后调用resolve。这将改变Promise的状态fulfilled。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve() // Change status to 'fulfilled'
}, 2000)
})
我们可以通过在创建它之后立即记录promise来看到这种变化,然后resolve在调用之后大约2秒后再次记录。
注意Promise从pending到resolved。
当Promise的状态发生变化时,如何监听?
在我看来,这是最重要的问题。很酷我们知道如何创建Promise并改变其状态,但如果我们在状态发生变化后不知道如何做任何事情,那就毫无价值。
我们还没有谈到的一件事是Promise实际上是什么。当你创建一个时new Promise,你真的只是创建一个普通的旧JavaScript对象。该对象可以调用两个方法then,和catch。这是关键。当promise的状态更改fulfilled为时,.then将调用传递给的函数。当promise的状态更改rejected为时,.catch将调用传递给的函数。这意味着一旦你创建了一个promise,如果异步请求成功,你将传递你想要运行的函数.then。如果异步请求失败,你将传递要运行的功能.catch。
我们来看一个例子吧。我们将setTimeout再次使用fulfilled在两秒钟(2000毫秒)之后将Promise的状态更改为。
function onSuccess () {
console.log('Success!')
}
function onError () {
console.log('
【译】异步JavaScript的演变史:从回调到Promises再到Async/Await的更多相关文章
- js中回调函数,promise 以及 async/await 的对比用法 对比!!!
在编程项目中,我们常需要用到回调的做法来实现部分功能,那么在js中我们有哪些方法来实现回调的? 方法1:回调函数 首先要定义这个函数,然后才能利用回调函数来调用! login: function (f ...
- C#基础——谈谈.NET异步编程的演变史
http://www.cnblogs.com/fzrain/p/3545810.html 前言 C#5.0最重要的改进,就是提供了更强大的异步编程.C#5.0仅增加两个新的关键字:async和awai ...
- JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧 - 学习笔记
那么,谁会告诉 JS 引擎去执行你的程序?事实上,JS 引擎不是单独运行的 —— 它运行在一个宿主环境中,对于大多数开发者来说就是典型的浏览器和 Node.js.实际上,如今,JavaScript 被 ...
- 异步模式:Callbacks, Promises & Async/Await
[译]异步JavaScript的演变史:从回调到Promises再到Async/Await https://www.i-programmer.info/programming/theory/8864- ...
- How Javascript works (Javascript工作原理) (四) 事件循环及异步编程的出现和 5 种更好的 async/await 编程方式
个人总结: 1.讲解了JS引擎,webAPI与event loop合作的机制. 2.setTimeout是把事件推送给Web API去处理,当时间到了之后才把setTimeout中的事件推入调用栈. ...
- JavaScript是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式!
为什么单线程是一个限制? 在发布的第一篇文章中,思考了这样一个问题:当调用堆栈中有函数调用需要花费大量时间来处理时会发生什么? 例如,假设在浏览器中运行一个复杂的图像转换算法. 当调用堆栈有函数要执行 ...
- js异步回调Async/Await与Promise区别 新学习使用Async/Await
Promise,我们了解到promise是ES6为解决异步回调而生,避免出现这种回调地狱,那么为何又需要Async/Await呢?你是不是和我一样对Async/Await感兴趣以及想知道如何使用,下面 ...
- 【译】JavaScript async / await:好的部分,陷阱和如何使用
async/await提供了一种使用同步样式代码异步访问资源的选项,而不会阻塞主线程.然而,使用它有点棘手.在本文中,我们将从不同的角度探讨async / await,并将展示如何正确有效地使用它们. ...
- C#多线程和异步(二)——Task和async/await详解
一.什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务 ...
随机推荐
- U3D虚拟摇杆制作
来自https://www.cnblogs.com/jiuxuan/p/7453762.html 1.创建两个Image,修改第一个Image名称为 Background,把第二个Image放入 Ba ...
- Mysql 数据库增删改查
数据插入 语法:INSERT INTO Table_name(field1,field2……fieldN) values(value1,vlaue2,…valueN) 单行插入用户类型 INSERT ...
- linux强制将数据写入磁盘,防止丢失内存的数据
sync命令文件系统管理 sync命令用于强制被改变的内容立刻写入磁盘,更新超块信息. 在Linux/Unix系统中,在文件或数据处理过程中一般先放到内存缓冲区中,等到适当的时候再写入磁盘, 以提高系 ...
- css之relative
一.relative对absolute的限制作用 1.限制left/top/right/bottom定位.absolute默认是在也没的左上角,当父类设定为relative,absolute就被限制在 ...
- 树·二叉查找树ADT(二叉搜索树/排序树)
1.定义 对于每个节点X,它的左子树中所有的项的值小于X的值,右子树所有项的值大于X的值. 如图:任意一个节点,都满足定义,其左子树的所有值小于它,右子树的所有值大于它. 2.平均深度 在大O模型中, ...
- Modbus库开发笔记之四:Modbus TCP Client开发
这一次我们封装Modbus TCP Client应用.同样的我们也不是做具体的应用,而是实现TCP客户端的基本功能.我们将TCP客户端的功能封装为函数,以便在开发具体应用时调用. 对于TCP客户端我们 ...
- 软件测试作业 - fault error failure
给出的题目如下: 我的解答如下: For program 1:1. where i > 0 is the fault , it should be changed to i>= 0 to ...
- Windows 批处理大全(附各种实例)
Windows 批处理大全(附各种实例) 2009年07月19日 21:31:00 阅读数:2552 批处理文件是无格式的文本文件,它包含一条或多条命令.它的文件扩展名为 .bat 或 .cmd.在命 ...
- Confluence 6 MySQL 3.x 字符集编码问题
MySQL 3.x is 已知在大写和小写转换的时候有些问题(non-ASCII). 问题诊断 请按照 Troubleshooting Character Encodings 页面中的内容对问题进行诊 ...
- python之多线程通信
共享变量通信 事实上共享变量通信是会造成线程安全的,除非我们对这个共享变量是有足够了解的,如非必要就不要使用共享变量在线程间进行通信 Queue通信 理解不深入,暂不写