JS解释编译简要过程
源代码通过解析器生成AST(Abstract syntax tree),然后生成字节码(介于高级语言与机器语言之间的中间代码),再通过解释器逐行解释并执行,借助JIT(Just in time)技术监测代码执行过程,根据代码执行频率从高到低将代码标记为不同等级,低等级的通过基线编译器编译,高等级的通过优化编译器编译,之后将执行编译后的代码,而一旦优化后代码不再满足优化条件,则将其降级到基线编译器或解释器中去处理,这就是去优化;
对JS单线程非阻塞的理解
单线程:JS代码一定是在单个线程中同步执行的(JS引擎线程);
非阻塞:异步操作不会阻塞同步代码的执行,异步操作实际是由浏览器代理,然后基于Event Loop机制,异步操作完成后的回调函数会由JS引擎执行;现代浏览器有哪些进程和线程
浏览器包含以下进程:
- 浏览器进程,控制整个浏览器,负责协调其他进程间的协同工作;
- GPU进程,负责浏览器界面的渲染;
- 网络进程,处理网络请求或响应;
- 渲染进程,解析并渲染标签页内容;
- 插件进程,运行浏览器插件;
渲染进程包含以下线程:
- GUI渲染线程,解析html和CSS、构建DOM树、CSSOM树、渲染树、绘制页面;
- JS引擎线程,编译、解释JS代码,并进行优化或去优化,与GUI渲染线程互斥;
- 定时器线程,提供计时功能,如setTimeout、setInterval;
- http线程,提供http处理能力;
- 事件触发线程,控制Event Loop;
- 合成线程,将GUI渲染线程生成的
Browser Event Loop
JS同步任务只在一个线程中执行,称之为主线程,主线程通过执行栈的机制顺序执行所有代码;
异步代码就将其交由浏览器其他线程代劳,JS线程继续执行同步代码,期间如果异步操作完成,则将对应的回调函数加入任务队列,最后等执行栈中的函数都执行完,此时将任务队列中的回调函数加入执行栈中继续执行,构成一个循环,即Event Loop;
如上所述,实际上异步操作会调用浏览器提供的WebAPI,由浏览器代理,浏览器通过其他线程(如HTTP线程,定时器线程等)处理异步操作,等到完成后再将异步操作的回调函数加入任务队列,注意,这个过程进行的同时,JS线程继续执行其他代码;
任务队列又分为两种——宏任务和微任务;
- 宏任务:
<script/>
的所有代码,setTimeout, setInterval, postMessage
的回调; - 微任务:
Promise.prototype.then
的回调,MutationObserver
实例化时的回调;
处理任务队列时,优先处理微任务,最后处理宏任务;
一个宏任务处理完成后重新渲染UI,然后再处理下一个宏任务;
- 宏任务:
Node.js Event Loop
Node Event Loop与Browser一样,都是在单个线程中运行的,Node将异步操作交给系统处理,Browser则是交给浏览器处理,当操作完成时再去执行对应的回调函数,但机制又有所不同,Node Event Loop分为以下几个阶段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘- Timers:setTimeout,setInterval;
- Pending callbacks:执行上一阶段被延迟的I/O回调;
- Idle & Prepare:Node内部使用;
- Poll:触发新的I/O事件,执行I/O相关的回调;
- Check:setImmediate
- Close callbacks:主线程结束时的回调;
Event Loop将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列为空或超过最大回调数,然后进入下一阶段,如此循环;
Poll阶段会同步执行队列中的所有回调,且每个回调都可能触发更多事件,引入更多回调,除非超过最大回调数,因此Poll阶段可能会执行很长时间,进而导致Timers阶段的回调函数不能在精确的时间点上触发;
Poll阶段队列空时,如果有setImmediate回调,才会进入Check阶段,否则将一直在该阶段,等待queue注册进新回调函数并立刻执行;
每轮循环之间,会检查是否还有需要等待的异步事件(I/O或定时器),如果没有则结束循环;
process.nextTick(callback, arg)不属于Event Loop机制,它被确保在当前执行栈结束,进入下一个Tick之前执行,它不受当前Event Loop所在阶段限制;
Html解析过程
整体流程:
- 字节流转码成字符串;
- 字符串解析成Token;
- 根据Token 生成DOM 节点;
a. 解析到script 元素,会阻塞DOM 节点的生成;
b. 解析到link 元素,会加载CSS 资源; - 构建DOM Tree,待CSS 加载完毕,同时开始构建CSSOM Tree(互不影响);
- 合并DOM Tree 和CSSOM Tree 为Render Tree;
- Layout;
- Paint;
优化策略:
- script 元素设置属性async 或defer,避免阻塞DOM 构建(async 是异步加载,defer 是延迟执行);
- link 元素设置rel 属性为preload,预加载其他页面可能用到的样式;
- 减少回流和重绘:
Script 阻塞DOM 构建:解析到普通script 元素,需要等获取到资源且CSSOM 构建完成,才会立即执行脚本,这个过程会阻塞DOM 的构建;
Script async & defer:async 是异步加载,获取资源这个过程将不再阻塞DOM 构建,但是获取之后效果同普通script 元素是一样的,会阻塞DOM 的构建(本质是因为渲染线程与JS引擎线程互斥);无序加载脚本;
defer 是延迟执行,浏览器DOM 构建完毕,且JS 资源获取完毕之后再执行JS;可以有序加载脚本;
Layout & Paint:根据Render Tree 计算每个元素在页面上的位置、尺寸,称为Layout(布局)阶段;
将每个元素渲染到页面上,称为Paint(绘制)阶段;
Repaint:元素颜色、背景等改变,会重复进入Paint 阶段,称为重绘;
Re-layout:元素位置、尺寸改变,会重复进入Layout 阶段,称为回流,回流之后一定会发生重绘;
导致发生回流的操作:
- 添加、删除页面元素;
- 使用display 控制页面元素的显隐;
- 改变width,height,margin,border,padding,content;
- 窗口Resize;
- 读写offsetWidth/offsetHeight;
- 写style;
导致发生重绘的操作:改变color,background,outline,text-decoration,border-radius,box-shadow 等;
减少回流和重绘的方法:
- 动画使用requestAnimationFrame 控制;
- 尽量少读写offsetWidth 等属性;
- 使频繁重绘/回流的节点脱离文档流,避免其他节点受其影响也发生回流/重绘;