侧边栏壁纸
  • 累计撰写 218 篇文章
  • 累计创建 59 个标签
  • 累计收到 5 条评论

JS 事件循环学习笔记

barwe
2022-08-04 / 0 评论 / 0 点赞 / 800 阅读 / 3,433 字
温馨提示:
本文最后更新于 2022-08-04,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

单线程语言

JavaScript 是单线程语言,因为语法层面 JavaScript 根本没有提供创建多线程的 API,像其他语言

  • C++ 可以通过引入 pthread 头文件使用pthread_create()函数创建新的线程
  • Java 通过new Thread()可以初始化一个新线程
  • Python 提供了thread模块创建多线程
  • ……

然而 JavaScript 什么也没提供,它所有的函数都是在 主线程 中顺序执行的,同一时间只能执行某一个函数。

这个特性是由它特殊的应用场景决定的:JavaScript 用来处理页面和用户的交互操作,这种交互最终通过修改 DOM 反馈给用户。

DOM 不能同时被多个线程同时修改,这会带来非常不好的用户体验,所以在浏览器中只有执行 JavaScript 的 主线程 能够操作 DOM。

异步靠环境实现

异步 (asynchronous) 就是同一时间能够执行多个任务,显然 JavaScript 本身实现不了这个,因为它根本没有创建多线程的 API。

目前 JavaScript 就两个运行环境:浏览器和 Node.js,JavaScript 的 异步 特性实际上是运行 JavaScript 的运行环境实现的。

浏览器和 Node.js 各自实现 异步 的方案大致相同,又有所区别。

以浏览器为例,浏览器本身就是个多进程、多线程的大型应用,例如

  • 浏览器主进程
  • CPU 进程
  • 第三方插件进程
  • 渲染进程

其中 渲染进程 负责执行 JavaScript 代码,渲染页面和处理与用户的交互操作。

浏览器为 渲染进程 实现了多个线程,例如

  • GUI 渲染线程
  • JS 引擎线程
  • 事件触发线程
  • 定时器触发线程
  • 异步 http 请求线程

浏览器本身提供了一套方案来帮助 JavaScript 实现 异步 操作。

Node.js 大同小异。

消息队列和事件循环

一个 函数(包含普通函数、异步函数、类的构造函数、具名/匿名函数等)可以视为一个 任务

JavaScript 中的每个函数调用可以分成两类:同步任务异步任务

同步任务主线程 直接执行,此时如果 同步任务 比较耗时,那后面的其他任务就只能耐着性子等了。

异步任务 需要运行宿主(浏览器或者 Node.js)提供 其他线程 配合着执行,主线程 处理 异步任务 实际上只需要分派一下任务给宿主提供的其他线程即可,这个过程是很快的。

所以主线程能够很快(非阻塞)的执行完第一波主任务。

为什么说是第一波任务?主线程分派出去的异步任务最终还得收回来,由主线程自己执行回调,最终更新页面。

主线程分派任务、在异步任务执行完成之后主线程继续执行回调,这些都需要宿主来实现。

基本上所有语言都用 调用栈(Call Stack)来记录函数的执行过程,每个线程都有自己的调用栈。

setTimeout(() => console.log(0), 0)
console.log(1)

上面使用setTimeout()函数创建了一个定时器,它是一个异步任务。

JavaScript 主线程执行这段程序调用栈的变化如下:

  • setTimeout进栈,执行:
    • 检测到异步任务,交给宿主其他线程处理
  • setTimeout出栈
  • console.log进栈,执行:
    • 打印 1
  • console.log出栈

上述过程执行了两个任务:setTimeoutconsole.log(1),显然这个程序还没执行完,setTimeout还有个回调没有执行。

setTimeout任务被交给了宿主提供的 定时器触发线程 执行,定时器触发线程拿到这个任务,开始计时,指定的时间一到,就需要将预先设置的 回调任务 返回给 主线程

定时器触发线程显然不能影响主线程的执行过程,就算你立马可以返回回调任务(例如上面延时为 0 的定时器任务),也得等主线程有空了才能执行。

那么主线程什么时候有空呢?一个简单的方案就是,调用栈 里面没有东西了就说明主线程空闲了。

在此之前,定时器触发线程只能将要执行的回调任务放到一个临时的地方,即 消息队列(Message Queue)中。

消息队列只是一个数据结构,它不能自己判断什么时候需要将活儿派给主线程去干,此时就还需要一个宿主提供一个线程来专门干这个事,这就是 事件触发线程

事件触发线程 可以循环检测 消息队列调用栈,当消息队列中有待执行的任务以及调用栈中没有任务时,就取出一个任务放入调用栈中,然后主线程从调用栈取出任务开始执行。

执行完成之后将该任务出栈,此时调用栈又空了,然后 事件触发线程 重复这个过程。

宿主提供的第三方线程处理异步任务,处理完成之后将回调任务放入消息队列,通过事件触发线程的循环检测,在调用栈变空之后将消息队列中的任务依次放入调用栈,从而让主线程继续执行回调任务。

这种机制就是 事件循环(Event Loop)。

宏任务和微任务

ES6 引入了 微任务(microtask)的概念,它提高了异步任务执行的优先级。

异步任务 被细化成了 宏任务微任务

  • 原来常见的大部分异步任务都可以看做是一个宏任务
  • Promise,``MutationObserver,process.nextTick`等定义的任务都属于微任务

宏任务和微任务都有自己的任务队列(消息队列),它们执行的时机略有不同。

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

setTimeout(() => {
    console.log(1)
    new Promise(resolve => {
        console.log(2)
        resolve(3)
    }).then((v) => {
        console.log(4)
        console.log(v)
    })
}, 0)

console.log(5)
  • line1:setTimeout 进栈:它是一个异步函数,它将被交给定时器触发线程计时,1s 后回调任务会被推入 宏任务队列
  • line1:setTimeout 出栈
  • line5:setTimeout 进栈:它也会被交给定时器触发线程计时,0s 后回调任务被推入 宏任务队列
  • line5:setTimeout 出栈
  • line16:console.log 入栈:打印 5
  • line16:console.log 出栈

然后栈空了。上述过程非常块,因为没有阻塞操作。

事件触发线程从 宏任务队列 中取出 line5:setTimeout 的回调任务

  • line5:setTimeout callback 入栈
  • line6:console.log 入栈:打印 1
  • line6:console.log 出栈
  • line7:new Promise() 入栈:它的回调会被立即执行
  • line8:console.log 入栈:打印 2
  • line8:console.log 出栈
  • line9:resolve 入栈:更新 Promise 对象的状态为 'fullFilled', 值为 3
  • line9:resolve 出栈
  • line10:then 入栈:设置 Promise 对象的 onFullFilled 回调,因为状态已经改变,line10:then callback 直接被推入 微任务队列
  • line10:then 出栈

此时 宏任务队列 中还有一个任务,微任务队列 中也出了一个任务。

在每次执行完 宏任务 之后,都会将 微任务队列 中的 微任务 逐个执行清空。

在清空微任务队列的过程可能有新的微任务进来,也会一并执行清空。

所以接下来

  • line10:then callback 入栈
  • line11:console.log 入栈,打印 4,出栈
  • line12:console.log 入栈,打印 3,出栈
  • line10:then callback 出栈

现在调用栈又空了,如果距离 line1:setTimeout 调用已经过去 1s,事件触发线程会将 宏任务队列 中的一个任务取出来放进去

  • line1:setTimeout callback 入栈
  • line2:console.log 入栈,打印 0,出栈
  • line1:setTimeout callback 出栈

现在调用栈又空了,微任务队列也空了,宏任务队列也空了,主线程就空闲了。

所以最后的打印顺序是:5 1 2 4 3 0

总结一下:

  • ES6 之后宿主会维护两个消息队列:宏任务队列和微任务队列
  • 从宏任务队列中取出一个宏任务执行完成之后,都会立即清空微任务队列中的所有任务,包括清空过程中新增的微任务
  • 创建宏任务的异步函数一般有:I/O, setTimeout, setInterval, setImmediate (Node.js), requestAnimationFrame (Browser)
  • 创建微任务的异步函数一般有:process.nextTick (Node.js), MutationObserver (Browser), Promise.then/catch/finally
0

评论区