EventLoop其实如此简单

浏览器的EventLoop

浏览器机制:

浏览器的主要组件包括:
  1. 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的你请求的页面外,其他显示的各个部分都属于用户界。
  2. 浏览器引擎 - 在用户界面和渲染引擎之间传送指令。
  3. 渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  4. 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  5. 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  6. JavaScript 解释器。用于解析和执行 JavaScript 代码,比如chrome的javascript解释器是V8。
  7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如Cookie。新的HTML规范(HTML5)定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。
浏览器渲染流程:

  1. render:渲染引擎解析HTML文档,并将文档中的标签转化为dom节点树,即”内容树”。同时,它也会解析外部CSS文件以及syle标签中的样式数据。这些样式信息连同HTML中的”可见内容”一道,被用于构建另一棵树——”渲染树(Render树)”。渲染树由一些带有视觉属性(如颜色、大小等)的矩形组成,这些矩形将按照正确的顺序显示在频幕上。
  2. layout:渲染树构建完毕之后,将会进入”布局”处理阶段,即为每一个节点分配一个屏幕坐标。
  3. painting:即遍历render树,并使用UI后端层绘制每个节点。
浏览器的单线程和任务队列:

  1. 浏览器是单线程,Js的主要用途是与用户互动以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定Js同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器不知道以哪个线程为准,会产生混乱。
  2. 浏览器是单线程,并表示只有一个线程而是只拥有一个主线程js解析和ui渲染,其他异步任务有其单独的线程,例如:DOM事件、ajax调用、setTimeout
  3. dom事件、ajax调用、定时器等异步任务会开单独的线程,它们会往异步队列中存放回调函数,不阻塞主线程的运行
  4. 主线程执行完成之后会从异步队列中取出回调函数运行
  5. 异步队列中存在宏任务队列(task)和微任务(microtask)队列
  6. 宏任务:script(内嵌和外链)、setImmediate、MessageChannel、setTimeout,微任务:Promise.then、MutationObserver

浏览器EventLoop过程:

宏任务处理:
  1. 选择当前要执行的任务队列task,选择一个最先进入任务队列的任务,如果没有任务可以选择,则会跳转至microtask的执行步骤。 将事件循环的当前运行任务设置为已选择的任务。
  2. 运行宏任务。
  3. 将事件循环的当前运行任务设置为null。
  4. 将运行完的任务从任务队列task中移除。 microtasks步骤:进入microtask检查点(performing a microtask checkpoint )。
  5. 更新界面渲染。
  6. 返回第一步。
微任务处理(microtask的执行步骤):
  1. 设置进入microtask检查点的标志为true。
  2. 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当前运行任务为已选择的microtask
  3. 运行microtask;设置事件循环的当前运行任务为null;将运行结束的microtask从microtask队列中移除。
  4. 对于相应事件循环的每个环境设置对象(environment settings object),通知它们哪些promise为rejected。
  5. 清理indexedDB的事务。
  6. 设置进入microtask检查点的标志为false。

上面的过程可以总结为:

  1. 查看task中是否存在任务,如果存在则执行宏任务,执行完毕将其从任务队列task中删除
  2. 如果task中不存在任务,查看microtask是否存在任务,存在执行微任务,执行完毕将其从microtask;否则执行下一轮循环重新查看task
代码实例分析:
console.log('script start');

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

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

console.log('script end');

//script start
//script end
//promise1
//promise2
//setTimeout
复制代码
  1. 开始时,task中只有script,则script中所有函数放入stack中按顺序执行执行。
  2. 执行到setTimeout,script执行完后会将回调函数放入task队列中,将在下一个事件循环中执行。
  3. 执行到Promise,Promise属于microtask,所以会将第一个.then()放入microtask队列。
  4. 当script代码执行完毕后,此时task为空。开始检查microtask队列,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。
  5. microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',异步代码执行完毕。

node的EventLoop

node中的处理流程:

  1. v8引擎从上到下解析node主程序
  2. 当调用fs,buffer等nodeAPI时会调用底层的libuv函数库,利用多线程+事件池实现同步非阻塞先将回调放在异步队列EventQueue中
  3. 当调用底层的libuv库的方法成功后会找到EventQueue中相应的回调函数执行,并将结果返回。

node中的EventLoop和浏览器中的EventLoop存在一些差别,node是通过多线程来实现的,可以同时处理多个任务。当其中一个任务完成时,相应的callback被插入到轮询队列中,最终被执行。

node中的任务队列:

  1. timers:执行setTimeout()和setInterval安排的回调
  2. I/O callbacks: 执行几乎所有异常的close回调,由timer和setImmediate执行的回调。
  3. idle,prepare: 只用于内部
  4. poll : 获取新的I/O事件,node在该阶段会适当的阻塞
  5. check : setImmediate的回调被调用
  6. close callbacks: e.g socket.on(‘close’,…);

node中EventLoop流程:

  1. timers,定时器阶段: 执行定时任务(setTimeOut(), setInterval())
  2. poll 轮询阶段:
    • 处理到期的定时器任务,然后(因为最开始阶段队列为空,一旦队列为空,就会检查是否有到期的定时器任务)
    • 处理队列任务,直到队列空,或达到上限
    • 如果队列为空:如果setImmediate,终止轮询阶段,进入检查阶段执行。如果没setImmediate,查看有没有定时器任务到期,有的话就到timers阶段,执行回调函数.
  3. check 检查阶段:轮询阶段空闲,且有setImmediate的时候,进入检查阶段

上述的五个阶段都是按照先进先出的规则执行回调函数。按顺序执行每个阶段的回调函数队列,直至队列为空或是该阶段执行的回调函数达到该阶段所允许一次执行回调函数的最大限制后,才会将操作权移交给下一阶段,否则的话不会进入下一个阶段。

区分setImmediate()与setTimeout()

从上面的poll和check阶段的逻辑,我们可以看出setImmediate和setTimeout、setInterval都是在poll阶段执行完当前的I/O队列中相应的回调函数后触发的。但是这两个函数却是由不同的路径触发的:

  1. setImmediate函数,是在当前的pollqueue对列执行后为空或是执行的数目达到上限后,eventloop直接调入check阶段执行setImmediate函数。
  2. setTimeout、setInterval则是在当前的pollqueue对列执行后为空或是执行的数目达到上限后,eventloop去timers检查是否存在已经到期的定时器,如果存在直接执行相应的回调函数。
  3. 程序中既有setTimeout和setImmediate时,在非I/O循环(主模块)中,顺序不固定;在I/O循环中setImmdiate回调总是先执行
//在非I/O循环(主模块)中,顺序不固定
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
复制代码
// 在I/O循环中setImmdiate回调总是先执行
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
复制代码

区分process.nextTick()与setImmediate()

  1. process.nextTick() 函数是不管当前正在eventloop的哪个阶段,在当前阶段执行完毕后,跳入下个阶段前的瞬间执行;setImmediate() 函数是在poll阶段后进去check阶段事执行
  2. process.nextTick() 函数的应用
//允许线程在进入event loop下一个阶段前做一些关于处理异常、清理一些无用或无关的资源。
function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,new TypeError('argument should be string'));
}
复制代码
//在进入下个event loop阶段前,并且回调函数还没有释放回调权限时执行一些相关操作。
//在MyEmitter构造函数实例化前注册“event”事件,这样就可以保证实例化后的函数可以监听“event”事件。
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});
复制代码

结语:

以上就是关于EventLoop的介绍,如果有错误欢迎指正,本文参考:

  1. 什么是浏览器事件循环(EventLoop)
  2. 不要混淆nodejs和浏览器中的event loop
  3. 快速掌握Nodejs系列之—Events模块
  4. 深入理解nodejs Event loop
  5. Nodejs 解读event loop的事件处理机制
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值