浏览器多进程和事件循环详解

参考链接

由于网上的关于浏览器进程和JS进程、JS线程和事件循环之间的关系模糊不清,这里主要是查阅资料进行详细汇总

关于浏览器多进程和JS单线程的介绍:https://juejin.im/post/5a6547d0f265da3e283a1df7
关于Node事件循环的参考:https://juejin.im/post/5c337ae06fb9a049bc4cd218
关于浏览器时间循环的参考:https://juejin.im/post/5b8f76675188255c7c653811
关于宏任务微任务的详细介绍:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

多进程的浏览器

浏览器包含以下进程

  1. Browser进程,浏览器的主进程(负责协调,控制),只有一个
  2. 第三方插件进程
  3. GPU进程,最多一个,用于3D绘制
  4. 浏览器渲染进程(浏览器内核):默认每个tab页面一个进程,互不影响。主要作用为(页面渲染、脚本执行、事件处理)

浏览器组成概览:

image

区分进程和线程

  • 进程是一个工厂,工厂有它的独立资源
  • 工厂之间相互独立
  • 线程是工厂中的工人,多个工人协作完成任务
  • 工厂内有一个或多个工人
  • 工人之间共享空间

转换成专业术语:

  • 进程是系统分配的内存,是cpu资源分配的最小单位(独立的一块内存)
  • 进程之间相互独立
  • 多个线程在进程中协作完成任务
  • 一个进程由一个或者多个线程组成
  • 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)
  • 线程是cpu调度的最小单位

每打开一个tab页,相当于创建了一个独立的浏览器渲染进程
通过右上角->更多工具->任务管理器可以查看到:


image

可以查看到进程的类型:


image

多进程优势

  • 单个page 崩溃不会影响整个浏览器
  • 避免第三方插件崩溃影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

浏览器内核(渲染进程)

页面的渲染,JS的执行,事件的循环,都在这个进程内进行。
浏览器的渲染进程是多线程的。

渲染进程的多线程

  1. GUI渲染线程
    • 负责渲染浏览器界面,解析HTML\CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列重等到JS引擎空闲时立即被执行。
  2. JS引擎线程
    • 也称为JS内核,负责处理JS脚本程序。例如V8引擎
    • JS引擎线程负责解析JS脚本,运行代码
    • JS引擎等待任务队列中的任务到来,然后加以处理,一个Tab页(渲染进程)无论什么时候都只有一个JS线程在运行JS程序
    • GUI渲染线程和JS引擎线程互斥,所以如果JS执行时间过长,就会造成页面的渲染不连贯,阻塞页面渲染。
  3. 事件触发线程
    • 用来控制事件循环
    • 当JS引擎执行代码例如setTimeOut,或者其他线程(鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 由于JS单线程的关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(JS引擎空闲时才会去执行 )
  4. 定时触发器线程
    • setInterval与setTimeout所在线程
    • 浏览器定时计数器并不是由js引擎计数的(因为js引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确)
  5. 异步http请求线程
    • XMLHTTPRequest在连接后新开一个线程请求
    • 检测到状态变更时,如果有设置回调函数,异步线程产生状态变更事件,将这个回调再放入事件队列中,再由js引擎执行。

由于JS可以操作DOM,如果在修改这些元素属性同时渲染界面,那么渲染线程前后获得的元素数据就可能不一致。
如果js执行时间过长,就会阻塞页面。

浏览器Browser进程和浏览器内核的通信过程

  1. Browser进程收到用户请求,首先需要获取页面内容,通过网络下载资源,随后将该任务通过RenderHost接口传递给渲染进程(内核)
  2. 渲染进程接收到消息,简单解释后,交给渲染线程,然后开始渲染
    • 渲染线程接收到请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(可能造成回流与重绘)
    • 最后渲染进程将结果传递给Browser进程
  3. Browser进程接受到结果并将结果绘制出来

WebWorker

创建Worker时,js引擎向浏览器申请开一个子线程,子线程是浏览器开的,完全受主线程控制,而且不能操作DOM
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
所以如果有非常耗时的工作,可以单独开一个worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程,只待计算出结果后,将结果通信给主线程即可。

浏览器渲染流程

输入URL,浏览器主进程接管,开启一个下载线程,进行HTTP请求,将响应内容转发给渲染进程。

输入URL的浏览器行为

整个过程被分为9个小块:

  • 提示卸载旧文档
  • 重定向/卸载旧文档
  • 应用缓存
  • DNS域名解析
  • TCP握手
  • HTTP请求处理
  • HTTP响应处理
  • DOM处理
  • 文档装载完成

渲染(上图的Processing阶段)可以分为以下步骤:

  1. 解析HTML建立DOM
  2. 解析CSS建立CSSOM
  3. 合成render树(Layout/reflow)
  4. 绘制render树(paint)
  5. 浏览器将各层的信息发给GPU,GPU将各层合成(composite),显示在屏幕上

load事件与DOMContentLoaded事件的先后

  • DOMContentLoaded事件触发时,仅当DOM加载完成,不包括样式表,图片
  • 当onload事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成。

CSS加载不会阻塞DOM树解析,但会阻塞render树渲染,因为render树需要css信息,所以css要在头部加载

回流(reflow)与重绘(repaint)

网页生成的时候,至少会渲染一次,用户访问的过程中,还会不断的重新渲染
以下三种情况,会导致网页重新渲染:

  • 修改DOM
  • 修改样式表
  • 用户事件(比如鼠标悬停、页面滚动、输入框输入文字、改变窗口大小等等)

重新渲染,就需要重新生成布局和重新绘制。前者叫做回流也叫重排(reflow)后者叫做重绘(repaint)。重绘不一定需要重排,比如改变某个元素的颜色,就只会触发重绘,因为布局没有改变。但是重排必然导致重绘,比如改变一个元素的位置。

优化

提高网页性能,就是要降低重排和重绘的频率,尽量少触发重新渲染。

DOM变动和样式变动,都会触发重新渲染,但是浏览器已经很智能了,会尽量把所有的变动集中在一起,排成一个队列,然后一次性执行,避免多次渲染。

例如:

div.style.color = 'blue';
div.style.marginTop = '30px';

元素有两个样式变动,但是浏览器只会触发一次重排和重绘
如果写的不好,就会触发两次重排和重绘

div.style.color = 'blue';
var margin = parseInt(div.style.marginTop);
div.style.marginTop = (margin + 10) + 'px';
//上面代码对div元素设置背景色后,第二行要求浏览器给出该元素的位置,所以浏览器不得不立即重排。

如果有以下读操作,都会引发浏览器立即重新渲染。

offsetTop/offsetLeft/offsetWidth/offsetHeight
scrollTop/scrollLeft/scrollWidth/scrollHeight
clientTop/clientLeft/clientWidth/clientHeight
getComputedStyle()

所以,从性能角度考虑,尽量不要把读操作和写操作放在一个语句里面

// bad
div.style.left = div.offsetLeft + 10 + "px";
div.style.top = div.offsetTop + 10 + "px";

// good
var left = div.offsetLeft;  //读
var top  = div.offsetTop;   //读
div.style.left = left + 10 + "px"; //写
div.style.top = top + 10 + "px";  //写

一般规则

  • 样式表越简单,重排和重绘就越快。
  • 重排和重绘的DOM元素层级越高,成本就越高。
  • table元素的重排与重绘成本,要高于div元素
1.DOM的多个读操作,应该放在一起,不要两个读操作之间,加入一个写操作

2.如果某个样式是通过重排得到的,那么最好缓存结果,避免下一次用到的时候,浏览器又要重排

3.不要一条条的改变样式,而要通过改变class或者csstext属性,一次性改变样式
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";

// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";

4.尽量使用离线DOM,而不是真实的网面DOM,来改变元素样式,比如操作Document Fragment对象,完成后再把这个对象加入DOM。再比如使用cloneNode()方法,在克隆的节点商进行操作,然后再用克隆的节点替换原始节点。

5.先将元素设为dispaly:none(需要一次重排和重绘),然后对这个节点进行100次操作,最后再恢复显示(需要1次重排和重绘)。这样一来,就用两次重新渲染,取代了可能高达100次的重新渲染。

6.position属性为absolute或者fixed的元素,重排的开销会比较小,因为不用考虑它对其他元素的影响,但会引起图层的重绘。

7.只在必要的时候,才将元素的dispaly属性为可见,因为不可见的元素不影响重排和重绘,另外visibility:hidden的元素只对重绘有影响,不影响重排。

8.使用虚拟DOM的脚本库

9.使用 window.requestAnimationFrame()、window.requestIdleCallback() 这两个方法调节重新渲染

10.使用硬件加速的复合图层

普通图层和复合图层

浏览器渲染的图层一般包括两大类:普通图层以及复合图层
普通文档流内可以理解为一个复合图层。其次absolute布局和fixed,虽然可以脱离普通文档流,但它仍然属于默认复合层

可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘。
GPU中各个复合图层是单独绘制的,所以互不影响

将元素变成新的复合图层的方法(硬件加速):

  • css translate3d、translateZ
  • opacity属性/过渡动画,执行过程中才会创建合成层,动画没有开始或者结束后元素还会回到之前的状态
  • will-change属性,<video><iframe><canvas><webgl>等元素

absolute和硬件加速的差别:
asolute虽然可以脱离普通文档流,但仍属于默认复合图层,所以就算absolute的改变不会影响普通文档流中的render树(也就是不会引起其他元素的重排),但是仍然会引起重绘,由于浏览器绘制时,是整个复合图层绘制的,所以absolute仍然会影响整个复合图层的绘制。
硬件加速在新的复合图层,不互相影响。

同步任务和异步任务

js分为同步任务和异步任务

同步任务都在主线程上执行,形成一个执行栈

主线程运行时会产生执行栈,栈中的代码调用某些api时(例如DOM、ajax、计时器)有返回结果时,它们会在任务队列中添加任务。

事件触发线程管理着任务队列,只要异步任务有了运行结果,就在任务队列中放置一个任务。

一旦执行栈中所有同步任务执行完毕,系统就会读取任务队列,执行任务队列中的回调。

所以setTimeout推入的事件不一定能准时执行,因为可能在它推入到任务队列时,主线程还不空闲,正在执行其他代码,所以自然有误差。

setTimeout和setInterval语句的执行是在主线程中的,但他们的计时是由定时器线程控制的,计时结束后定时器线程会将回调推入到任务队列中

image

JS线程的内存空间

内存空间分为栈(stack)、堆(heap)、池(一般归类于栈)

6种基本类型Number、Boolean、String、Undefined、Null、Symbol(es6)是存储在栈内存中的
其他类型Function、Array、Object(实际上function和array都是基于Object实现的),存储在堆内存中,但地址存储在栈内存中
栈(stack)遵循的原则是“先进后出”,JS种的基本数据类型与指向对象的地址存放在栈内存中,此外还有一块栈内存用来执行JS主线程,执行栈(execution context stack)

通过以下两个例子可以了解到,浅拷贝即拷贝地址(拷贝对象属性的改变会引起源对象属性的改变),深拷贝则是新的一块内存

// 基本数据类型-栈内存
let a1 = 0;
// 基本数据类型-栈内存
let a2 = 'this is string';
// 基本数据类型-栈内存
let a3 = null;

// 对象的指针存放在栈内存中,指针指向的对象存放在堆内存中
let b = { m: 20 };
// 数组的指针存放在栈内存中,指针指向的数组存放在堆内存中
let c = [1, 2, 3];
复
image

定时器线程

调用setTimeout或者setInterval后,定时器线程控制特定时间后,将任务添加到任务队列

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

console. log('begin');
//先输出begin再hello

原因如下:

  1. 虽然代码的本意是0ms后就推入事件队列,但是W3C规定,setTimeout中低于4ms的时间间隔算为4ms。
  2. 即使真的0ms推入事件队列,也会先执行begin(可执行栈空闲后才会读取事件队列)

setTimeout与setInterval

  • setTimeout执行有误差
  • setInterval每次都精确的推入一个事件
  • 但setInterval有几个致命问题
  1. 累计效应,如果setInterval代码在再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔。
  2. 浏览器最小化时,setInterval回掉函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行。
    一般使用setTimeout或者requestAnimationFrame来替代setInterval

浏览器的Event Loop(事件循环机制)

宏任务和微任务都属于异步任务
宏任务:macrotask,也叫做tasks,一些异步任务的回调回进入macro task queue,等待后续被调用
这些异步任务包括:

  • setTimeout/setInterval
  • setImmediate(Node独有)
  • requestAnimationFrame(浏览器独有)
  • I/O
    • network events(http等协议请求)
    • 用户交互事件(例如鼠标点击click事件、键盘输入keydown事件等)
    • 文件相关操作(个人理解)
  • UI rendering(浏览器独有)

微任务:microtask,也叫jobs,一些异步任务的回调回依次进入micro task queue。
这些异步任务包括:

  • process.nextTick(Node独有,调用顺序在Promise之前)
  • Promise
  • Object.observe
  • MutationObserver(用于监听DOM)

Event Loop事件循环的完整流程:

image

从上图可以得出代码执行的具体流程:

  1. 执行全局的同步代码,同步代码中包含同步语句和异步语句(例如setTimeout和Promise)
  2. 同步代码执行完毕,执行栈Stack清空(代码往往由scirpt开始,为宏任务,所以下一步执行微任务)
  3. 从微任务队列microtask queue中取出位于队首的微任务回调,放入执行栈Stack中执行
  4. 继续将微任务队列中取于队首的任务放入执行栈中执行,直到微任务队列为空,如果在执行微任务回调的过程中,又产生了微任务,那么会被加入到微任务队列的队尾,也会在这个周期被调用执行
  5. 取出宏任务队列中处于队首的任务,放入执行栈中执行
  6. 执行完毕后,执行栈为空,继续重复3-5

可以注意到以下几点:

  • 宏任务队列macrotask queue一次只从队列中取出一个任务执行,执行完后就去执行微任务队列中的任务
  • 微任务队列中所有的任务都会被依次取出来执行,直到微任务队列为空
  • 渲染的时机处于微任务执行完毕之后,下一个宏任务执行之前

宏任务与微任务和线程之间的关系:

  • 执行栈处于JS引擎线程中,跟普通栈一样,具有先进后出的原则,这样确保了函数内能够访问外部变量
  • 宏任务和微任务的队列都存放的是宏任务和微任务的回调处理函数,这些队列的任务会被放入事件队列,事件队列是由事件触发线程管理的,事件队列是先进先出的
  • 宏任务setInterval、setTimeout是经过计时器线程计数后,将回调放入宏任务队列中的。即setInterval、setTimeout是由计时器线程管理,回调函数由事件触发器线程管理
  • 宏任务http请求是由异步HTTP请求线程处理的,处理完毕后将其回调函数放入宏任务队列中。
  • 宏任务队列和微任务队列都属于事件队列
console.log(1);

setTimeout(() => {
  //timeOutCallBack1
  console.log(2);
  Promise.resolve().then(() => {
    //promiseCallBack2
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  //promiseCallback1
  console.log(data);
})

setTimeout(() => {
  //timeOutCallBack2
  console.log(6);
})

console.log(7);

//执行顺序
1、4、7、5、2、3、6

分析:
第一步:执行同步代码
输出1、4、7
macrotask queue:[timeOutCallBack1,timeOutCallBack2]
microtask queue:[promiseCallback1]

第二步:执行微任务队列promiseCallback1
输出1、4、7、5
stack queue:[promiseCallback1]
macrotask queue:[timeOutCallBack1,timeOutCallBack2]
microtask queue:[]

第三步:由于微任务队列为空,执行宏任务队列队首timeOutCallBack1
输出1、4、7、5、2
此时timeOutCallBack1宏任务中加入了promiseCallBack2微任务
stack queue:[timeOutCallBack1]
macrotask queue:[timeOutCallBack2]
microtask queue:[promiseCallBack2]

第四步:微任务队列不为空,执行微任务队列promiseCallBack2
输出1、4、7、5、2、3
stack queue:[promiseCallBack2]
macrotask queue:[timeOutCallBack2]
microtask queue:[]

第五步:微任务为空,执行宏任务
stack queue:[timeOutCallBack2]
macrotask queue:[]
microtask queue:[]
输出1、4、7、5、2、3、6

再参考如下

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

//1、4、10、5、6、7、2、3、9、8

上面的例子都是由Promise作为微任务的典型代表,这里看一下DOMObserver是否也属于微任务:

<div class="outer">
  <div class="inner"></div>
</div>
// Let's get hold of those elements
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// Let's listen for attribute changes on the
// outer element
// 监听Outer元素的属性变动
new MutationObserver(function() {
  //监听后回调
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// Here's a click listener…
function onClick() {
  console.log('click');

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

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

  outer.setAttribute('data-random', Math.random());
}

// …which we'll attach to both elements
inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

假设我们点击了inner元素,会触发Inner和outer的点击事件(事件冒泡):
第一步:触发点击事件,推入inner、outer的onClick回调到宏任务队列
macrotasks queue:[innerOnclickCallback,outerOnclickCallback]

第二步:执行innerCallBack宏任务,此时将setTimeout回调推入宏任务队列(实际上是4ms之后),推入Promise回调进入微任务队列,修改outer属性,导致MutationObserver回调推入微任务队列(可以看出,要被推入宏、微任务队列,需要满足被触发的条件)
输出:click
execute stack:[innerOnclickCallback]
macrotasks queue:[outerOnclickCallback,innerSetTimeoutCallback]
microtasks queue:[innerPromiseCallback,innerObserverCallback]

第三部:宏任务执行完毕,执行微任务队列中的任务,直到微任务队列为空,也就是执行innerPromiseCallback,innerObserverCallback
输出:click promise mutate
execute stack:[innerPromiseCallback,innerObserverCallback] //这里为了方便简写,实际上是分开推入执行栈中执行的
macrotasks queue:[outerOnclickCallback,innerSetTimeoutCallback]
microtasks queue:[]

第四步:微任务队列为空,推入下一个宏任务outerOnclickCallback到执行栈中
输出:click promise mutate click
execute stack:[outerOnclickCallback]
macrotasks queue:[innerSetTimeoutCallback,outerSetTimeoutCallback]
microtasks queue:[outerPromiseCallback,outerObserverCallback]
后续如二三步
输出: click promise mutate click promise mutate click timeout timeout

不同浏览器可能有不同的输出,这是由于不同的浏览器和不同的版本可能会把promise和mutationObserver当做宏任务处理,这里以chrome为准

上面的例子是点击元素触发事件回调的,如果我们在代码对使用inner.click(),会同步的触发inner和outer的click回调,此时输出会变成:
click
click
promise
mutate
promise
timeout
timeout
这里mutate只输出一次是由于mutationObserver在监听outer时的回调函数还存在于microtask queue中,一次只能存在针对一个元素的mutationObserver回调

node下的Event Loop

Node下的事件循环分为6个阶段(6个宏任务队列),它们会按照顺序反复运行,每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行,当队列为空或者执行的回调函数数量达到系统设定的阈值,就会进入下一个阶段

image

node事件循环的顺序:
外部输入数据->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段->I/O事件回调阶段->闲置阶段(idle,prepare)->轮询阶段

  • timers:setTimeout、setInterval的回调
  • I/O:处理上一个轮循环中未执行的I/O回调
  • idle,prepare:node内部调用
  • poll:获取新的I/O事件,适当条件下将Node阻塞在这里
  • check:执行setImmediate()回调
  • close callbacks:执行socket的close事件回调
  1. timers阶段:timers会执行setTimeout和setInterval的回调,注意是全部执行完,直到timers队列为空,并且是由poll阶段控制的,同样的在Node中的事件也不是准确时间
  2. poll阶段:系统会做两件事
    • 回到timers阶段执行回调
    • 执行I/O回调
      在进入该阶段时如果没有设定timer的话,会发生以下两件事情
  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
  • 如果 poll 队列为空时,会有两件事发生
    • 如果有setImmediate回调需要执行,poll阶段会停止并且进入到 check 阶段执行回调
    • 如果没有setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2

这里和浏览器不一样的是,timer2在promise1之前输出,这是由于进入timers阶段时,会依次执行所有的timer任务,执行完毕后才会执行微任务

setTimeout和setImmediate:

  • setTimeout在poll阶段空闲时间执行,且等待时间到达后执行,在timer阶段执行
  • setImmediate在poll阶段完成时执行,即check阶段

二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout

由于IO回调是在poll阶段执行,当回调执行完毕后队列为空,则往check阶段执行setImmediate回调,再执行timers阶段回调

微任务差别:在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分

process.nextTick(function(){
    console.log(7);
});

new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});

process.nextTick(function(){
    console.log(8);
});
//输出3 4 7 8 5

node与浏览器事件循环差别

image
setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
  • 浏览器端的运行结果是timer1 promise1 timer2 promise2
  • node运行结果
    • node11运行结果和浏览器一致,执行一个宏任务后则执行微任务队列
    • node10以及之前的版本,看第一个定时器回调在执行栈时,第二个定时器回调是否在timer队列中
      • 如果第二个定时器未在timer队列,则输出:timer1 promise1 timer2 promise2
      • 如果第二个定时器在timer队列中,timer1 timer2 promise1 promise2,执行完timers队列后再执行微任务队列

总结

  1. 从JS可以阻塞DOM树渲染可以看出,为了优化首屏加载速度,尽量避免将非必须的JS加载,把非必须的JS放在onload内。
  2. CSS也会阻塞渲染树生成,非首屏必要的CSS可以按需加载,CSS加载要放在头部
  3. 尽量使用离线DOM,减少渲染,例如document Fragment
  4. 不要一条条的改变样式,而要通过改变class或者csstext属性,一次性改变样式
  5. 由于渲染过程为执行栈->微任务->渲染,所以长耗时的JS任务会影响渲染,可以开启webWorker进行长耗时任务
  6. 使用setTimeout代替setInterval,但setTimeout的计时也是不准的(如果执行栈阻塞)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容