回调(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(‘