前端经典面试题合集

事件循环

前端经典面试题合集_第1张图片

  • 默认代码从上到下执行,执行环境通过script来执行(宏任务)
  • 在代码执行过程中,调用定时器 promise click事件…不会立即执行,需要等待当前代码全部执行完毕
  • 给异步方法划分队列,分别存放到微任务(立即存放)和宏任务(时间到了或事情发生了才存放)到队列中
  • script执行完毕后,会清空所有的微任务
  • 微任务执行完毕后,会渲染页面(不是每次都调用)
  • 再去宏任务队列中看有没有到达时间的,拿出来其中一个执行
  • 执行完毕后,按照上述步骤不停的循环

例子

前端经典面试题合集_第2张图片 前端经典面试题合集_第3张图片

自动执行的情况 会输出 listener1 listener2 task1 task2

前端经典面试题合集_第4张图片 前端经典面试题合集_第5张图片

如果手动点击click 会一个宏任务取出来一个个执行,先执行click的宏任务,取出微任务去执行。会输出 listener1 task1 listener2 task2

前端经典面试题合集_第6张图片

console.log(1)

async function asyncFunc(){
   
  console.log(2)
  // await xx ==> promise.resolve(()=>{console.log(3)}).then()
  // console.log(3) 放到promise.resolve或立即执行
  await console.log(3) 
  // 相当于把console.log(4)放到了then promise.resolve(()=>{console.log(3)}).then(()=>{
   
  //   console.log(4)
  // })
  // 微任务谁先注册谁先执行
  console.log(4)
}

setTimeout(()=>{
   console.log(5)})

const promise = new Promise((resolve,reject)=>{
   
  console.log(6)
  resolve(7)
})

promise.then(d=>{
   console.log(d)})

asyncFunc()

console.log(8)

// 输出 1 6 2 3 8 7 4 5

1. 浏览器事件循环

涉及面试题:异步代码执行顺序?解释一下什么是 Event Loop

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变

js代码执行过程中会有很多任务,这些任务总的分成两类:

  • 同步任务
  • 异步任务

当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:

我们解释一下这张图:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数

以上就是js运行的整体流程

面试中该如何回答呢? 下面是我个人推荐的回答:

  • 首先js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务
  • 当同步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行
  • 任务队列可以分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行
  • 当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
setTimeout(function() {
   
  console.log(1)
}, 0);
new Promise(function(resolve, reject) {
   
  console.log(2);
  resolve()
}).then(function() {
   
  console.log(3)
});
process.nextTick(function () {
   
  console.log(4)
})
console.log(5)
  • 第一轮:主线程开始执行,遇到setTimeout,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到微任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。
  • 第二轮:从宏任务队列开始,发现setTimeout回调,输出1执行完毕,因此结果是25431

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

前端经典面试题合集_第7张图片

console.log('script start');

setTimeout(function() {
   
  console.log('setTimeout');
}, 0);

console.log('script end');

不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobsmacrotask 称为 task

console.log('script start');

setTimeout(function() {
   
  console.log('setTimeout');
}, 0);

new Promise((resolve) => {
   
    console.log('Promise')
    resolve()
}).then(function() {
   
  console.log('promise1');
}).then(function() {
   
  console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver
    • 前端经典面试题合集_第8张图片

宏任务

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O 网络请求完成、文件读写完成事件
  • UI rendering
  • 用户交互事件(比如鼠标点击、滚动页面、放大缩小等)

宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务

前端经典面试题合集_第9张图片

所以正确的一次 Event loop 顺序是这样的

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的响应界面响应,我们可以把操作 DOM 放入微任务中

  • JavaScript 引擎首先从宏任务队列(macrotask queue)中取出第一个任务
  • 执行完毕后,再将微任务(microtask queue)中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行;
  • 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出,循环往复,直到两个 queue 中的任务都取完。

前端经典面试题合集_第10张图片

总结起来就是:一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务

2. Node 中的 Event loop

当 Node.js 开始启动时,会初始化一个 Eventloop,处理输入的代码脚本,这些脚本会进行 API 异步调用,process.nextTick() 方法会开始处理事件循环。下面就是 Node.js 官网提供的 Eventloop 事件循环参考流程

  • Node 中的 Event loop 和浏览器中的不相同。
  • NodeEvent loop 分为6个阶段,它们会按照顺序反复运行

前端经典面试题合集_第11张图片 前端经典面试题合集_第12张图片 前端经典面试题合集_第13张图片

  • 每次执行执行一个宏任务后会清空微任务(执行顺序和浏览器一致,在node11版本以上)
  • process.nextTick node中的微任务,当前执行栈的底部,优先级比promise要高

整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。

  • Timers 阶段 :这个阶段执行 setTimeoutsetInterval的回调函数,简单理解就是由这两个函数启动的回调函数。
  • I/O callbacks 阶段 :这个阶段主要执行系统级别的回调函数,比如 TCP 连接失败的回调。
  • idle,prepare 阶段 :仅系统内部使用,你只需要知道有这 2 个阶段就可以。
  • poll 阶段poll 阶段是一个重要且复杂的阶段,几乎所有 I/O 相关的回调,都在这个阶段执行(除了setTimeoutsetIntervalsetImmediate 以及一些因为 exception 意外关闭产生的回调)。检索新的 I/O 事件,执行与 I/O 相关的回调,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。

前端经典面试题合集_第14张图片

  • check 阶段setImmediate() 回调函数在这里执行,setImmediate 并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分,如下代码所示。
const fs = require('fs');
setTimeout(() => {
    // 新的事件循环的起点
    console.log('1'); 
}, 0);
setImmediate( () => {
   
    console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {
   encoding: 'utf-8'}, (err, data) => {
   
    if (err) throw err;
    console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
   
    console.log('poll callback');
});
// 首次事件循环执行
console.log('2');

在这一代码中有一个非常奇特的地方,就是 setImmediate 会在 setTimeout 之后输出。有以下几点原因:

  • setTimeout 如果不设置时间或者设置时间为 0,则会默认为 1ms
  • 主流程执行完成后,超过 1ms 时,会将 setTimeout 回调函数逻辑插入到待执行回调函数 poll 队列中;
  • 由于当前 poll 队列中存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate

因此这也验证了这句话,先执行回调函数,再执行 setImmediate

  • close callbacks 阶段 :执行一些关闭的回调函数,如 socket.on('close', ...)

除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列 Process.nextTick()

可以认为,Process.nextTick() 会在上述各个阶段结束时,在进入下一个阶段之前立即执行(优先级甚至超过 microtask 队列)

事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢

前端经典面试题合集_第15张图片

  • 微任务 :在 Node.js 中微任务包含 2 种——process.nextTickPromise微任务在事件循环中优先级是最高的,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise也存在优先级,process.nextTick 高于 Promise
  • 宏任务 :在 Node.js 中宏任务包含 4 种——setTimeoutsetIntervalsetImmediateI/O。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列

我们可以看到有一个核心的主线程,它的执行阶段主要处理三个核心逻辑。

  • 同步代码。
  • 将异步任务插入到微任务队列或者宏任务队列中。
  • 执行微任务或者宏任务的回调函数。在主线程处理回调函数的同时,也需要判断是否插入微任务和宏任务。根据优先级,先判断微任务队列是否存在任务,存在则先执行微任务,不存在则判断在宏任务队列是否有任务,有则执行。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {
   encoding: 'utf-8'}, (err, data) => {
   
    if (err) throw err;
    console.log('read file success');
});
setTimeout(() => {
    // 新的事件循环的起点
    console.log('setTimeout'); 
}, 0);
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
   
    console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {
   
    console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');

分析下上面代码的执行过程

  • 第一个事件循环主线程发起,因此先执行同步代码,所以先输出 start,然后输出 end
  • 第一个事件循环主线程发起,因此先执行同步代码,所以先输出 start,然后输出 end;
  • 再从上往下分析,遇到微任务,插入微任务队列,遇到宏任务,插入宏任务队列,分析完成后,微任务队列包含:Promise.resolve 和 process.nextTick,宏任务队列包含:fs.readFile 和 setTimeout
  • 先执行微任务队列,但是根据优先级,先执行 process.nextTick 再执行 Promise.resolve,所以先输出 nextTick callback 再输出 Promise callback
  • 再执行宏任务队列,根据宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile,这里需要注意,先执行 setTimeout 由于其回调时间较短,因此回调也先执行,并非是 setTimeout 先执行所以才先执行回调函数,但是它执行需要时间肯定大于 1ms,所以虽然 fs.readFile 先于setTimeout 执行,但是 setTimeout 执行更快,所以先输出 setTimeout ,最后输出 read file success
// 输出结果
start
end
nextTick callback
Promise callback
setTimeout
read file success

前端经典面试题合集_第16张图片

当微任务和宏任务又产生新的微任务和宏任务时,又应该

你可能感兴趣的:(javascript,1024程序员节)