本文主要针对浏览器渲染阶段发生的以下任务进行分析:
- JS引擎线程运行js代码
- 异步线程处理异步代码
- Event Loop 轮询任务队列
- 任务队列中的宏任务和微任务
参考文章
浏览器如何渲染
核心问题:从浏览器获取url到渲染完页面之间经历了什么
三个阶段:
- 浏览器发送http请求
- 服务器接收请求并发送响应报文
- 浏览器接收响应报文并渲染
解析HTML文档,生成DOM树
遇到 link href css,下载解析css,生成CSSOM
link 新http线程
在构建 CSSOM 树时,会阻塞渲染,直至 CSSOM 树构建完成。并且构建 CSSOM 树是一个十分消耗性能的过程,所以应该尽量保证层级扁平,减少过度层叠,越是具体的 CSS 选择器,执行速度越慢
遇到 script src,下载运行js
defer/async
当 HTML 解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件。并且 CSS 也会影响 JS 的执行,只有当解析完样式表才会执行 JS,所以也可以认为这种情况下,CSS 也会暂停构建 DOM
浏览器与js进程
线程和进程
https://zhuanlan.zhihu.com/p/165950721
进程:可以理解为系统中正在运行的一个程序
- 是正在执行的一个程序实例。在浏览器中打开一个网页就是开启了一个进程
- 是资源拥有的最小单位。进程拥有独立的地址空间,不能直接访问其他进程的资源
- 进程间相互访问资源需要进程间通讯:管道、文件、套接字…(websocket,localSotrage)
线程:可以理解为轻量级的进程。
线程依附于进程。进程将任务分成很多子任务,创建不同线程执行子任务
是调度和分配的最小单位。
同一进程的线程之间共享进程的地址空间等资源
浏览器的线程
类别A:GUI 渲染线程
类别B:JS 引擎线程
类别C:EventLoop轮询处理线程
类别D:其他线程:
定时器触发线程 (setTimeout)
http 异步线程(AJAX)
**浏览器事件线程 (onclick)**等等。
AB互斥,也就是GUI在渲染时会阻塞js引擎,因为如果在GUI渲染时js改变了DOM,就会导致渲染不同步
下面逐个介绍
类别B js引擎线程
https://segmentfault.com/a/1190000011198232
js是单线程语言,但是宿主环境却是多线程的,目前有两种主流的宿主环境:
- 浏览器(Chorme V8 参考)
- node
javascript引擎线程称为主线程,是运行JS代码的线程(包括异步)
它是基于事件驱动单线程执行的(可以修改DOM,简单化处理了),必须符合ECMAScript规范。
JS引擎一直等待着event loop中任务的到来,然后加以处理
只有当前函数执行栈(处理同步操作)执行完毕,才会去任务队列(处理异步操作)中取任务执行
- Event loop:一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
浏览器无论什么时候都只有一个JS线程在运行JS程序。
函数执行栈
所有同步任务都在主线程上执行,形成一个执行栈
主线程会生成执行栈,处理函数进栈出栈
例子:运行下面代码的执行栈变化
1 | function bar() { |
任务队列 Task Queue
静态的队列存储结构,用于存储异步操作成功后的回调函数,注意是先判定异步操作成功再加入队列
有两种任务队列:
宏任务是由宿主发起的,而微任务由JavaScript自身发起
- 宏任务 macro task,ES6中叫task:优先级低,先定义的先执行
- script(全局任务全部代码)
- setTimeout, setInterval, setImmediate
- I/O, UI rendering,event listener,postMessage,MessageChannel(用于消息通讯)
- 微任务 micro task,ES6中叫job:微任务,优先级高,并且可以插队,不是先定义先执行
- process.nextTick(Node独有)
- Object.observer(已废弃), MutationObserver.
- Promise(有些实现的promise将then方法放到了宏任务中),Promise本身是同步的,Promise.then是异步的
类别D 异步操作相关线程
遇到异步代码就放到相应的线程
setTimeout(fun A)
定时器触发线程- 定时器触发线程在接收到代码时就开始计时,时间到了将回调函数扔进队列
ajax(fun B)
http异步线程- http 异步线程立即发起http请求,请求成功后将回调函数扔进队列
dom.onclick(fun C)
浏览器事件线程- 浏览器事件线程会先监听dom,直到dom被点击了,才将回调函数扔进队列
1、执行主线程扔过来的异步代码,并执行代码
2、保存着回调函数(例中的ABC),异步代码执行成功后,通知 Event Loop轮询处理线程 过来取相应的回调函数
类别C Event Loop轮询处理线程
https://www.ruanyifeng.com/blog/2014/10/event-loop.html
上面我们已经知道了,有3个东西
1、主线程,处理同步代码
- 先处理执行栈,再从任务队列取
2、类别D的线程,处理异步代码
- 区分计时器、http、dom事件
3、任务队列,存储着异步成功后的回调函数,一个静态存储结构
- 队列分为微任务和宏任务,微任务优先级更高
而发生的事情就是:
- 主线程中检测到的异步任务交给异步线程
- 把异步线程处理完的回调函数放到任务队列
- Event Loop:当函数执行栈为空时,取一个任务队列中的函数来执行
有两种任务队列:微任务和宏任务
微任务和宏任务
宏任务是由宿主(浏览器或Node)发起的,而微任务由JavaScript自身发起
- 宏任务 macro task,ES6中叫task:执行慢,优先级低,先定义的先执行
- script(全局任务全部代码)
- setTimeout, setInterval, setImmediate
- I/O, UI rendering,event listener,postMessage,MessageChannel(用于消息通讯)
- 微任务 micro task,ES6中叫job:执行快,微任务,优先级高,并且可以插队,不是先定义先执行
- process.nextTick(Node独有)
- Object.observer(已废弃), MutationObserver.
- Promise(有些实现的promise将then方法放到了宏任务中),Promise本身是同步的,Promise.then是异步的
浏览器中的Event Loop
浏览器对宏任务和微任务的处理不同:
- JavaScript引擎首先从宏任务队列(macrotask queue)中取出第一个任务;
- 执行完毕后,再将微任务(microtask queue)中的所有任务取出,按照顺序分别全部执行(这里包括不仅指开始执行时队列里的微任务),如果在这一步过程中产生新的微任务,也需要执行;
- 然后再从宏任务队列中取下一个,执行完毕后,再次将 microtask queue 中的全部取出
- 循环往复,直到两个 queue中的任务都取完。
面经
为什么setTimeout不准时
因为需要将主线程里所有的代码运行,所有同步代码运行完后,才会取任务队列中的setTimeout的回调运行,很可能虽然setTimeout在3秒后被放入任务队列,但是其实这时候函数执行栈里还没有全执行完。
附,**requestAnimationFrame
** 是准时的。
使用 requestAnimationFrame
实现的动画效果比 setTimeout
好的原因如下:
- 使用
requestAnimationFrame
不需要设置具体的时间;- 它提供一个原生的API去执行动画的效果,它会在一帧(一般是
16ms
)间隔内根据选择浏览器情况去执行相关动作。 setTimeout
是在特定的时间间隔去执行任务,不到时间间隔不会去执行,这样浏览器就没有办法去自动优化
- 它提供一个原生的API去执行动画的效果,它会在一帧(一般是
requestAnimationFrame
里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次;- 如果页面未激活的话,
requestAnimationFrame
也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销。
看代码说输出
为什么promise.then比setTimeout先执行:
因为Promise.then的回调是微任务,setTimeout的回调是宏任务,微任务可以插队
一般这个问题出现在看代码说输出的题型
详见:面经整理1:异步输出——Promise,SetTimeout与async
Todo
Nodejs
和浏览器的区别
Event Loop