关于前端:JS事件循环-Event-Loop

4次阅读

共计 5454 个字符,预计需要花费 14 分钟才能阅读完成。

前言

刚学前端的时候始终听他人说 JS 是单线程、单线程、单线程的,其实残缺的应该是在浏览器环境下 JS 执行引擎是单线程的。

那么什么是线程?为什么 JS 是单线程的?

1. 过程和线程

过程和线程的次要差异在于它们是不同的操作系统资源管理形式。过程有独立的地址空间,一个过程解体后,在保护模式下不会对其它过程产生影响,而线程只是一个过程中的不同执行门路。

我的了解,一个程序运行,至多有一个过程,一个过程至多有一个线程,过程是操作系统分配内存资源的最小单位,线程是 cpu 调度的最小单位。

打个比方,过程好比一个工厂,线程就是外面的工人,工厂内有多个工人,外面的工人能够共享外面的资源,多个工人能够一起协调工作,相似于多线程并发执行。

2. 浏览器是多过程的

关上 windows 工作管理器,能够看到浏览器开了很多个过程,每一个 tab 页都是独自的一个过程,所以一个页面解体当前并不会影响其余页面

浏览器蕴含上面几个过程:

  • Browser 过程:浏览器的主过程(负责协调、主控),只有一个
  • 第三方插件过程:每种类型的插件对应一个过程,仅当应用该插件时才创立
  • GPU 过程:最多一个,用于 3D 绘制等
  • 浏览器渲染过程(浏览器内核)(Renderer 过程,外部是多线程的):默认每个 Tab 页面一个过程,互不影响

3. 浏览器渲染过程

浏览器渲染过程是多线程的,也是一个前端人最关注的,它包含上面几个线程:

  • GUI 渲染线程

    • 负责渲染浏览器界面,解析 HTML,CSS,构建 DOM 树和 RenderObject 树,布局和绘制等。
    • 当界面须要重绘(Repaint)或因为某种操作引发回流 (reflow) 时,该线程就会执行
    • GUI 渲染线程与 JS 引擎线程是互斥的,当 JS 引擎执行时 GUI 线程会被挂起(相当于被解冻了),GUI 更新会被保留在一个队列中等到 JS 引擎闲暇时立刻被执行。
  • JS 引擎线程

    • 也称为 JS 内核,负责解决 Javascript 脚本程序。(例如 V8 引擎)
    • JS 引擎线程负责解析 Javascript 脚本,运行代码。
    • JS 引擎始终期待着工作队列中工作的到来,而后加以解决,一个 Tab 页(renderer 过程)中无论什么时候都只有一个 JS 线程在运行 JS 程序
    • 同样留神,GUI 渲染线程与 JS 引擎线程是互斥的,所以如果 JS 执行的工夫过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  • 事件触发线程

    • 归属于浏览器而不是 JS 引擎,用来管制事件循环(能够了解,JS 引擎本人都忙不过来,须要浏览器另开线程帮助)
    • 当 JS 引擎执行代码块如 setTimeOut 时(也可来自浏览器内核的其余线程, 如鼠标点击、AJAX 异步申请等),会将对应工作增加到事件线程中
    • 当对应的事件合乎触发条件被触发时,该线程会把事件增加到待处理队列的队尾,期待 JS 引擎的解决
    • 留神,因为 JS 的单线程关系,所以这些待处理队列中的事件都得排队期待 JS 引擎解决(当 JS 引擎闲暇时才会去执行)
  • 定时触发器线程

    • 传说中的 setInterval 与 setTimeout 所在线程
    • 浏览器定时计数器并不是由 JavaScript 引擎计数的,(因为 JavaScript 引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的精确)
    • 因而通过独自线程来计时并触发定时(计时结束后,增加到事件队列中,期待 JS 引擎闲暇后执行)
    • 留神,W3C 在 HTML 规范中规定,规定要求 setTimeout 中低于 4ms 的工夫距离算为 4ms。
  • 异步 http 申请线程

    • 在 XMLHttpRequest 在连贯后是通过浏览器新开一个线程申请
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由 JavaScript 引擎执行。

4. JS 引擎是单线程的

为什么 js 引擎是单线程的,一个起因是多线程复杂度会更高,另一个问题是后果可能是不可预期的:假如 JS 引擎是多线程的,有一个 div,A 线程获取到该节点设置了属性,B 线程又删除了该节点,so what?多线程并发执行下该怎么操作呢?

或者这就是为什么 JS 引擎是单线程的,代码从上而下程序的预期执行,尽管升高了编程老本,但也有其余问题,如果某个操作很耗时间,比方,某个计算操作 for 循环遍历 10000 万次,就会阻塞前面的代码造成页面卡顿 … …

GUI 渲染线程与 JS 引擎线程互斥的 ,是为了避免渲染呈现不可预期的后果,因为 JS 是能够获取 dom 的,如果批改这些元素属性同时渲染界面(即 JS 线程和 UI 线程同时运行),那么渲染线程前后取得的元素数据就可能不统一了。所以 JS 线程执行的时候,渲染线程会被挂起;渲染线程执行的时候,JS 线程会挂起, 所以 JS 会阻塞页面加载,这也是为什么 JS 代码要放在 body 标签之后,所有 html 内容之前;为了避免阻塞页面渲造成白屏

5. WebWorker

下面说了,JS 是单线程的,也就是说,所有工作只能在一个线程上实现,一次只能做一件事。后面的工作没做完,前面的工作只能等着。随着电脑计算能力的加强,尤其是多核 CPU 的呈现,单线程带来很大的不便,无奈充分发挥计算机的计算能力。

Web Worker,是为 JavaScript 发明多线程环境,容许主线程创立 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后盾运行,两者互不烦扰。等到 Worker 线程实现计算工作,再把后果返回给主线程。这样的益处是,一些计算密集型或高提早的工作,被 Worker 线程累赘了,主线程(通常负责 UI 交互)就会很晦涩,不会被阻塞或拖慢。

Web Worker 有几个特点:

  • 同源限度:调配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
  • DOM 限度:不能操作 DOM
  • 通信联系:Worker 线程和主线程不在同一个上下文环境,它们不能间接通信,必须通过音讯实现。
  • 脚本限度 :不能执行 alert() 办法和 confirm()办法
  • 文件限度:无奈读取本地文件

6. 浏览器渲染流程

上面是浏览器渲染页面的简略过程,具体讲又能够开一篇文章了~. ~:《从输出 URL 到页面渲染实现产生了什么》

    1. 用户输出 url,DNS 解析成申请 IP 地址
    1. 浏览器与服务器建设连贯(tcp 协定、三次握手),服务端解决返回 html 代码块
    1. 浏览器承受解决,解析 html 成 dom 树、解析 css 成 cssobj
    1. dom 树、cssobj 联合成 render 树
    1. JS 依据 render 树进行计算、布局、重绘
    1. GPU 合成,输入到屏幕

JS 事件循环

下面扯皮了一大堆,上面开始进入正题

1. 同步工作和异步工作

JS 有两种工作:

  • 同步工作
  • 异步工作

同步工作,顾名思义就是代码是同步执行的,异步代码就是代码是异步执行的,为什么 JS 要这么分呢?

咱们假如 JS 全副代码都是同步执行的,一个打包过后的 JS 有 10000 行代码,如果开始就遇到 setTimeout, 那么就须要等 100 秒能力执行前面的代码 … … 如果两头还有一些 io 操作和异步申请等,想想都令人解体

setTimeout(()=>{// todo},100000)

// 上面省略 10000 行代码

因为同步执行异步工作比拟耗时间,而且代码中绝大部分都是同步代码,所以咱们能够先执行同步代码,把这些异步工作交给其余线程去执行,如定时触发器线程、异步 http 申请线程等,而后等这些异步工作实现了再去执行他们。这种 调度同步、异步工作的策略,就是JS 事件循环

    1. 执行整体代码,如果是同步工作,就间接在主线程上执行,造成一个执行栈
    1. 当遇到异步工作的时候如网络申请等,就交给其余线程执行, 当异步工作执行完了,就往事件队列外面塞一个回调函数
    1. 一旦执行栈中的所有同步工作执行结束(即执行栈空),就会读取事件队列,取一个工作塞到执行栈中,开始执行
    1. 始终反复步骤 3

这就是事件循环了,确保了同步和异步工作有条不絮的执行,只有以后所有同步工作执行完了,主线程才会去读取事件队列, 看看有没有工作(异步工作执行完的第回调)要执行,每次取一个来执行。

老成长谈的 setTimeout

setTimeout(() => {console.log('异步工作');
}, 0);

console.log('同步工作');

置信你狠容易就能了解上面的执行后果,主线程扫描整体代码:

  • 发现有个异步工作 setTimeout,就挂起交由定时器触发线程(定时器会在期待了指定的工夫后将后果以回调模式放入到事件队列中期待读取到主线程执行),
  • 发现同步工作 console,间接塞入执行栈执行
  • 从上到下执行完了一遍
  • 执行栈处于闲暇状态,查看事件队列是否有工作(此时定时器执行完了),取出一个工作塞到执行战中执行
  • 事件队列清空

2. 宏工作(macro-task)、微工作(micro-task)

1. 宏工作、微工作

除了狭义的同步工作和异步工作,JavaScript 单线程中的工作能够细分为宏工作和微工作:

  • macro-task:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • process. nextTick, Promises, Object. observe, MutationObserver

2. 事件循环与宏工作、微工作

每次执行栈执行的代码就是一个宏工作(包含每次从事件队列中获取一个事件回调并放到执行栈中执行)

再检测本次循环中是否寻在微工作,存在的话就顺次从微工作的工作队列中读取执行完所有的微工作,再读取宏工作的工作队列中的工作执行,再执行所有的微工作,如此循环。JS 的执行程序就是每次事件循环中的宏工作 - 微工作。

  • 第一次事件循环,整段代码作为宏工作进入主线程执行
  • 同步代码被间接推到执行栈执行,遇到异步代码就挂起交由其余线程执行(执行完会往事件队列塞回调)
  • 同步代码执行完,读取微工作队列,若有执行所有微工作,微工作清空
  • 页面渲染
  • 从事件队列面里取一个宏工作塞入执行栈执行
  • 如此重复

用代码翻译一下就是

# 宏工作
for (let macrotask of macrotask_list) {
  # 执行一个宏工作
  macrotask(); 
  # 执行所有微工作
  for (let microtask of microtask_list) {microtask();
  }
  #  UI 渲染
  ui_render();}

3. 事件循环与页面渲染

在 ECMAScript 中,microtask(微工作) 称为 jobs,macrotask(宏工作) 可称为 task。

浏览器为了可能使得 JS 外部 task 与 DOM 工作可能有序的执行,会在一个 task 执行完结后,在下一个 task 执行开始前,对页面进行从新渲染:

(task -> 渲染 -> task ->…)

让咱们看一下例子,咱们有一个 id 为 app 的 div

<div id="app"> 宏工作、微工作 </div>

执行上面的代码会产生什么?

document. querySelector('#app').style.color = 'yellow'; 

Promise. resolve(). then(() => {document. querySelector('#app').style.color = 'red'; 
}); 

setTimeout(() => {document.querySelector('#app').style.color = 'blue'; 
  Promise.resolve(). then(() => {for (let i = 0; i < 99999; i++) {console.log(i);
    }
  }); 
}, 17); 

咱们间接看一下运行后果:

文字会先变红,而后过一段时间后会变蓝;咱们剖析一下程序是如何运行的:

  • 第一轮事件循环,遇到第一个同步工作塞进执行栈执行,dom 操作使文字 变黄, 遇到第二个是 Promise 微工作塞到微工作队列,持续往下,遇到宏工作 setTimeout 交由定时器触发线程
  • 第一轮宏工作执行完了,查看微工作队列发现有工作,执行并清空队列,dom 操作使文字 变红,此时 setTimeout 还没执行完
  • GUI 渲染线程进行渲染,使文字变红
  • 第二轮循环,执行栈为空,查看微工作队列为空,持续检测事件队列,发现曾经有后果了,塞入执行栈中执行
  • 执行 setTimeout 里的回调,执行第一个同步工作,dom 操作使文字变蓝,第二个是微工作塞入微工作队列,同步工作执行完了,发现微工作中有工作执行并清空队列,微工作里 console 是同步工作,此时 JS 线程始终在执行,GUI 渲染线程被挂起,始终等到外面的同步工作执行完
  • GUI 渲染线程进行渲染,使文字变蓝
  • 事件循环完结

HTML5 标准规定了 setTimeout()的第二个参数的最小值(最短距离),不得低于 4 毫秒,如果低于这个值,就会主动减少。

其中有一个问题是,谷歌下经测试并不玩全遵循两个宏工作之间执行 ui 渲染(谷歌的优化策略?),把 setTimeout 事件设置为 0,发现文字不会由黑 > 红 > 蓝,而是间接黑 > 蓝,为了模仿成果所以我把工夫距离设置为了 17ms(我的屏幕是 60HZ 也就是 16. 67ms 刷新一次)

4. Vue. $nextTick

应用 vue 的小伙伴们可能工作中可能会常常用到这个 api,Vue 的官网介绍:

将回调提早到下次 DOM 更新循环之后执行。在批改数据之后立刻应用它,而后期待 DOM 更新。

其外部实现就是利用了 microtask(微工作),来延时执行一段代码(获取 dom 节点的值), 即以后所有同步代码执行完后执行 microtask(微工作),可参照之前的文章:

Vue nextTick 源码

参考

原创 过程和线程的区别

Web Worker 应用教程

JS 是单线程,你理解其运行机制吗?

文章中的所有图片均来自网络

源码

源码


END

正文完
 0