异步编程与同步编程
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
时间是一种幻觉,午餐时间更是如此。
- 道格拉斯·亚当斯,《银河系漫游指南》
介绍
在编程中,同步操作会阻塞指令直到任务完成,而异步操作则可以在不阻塞其他操作的情况下执行。异步操作通常通过触发事件或调用提供的回调函数来完成。
分解 JavaScript
Javascript 具有:
- 调用栈
- WebAPI
- 事件循环
- 回调队列
调用栈是指程序将立即执行的工作。
let i = 0 // declare a mutable variable
i += 1 // add one to the variable
console.log(i) // log the variable
在上面的例子中,声明变量、将变量的值加一以及记录变量的值,这三条指令都会被添加到调用栈中。Web API 是 JavaScript 运行环境中可用的方法。在浏览器中,`get`window及其方法都是 Web API 的一部分。Web API 执行完毕后,会将回调放入回调队列。
事件循环会等待调用栈完成已加载的任务。一旦事件循环检测到调用栈已清空,它就会从回调队列中向调用栈添加任务。考虑window.setTimeout使用定时器0和一个callback在变量声明之前就使用该变量的函数。
window.setTimeout(() => console.log(i), 0)
let i = 0
i += 1
我们得到的不是错误而是正确答案1,这是因为该函数使用console.log了第一个 WebAPI 指令的参数window.setTimeout。当计时器结束时,回调函数会被移到回调队列中。一旦调用栈被清空(声明变量并将其值加一),我们的函数就会被调用,此时就可以安全地使用该变量了。
从回调函数开始
回调函数一旦被添加到调用栈中就会执行。在前面的示例中,回调函数依赖于定时器来完成,但其他 API 可能有不同的执行条件。以下是一个 Node.js 示例:
const fs = require('fs')
const content = 'Logging to a file'
fs.writeFile('test.txt', content, err => {
if (err) {
throw err
}
console.log('logs completed')
})
console.log('end script')
writeFile API 完成后,将调用回调函数:
- 打开或创建文件
- 写入文件
- 关闭指定位置的文件
fs.writeFile由于是异步的,因此console.log('end script')会在 writeFile 工作完成之前调用。
要同步执行此操作,需要进行哪些更改?
const fs = require('fs')
const content = 'Logging to a file'
try {
fs.writeFileSync('test.txt', content)
console.log('logs completed')
} catch (err) {
throw err
}
Atry {} catch() {}和同步写入文件函数的使用writeFileSync。如果err抛出异常,console.log则不会调用该函数。
同步操作
同步操作会阻塞后续操作,直到其完成。由于计算机运行速度很快,阻塞操作有时看起来似乎不是问题。例如:创建一个数组并记录数组中的值。
Array
.from({ length: 5 }, (v, i) => i + 1)
.forEach(value => console.log(value))
但是,如果数组长度为 5000,则需要更长时间才能将其记录到日志中。时间上的差异是由于线程被锁定的时间更长造成的。
对资源进行同步调用可能会导致响应时间过长,从而导致 UI 卡顿,直到资源响应为止。例如:
const request = new XMLHttpRequest()
request.open('GET', 'https://httpstat.us', false)
request.send(null)
if (request.status === 200) {
console.log(request.responseText)
}
向数据库等自有服务发出请求也能达到同样的效果。一个普通的网页会在各种特殊情况下发出许多请求,作为开发者,你肯定希望这些请求尽快启动,但同时也要保证页面其他部分能够加载完毕,以便这些请求能够顺利进行。
这时,异步操作就显得尤为重要。
异步操作
异步操作独立于主程序流程执行。异步代码的常见用途是查询数据库并使用查询结果。传入回调函数是与响应或错误进行交互的一种方法。
const database = require('thecoolestnewestdbframework')
database('table')
.select('*')
.asCallback((err, res) => {
if (err) {
throw err
}
// do something with the result
})
当数据库加载并响应请求时,页面的其余部分或其他资源无法加载。
承诺和异步操作
Promise 是与异步代码交互的另一种方式。在上面的例子中,如果 const database 返回的是一个 Promise,那么我们可以这样写:
const database = require('thecoolestnewestdbframework')
database('table')
.select('*')
.then(res => {
// do something with the result
})
.catch(err => throw err)
Promise 代表异步执行的工作。当 Promise 解析完成时,结果可以被捕获为错误,也可以在 then 方法中使用。then 方法返回一个 Promise,这意味着 then 可以链式调用,为下一个 then 方法返回另一个 Promise。
const database = require('thecoolestnewestdbframework')
database('table')
.select('*')
.then(res => {
// do something with result
return somethingDifferent
})
.then(res => {
return database('other_table')
.select('*')
.where('id', res)
})
.then(res => {
// do something else
})
.catch(err => throw err)