浅谈浏览器与node中的事件循环

区分进程与线程

很多新手对于进程和线程的关系都分不清,这很正常。

先从概念说起

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

举个例子

进程就像一个公司,公司有它独立的资源、空间

公司之间是相互独立的

线程是隶属于该公司下面的员工

一家公司下面有一个或者多个员工

员工之间共享资源、空间

完善一下概念

公司的资源、空间 -> 系统分配的内存(独立的一块内存)

公司之间的相互独立 -> 进程之间相互独立

多个员工合作完成任务 -> 多个线程在进程中协作完成任务

公司内有一个或多个员工 -> 一个进程由一个或多个线程组成

员工之间共享资源、空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

浏览器是多进程的

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了 (所以每一个Tab标签对应一个进程并不一定是绝对的)

浏览器有哪些进程

  1. Browser进程:浏览器的主进程(负责协调、主控),只有一个

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  2. 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建

  3. GPU进程:最多一个,用于3D绘制等

  4. 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为

    • 页面渲染,脚本执行,事件处理等

前端重点:浏览器渲染进程

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

    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运行代码。
    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序
    • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  2. 事件触发线程

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
  3. 定时触发器线程

    • 传说中的setInternal与setTimeout所在线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    • 注意,W3C在HTML5标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  4. 异步http请求线程

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

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,所以自然有误差。

浏览器中的Event Loop

  1. 所有同步任务都在主线程上执行,形成一个执行栈
  2. 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列然后渲染,将队列中的事件放到执行栈中依次执行
  4. 主线程从任务队列中读取事件,这个过程是循环不断的

node中的Event Loop

我们先来张图看看node是如何工作的

  • 我们写的js代码会交给v8引擎进行处理
  • 代码中可能会调用nodeApi,node会交给libuv库处理
  • libuv通过阻塞i/o和多线程实现了异步io
  • 通过事件驱动的方式,将结果放到事件队列中,最终交给我们的应用。

事件循环进阶:macrotask与microtask

上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题:

console.log('开始');

setTimeout(function() {
    console.log('延时器');
}, 0);

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
}).then(function() {
    console.log('promise3');
}).then(function() {
    console.log('promise4');
});

console.log('结束');
复制代码

现在,它的执行顺序是这样的

开始
结束
promise1
promise2
promise3
promise4
延时器
复制代码

为什么呢?因为Promise里有了一个一个新的概念:microtask

或者,进一步,JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task

它们的定义?区别?简单点可以按如下理解:

  • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
    • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
    • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(`task->渲染->task->...`)
复制代码
  • microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

    • 也就是说,在当前task任务后,下一个task之前,在渲染之前
    • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
    • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前) 分别很么样的场景会形成macrotask和microtask呢?
  • macrotask:主代码块、setTimeout、setInterval、Promise的构造函数是同步的、setImmediate、I/O、UIrendering(可以看到,事件队列中的每一个事件都是一个macrotask)

  • microtask:Object.observe (已废弃),MutationObserver(不兼容,已废弃),MessageChannel(vue中 nextClick 实现原理)等

浏览器和 node 环境执行顺序不同,浏览器是先把一个栈以及栈中的微任务走完,才会走下一个栈。node 环境里面是把所有栈走完,才走微任务

node环境

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

  • timers:执行满足条件的setTimeout、setInterval回调。
  • I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
  • idle,prepare:可忽略
  • poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
  • check:执行setImmediate的回调。
  • close callbacks:关闭所有的closing handles,一些onclose事件。

几个队列

除上述循环阶段中的任务类型,我们还剩下浏览器和node共有的microtask和node独有的process.nextTick,我们称之为Microtask Queue和NextTick Queue。 我们把循环中的几个阶段的执行队列也分别称为Timers Queue、I/O Queue、Check Queue、Close Queue。

循环之前

在进入第一次循环之前,会先进行如下操作:

  • 同步任务
  • 发出异步请求
  • 规划定时器生效的时间
  • 执行process.nextTick()

开始循环

按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:

  • 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
  • 进入下轮循环。

可以看出,nextTick优先级比promise等microtask高。setTimeout和setInterval优先级比setImmediate高。

注意

  • 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
  • setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。

参考文章

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值