首页 > 编程语言 >JavaScript – event loop 事件循环, 单线程, Web Worker

JavaScript – event loop 事件循环, 单线程, Web Worker

时间:2022-10-30 00:34:45浏览次数:79  
标签:Web 异步 单线程 代码 JavaScript JS callback 线程 执行

前言

因为要写 RxJS 系列, 有一篇要介绍 scheduler. 它需要基础的 JS 执行机制, 于是就有了这里篇. 

顺带也介绍以下 Web Worker 呗.

 

参考

知乎 – 详解JavaScript中的Event Loop(事件循环)机制

掘金 – 彻底搞懂JavaScript事件循环

关于JavaScript单线程的一些事

阮一峰 – Web Worker 使用教程

 

游览器与 JavaScript 的线程

游览器是多线程的

但是呢, 负责执行 JavaScript 的却只有一条 JS 线程.

UI 渲染则是由另一条 GUI 线程负责. 虽然是分开两条线程, 但是游览器有个规则, 这 2 条线程不能同时运行. 

所以 JS 跑的时候 UI 是不跑的. 这也就限制了 JS 不能处理耗时运算, 一旦 JS 跑太久, UI 不渲染, animations 什么的全都会卡死.

 

JavaScript 的执行机制

first JS code

游览器从 URL 下载到 HTML 后, 开始解析, 当遇到 <script> 标签后去获取 JS 代码 (inline or src)

然后依据 defer or async 决定什么时候执行. 

JS code to 执行栈 (execution stack)

当要执行时, 游览器会把 JS 代码放入 exec stack (想象它是一个 box)

exec stack 接获代码后就开始执行. 我们先用简单的同步代码为例子.

const value = '';
for (const number of [1, 2, 3, 4, 5]) {
  console.log(number);
}

执行完以后, JS 线程休息, 轮到 UI 线程去渲染. 这样就算完成了一个周期 (执行 JS + 渲染 = 1 周期)

执行异步代码

上面我们以同步代码为例, 这里我们换成异步代码 (Ajax). 

执行 JS...遇到 Ajax...发送请求....这时就遇到一个等待的问题.

请求发送以后, 需要等待 server response, 这可能是一个漫长的过程. 如果 JS 线程就傻傻的等. 那么 UI 渲染就完蛋了 (记得, JS 线程一定要尽快的执行完, 完成一个周期)

于是就有了异步这个概念, 我们把要等待 response 才能继续执行的代码叫 callback, 不需要等待 response 依然能继续执行的代码叫同步代码.

当 exec stack 遇到异步代码后, 它会把 callback 存起来, 然后继续执行后续的同步代码. 这样 JS 线程就不需要傻傻等了. 

执行完同步代码后, 就渲染 UI. 这样一个周期就结束了.

callback to event queue

当 response 回来以后, 游览器会找出刚才保存的 callback 代码. 然后把它放进 event queue (想象它是另一个 box). 

然后等待 exec stack 完成当前的周期, 再把 event queue 的代码放进 exec stack, 然后周而复始.

其它异步代码

除了上面提到的 Ajax 以外. SetTimeout, Event Listenner, Promise 这些都是异步代码, 都有 callback.

Macro Task vs Micro Task

异步代码中还有细分 Macro Task 和 Micro Task.

SetTimeout, Event Listenner 属于 Macro Task 

Promise.resolve, MutaionObserver 属于 Micro Task

它们的执行时机不同.

exec stack 执行完 JS 代码后, 会先去看 event queue 有没有 Micro Task 可以执行. 如果有就执行. 没有的话就完成这次周期.

然后去看 event queue 有没有 Macro Task 可以执行. 

Promise, SetTimeout, requestAnimationFrame 触发时机

requestAnimationFrame(() => {
  console.log('4. async requestAnimationFrame – next next cycle');
});

setTimeout(() => {
  console.log('3. async setTimeout – next cycle');
}, 4);

Promise.resolve().then(() => {
  console.log('2. async Promise – this cycle');
});

console.log('1. sync console – this cycle');

结果

Console 是同步代码, 在当前周期执行.

Promise 是异步代码, callback 进 event queue, 同时它是 Micro Task. exec stack 执行完后会马上去执行 Micro Task 才结束周期. 所以它依然是当前周期.

SetTimeout 是异步代码 Macro Task. 它的默认值是 4ms 后执行. 所以它会进入 event queue, 肯定不是当前周期.

requestAnimationFrame 是异步代码 Macro Task, 它大约是 60ms 后执行 (这个不一定哦, 有时候甚至不到 4ms 它就执行了. 游览器有它的算法), 它也是进入 event queue, 肯定不是当前周期.

上面 4 行代码, Macro Task 就会产生新的周期, 所以一定会有 3 个周期.

为什么 SetTimeout 时机不准?

timeout 的计数不是 JS 线程负责的, 游览器有一条计数的线程. 时间到的时候, callback 会被放入 event queue.

但是并不一定马上执行. 如果 exec stack 正忙着, 时间自然就被耽搁了. 就不准了咯.

所以, 不管有多少线程帮忙分工, 执行 JS 的始终只有一条 JS 线程. 所以还是有许多局限的.

耗时的 CPU 操作导致阻塞

异步的解决思路是靠 callback, JS 线程不等待就不会阻塞.

但是如果我跑 for loop 100亿次呢? JS 线程忙不过来, 那就导致 UI 不渲染, 一样完蛋.

所以才有了 Web Worker. Web Worker 和 SetTimeout 是一样的概念 (游览器给予多一条线程来帮忙计数, 于是 JS 线程就 free 了)

当开启 Web Worker 后, 游览器会创建一条线程去处理 JS 代码 (比如 for loop 100亿次), 这时 JS 线程就 free 了. 

然后就等 callback 咯.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

JavaScript 执行机制 Event loop

要想搞清楚执行机制, 需要理解许多术语.

同步, 异步, 执行栈, 事件队列, 宏任务, 微任务

这一篇已经讲的非常仔细了, 如果你想正规的去理解它的话, 建议你直接看那篇文章.

我这里只是简单描述以下.

同步代码 / 任务 / 操作

绝大部分我们写的代码都是同步的, 所以只要记得哪些是异步代码就可以分清楚了.

const str = '';
for (const value of [1, 2, 3]) {
  console.log(value);
}

同步代码是会导致 UI 阻塞的, 比如 for loop 100 亿次, 整个页面都会卡死掉. animation 跑不了了.

解决方法就是通过 Web Worker 去跑 100亿次, 这样 JS 主线程就 free 了就不会阻塞到 UI 渲染.

异步代码 / 任务 / 操作

下面是常见的异步代码, 它们的特色就是都有 callback

setTimeout(() => {}, 1000);
setInterval(() => {}, 1000);
document.addEventListener('click', () => {});
fetch('/product.json').then(() => {});

ajax, IO, timer, listenner, 这些操作在 "等待" 的时候是不需要 CPU 的. 所以 JS 主线程没有必要去 "等" 它们的 response.

 

 

 

 

 

 

 

 

 

有 2 种操作是很耗时的.

同步操作 (CPU)

JS 只有一条主线程, 如果我们跑一个耗时的算法, 比如 for loop 100 亿次

那么整个页面都会卡死掉. animation 跑不了了. 这就是阻塞了. 

解决方法就是用 Web Worker. 这个我们下面再仔细讲.

异步操作 (IO / Network)

另一种耗时的操作是发 HTTP 请求 (ajax) 和 读取 IO.

但这种耗时和 for loop 100 亿次的耗时, 并不相同。

for loop 100亿次, 时间完完全全都花在 CPU 上.

ajax 呢, 它的时间是花在等待网路 response 上. 而不是 CPU.

所以, JS 引擎很聪明, 等 IO, Network 的时候, CPU 是可以继续用的.

等磁盘和网路接收到资料后, 给 CPU 一个 callback 就可以链接回去了.

这个就叫异步, 这就是为什么 ajax 请求不管多久, 页面都不会卡, 因为等 ajax 的时候, JS 线程是 free 的.

执行栈与事件队列

执行栈是一个 box. 里面装着预执行的 JS 代码. 这些代码就排队咯. 一个一个按顺序执行.

一直到执行完毕, 游览器就会渲染页面.

那是谁把 JS 代码放进去的呢? 

第一, <script> 万物的起点, 游览器解析到 <script> 标签, 拿到 JS 代码后就会丢进去执行栈

第二, 事件触发. event listenner, setTimeout, promise callback 等等

同步/异步 操作

当执行栈遇到一个同步操作 (for loop), 它就执行咯.

如果遇到一个异步操作 (ajax), 那它发了请求之后不会做任何等待, 会继续执行接下来的代码, 一直到把所有在执行栈里操作执行完.

而 ajax response 后, 会先进入到事件列队里等待. 只有当执行栈空闲了以后, 它才会把事件列队的 callback 代码放入到执行栈中执行.

 

 

 

 

 

 

 

 

 

  

 

标签:Web,异步,单线程,代码,JavaScript,JS,callback,线程,执行
From: https://www.cnblogs.com/keatkeat/p/16839839.html

相关文章