JavaScript 引擎是单线程的,这就意味着同一时间引擎自身只能做一件事,如果有很多事情要做,就必须一件一件来,在 JavaScript 中如果前面的某个任务需要耗费很长时间,后面的任务就被阻塞了,同时也无法响应用户的操作,例如 click 事件,看起来就像浏览器卡住了。
如果我们在浏览器中要通过 API 向服务器请求数据,就需要使用 XMLHttpRequest 对象发送一个 Ajax 请求,同时还会监听 HTTP 响应事件并指定一个回调函数来拿到这个数据,如果这个请求是同步的,那么在收到 HTTP 响应之前 JS 引擎就会一直处于阻塞状态,无法执行后面的代码也无法响应用户的交互操作。
如果是异步的就不会通过阻塞 JS 引擎的方式来等待 HTTP 响应,JS 引擎会告诉宿主环境(浏览器或者 Node)在收到 HTTP 响应之后将回调函数插入事件循环队列的末尾,然后自己会继续执行后面的代码,在未来的某个时间点,宿主环境收到这个 HTTP 响应之后就会将回调函数插入事件循环队列的末尾,在事件循环队列里的回调函数最终都会被 JS 引擎按顺序一一执行。
在 You Don't Know JS 中有一段代码可以很形象的表现出事件循环的基本模型
// `eventLoop` is an array that acts as a queue (first-in, first-out)
var eventLoop = []
var event
// keep going "forever"
while (true) {
// perform a "tick"
if (eventLoop.length > 0) {
// get the next event in the queue
event = eventLoop.shift()
// now, execute the next event
try {
event()
} catch (err) {
reportError(err)
}
}
}
可以这么理解,JS 引擎在执行完同步代码之后(或者说 call stack 变空后)就会执行上面这个 while 死循环,程序初始化之后触发的所有操作都会完成后将对应的回调函数 push 到事件循环队列里,JS 引擎会一个接一个按顺序取出队列里的回调函数执行它。
通常有以下几种情况会触发异步操作:
最简单的一个例子:
setTimeout(() => console.log(2), 0)
console.log(1)
第一行表示立即将 setTimeout 的第一个参数添加到任务队列末尾,接着执行第二行, 最后才会执行任务队列中的任务,最终会打印出 1 2
需要注意的是,setTimeout 和 setInterval 在执行时间上是不可靠的,setTimeout 表示在指定的延迟后将第一个参数添加到任务队列末尾,setInterval 表示每隔指定的时间就将第一个参数添加到任务队列末尾。
setTimeout(() => console.log(2), 0)
task() // 假设这个函数会耗时 10 秒
上面的代码在开始执行时就会立即将 setTimeout 的第一个参数添加的任务队列末尾,但是在 10 秒后才会打印出 0
setInterval(() => console.log(new Date()), 2000) // 本意表示每隔两秒打印一次当前时间
task() // 假设这个函数会耗时 10 秒
上面的代码会在 10 秒后连续 5 次打印出当前时间, 原因就在于每隔指定的时间 setInterval 就会不分青红皂白无脑将第一个参数添加到任务队列末尾,等到 javascript 主线程空闲了开始取出队列中的任务执行时也是无脑的,只要队列中还有任务它就会一个接一个的取出来执行,所以传给 setInterval 的函数也是无法保证执行时间的。
如果你需要使用 setTimeout 实现一个动画,可以使用 requestAnimationFrame 代替,如果要兼容不支持 requestAnimationFrame 的浏览器,可以使用浏览器特性检测区别对待。
在浏览器环境下异步任务基本分为以下两种类型:
考虑如下代码
setTimeout(() => console.log(0), 0) // 注意这里
const promise = new Promise(function(resolve, reject) {
console.log(1)
resolve()
})
promise.then(() => console.log(2))
console.log(3)
Promise 的执行器会在 Promise 创建时立即执行,所以会首先打印出 1,执行器的 resolve 和 reject 函数都是异步操作,传给 promise.then 的回调函数会在执行器内部的 resolve 函数执行后执行,如果上面的代码去掉第一行,那么最终会依次输出 1 3 2
如果没有去掉第一行,就会输出 1 3 2 0
上面代码中 Promise 的执行器内部的 resolve 和 reject 函数虽然是异步操作,但是它们并不会添加到 macrotasks,而是添加到 microtasks,它会在每次 Event Loop 迭代结束之前全部执行完毕,所以等主线程空下来的时候会先执行完 microtasks 中的所有任务才会开始执行 macrotasks 中的任务。
例如 Promise 的 onRejected 和 onFulfilled 回调函数就是微任务,其他还有 window.queueMicrotask
传给 requestAnimationFrame 的回调函数浏览器会在下一次重绘之前调用
常见的有 window.setTimeout 以及各种 DOM 交互事件,例如点击和滚动事件。
微任务 > requestAnimationFrame > 宏任务
执行以下代码
setTimeout(() => {
console.log(3)
}, 0)
Promise.resolve().then(() => {
console.log(2)
})
console.log(1)
上面的代码最终输出:
1
2
3
本文原载于:baiyun.me
JavaScript 引擎是单线程的,这就意味着同一时间引擎自身只能做一件事,如果有很多事情要做,就必须一件一件来,在 JavaScript 中如果前面的某个任务需要耗费很长时间,后面的任务就被阻塞了,同时也无法响应用户的操作,例如 click 事件,看起来就像浏览器卡住了。
如果我们在浏览器中要通过 API 向服务器请求数据,就需要使用 XMLHttpRequest 对象发送一个 Ajax 请求,同时还会监听 HTTP 响应事件并指定一个回调函数来拿到这个数据,如果这个请求是同步的,那么在收到 HTTP 响应之前 JS 引擎就会一直处于阻塞状态,无法执行后面的代码也无法响应用户的交互操作。
如果是异步的就不会通过阻塞 JS 引擎的方式来等待 HTTP 响应,JS 引擎会告诉宿主环境(浏览器或者 Node)在收到 HTTP 响应之后将回调函数插入事件循环队列的末尾,然后自己会继续执行后面的代码,在未来的某个时间点,宿主环境收到这个 HTTP 响应之后就会将回调函数插入事件循环队列的末尾,在事件循环队列里的回调函数最终都会被 JS 引擎按顺序一一执行。
在 You Don't Know JS 中有一段代码可以很形象的表现出事件循环的基本模型
// `eventLoop` is an array that acts as a queue (first-in, first-out)
var eventLoop = []
var event
// keep going "forever"
while (true) {
// perform a "tick"
if (eventLoop.length > 0) {
// get the next event in the queue
event = eventLoop.shift()
// now, execute the next event
try {
event()
} catch (err) {
reportError(err)
}
}
}
可以这么理解,JS 引擎在执行完同步代码之后(或者说 call stack 变空后)就会执行上面这个 while 死循环,程序初始化之后触发的所有操作都会完成后将对应的回调函数 push 到事件循环队列里,JS 引擎会一个接一个按顺序取出队列里的回调函数执行它。
通常有以下几种情况会触发异步操作:
最简单的一个例子:
setTimeout(() => console.log(2), 0)
console.log(1)
第一行表示立即将 setTimeout 的第一个参数添加到任务队列末尾,接着执行第二行, 最后才会执行任务队列中的任务,最终会打印出 1 2
需要注意的是,setTimeout 和 setInterval 在执行时间上是不可靠的,setTimeout 表示在指定的延迟后将第一个参数添加到任务队列末尾,setInterval 表示每隔指定的时间就将第一个参数添加到任务队列末尾。
setTimeout(() => console.log(2), 0)
task() // 假设这个函数会耗时 10 秒
上面的代码在开始执行时就会立即将 setTimeout 的第一个参数添加的任务队列末尾,但是在 10 秒后才会打印出 0
setInterval(() => console.log(new Date()), 2000) // 本意表示每隔两秒打印一次当前时间
task() // 假设这个函数会耗时 10 秒
上面的代码会在 10 秒后连续 5 次打印出当前时间, 原因就在于每隔指定的时间 setInterval 就会不分青红皂白无脑将第一个参数添加到任务队列末尾,等到 javascript 主线程空闲了开始取出队列中的任务执行时也是无脑的,只要队列中还有任务它就会一个接一个的取出来执行,所以传给 setInterval 的函数也是无法保证执行时间的。
如果你需要使用 setTimeout 实现一个动画,可以使用 requestAnimationFrame 代替,如果要兼容不支持 requestAnimationFrame 的浏览器,可以使用浏览器特性检测区别对待。
在浏览器环境下异步任务基本分为以下两种类型:
考虑如下代码
setTimeout(() => console.log(0), 0) // 注意这里
const promise = new Promise(function(resolve, reject) {
console.log(1)
resolve()
})
promise.then(() => console.log(2))
console.log(3)
Promise 的执行器会在 Promise 创建时立即执行,所以会首先打印出 1,执行器的 resolve 和 reject 函数都是异步操作,传给 promise.then 的回调函数会在执行器内部的 resolve 函数执行后执行,如果上面的代码去掉第一行,那么最终会依次输出 1 3 2
如果没有去掉第一行,就会输出 1 3 2 0
上面代码中 Promise 的执行器内部的 resolve 和 reject 函数虽然是异步操作,但是它们并不会添加到 macrotasks,而是添加到 microtasks,它会在每次 Event Loop 迭代结束之前全部执行完毕,所以等主线程空下来的时候会先执行完 microtasks 中的所有任务才会开始执行 macrotasks 中的任务。
例如 Promise 的 onRejected 和 onFulfilled 回调函数就是微任务,其他还有 window.queueMicrotask
传给 requestAnimationFrame 的回调函数浏览器会在下一次重绘之前调用
常见的有 window.setTimeout 以及各种 DOM 交互事件,例如点击和滚动事件。
微任务 > requestAnimationFrame > 宏任务
执行以下代码
setTimeout(() => {
console.log(3)
}, 0)
Promise.resolve().then(() => {
console.log(2)
})
console.log(1)
上面的代码最终输出:
1
2
3
本文原载于:baiyun.me