1. 虚拟DOM
-
什么是虚拟DOM
对比真实的DOM, 虚拟DOM就是个js对象, 将真实的dom生成一个vnode集合成为虚拟dom树
-
操作 DOM ,天生就慢
浏览器一般由七个模块组成,User Interface(用户界面)、Browser engine(浏览器引擎)、Rendering engine(渲染引擎)、Networking(网络)、JavaScript Interpreter(js解释器)、UI Backend(UI 后端)、Date Persistence(数据持久化存储)
渲染引擎 负责解析DOM文档和CSS规则并将内容排版到浏览器中显示有样式的界面,我们常说的浏览器内核主要指的就是渲染引擎
而 JS 引擎是用来解释执行JS脚本的模块,如 chrome的V8 引擎、JavaScriptCore
所以当我们通过 JS 操作 DOM 的时候,实际上涉及到两个线程之间的通信,由此带来了一些性能上的损耗
在《高性能JavaScript》中,将DOM和JavaScript比喻成两个岛屿,通过JavaScript访问DOM,就需要经过这座桥,频繁的访问DOM,过桥的次数就会多了起来,开销自然也多了起来
-
虚拟DOM的优点
- 性能提升
-
减少引擎间的切换, 因为浏览器对于dom和js执行分别是dom引擎和js引擎、v8引擎, 两个是独立的,但是在一个线程中, 如果直接使用js操作dom就需要挂起js引擎执行dom引擎, 来回对这两个进行操作, 使用虚拟dom就相当于直接调用js引擎, js执行比dom快, 相当于使用js执行换dom执行速度
-
减少大面积页面的重排和重绘, 虚拟dom使用diff算法会对新旧两次vnode对比, 算出真正更新的点, 最小限度渲染局部页面
- 跨端: 虚拟dom以js对象为基础,不依赖客户端环境
框架不一定比原生快, 框架保证的是性能的下限,例如渲染大量dom时, 多一层虚拟dom的计算, 会比innerhtml直接插入慢
2. 垃圾回收机制
代码执行期间有三块内存: 代码空间、 栈空间、 堆空间
- 代码空间: 不存在垃圾回收
- 栈空间: 调用栈通过栈状态指针的移动来销毁释放内存 [不需要通过v8垃圾回收]
- 堆空间: 使用v8的垃圾回收机制
3. V8如何执行一段JS代码
几个关键概念: 编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、字节码(Bytecode)、即时编译器(JIT)
编译型语言: 在程序执行之前, 需要经过编译器的编译过程, 并且编译之后会直接保留机器能读懂的二进制文件, 每次运行程序都直接运行二进制人间, 而不需要再次进行重新编译, 例如C/C++ 、GO
解释型语言: 每次运行时都需要通过解释器对程序进行动态解释和执行, 例如Python、JS
Babel 主要用于 JavaScript 代码的预处理阶段 , Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。
生成抽象语法树(AST)的步骤: 先分词再解析
- 分词又称词法分析: 就是把代码拆分为一个个token, token指的的是语法上不可能再分的, 最小的单个字符或字符串
- 解析又称为语法分析: 将token按照语法规则生成抽象语法树(AST), 如果符合语法规则能生成ast, 不符合抛出“语法错误”
v8执行一段js代码
-
生成抽象语法树AST、生成执行上下文
分词、解析生成抽象语法树 、生成执行上下文,执行上下文就是调用栈
-
生成字节码
解释器根据抽象语法树AST生成字节码, 并且解释执行字节码, (字节码是一种介于AST和机器码之间的代码, 它与特定类型的机器码无关, 需要解释器将其转化为机器码才能执行) 机器码所占用的空间远远大于字节码, 所以引入字节码能减少系统中的内存使用
-
执行代码 解释器对字节码进行逐条解析, 如果遇到热点代码 , 将热点代码整块编译为机器码, 这样大大提升了代码的执行速度 (热点代码: 一段代码被反复执行多次)
V8用到了字节码+JIT(即时编译) ,下边为JIT的技术
4. 为什么说V8 执行时间越久,执行效率越高
是因为解释器对字节码进行逐条解析时, 会有更多的代码成为热点代码后, 转化为机器码执行
5. setTimeout是如何实现的
我们说的事件循环是一种概念, 是负责管理和调度任务的, 消息队列是一种数据结构, 是事件循环的实现, 对于宏任务和微任务消息队列是按照先进先出的形式执行的, 但是setTimeout是规定时间的, 这个是在哪里记录的
对于这种延迟执行的任务, 事件循环会维护一个专门的延迟队列, 记录开始执行时间和结束执行事件, 并且在消息队列中调用, 每次执行一个任务时候, 都会调用延迟队列
setTimeOut的弊端
-
如果当前任务执行的事件太长, 会影响定时任务的执行
-
如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
function cb() { setTimeout(cb, 0); } setTimeout(cb, 0);
-
未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
-
延时执行时间有最大值
32 个 bit 来存储延时值的,32bit 最大只能存放的数字是 2147483647 毫秒,这就意味着,如果 setTimeout 设置的延迟值大于 2147483647 毫秒(大约 24.8 天)时就会溢出,这导致定时器会被立即执行。
6. XMLHttpRequest的线程原理
对于setTimeOut是将延迟任务放到了延迟队列中, 而XMLHttpRequest发起请求是由网络进程执行, 通过IPC通信
7. Promise 的工作原理
Promise 解决的是异步编码风格的问题, 解决的是回调地域的问题, 它通过了1、实现了函数的延时绑定, 绑定了resolve上 2 、 将回调函数 onResolve 的返回值穿透到最外层解决了循环嵌套
- Promise 中为什么要引入微任务?
- Promise 中是如何实现回调函数返回值穿透的?
- Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获异常的函数?
7. async/await和promise的区别
-
生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的 , 生成器的底层实现机制事协程
function* genDemo() { console.log(" 开始执行第一段 "); yield "generator 2"; console.log(" 开始执行第二段 "); yield "generator 2"; console.log(" 开始执行第三段 "); yield "generator 2"; console.log(" 执行结束 "); return "generator 2"; } console.log("main 0"); let gen = genDemo(); console.log(gen.next().value); console.log("main 1"); console.log(gen.next().value); console.log("main 2"); console.log(gen.next().value); console.log("main 3"); console.log(gen.next().value); console.log("main 4");
-
协程是一种比线程更加轻量级的存在,你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程
-
async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用
8. 聊聊react的时间切片
都知道react的核心在于任务调度, 在任务调度中采用协程的方式来进行时间切片, 其实和aysnc/await的原理差不多, 就是让出主线程, 让浏览器处理UI绘制,用户输入等紧急任务
该功能实现是在react的 任务调度循环 中实现, workLoop中
时间切片原理: 消费任务队列的过程中, 可以消费1~n
个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback
之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用.
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
currentTask = peek(taskQueue); // 获取队列中的第一个任务
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行回调
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 回调完成, 判断是否还有连续(派生)回调
if (typeof continuationCallback === 'function') {
// 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
currentTask.callback = continuationCallback;
} else {
// 把currentTask移出队列
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
// 如果任务被取消(这时currentTask.callback = null), 将其移出队列
pop(taskQueue);
}
// 更新currentTask
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
} else {
return false; // task队列已经清空, 返回false.
}
}
9. 聊聊react的可中断渲染
在时间切片的基础之上, 如果单个task.callback
执行时间就很长(假设 200ms). 就需要task.callback
自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时(源码链接), 如遇超时就退出fiber树构造循环
, 并返回一个新的回调函数(就是此处的continuationCallback
)并等待下一次回调继续未完成的fiber树构造
.
10. 聊聊react的异步渲染
在react执行调度和取消调度时,采用new MessageChannel来对调度发起任务, 但是MessageChannel在浏览器中是 宏任务 , 所以调度中心永远是 异步执行 函数
11. fiber树的构造
- 从
scheduler
调度中心的角度来看, 它是任务队列taskQueue
中的一个具体的任务回调(task.callback
) - 从React 工作循环的角度来看, 它属于
fiber树构造循环
fiber树构建
- 初次创建: 在
React
应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树. - 对比更新:
React
应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber
之前需要和旧fiber
进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.
fiber对象
是通过ReactElement
对象进行创建的, 多个fiber对象
构成了一棵fiber树
, fiber树
是构造DOM树
的数据模型, fiber树
的任何改动, 最后都体现到DOM树
.
fiber树
的构造过程, 实际上就是ReactElement
对象到fiber
对象的转换过程
12. setState到底是同步还是异步
- 如果逻辑进入
flushSyncCallbackQueue
(executionContext === NoContext
), 则会主动取消调度, 并刷新回调, 立即进入fiber树
构造过程. 当执行setState
下一行代码时,fiber树
已经重新渲染了, 故setState
体现为同步. - 正常情况下, 不会取消
schedule调度
. 由于schedule调度
是通过MessageChannel
触发(宏任务), 故体现为异步.
同步:
-
首先在
legacy模式
下 (React 16.3之前) -
在执行上下文为空的时候去调用
setState
- 可以使用异步调用如
setTimeout
,Promise
,MessageChannel
等 - 可以监听原生事件, 注意不是合成事件, 在原生事件的回调函数中执行 setState 就是同步的
- 可以使用异步调用如
异步:
- 如果是合成事件中的回调,
executionContext |= DiscreteEventContext
, 所以不会进入, 最终表现出异步 - concurrent 模式下都为异步 (React 18.0以后)
13. react中key的作用
在react
中key
是服务于diff算法
, 它的默认值是null
, 在diff算法
过程中, 新旧节点是否可以复用, 首先就会判定key
是否相同, 其后才会进行其他条件的判定. 在源码中, 针对多节点(即列表组件)如果直接将key
设置成index
和不设置任何值的处理方案是一样的, 如果使用不当, 轻则造成性能损耗, 重则引起状态混乱造成 bug
14. react的fiber架构和diff算法的联系和区分
- fiber架构是一个reconciler引擎, 特性有时间分片(Time Slicing) 、优先级调度 、 增量渲染、 可中断和恢复 来提升应用的响应速度
- diff算法通过高效的节点比较和列表处理, 确保最小化dom更新的的性能优化
react自己问答
1、react的设计理念
避免单线程占用, 影响应用响应, 通过实现异步可中断、时间分片对任务进行拆分和监听来处理任务优先级调度, 同时分离副作用, 避免污染来解藕函数
2、react的代码架构
- Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile
- Reconciler(协调器): 找出哪些节点发生了改变,并打上不同的Flags(旧版本react叫Tag)
- Renderer(渲染器): 将Reconciler中打好标签的节点渲染到视图上
3、说一下react的fiber双缓存
react对于页面的更新使用了两棵独立的fiber树, 一棵是页面UI的fiber树, 一棵是正在计算更新的fiber树, 当正在更新的fiber树准备好后, commit切换两棵树, 正在计算的fiber树就称为展示的UIfiber树, 这么做不阻塞更新, 在生成计算更新的fiber树的时候, 页面UI不受影响, 同时因为fiber树的生成和更新是在内存中的, 而且两棵树独立, 所以中断和恢复不影响当前UI, 对于性能方面, 在fiber树更新过程中, 他是拆分小任务协调执行完成, 如果有优先级更高的任务进入, 会先停止fiber树继续渲染, 等空闲时候执行, 所以性能得以提升
4、react的任务优先级调度是怎么实现的?
lane通过二进制来表示优先级, 二进制中的1表示位置, 1越多,优先级越低
5、说一下react18的concurrent并发模式
concurrent其实是很多功能的合集(lane、fiber架构、schedule等), 它的目的就是提高响应,核心就是实现异步可中断, 优先级调度的更新
浏览器的fps(帧率)是60hz, 因为js会影响dom的渲染和合成, react17以后对于一帧将时间拆分, 给js一段时间片执行, 如果超时就下一帧继续执行, 不影响渲染
Fiber为concurrent架构提供了数据层面的支持。
Scheduler为concurrent实现时间片调度提供了保障。
Lane模型为concurrent提供了更新的策略
上层实现了batchedUpdates和Suspense
6、react中的jsx
jsx是React.createElement的语法糖,jsx通过babel转化成React.createElement函数,React.createElement执行之后返回jsx对象,也叫virtual-dom,Fiber会根据jsx对象和current Fiber进行对比形成workInProgress Fiber
7、fiber架构完成的事情
1、工作单元、任务分解
2、增量渲染
3、根据优先级暂停、继续、排列优先级
4、保存状态
8、说一下react的diff算法
react的diff算法发生在Reconciler中, 对于diff算法的比较分单节点diff和多节点diff, 多节点和单节点区分页面直接一个div还是React.Fragment
react为了降低时间复杂度, 在diff算法比较有3个原则:
1、只对同级比较, 跨层级不复用
2、type不同的dom, 直接设置delete,新增节点
3、设置key对diff过程进行复用
多节点通过三次循环拆解为单节点, 然后对多节点进行内部diff
9、说一下react的时间片
react的时间片是scheduler实现的, 我们js堵塞了流程, 页面绘制就会卡顿, 所以schedule借助messageChannel实现在浏览器绘制前制定时间片, 也就是对帧率进行拆分任务, 如果规定时间内diff对比没对比完, 主动让出主线程, 其实对于空闲时间处理高缓存的任务requestIdelC callback更好,但是该API有浏览器兼容, 所以就用了messagechannel
10、说一下react的优先级调度
schedule可以调度优先级, 但是lane是更加颗粒度小的调度, 他的调度主要通过时间, 过期时间越长越先执行
11、fiber是什么,为啥能提高性能
Fiber是一个js对象,能承载节点信息、优先级、updateQueue,同时它还是一个工作单元。
-
-
Fiber双缓存可以在构建好wip Fiber树之后切换成current Fiber,内存中直接一次性切换,提高了性能
-
Fiber的存在使异步可中断的更新成为了可能,作为工作单元,可以在时间片内执行工作,没时间了交还执行权给浏览器,下次时间片继续执行之前暂停之后返回的Fiber
-
Fiber可以在reconcile的时候进行相应的diff更新,让最后的更新应用在真实节点上
-