异步JavaScript的演化史:从回调到Promise再到Async/Await
2021-03-09 20:29
标签:spl 返回值 匹配 如何 use rehat 运行 enc console 图片 在这里我假设你对回调一无所知,如果事实并非如此的话,那么你可以将内容稍微往后拖动一下。 事实上,不仅我能按下按钮,你 也可以,任何人 按下按钮的效果都是一样的。只要按下按钮,不管你是否愿意,这个机器就会开始运行。 在上面的代码中,我们将add函数赋值给了三个不同的变量:me、you和someoneElse。有很重要的一点需要注意,原始的add和我们创建的每个变量都指向的相同的内存点。在不同的名字之下,它们实际上是完全相同的内容。所以,当我们调用me、you或someoneElse的时候,就像调用add一样。 你可能会觉得这有些诡异,但是这里没有任何新东西。此时,我们不再是在add上“按下按钮”,而是将add作为参数传递给addFive,将其重命名为addReference,然后我们“按下按钮”或者说调用它。 这种模式看上去应该是非常熟悉的,它到处可见。如果你曾经用过 JavaScript 的 Array 方法,那么你所使用的就是回调。如果你用过 lodash,那么你所使用的就是回调。如果你用过 jQuery,那么你所使用的也是回调。 一般而言,回调有两种常见的使用场景。首先,也就是我们在.map和 _.filter样例中所看到的,对于从一个值转换成另一个值的场景,这是一种非常好的抽象。我们可以说“这里有一个数组和一个函数。基于我给你的函数得到一个新的值”。其次,也就是我们在 jQuery 样例中所看到的,将函数的执行延迟至一个特定的时间。“这里有一个函数,当 id 为btn的元素被点击时,执行这个函数”。我们接下来会主要关注第二个使用场景,“将函数的执行延迟至一个特定的时间”。 在获取到用户的数据之前,我们是不能更新应用的 UI 的。那么我们是怎么做的呢?我们可以说,“这是一个对象。如果请求成功的话,那么调用success,并将用户的数据传递给它。如果请求没有成功的话,那么调用error并将错误对象传递给它。你不用关心每个方法是做什么的,只需要确保在应该调用它们的时候,去进行调用就可以了。这个样例完美地阐述了如何使用回调进行异步请求。 如果你需要帮助的话,可以参考一下这些代码的在线版本 (https://codesandbox.io/s/v06mmo3j7l)。 如果你需要帮助的话,可以参考一下这些代码的在线版本 (https://codesandbox.io/s/m587rq0lox)。 因为你不是调用criticalFunction的人,因此你完全无法控制它何时被调用以及使用什么参数进行调用。大多数情况下,这都不是什么问题,但是一旦出现问题的话,就不是什么小问题。 你有没有不预约就进入一家繁忙餐厅的经历?在这种情况下,餐厅需要有一种方式在出现空桌时能够联系到你。过去,他们只会把你的名字记录下来并在出现空桌的时候呼喊你的名字。随后,他们自然而然地寻找更有意思的方案。有一种方式是他们不再记录你的名字,而是记录你的电话号码,当出现空桌的时候,他们就可以为你发送短信。这样一来,你就可以离开最初的呼喊范围了,但是更重要的是,这种方式允许他们在任何时候给你的电话发送广告。听起来很熟悉吧?应该是这样的!当然也可能并非如此。这种方式可以用来类比回调。将你的电话号码告诉餐厅就像将你的回调函数交给第三方服务一样。你期望餐厅在有空桌的时候给你发送短信,同样我们也期望第三方服务在合适的时候以它们承诺的方式调用我们函数。但是,一旦电话号码或回调函数交到了他们的手里,我们就完全失去对它的控制了。 如果你之前没有用过的话,它的想法其实非常简单。按照这种方式,他们不会记录你的名字或电话号码,而是给你一个这样的设备。当这个设备开始嗡嗡作响和发光时,就意味着有空桌了。在等待空桌的时候,你可以做任何你想做的事情,但此时你不需要放弃任何的东西。实际上,恰恰相反,是 他们 需要给 你 东西,这里没有所谓的控制反转。 2)如何改变 Promise 的状态? 分别用日志记录刚刚创建之时和大约两秒钟之后resolve已被调用时的 Promise,我们可以看到状态的变化了: 请注意,Promise 从 异步JavaScript的演化史:从回调到Promise再到Async/Await 标签:spl 返回值 匹配 如何 use rehat 运行 enc console 原文地址:https://blog.51cto.com/15057848/2568199
作者|Tyler McGinnis
译者|张卫滨
本文以实际样例阐述了异步 JavaScript 的发展过程,介绍了每种实现方式的优势和不足,能够帮助读者掌握相关技术的使用方式并把握技术发展的脉络。
我最喜欢的一个站点叫做 BerkshireHathaway.com,它非常简单、高效,从 1997 年创建以来它一直都能很好地完成自己的任务。尤其值得注意的是,在过去的 20 年间,这个站点从来没有出现过缺陷。这是为什么呢?因为它是静态的,从建立到现在的 20 年间,它几乎没有发生过什么变化。如果你将所有的数据都放在前面的话,搭建站点是非常简单的。但是,如今大多数的站点都不会这么做。为了弥补这一点,我们发明了所谓的“模式”,帮助我们的应用从外部抓取数据。同其他大多数事情一样,这些模式都有一定的权衡,并随着时间的推移在发生着变化。在本文中,我们将会分析三种常用模式的优劣,即回调(Callback)、Promise 和 Async/Await,并从历史发展的维度讨论一下它们的意义和发展。
我们首先从数据获取模式的最原始方式开始介绍,那就是回调。回调
当我第一次学习编程的时候,它就帮助我形成了一种思考方式,那就是将功能视为一种机器。这些机器能够完成任何你希望它能做到的事情,它们甚至能够接受输入并返回值。每个机器都有一个按钮,如果你希望这个机器运行的话,就按下按钮,这个按钮也就是 ()。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 - 按下按钮,运行机器。
you(2,3) // 5 - 按下按钮,运行机器。
someoneElse(2,3) // 5 - 按下按钮,运行机器。
如果我们将add传递给另外一台机器又会怎样呢?需要记住,不管谁按下这个“()”按钮,它都会执行。function add (x, y) {
return x + y
}
function addFive (x, addReference) {
return addReference(x, 5) // 15 - 按下按钮,运行机器。
}
addFive(10, add) // 15
这里涉及到了 JavaScript 的一些重要概念。首先,就像可以将字符串或数字以参数的形式传递给函数一样,我们还可以将函数的引用作为参数进行传递。但我们这样做的时候,作为参数传递的函数被称为回调函数(callback function),而接收回调函数传入的那个函数则被称为高阶函数(higher order function)。
因为术语非常重要,所以对相同功能的代码,我们进行变量的重命名,使其匹配它们所要阐述的概念:function add (x,y) {
return x + y
}
function higherOrderFunction (x, callback) {
return callback(x, 5)
}
higherOrderFunction(10, add)
[1,2,3].map((i) => i + 5)
_.filter([1,2,3,4], (n) => n % 2 === 0 );
$(‘#btn‘).on(‘click‘, () =>
console.log(‘Callbacks are everywhere‘)
)
现在,我们只看到了同步操作的样例。正如我们在本文开始时提到的那样,我们所构建的大多数应用都不会将数据预先准备好,而是用户在与应用进行交互时,按需抓取外部的数据。通过上面的介绍,我们很快就能判断得出这个场景非常适合使用回调,因为它允许我们“将函数的执行延迟至一个特定的时间”。我们能够顺理成章的将这句话应用到数据获取的情景中。此时不再是将函数的执行延迟到一个特定的时间,而是将函数的执行延迟至我们得到了想要的数据之后。jQuery 的getJSON方法可能是这种模式最常见的样例:// updateUI 和 showError 的内容无关紧要。
// 假定它们所做的工作与它们的名字相同。
const id = ‘tylermcginnis‘
$.getJSON({
url: `https://api.github.com/users/${id}`,
success: updateUI,
error: showError,
})
到此为止,我们已经学习了回调是什么以及它如何为同步代码和异步代码带来收益。我们还没有讨论回调的阴暗面。看一下下面的代码,你能告诉我它都做了些什么吗?// updateUI、showError 和 getLocationURL 的内容无关紧要。
// 假定它们所做的工作与它们的名字相同。
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
})
})
注意一下,我们只是多添加了几层回调。首先,我们还是声明,如果不点击 id 为btn的元素,那么原始的 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)
})
好了,函数的名称能够帮助我们理解到底会发生什么,但客观地说,它真的“更好”了吗?其实也没好到哪里去。我们只是给回调地狱这个问题上添加了一块创可贴。问题依然存在,也就是我们会自然地按照顺序进行思考,即便有了额外的函数,嵌套回调也会打断我们顺序思考的方式。
回调方式的另外一个问题与 控制反转(inversion of control) 有关。当你编写回调的时候,你会假设自己将回调交给了一个负责任的程序,这个程序会在(并且仅会在)应该调用的时候调用你的回调。你实际上将控制权交给了另外一个程序。当你在处理 jQuery、lodash 这样的库,甚至普通 JavaScript 时,可以安全地假设回调函数会在正确的时间以正确的参数进行调用。但是,对于很多第三方库来说,回调函数是你与它们进行交互的接口。第三方库很可能有意或无意地破坏与你的回调进行交互的方式。function criticalFunction () {
// 这个函数一定要进行调用,并且要传入正确的参数
}
thirdPartyLib(criticalFunction)
Promise
幸好,还有另外一种解决方案。这种方案的设计允许你保留所有的控制权。你可能之前见过这种方式,那就是他们会给你一个蜂鸣器,如下所示。
蜂鸣器一定会处于如下三种状态之一:pending、fulfilled或rejected。
pending:默认状态,也是初始态。当他们给你蜂鸣器的时候,它就是这种状态。
fulfilled:代表蜂鸣器开始闪烁,你的桌子已经准备就绪。
rejected:如果蜂鸣器处于这种状态,则代表出现了问题。可能餐厅要打烊,或者他们忘记了晚上有人要包场。
再次强调,你作为蜂鸣器的接收者拥有完全的控制权。如果蜂鸣器处于fulfilled状态,你就可以就坐了。如果它进入fulfilled状态,但是你想忽略它,同样也可以。如果它进入了rejected状态,这非常糟糕,但是你可以选择去其他地方就餐。如果什么事情都没有发生,它会依然处于pending状态,你可能吃不上饭了,但是同时也没有失去什么。
现在,你已经掌握了餐厅蜂鸣器的事情,接下来,我们将这个知识用到其他重要的地方。
如果说将电话号码告诉餐厅就像将回调函数交给他们一样的话,那么接受这个蜂鸣器就像我们所谓的“Promise”一样。
像以往一样,我们首先从 为什么 开始。Promise 为什么会存在呢?它的出现是为了让异步请求所带来的复杂性更容易管理。与蜂鸣器非常类似,Promise会处于如下三种状态中的某一种: pending、fulfilled或rejected。但是与蜂鸣器不同,这些状态代表的不是饭桌的状态,它们所代表的是异步请求的状态。
如果异步请求依然还在进行,那么Promise的状态会是pending。如果异步请求成功完成的话,那么Promise会将状态转换为fulfilled。如果异步请求失败的话,Promise会将状态转换为rejected。蜂鸣器的比喻非常贴切,对吧?
理解了 Promise 为什么会存在以及它们的三种不同状态之后,我们还要回答三个问题:
如何创建 Promise?
如何改变 Promise 的状态?
如何监听 Promise 状态变化的时间?
1)如何创建 Promise?
这个问题非常简单,你可以使用new创建Promise的一个实例:const promise = new Promise()
Promise的构造函数会接收一个参数,这个参数是一个(回调)函数。该函数会被传入两个参数resolve和reject。
resolve:一个能将 Promise 状态变为fulfilled的函数;
reject:一个能将 Promise 状态变为rejected的函数;
在下面的代码中,我们使用setTimeout等待两秒钟然后调用resolve,这样会将 Promise 的状态变为fulfilled:const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve() // 将状态变为“fulfilled”
}, 2000)
})
3)如何监听 Promise 状态变化的时间?
我认为,这是最重要的一个问题。我们已经知道了如何创建 Promise 和改变它的状态,这非常棒,但是如果我们不知道如何在状态变化之后做一些事情的话,这其实是没有太大意义的。
我们还没有讨论的一件事就是 Promise 到底是什么。当我们通过new Promise创建 Promise 的时候,你实际创建的只是一个简单的 JavaScript 对象,这个对象可以调用两个方法then和catch。这是关键所在,当 Promise 的状态变为fulfilled的时候,传递给.then的函数将会被调用。如果 Promise 的状态变为rejected,传递给.catch的函数将会被调用。这就意味着,在你创建 Promise 的时候,要通过.then将你希望异步请求成功时调用的函数传递进来,通过.catch将你希望异步请求失败时调用的函数传递进来。
看一下下面的样例。我们依然使用setTimeout在两秒钟(2000 毫秒)之后将 Promise 的状态变为fulfilled:
function onSuccess () {
console.log(‘Success!‘)
}
function onError () {
console.log(‘
文章标题:异步JavaScript的演化史:从回调到Promise再到Async/Await
文章链接:http://soscw.com/index.php/essay/62439.html