1、解释 JavaScript 的执行机制。
JavaScript 的执行机制基于事件循环。事件循环包括一个任务队列(Task Queue)和一个微任务队列(Microtask Queue)。当一个函数被调用时,它被添加到微任务队列中。事件循环每次迭代都会先执行微任务队列中的所有任务,然后执行一个宏任务(Macrotask),例如脚本代码、setTimeout、setInterval、setImmediate、I/O、UI 渲染等。当一个宏任务执行完成后,事件循环会再次检查微任务队列,并执行所有的微任务。这个过程会一直循环下去,因此被称为“事件循环”。
2、解释 JavaScript 中的事件循环。
事件循环是 JavaScript 的核心运行机制,它负责调度和执行所有的任务。事件循环包括一个任务队列(Task Queue)和一个微任务队列(Microtask Queue)。当一个函数被调用时,它被添加到微任务队列中。事件循环每次迭代都会先执行微任务队列中的所有任务,然后执行一个宏任务(Macrotask),例如脚本代码、setTimeout、setInterval、setImmediate、I/O、UI 渲染等。当一个宏任务执行完成后,事件循环会再次检查微任务队列,并执行所有的微任务。这个过程会一直循环下去,因此被称为“事件循环”。
3、解释 JavaScript 中的任务队列和微任务队列。
任务队列(Task Queue)和微任务队列(Microtask Queue)都是事件循环中的重要概念。任务队列用于存储宏任务(Macrotask),例如脚本代码、setTimeout、setInterval、setImmediate、I/O、UI 渲染等。当一个宏任务被添加到任务队列中时,它会在下一个事件循环中被执行。微任务队列用于存储微任务(Microtask),例如 Promise 的回调函数、process.nextTick 等。微任务会在当前事件循环的宏任务执行完成后立即执行,无论它们何时被添加到微任务队列中。
4、为什么 setTimeout(fn, 0) 的执行会发生在下一轮事件循环?
setTimeout(fn, 0) 的执行会发生在下一轮事件循环,因为 setTimeout 将函数 fn 添加到任务队列中,而任务队列中的宏任务会在下一个事件循环中被执行。由于 setTimeout 的延迟参数为 0,所以 fn 会在下一个事件循环中被立即执行。
5、什么是宏任务和微任务?它们之间的区别是什么?
宏任务(Macrotask)和微任务(Microtask)是事件循环中的两种不同类型的任务。
宏任务包括整个脚本代码、setTimeout、setInterval、setImmediate、I/O、UI 渲染等。
微任务包括 Promise 的回调函数、process.nextTick 等。
它们之间的区别在于执行时机和执行顺序。每次事件循环迭代时,都会执行一个宏任务和所有的微任务,但是宏任务的执行会打断微任务的执行。因此,微任务的执行优先级高于宏任务。
6、解释一下 JavaScript 中的 Promise.then 和 Promise.catch 是如何工作的?它们和事件循环有什么关系?
Promise.then 和 Promise.catch 是 JavaScript 中处理异步操作的重要函数。Promise 对象代表一个异步操作的最终完成(或失败)及其结果值。
Promise.then 方法返回一个新的 Promise 对象,称为 thenable。thenable 表示一个可以像 Promise 一样进行处理的类似 Promise 对象。当 Promise 完成时,thenable 的回调函数将被触发。
Promise.catch 方法返回一个 Promise,该 Promise 在第一个拒绝的 Promise 或 thenable 拒绝时触发。
Promise.then 和 Promise.catch 都与事件循环有关。在 JavaScript 的执行模型中,有一个任务队列(task-queue)和一个微任务队列(microtask-queue)。当一个异步事件(如定时器、Ajax 请求)完成后,它的回调函数会被添加到微任务队列中。在下一次循环中,JavaScript 会先处理微任务队列中的所有任务,然后再处理任务队列中的任务。Promise.then 和 Promise.catch 的回调函数会在微任务队列中等待执行。当 Promise 完成或被拒绝时,相应的回调函数会被添加到微任务队列中,并在下一次循环中执行。
7、解释一下 JavaScript 中的 MutationObserver 和它与事件循环的关系。
MutationObserver 是一个提供监控 DOM 树更改的能力的接口。它被设计为在 DOM 树更改时提供低延迟的通知。MutationObserver 与事件循环的关系在于它的回调函数会在微任务队列中等待执行。当 DOM 树发生更改时,MutationObserver 的回调函数会被添加到微任务队列中,并在下一次循环中执行。
8、描述一下 JavaScript 中的 call stack 和 event loop。
在 JavaScript 中,执行环境(Execution Context)是用于描述 JavaScript 代码运行的环境的概念。当 JavaScript 代码被执行时,会创建一个新的执行环境,并且该环境会一直存在直到代码执行完成。执行环境主要包括 call stack 和 heap。
call stack 是一个后入先出(LIFO)的数据结构,用于存储当前执行中的函数调用。当一个函数被调用时,它被添加到 call stack 的顶部。当函数执行完成时,它从 call stack 中移除。
event loop 是一个循环的过程,它会不断地检查 call stack 是否为空。如果 call stack 不为空,event loop 会从 call stack 中取出一个任务(可以是宏任务也可以是微任务)执行,然后将结果添加到微任务队列中。然后,event loop 会检查微任务队列是否为空。如果微任务队列不为空,event loop 会从微任务队列中取出所有的微任务并执行它们。然后,event loop 会再次检查 call stack 是否为空,并重复这个过程。
9、什么是 JavaScript 的事件循环?请简述其工作原理。
事件循环是 JavaScript 的核心执行机制。它由一个或多个循环组成,每个循环称为一次循环或一次迭代。在每次循环中,事件循环会按照一定的顺序执行一系列的任务,包括处理用户交互、执行脚本代码、处理网络请求等。
事件循环的工作原理如下:
- 执行单次循环:事件循环开始执行单次循环。在单次循环中,事件循环会按照一定的顺序执行一系列的任务,包括处理用户交互、执行脚本代码、处理网络请求等。这些任务被称为宏任务(macro-tasks),包括 script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering 等。在单次循环的末尾,事件循环会检查微任务队列(microtask-queue)。
- 处理微任务:如果微任务队列中有任务,事件循环会立即执行所有的微任务,直到微任务队列为空。微任务包括 Promise.then、process.nextTick、MutationObserver 等。在单次循环的末尾处理微任务是很重要的,因为它们通常与异步操作相关联,而这些操作需要在当前的同步操作之后立即执行。
- 检查终止条件:如果当前的单次循环已经执行了所有的宏任务和微任务,并且没有其他的同步操作需要执行,那么事件循环就会结束当前的单次循环并开始下一个单次循环。否则,事件循环会继续等待下一个宏任务到达并开始下一个单次循环。
10、事件循环中的 microtask 和 macrotask 有什么区别?
在 JavaScript 的事件循环中,任务被分为两种类型:microtask 和 macrotask。
Macrotask:Macrotask 是指那些需要较长时间才能完成的任务,例如 script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI rendering 等。
Microtask:Microtask 是一种 JavaScript 任务,它的执行优先级高于 macrotask。Microtask 包括但不限于 Promise 的回调函数、MutationObserver 的回调函数等。
事件循环中的 microtask 和 macrotask 的主要区别在于它们的执行优先级和执行时间。Macrotask 会等待同步代码执行完毕后,再在下一次事件循环中执行,而 microtask 会在同步代码执行完毕后立即执行,无论是否还有 macrotask 等待执行。因此,microtask 的执行优先级高于 macrotask。
11、JavaScript 中的 Promise 和 microtask 有何关系?
Promise 和 microtask 有密切的关系。当 Promise 的状态发生改变时(即从 pending 变为 resolved 或 rejected),会触发 microtask 队列的执行。这意味着,Promise 的回调函数(无论是 .then 还是 .catch)都会被添加到 microtask 队列中,并在下一次事件循环中执行。
因此,我们可以利用 Promise 和 microtask 的关系来确保在异步操作完成之后执行的代码能够立即执行,而不需要等待当前的同步代码执行完毕。例如,如果我们在一个 Promise 的回调函数中进行异步操作,那么这个异步操作也会被添加到 microtask 队列中,并在下一次事件循环中执行。因此,我们可以利用这个特性来确保在异步操作完成之后执行的代码能够立即执行,而不需要等待当前的同步代码执行完毕。
案例
1.微任务:Promise 回调函数,宏任务:setTimeout 回调函数
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('end');
// start
// end
// Promise
// setTimeout
解析: 首先输出 start,然后通过 setTimeout 方法注册了一个回调函数,它会被添加到宏任务队列中。接着创建了一个 Promise 实例,并且通过 then 方法注册了一个回调函数,在 Promise 对象的状态改变时会执行这个回调函数。接着输出 end。因为 Promise 回调函数是微任务,所以它会被添加到微任务队列中,等待执行。等到主线程的同步任务执行完毕后,JavaScript 引擎会先执行微任务队列中的任务,输出 Promise,然后执行宏任务队列中的任务,输出 setTimeout。
2.微任务:process.nextTick 回调函数,宏任务:setImmediate 回调函数
console.log('start');
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('process.nextTick'));
console.log('end');
// start
// end
// process.nextTick
// setImmediate
解析: 在 Node.js 环境中,process.nextTick 回调函数是排在微任务队列最前面的,优先级比 Promise 回调函数还要高。而 setImmediate 回调函数是排在宏任务队列最后面的。所以,以上代码会先输出 start,然后输出 end。等到主线程的同步任务执行完毕后,JavaScript 引擎会先执行微任务队列中的任务,输出 process.nextTick,然后执行宏任务队列中的任务,输出 setImmediate。
3.微任务:Promise 回调函数,宏任务:requestAnimationFrame 回调函数
console.log('start');
requestAnimationFrame(() => console.log('requestAnimationFrame'));
Promise.resolve().then(() => console.log('Promise'));
console.log('end');
// start
// end
// Promise
// requestAnimationFrame
解析: 首先输出 start,然后通过 requestAnimationFrame 方法注册了一个回调函数,它会被添加到宏任务队列中。接着创建了一个 Promise 实例,并且通过 then 方法注册了一个回调函数,在 Promise 对象的状态改变时会执行这个回调函数。接着输出 end。因为 Promise 回调函数是微任务,所以它会被添加到微任务队列中,等待执行。等到主线程的同步任务执行完毕后,JavaScript 引擎会先执行微任务队列中的任务,输出 Promise,然后执行宏任务队列中的任务,输出 requestAnimationFrame。
4.微任务:Promise 回调函数,宏任务:XMLHttpRequest 回调函数
console.log('start');
const xhr = new XMLHttpRequest();
xhr.open('get','http://localhost:4000/get',true)
xhr.onload = () => console.log('XMLHttpRequest');
xhr.send();
Promise.resolve().then(() => console.log('Promise'));
console.log('end');
//start
//end
//Promise
//XMLHttpRequest
解析: 首先输出 start,然后创建了一个 XMLHttpRequest 对象,并且通过 open 方法和 send 方法发送了一个 GET 请求。接着通过 onload 方法注册了一个回调函数,在请求成功后会执行这个回调函数。接着创建了一个 Promise 实例,并且通过 then 方法注册了一个回调函数,在 Promise 对象的状态改变时会执行这个回调函数。接着输出 end。因为 Promise 回调函数是微任务,所以它会被添加到微任务队列中,等待执行。等到主线程的同步任务执行完毕后,JavaScript 引擎会先执行微任务队列中的任务,输出 Promise,然后等待 XMLHttpRequest 对象的回调函数执行。当请求成功后,JavaScript 引擎会执行宏任务队列中的任务,输出 XMLHttpRequest。
5.最终的案例:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('settimeout')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
script start、async1 start 、saync2、promise1、script end、async1 end、promise2、settimeout
解析:
1.执行整段代码,第一次执行同步任务:log(script start)
2.遇到定时器为异步任务先不执行
3.遇到async1()执行 打印async1 start 然后执行await 打印async2之后阻塞async1 end将其加入微任务列表
4.跳出函数继续向下执行 碰到new promise执行 打印promise1 之后遇到.then为微任务加入为微物列表
5.跳出后执行最后一行代码 打印script end 至此第一轮宏任务执行完毕 开始执行微任务
6.此时微任务列表按顺序打印 async1 end、promise2微任务 至此微任务执行完毕
7.开启新一轮的事件循环 开始执行下一个宏任务 即定时器 打印settimeout