Js从callbacks到sync/await

callbacks

在JavaScript中,callbacks是一个比较宽泛的概念,当你将函数的引用作为参数传递给一个函数时,这个作为参数传递的函数就称作回调函数。比如:

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函数就可以称作回调函数。所以说,callabcks通常有两种用途,一种就是作为处理函数,对数据进行处理,前端程序员应该很熟悉如下的用法:

[1,2,3].map((i) => i + 5)

_.filter([1,2,3,4], (n) => n % 2 === 0 );

代码中使用了lambda表达式,算是一种匿名函数。另一种使用方法更为广泛,延迟执行某个函数,到特定的时间、或者等到数据,或者是等用户进行了操作:

$('#btn').on('click', () =>console.log('Callbacks are everywhere')
)const id = 'tylermcginnis'$.getjsON({
url: `https://api.github.com/users/${id}`,success: updateUI,
error: showError,
})

这也是本文所谈到的异步编程。在上面的代码中getJSON调用会立即返回,不会阻塞主线程运行,数据获取成功之后,会调用updateUI,如果失败,则调用showError。

看似异步编程在JavaScript中得到了解决,但callbacks这种方案并不完美。第一个不足之处,就是所谓的“回调地狱”。看以下一段代码:

// 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
})
})

有没有觉得晕,人通常习惯于线性思维,顺序执行的代码容易理解,但上面的代码嵌套太多。这还不是嵌套最多的,我之前编写微信小程序,参考的代码有嵌套七八层的,看得令人绝望。这种多层嵌套容易出错,也不好调试。虽然我们可以采用一些模块化技术,改善代码的阅读性,但无法从根本上解决这一问题。

callbacks的另一个问题是“控制反转”,当你的代码调用另一个函数,如果这个函数并不是你编写的,你就失去了控制权。万一你调用的回调函数执行了非常耗时的操作,但又没有考虑异步,你也无法控制。如果你调用的是jQuery、lodash以及JavaScript内置库时,可以放心的假设它们会及时返回。但是,对于众多第三方库,你还会这么放心吗?第三方库可能有意或无意破坏了它们与回调的交互方式。

 

Promise

为了解决callbacks的种种不足,一些聪明人提出了Promise的思路。为了理解这一方案,我们先从日常生活的一个场景出发,作为一名都市人,估计大家都有去餐馆等位子的经历吧!最傻的一种方式就是叫号,这也是大多数餐厅采用的方法,大家都排在餐厅的门口,有了空位再按先来后到的顺序就餐。后来有的商家做了改进,留下电话号码,快到有位子的时候,通过短信或者微信通知。在等待的这段时间,客户可以在附近逛逛,只要不是离得太远。仔细想想,第一种方式类似于编程中的同步模型,客户需要一直死等,第二种方式类似于前面的回调模型。回调模式的问题在哪?想想我们平常收到的推销电话,有没有可能就是你在一次不经意的留下电话号码招来的?我们无法保证每个餐厅都能按良心办事,只用于这次的餐厅等位通知。

两种方式都存在不足,于是有人想出了第三种方案,就是如下图所示的蜂鸣器:

Js从callbacks到sync/await

这种小装备在国内不多见,反正我是没见过。不过简单解释一下,很容易明白其工作原理。当蜂鸣器嗡嗡作响并发光时,表明已经有桌子空出来。实际上,蜂鸣器将处于三种不同状态之一:待处理、接受或拒绝。

  • 待处理是默认的初始状态。当他们给您蜂鸣器时,它就处于这种状态。

  • 蜂鸣器闪烁表明您的桌子准备就绪,蜂鸣器处于 接受 状态。

  • 出现问题时(也许餐厅快要关门了,或者他们忘了有人把餐厅租了一晚),蜂鸣器将处于 拒绝 状态。

在现实中,这种方案有很多细节需要考虑,蜂鸣器通讯范围多广(会不会走太远,收不到信号?)、客人拿了蜂鸣器不归还怎么办?但是将这种方案用在解决JavaScript中的异步问题,就不存在上述问题,又能很好的解决控制权反转问题,这就是JavaScript中的Promise。

Promise有三种状态:pending, fulfilled和rejected。如果异步请求仍在进行中,则Promise的状态将为pending。如果异步请求已成功完成,则Promise将变为fulfilled状态。如果异步请求失败,则Promise将变为rejected状态。是不是和前面用于解决餐厅等位问题的蜂鸣器很像?

了解Promise存在的原因以及它们可能处于的不同状态后,我们还需要回答三个问题:

  • 如何创建Promise?

  • 如何更改Promise的状态?

  • 当Promise状态发生变化时,您该如何监听?

 

创建Promise

第一个问题很好回答,直接new一个Promise的实例即可:

const promise = new Promise()

注意并非所有浏览器都支持Promise对象,自 Chrome 32、Opera 19、Firefox 29、Safari 8 和 Microsoft Edge 起,promise 默认启用,所以使用前请确认你所使用的浏览器内核。

 

修改Promise的状态

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后立即输出Promise值,然后在大约2秒钟后resolve被调用后再次输出Promise值,来观察到这种变化。注意到没有,Promise从 pending 状态变为 resolved 。

 

监听Promise状态变化

这是最重要的问题。如果状态更改后我们不知道如何做,那毫无用处。

创建新的Promise时,实际上只是在创建一个普通的JavaScript对象。该对象可以调用then和catch这两个方法,这两个方法都接受一个回调函数作为参数。当Promise的状态变为fulfilled时,传递给.then的函数将被调用。当一个Promise的状态更改为rejected时,将调用传递给.catch的函数。

让我们来看一个例子。我们将再次使用setTimeout两秒钟(2000毫秒)后将Promise状态变为fulfilled。

function onSuccess () {console.log('Success!')
}function one rror () {console.log(':hankey:')
}const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

尝试着运行上面的代码,大约2秒钟后,在浏览器控制台中可看到“Success!”。

这个过程发生了什么?首先,当我们创建Promise时,我们在约2000毫秒后调用了resolve,这将Promise的状态更改为fulfilled。其次,我们将onSuccess函数传递给promises的.then方法。这样做,我们告诉了Promise,当Promise的状态更改为fulfilled时调用onSuccess,它在大约2000毫秒后执行。

再来看看rejected情况下的代码:

function onSuccess () {console.log('Success!')
}function one rror () {console.log(':hankey:')
}const promise = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
}, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

这种情况下,onError将会被调用,因为2000毫秒后,reject被调用了。

回头再看看前面的异步代码:

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)
})

用Promise的方式该如何改写呢?首先看看getUser这个函数的改写:

function getUser(id) {return new Promise((resolve, reject) => {
$.getJSON({
url: `https://api.github.com/users/${id}`,success: resolve,
error: reject
})
})
}

注意到没有,getUser的参数有所变化,仅接收ID,不再需要其他两个回调函数,保证不会发生 控制反转 。如果请求成功,则将调用resolve;如果发生错误,则将调用reject。

同样的方式改写getWether函数:

function getWeather(user) {return new Promise((resolve, reject) => {
$.getJSON({
url: getLocationURL(user.location.split(',')),
success: resolve,
error: reject,
})
})
}

接着改写按钮点击处理:

$("#btn").on("click", () => {const userPromise = getUser('tylermcginnis')

userPromise.then((user) => {const weatherPromise = getWeather(user)
weatherPromise.then((weather) => {
updateUI({
user,
weather: weather.query.results
})
})

weatherPromise.catch(showError)
})

userPromise.catch(showError)
})

代码的逻辑就是根据id获取用户信息,然后通过用户所在的地理位置获取天气信息,最后更新到用户界面上。

整条逻辑就像是一个线性处理过程,事实上,通过Promise的链式结构,我们可以将代码写得更紧凑一些。

$("#btn").on("click", () => {
getUser("tylermcginnis")
.then(getWeather)
.then((weather) => {// We need both the user and the weather here.// Right now we just have the weatherupdateUI() // ????})
.catch(showError)
})

上面的代码看起来很简练,但实际上隐藏着一个问题。在第二个.then中,我们要调用updateUI。问题是我们需要同时给updateUI传递用户和天气。但上面的代码中,我们只传递了天气信息,而没有用户信息。我们需要以某种方式找到一种实现方法,以便在getWeather返回的Promise在resolve时,用户和天气都可以传递。

解决问题的关键在于,resolve只是一个函数,传递给它的任何参数都将传递给给.then的函数。这意味着在getWeather内部,如果我们调用自己的resolve方法,则可以将天气和用户传递给它。这样,链中的第二个.then方法将同时接收用户和天气作为参数。

function getWeather(user) {return new Promise((resolve, reject) => {
$.getJSON({
url: getLocationURL(user.location.split(',')),
success(weather) {
resolve({ user, weather: weather.query.results })
},
error: reject,
})
})
}

$("#btn").on("click", () => {
getUser("tylermcginnis")
.then(getWeather)
.then((data) => {// Now, data is an object with a// "weather" property and a "user" property.updateUI(data)
})
.catch(showError)
})

比较以下Callbacks和Promise的实现代码,是不是Promise更容易理解?

// Callbacks :no_entry_sign:getUser("tylermcginnis", (user) => {
getWeather(user, (weather) => {
updateUI({
user,
weather: weather.query.results
})
}, showError)
}, showError)// Promises :white_check_mark:getUser("tylermcginnis")
.then(getWeather)
.then((data) => updateUI(data))
.catch(showError);

资源搜索网站大全 https://www.renrenfan.com.cn 广州VI设计公司https://www.houdianzi.com

async/await

上面的Promise方案解决了Callbacks的两大重要缺陷,但还存在不足,我们需要将用户数据从第一个异步请求一直传递到最后一个.then。这使得我们修改getWeather函数,使其可以传递用户。

有没有什么方法可以让我们以编写同步代码的方式编写异步代码呢?假如我们以同步方式实现上述的功能,大概写法如下:

$("#btn").on("click", () => {const user = getUser('tylermcginnis')const weather = getWeather(user)

updateUI({
user,
weather,
})
})

如何让Javascript引擎知道这里getUser和getWeather实际上是一个异步方法呢?这时就该async/await登场了。

$("#btn").on("click", async () => {const user = await getUser('tylermcginnis')const weather = await getWeather(user.location)

updateUI({
user,
weather,
})
})

首先,函数前的async修饰告诉引擎,该函数中存在异步调用。其次,代码中的await则表示这个调用是一个异步调用,将返回一个Promise。在await的地方,代码将等待,直到异步调用返回Promise。

函数前加上async,代表函数将返回一个Promise,即使像下面这样的空函数,也会隐式返回一个Promise:

async function getPromise(){}const promise = getPromise()

如果async函数返回了值呢?如以下代码所示,该值将封装到Promise中:

async function add (x, y) {return x + y
}

add(2,3).then((result) => {console.log(result) // 5})

需要注意的是,await只能用在async函数中,比如下面的代码,会出错:

$("#btn").on("click", () => {const user = await getUser('tylermcginnis') // SyntaxError: await is a reserved wordconst weather = await getWeather(user.location) // SyntaxError: await is a reserved wordupdateUI({
user,
weather,
})
})

也就是说,当async加到函数时,会产生两种结果:

  • 使函数本身返回(或包装返回的内容)一个promise

  • 可以在其中使用await。

 

小结

好了,关于JavaScript中的异步编程就探讨到这儿,是不是和我们平常采用的Python、Java或C++语言不太一样。有人说,学一门语言,实际上是学习一种编程思路,你没有想到JavaScript会用这种方式来解决异步编程吧!

上一篇:Tensorflow2.0--Keras实战


下一篇:【tensorflow2.0】回调函数callbacks