事件循环
事件循环是了解 Node.js 最重要的方面之一。
一个简单的事件循环的阐释
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
bar()
baz()
}
foo()
输出
foo
bar
baz
当运行此代码时,会首先调用 foo()。 在 foo() 内部,会首先调用 bar(),然后调用 baz()。
此时,调用堆栈如下所示:
入队函数执行
上面的示例看起来很正常,没有什么特别的:JavaScript 查找要执行的东西,并按顺序运行它们。
让我们看看如何将函数推迟直到堆栈被清空。
setTimeout(() => {}, 0) 的用例是调用一个函数,但是是在代码中的每个其他函数已被执行之后。
举个例子:
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
输出
foo
baz
bar
当运行此代码时,会首先调用 foo()。 在 foo() 内部,会首先调用 setTimeout,将 bar 作为参数传入,并传入 0 作为定时器指示它尽快运行。 然后调用 baz()。
此时,调用堆栈如下所示:
消息队列
当调用 setTimeout() 时,浏览器或 Node.js 会启动定时器。 当定时器到期时(在此示例中会立即到期,因为将超时值设为 0),则回调函数会被放入“消息队列”中。
在消息队列中,用户触发的事件(如单击或键盘事件、或获取响应)也会在此排队,然后代码才有机会对其作出反应。 类似 onLoad 这样的 DOM 事件也如此。
事件循环会赋予调用堆栈优先级,它首先处理在调用堆栈中找到的所有东西,一旦其中没有任何东西,便开始处理消息队列中的东西。
我们不必等待诸如 setTimeout、fetch、或其他的函数来完成它们自身的工作,因为它们是由浏览器提供的,并且位于它们自身的线程中。 例如,如果将 setTimeout 的超时设置为 2 秒,但不必等待 2 秒,等待发生在其他地方。
ES作业队列
ECMAScript 2015 引入了作业队列的概念,Promise 使用了该队列(也在 ES6/ES2015 中引入)。 这种方式会尽快地执行异步函数的结果,而不是放在调用堆栈的末尾。
在当前函数结束之前 resolve 的 Promise 会在当前函数之后被立即执行。
有个游乐园中过山车的比喻很好:消息队列将你排在队列的后面(在所有其他人的后面),你不得不等待你的回合,而工作队列则是快速通道票,这样你就可以在完成上一次乘车后立即乘坐另一趟车。
const bar = () => console.log('bar')
const baz = () => console.log('baz')
const foo = () => {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) =>
resolve('应该在 baz 之后、bar 之前')
).then(resolve => console.log(resolve))
baz()
}
foo()
结果
foo
baz
应该在 baz 之后、bar 之前
bar
这是 Promise(以及基于 promise 构建的 async/await)与通过 setTimeout() 或其他平台 API 的普通的旧异步函数之间的巨大区别。
process.nextTick()
当尝试了解 Node.js 事件循环时,其中一个重要的部分就是 process.nextTick()。
每当事件循环进行一次完整的行程时,我们都将其称为一个滴答。
当将一个函数传给 process.nextTick() 时,则指示引擎在当前操作结束(在下一个事件循环滴答开始之前)时调用此函数:
process.nextTick(() => {
//做些事情
})
事件循环正在忙于处理当前的函数代码。
当该操作结束时,JS 引擎会运行在该操作期间传给 nextTick 调用的所有函数。
这是可以告诉 JS 引擎异步地(在当前函数之后)处理函数的方式,但是尽快执行而不是将其排入队列。
调用 setTimeout(() => {}, 0) 会在下一个滴答结束时执行该函数,比使用 nextTick()(其会优先执行该调用并在下一个滴答开始之前执行该函数)晚得多。
当要确保在下一个事件循环迭代中代码已被执行,则使用 nextTick()。
setImmediate()
当要异步地(但要尽可能快)执行某些代码时,其中一个选择是使用 Node.js 提供的 setImmediate() 函数:
作为 setImmediate() 参数传入的任何函数都是在事件循环的下一个迭代中执行的回调。
setImmediate() 与 setTimeout(() => {}, 0)(传入 0 毫秒的超时)、process.nextTick() 有何不同?
传给 process.nextTick() 的函数会在事件循环的当前迭代中(当前操作结束之后)被执行。 这意味着它会始终在 setTimeout 和 setImmediate 之前执行。
延迟 0 毫秒的 setTimeout() 回调与 setImmediate() 非常相似。 执行顺序取决于各种因素,但是它们都会在事件循环的下一个迭代中运行。
探索 JavaScript 定时器
setTimeout()
当编写 JavaScript 代码时,可能希望延迟函数的执行。
这就是 setTimeout 的工作。 指定一个回调函数以供稍后执行,并指定希望它稍后运行的时间(以毫秒为单位)的值:
setTimeout(() => {
// 2 秒之后运行
}, 2000)
setTimeout(() => {
// 50 毫秒之后运行
}, 50)
该语法定义了一个新的函数。 可以在其中调用所需的任何其他函数,也可以传入现有的函数名称和一组参数:
const myFunction = (firstParam, secondParam) => {
// 做些事情
}
// 2 秒之后运行
setTimeout(myFunction, 2000, firstParam, secondParam)
setTimeout 会返回定时器的 id。 通常不使用它,但是可以保存此 id,并在要删除此安排的函数执行时清除它:
const id = setTimeout(() => {
// 应该在 2 秒之后运行
}, 2000)
// 改变主意了
clearTimeout(id)
如果将超时延迟指定为 0,则回调函数会被尽快执行(但是是在当前函数执行之后):
setTimeout(() => {
console.log('后者 ')
}, 0)
console.log(' 前者 ')
输出:前置 后者
setInterval()
setInterval 是一个类似于 setTimeout 的函数,不同之处在于:它会在指定的特定时间间隔(以毫秒为单位)一直地运行回调函数,而不是只运行一次:
setInterval(() => {
// 每 2 秒运行一次
}, 2000)
上面的函数每隔 2 秒运行一次,除非使用 clearInterval 告诉它停止(传入 setInterval 返回的间隔定时器 id):
const id = setInterval(() => {
// 每 2 秒运行一次
}, 2000)
clearInterval(id)
通常在 setInterval 回调函数中调用 clearInterval,以使其自行判断是否应该再次运行或停止。 例如,此代码会运行某些事情,除非 App.somethingIWait 具有值 arrived:
const interval = setInterval(() => {
if (App.somethingIWait === 'arrived') {
clearInterval(interval)
return
}
// 否则做些事情
}, 100)
递归的setTimeout
setInterval 每 n 毫秒启动一个函数,而无需考虑函数何时完成执行。
如果一个函数总是花费相同的时间,那就没问题了,函数可能需要不同的执行时间,这具体取决于网络条件,也许一个较长时间的执行会与下一次执行重叠,为了避免这种情况,可以在回调函数完成时安排要被调用的递归的 setTimeout
const myFunction = () => {
// 做些事情
setTimeout(myFunction, 1000)
}
setTimeout(myFunction, 1000)
这样就能确保两次执行之间间隔指定时间
参考
【1】Node.js 事件循环
【2】了解 process.nextTick()
【3】探索 JavaScript 定时器