目录

🍎 JavaScript 事件循环相关

相关问题:

  • 对事件循环的理解。

# 概念

JavaScript 是一门单线程的语言。意味着同一时间内只做一件事情,但不意味着单线程就是阻塞,而实现单线程不阻塞的方法就是事件循环。

JavaScript 中的所有的任务都可以分为 同步任务 和 异步任务:

  • 同步任务:立即执行的任务,同步任务一般直接进入主线程中;
  • 异步任务:异步执行的任务。例如 AJAX 网络请求; setTimeout 定时函数。

如下图,同步任务与异步任务的运行流程图:

image-20220702232908991

同步任务进入主线程,即主执行栈;异步任务进入任务队列,主线程内的任务执行完毕为空,就会去任务队列中读取对应的任务,推入到主线程执行,上述过程不断重复形成 事件循环

#

🌰 例子:

::: demo[vanilla]

<html>
  <button id="btn">
    启动定时器
  </button>
</html>
<script>
document.getElementById("btn").onclick = function () {
  var start = Date.now();
  alert("启动定时器前");
  setTimeout(function () {
    console.log("定时器执行了", Date.now() - start);
  });
  alert("启动定时结果");
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

:::

定时器真的是定时执行吗:定时器并不能保证真正定时执行,一般会存在可以接受的延迟,但也有可能受代码量的影响带来不能接受的很长的延迟时间。

定时器回调函数是在分线程执行的吗:在主线程执行的,JavaScript 是单线程的。

定时器是如何实现的:事件循环模型。

如何证明 JavaScript 是单线程的:

  • setTimeout() 的回调函数是在主线程执行的。
  • 定时器回调数只有运行栈中的代码全部运行完才有可能执行。

为什么 JavaScript 要用单线程模式,而不用多线程模式:

  • JavaScript 的单线程 ** 与它的用途有关:** 作为浏览器的脚本语言,JavaScript 的主要用途是与用户互动以及操作 DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题

# 微任务与宏任务

🌰 例子:

console.log(1)

setTimeout(()=>{
    console.log(2)
}, 0)

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

console.log(3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

按照上面的 事件循环机制:

  • console.log(1) ,同步任务,主线程中执行;
  • setTimeout() ,异步任务,放到 Event Table ,0 毫秒后 console.log(2) 回调推入 Event Queue 中;
  • new Promise ,同步任务,主线程直接执行;
  • .then ,异步任务,放到 Event Table
  • console.log(3) ,同步任务,主线程执行;

所以按照这个分析,输出的结果应该是: 1 => new Promise => 3 => 2 => then

但是实际的结果应该是: 1 => new Promise => 3 => then => 2

因为异步任务的执行也有顺序。事件队列其实是一个 先进先出的数据结构,排在前面的事件优先被主线程读取。但是这里 .then 的执行却早于 setTimeout 的回调事件。

原因在于异步任务还可以细分为 微任务与宏任务。

# 微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后,当前宏任务结束之前。

常见的微任务有:

# 宏任务

宏任务的时间粒度较大,执行的时间间隔不能精确控制,对于一些高实时性的需求不太符合。

常见的宏任务有:

  • script
  • setTimeout
  • UI 渲染事件 / UI rendering
  • postMessage / MessageChannel
  • setImmediateI/O (NodeJS)

# 微任务与宏任务的关系

事件循环中:

image-20220703001451670

执行一个宏任务,如果遇到微任务就将它放进微任务的事务队列中。

当前宏任务执行完成之后,会查看微任务的时间队列,然后将里面的所有微任务依次执行完。

回到上面的例子:

  • 遇到 console.log(1) ,直接打印 1
  • 遇到定时器,属于新的宏任务,留着后面执行;
  • 遇到 new Promise ,这个是直接执行的,打印 'new Promise'
  • 遇到 .then 属于微任务,放入微任务队列,后面再执行;
  • 遇到 console.log(3) 直接打印 3
  • 本轮宏任务执行完毕,然后去微任务列表中查看是否有微任务,发现 .then 的回调,执行它,打印 ‘.then’
  • 当一次宏任务执行完,再去执行新的宏任务,这里就剩下一个宏任务,执行它,打印 2

# asyncawait

async 是异步的意思, await 则可以理解为 async wait 。所以可以理解 async 就是用来声明一个异步方法,而 await 是用来等待异步方法执行。

# async

async 函数返回一个 promise 对象。

🌰 例子:

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}
1
2
3
4
5
6
7
8

两种方法等效。

# await

正常情况下, await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

🌰 例子:

async function f() {
  return await 123
  // return 123
}

f().then(v => console.log(v)) // 123
1
2
3
4
5
6

不管 await 后面跟着什么, await 都会阻塞后面的代码:

async function fn1() {
  console.log(1)
  await fn2()
  console.log(2)
}

async function fn2 (){
  console.log('fn2')
}

fn1()
console.log(3)
1
2
3
4
5
6
7
8
9
10
11
12

await 会阻塞下面的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码。

所以输出的结果的顺序为 1 => fn2 => 3 => 2

📢 上次更新: 2022/09/02, 10:18:16