JS笔记《异步与事件循环》

101 阅读4分钟

浏览器的进程模型

  • 进程:程序运行需要有它专属的内存空间,可以把这块内存空间简单的理解为进程。每个应用至少有一个进程,进程之间相互独立,即使要通信,也需要双方同意。

  • 线程:一个进程至少有一个线程,进程开启后会自动创建一个线程,该线程称为主线程。如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,一个进程中可以包含多个线程。

  • 浏览器是多进程多线程的,当浏览器启动时,为了减少连环崩溃的几率,会自动启动多个进程,其中最主要的进程有:

  1. 浏览器进程:主要负责界面显示(非网页)、用户交互(非网页内)、子进程管理等。该进程会启动多个线程处理不同的任务。
  2. 网络进程:负责加载网络资源,该进程会启动多个线程处理不同的网络任务。
  3. 渲染进程:渲染进程启动后会开启一个渲染主线程,负责执行HTML、CSS、JS代码。默认情况下,浏览器会为每一个标签页开启一个新的渲染进程,以保证不同标签页之间不相互影响。

同步与异步

同步

  • JS是一门单线程的语言,因为它运行在渲染主进程中,而渲染主线程在每个标签页中只有一个,意味着同一时间只能做一件事。渲染主线程要执行解析HTML、解析CSS、执行JS等,如果使用同步的方式执行计时任务、网络通信、事件监听等任务时,就会导致主线程阻塞,从而影响消息队列中其它任务执行,给用户造成卡死现象。

异步

  • 当执行到计时器、网络通信、事件监听任务时,主线程会将这些任务交给一个新线程去处理,自身立即结束任务的执行,继续执行消息队列中的任务。当其他线程完成时会通知主线程,将回调函数包装成任务加入到消息队列的末尾,等待主线程执行。这样浏览器就永不会阻塞,保证了单线程的流畅运行。

异步任务

宏任务

  • <script>代码块
  • 定时器 setTimeout、setInterval
  • 事件监听 addEventListener
  • ajax
  • 回调函数

微任务

  • Promise(Promise是同步任务,执行resolve或reject时是异步操作)
  • MutationObserver(会在指定的DOM发生变化时被调用)
  • process.nextTick

执行栈

  • 当一个函数被调用之前,JS会解析这段代码,创建一个函数执行上下文(执行环境),将其中同步代码按照执行顺序加入到执行栈中,然后从栈顶开始执行。当函数执行结束时,JS会退出当前函数的执行环境,对应的执行上下文销毁,然后回到上一个执行环境继续执行,直到执行栈中的代码全部执行完毕。

v2-2f761eb83b50f53d741e6aa1f15a9db1_b.webp

事件循环与消息队列

  1. 当遇到异步代码时,JS并不会一直等待其返回结果,而是将其交给其他子线程去处理,主线程会继续执行执行栈中的同步代码。

  2. 当子线程接收到处理结果后,会将对应的回调函数放置到对应的消息队列(宏任务队列、微任务队列)中,等待执行。

  3. 当执行栈中的任务全部执行完毕或遇到空闲时间时,主线程开始检查微任务队列,按照顺序执行队列中的所有微任务。

  4. 当微任务执行完毕后,主线程检查宏任务队列,取出第一个宏任务进行执行。

  5. 当宏任务执行完毕后,再次检查微任务队列,如果有微任务继续执行所有微任务。

  6. 执行下一个宏任务……循环往复……

以上整个过程叫做事件循环

代码解析

// 位置 1
setTimeout(function () {
  console.log('timeout1');
}, 1000);

// 位置 2
console.log('start');

// 位置 3
Promise.resolve().then(function () {
  // 位置 5
  console.log('promise1');
  // 位置 6
  Promise.resolve().then(function () {
    console.log('promise2');
  });
  // 位置 7
  setTimeout(function () {
    // 位置 8
    Promise.resolve().then(function () {
      console.log('promise3');
    });
    // 位置 9
    console.log('timeout2')
  }, 0);
});

// 位置 4
console.log('done');

// 1. 位置1为异步宏任务,1s后放入宏任务队列
// 2. 打印位置2 start
// 3. 位置3 then() 为异步微任务,放入微任务队列
// 4. 打印位置4 done 
// 5. 执行微任务队列,打印位置5 promise1
// 6. 位置6 then() 为异步微任务,放入微任务队列
// 7. 位置7为异步宏任务,放入宏任务队列
// 8. 执行微任务队列,打印位置6 promise2
// 9. 执行宏任务队列,将位置8 then()放入微任务队列,打印位置9 timeout2
// 10.执行微任务队列,打印位置8 promise3
// 11.1s后,执行宏任务,输出timeout1