浏览器的EventLoop
浏览器机制:
浏览器的主要组件包括:
用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的你请求的页面外,其余显示的各个部分都属于用户界。
浏览器引擎 - 在用户界面和渲染引擎之间传送指令。
渲染引擎 - 负责显示请求的内容。若是请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
网络 - 用于网络调用,好比 HTTP 请求。其接口与平台无关,并为全部平台提供底层实现。
用户界面后端 - 用于绘制基本的窗口小部件,好比组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操做系统的用户界面方法。
JavaScript 解释器。用于解析和执行 JavaScript 代码,好比chrome的javascript解释器是V8。
数据存储。这是持久层。浏览器须要在硬盘上保存各类数据,例如Cookie。新的HTML规范(HTML5)定义了“网络数据库”,这是一个完整(可是轻便)的浏览器内数据库。
浏览器渲染流程:
render:渲染引擎解析HTML文档,并将文档中的标签转化为dom节点树,即”内容树”。同时,它也会解析外部CSS文件以及syle标签中的样式数据。这些样式信息连同HTML中的”可见内容”一道,被用于构建另外一棵树——”渲染树(Render树)”。渲染树由一些带有视觉属性(如颜色、大小等)的矩形组成,这些矩形将按照正确的顺序显示在频幕上。
layout:渲染树构建完毕以后,将会进入”布局”处理阶段,即为每个节点分配一个屏幕坐标。
painting:即遍历render树,并使用UI后端层绘制每一个节点。
浏览器的单线程和任务队列:
浏览器是单线程,Js的主要用途是与用户互动以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定Js同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器不知道以哪一个线程为准,会产生混乱。
浏览器是单线程,并表示只有一个线程而是只拥有一个主线程js解析和ui渲染,其余异步任务有其单独的线程,例如:DOM事件、ajax调用、setTimeout
dom事件、ajax调用、定时器等异步任务会开单独的线程,它们会往异步队列中存放回调函数,不阻塞主线程的运行
主线程执行完成以后会从异步队列中取出回调函数运行
异步队列中存在宏任务队列(task)和微任务(microtask)队列
宏任务:script(内嵌和外链)、setImmediate、MessageChannel、setTimeout,微任务:Promise.then、MutationObserver
浏览器EventLoop过程:
宏任务处理:
选择当前要执行的任务队列task,选择一个最早进入任务队列的任务,若是没有任务能够选择,则会跳转至microtask的执行步骤。 将事件循环的当前运行任务设置为已选择的任务。
运行宏任务。
将事件循环的当前运行任务设置为null。
将运行完的任务从任务队列task中移除。 microtasks步骤:进入microtask检查点(performing a microtask checkpoint )。
更新界面渲染。
返回第一步。
微任务处理(microtask的执行步骤):
设置进入microtask检查点的标志为true。
当事件循环的微任务队列不为空时:选择一个最早进入microtask队列的microtask;设置事件循环的当前运行任务为已选择的microtask
运行microtask;设置事件循环的当前运行任务为null;将运行结束的microtask从microtask队列中移除。
对于相应事件循环的每一个环境设置对象(environment settings object),通知它们哪些promise为rejected。
清理indexedDB的事务。
设置进入microtask检查点的标志为false。
上面的过程能够总结为:javascript
查看task中是否存在任务,若是存在则执行宏任务,执行完毕将其从任务队列task中删除
若是task中不存在任务,查看microtask是否存在任务,存在执行微任务,执行完毕将其从microtask;不然执行下一轮循环从新查看task
代码实例分析:
console.log('script start' );
set Timeout(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
//set Timeout
复制代码
开始时,task中只有script,则script中全部函数放入stack中按顺序执行执行。
执行到setTimeout,script执行完后会将回调函数放入task队列中,将在下一个事件循环中执行。
执行到Promise,Promise属于microtask,因此会将第一个.then()放入microtask队列。
当script代码执行完毕后,此时task为空。开始检查microtask队列,执行.then()的回调函数输出'promise1',因为.then()返回的依然是promise,因此第二个.then()会放入microtask队列继续执行,输出'promise2'。
microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,当即执行回调函数输出'setTimeout',异步代码执行完毕。
node的EventLoop
node中的处理流程:
v8引擎从上到下解析node主程序
当调用fs,buffer等nodeAPI时会调用底层的libuv函数库,利用多线程+事件池实现同步非阻塞先将回调放在异步队列EventQueue中
当调用底层的libuv库的方法成功后会找到EventQueue中相应的回调函数执行,并将结果返回。
node中的EventLoop和浏览器中的EventLoop存在一些差异,node是经过多线程来实现的,能够同时处理多个任务。当其中一个任务完成时,相应的callback被插入到轮询队列中,最终被执行。java
node中的任务队列:
timers:执行setTimeout()和setInterval安排的回调
I/O callbacks: 执行几乎全部异常的close回调,由timer和setImmediate执行的回调。
idle,prepare: 只用于内部
poll : 获取新的I/O事件,node在该阶段会适当的阻塞
check : setImmediate的回调被调用
close callbacks: e.g socket.on(‘close’,…);
node中EventLoop流程:
timers,定时器阶段: 执行定时任务(setTimeOut(), setInterval())
poll 轮询阶段:
处理到期的定时器任务,而后(由于最开始阶段队列为空,一旦队列为空,就会检查是否有到期的定时器任务)
处理队列任务,直到队列空,或达到上限
若是队列为空:若是setImmediate,终止轮询阶段,进入检查阶段执行。若是没setImmediate,查看有没有定时器任务到期,有的话就到timers阶段,执行回调函数.
check 检查阶段:轮询阶段空闲,且有setImmediate的时候,进入检查阶段
上述的五个阶段都是按照先进先出的规则执行回调函数。按顺序执行每一个阶段的回调函数队列,直至队列为空或是该阶段执行的回调函数达到该阶段所容许一次执行回调函数的最大限制后,才会将操做权移交给下一阶段,不然的话不会进入下一个阶段。node
区分setImmediate()与setTimeout()
从上面的poll和check阶段的逻辑,咱们能够看出setImmediate和setTimeout、setInterval都是在poll阶段执行完当前的I/O队列中相应的回调函数后触发的。可是这两个函数倒是由不一样的路径触发的:ajax
setImmediate函数,是在当前的pollqueue对列执行后为空或是执行的数目达到上限后,eventloop直接调入check阶段执行setImmediate函数。
setTimeout、setInterval则是在当前的pollqueue对列执行后为空或是执行的数目达到上限后,eventloop去timers检查是否存在已经到期的定时器,若是存在直接执行相应的回调函数。
程序中既有setTimeout和setImmediate时,在非I/O循环(主模块)中,顺序不固定;在I/O循环中setImmdiate回调老是先执行
//在非I/O循环(主模块)中,顺序不固定
set Timeout(function timeout () {
console.log('timeout' );
}, 0);
set Immediate(function immediate () {
console.log('immediate' );
});
复制代码
// 在I/O循环中set Immdiate回调老是先执行
const fs = require('fs' );
fs.readFile(__filename, () => {
set Timeout(() => {
console.log('timeout' );
}, 0);
set Immediate(() => {
console.log('immediate' );
});
});
复制代码
区分process.nextTick()与setImmediate()
process.nextTick() 函数是无论当前正在eventloop的哪一个阶段,在当前阶段执行完毕后,跳入下个阶段前的瞬间执行;setImmediate() 函数是在poll阶段后进去check阶段事执行
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的介绍,若是有错误欢迎指正,本文参考:chrome
什么是浏览器事件循环(EventLoop)
不要混淆nodejs和浏览器中的event loop
快速掌握Nodejs系列之—Events模块
深刻理解nodejs Event loop
Nodejs 解读event loop的事件处理机制