一篇文章搞定 JS 事件循环

773 阅读6分钟

前言

JS 的执行机制、event loop、JS 异步执行原理,其实都是同一个东西,在面试时很容易被问到,今天笔者就跟各位聊聊这个问题。

前置知识

先说点大家都知道的:

  1. Javascript是一门单线程的语言,为啥是单线程,不能是多线程?(假如是多线程的,两个线程在同一时间操作了 DOM,到底以谁为准呢?这门语言的用途决定了它只能是单线程)这么一来,那我们写的代码执行就得有顺序了,是需要排队的,一排队呢就发现有些动作会阻塞在那里,要等很久才能接着往下执行,JS 就发展出了 异步 这个概念。
  2. JavaScript引擎把我们写的代码当成一个个任务排队执行。
  3. 任务可以分为同步任务异步任务
  4. 同步按你的代码顺序在主线程执行,异步不按照代码顺序执行,异步的执行效率更高。
  5. 通俗的解释一下异步:就是从主线程发射一个子线程来完成任务,子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但主线程无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,是无法将它合并到主线程中去的,为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。
  6. 异步任务有哪些:接口请求,setTimeoutsetInterval, Promise.thenprocess.nextTick异步的意思其实就是说你不知道什么时候会成功的任务,就比如请求接口,你不知道它啥时候会成功吧,你只知道将来可能会成功,然后进入回调执行逻辑,也可能失败,都不会执行回调的逻辑。有些掘友可能就会说了:
setTimeout(() => console.log(1), 1000)

这不是很明确吗,1秒钟过后会在控制台打印1

其实这并不是那么准确的1秒钟,其等待的时间取决于任务队列里待处理的任务数量。这里的1秒钟是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。

基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。

接下来看个例子。

思考题

const { log } = console;
log(1);
setTimeout(() => {
  log(2);
});
new Promise((resolve, reject) => { 
  log(3);
  resolve();
}).then(() => {
  log(4);
});

各位掘友可以先自己思考下打印的顺序,再往下看自己做的对不对

解析

  1. 执行const { log } = console;,把console.log 的引用给 log变量
  2. 打印 1
  3. 遇到异步任务setTimeout, 把回调函数添加到异步队列等待调用
  4. 遇到Promise,先打印 3, 这个位置的代码还是同步执行的,调用resolve()后会执行.then,不过是异步执行,添加到异步任务队列
  5. 然后就没代码了,此时异步任务队列中有两个任务[() => {log(2);}, () => {log(4);}],各位是不是觉得先进队列的先执行,先打印2, 再打印4,然而并不是,异步任务还可以再细分为 微任务宏任务
  6. 先说结论,先打印4,最后打印2
  7. 正确的顺序:1 -> 3 -> 4 -> 2
  8. 你说你不信?那看图说话:

image.png

别告诉我连浏览器你都不信。好了说说微任务宏任务

微任务有哪些

  • Promise.then
  • process.nextTick(node)
  • Promise.catch
  • Promise.finally
  • await

宏任务有哪些

  • 整体的JS代码
  • setTimeout
  • setInterval

JS 的事件循环(event loop)

概念

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务 并发模型与事件循环 - JavaScript | MDN (mozilla.org)

图解事件循环

image.png

应该还算清晰明了吧,做个题试试?

复杂的思考题

setTimeout(() => {
    console.log('set1 ')
    new Promise((resolve, reject) => {
        console.log('pr1 ')
        resolve();
    }).then(() => {
        console.log('then1 ');
    })
})

setTimeout(() => {
    console.log('set2 ')
})

new Promise((resolve, reject) => {
    console.log('pr2 ')
    resolve();
}).then(() => {
    console.log('then2 ');
})

new Promise((resolve, reject) => {
    console.log('pr3 ')
    setTimeout(() => {
        console.log('set3 ');
    })
    resolve();
}).then(() => {
    console.log('then3 ');
})
console.log(1);

正确的顺序:pr2 pr3 1 then2 then3 set1 pr1 then1 set2 set3

带上async

async function async1() {
  console.log('async1 start')
  async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2')
}
console.log('script start')
setTimeout(function () {
  console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('promisel')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

正确的顺序:script start -> async1 start -> async2 -> async1 end -> promisel -> script end -> promise2 -> setTimeout

带上async await

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')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('promisel')
  resolve()
}).then(function () {
  console.log('promise2')
})
console.log('script end')

正确的顺序:script start -> async1 start -> async2 -> promisel -> script end -> async1 end -> promise2 -> setTimeout

其实就当作把每个await后面的代码放到.then里执行就好

举个例子:

function fn1() {
  return new Promise((resolve, reject) => {
    resolve(1)
  })
}

function fn2() {
  return new Promise((resolve, reject) => {
    resolve(2)
  })
}

// 用async await语法糖
async function test() {
  const res1 = await fn1()
  console.log(res1) // 1
  const res2 = await fn2()
  console.log(res2) // 2
}

// 相当于
function test() {
  const res1 = fn1()
  res1.then(val1 => {
    console.log(val1) // 1
    const res2 = fn2()
    res2.then(val2 => {
      console.log(val2) // 2
    })
  })
}

Vue 的nextTick是微任务还是宏任务

当我们使用 Vue 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。异步更新可以帮助我们避免过度渲染页面以提升性能。 先说答案,VuenextTick准确来说是优先微任务

那答案是怎么来的呢?源码看来的,VuenextTick的实现在 源码的src/core/util/next-tick.js中,大致逻辑如下图(感兴趣的掘友可自行下载源码阅读分析)

image.png 实现非常的清晰,timerFunc就是用来异步执行回调的,如果支持Promise, 优先使用Promise,这就是答案的由来。

总结

  • 同步的任务按顺序执行
  • 异步任务分为微任务 和 宏任务
  • 微任务: Promise.thenprocess.nextTick(node)Promise.catchPromise.finallyawait
  • 宏任务:整体的JS代码setTimeoutsetInterval
  • 当宏任务执行完,会先执行微任务队列中的任务直到微任务队列为空,才会去执行宏任务队列中的宏任务
  • VuenextTick准确来说是优先微任务

相信看到这里,各位掘友已经理解了Javascript异步执行机制。

动动你发财的小手,点个赞吧 🌹🌹🌹