虚拟dom_fiber相关汇总
# 虚拟DOM
# 简述一下virtual DOM 如何工作?
- 当数据发生变化,比如setState时,会引起组件重新渲染,整个UI都会以virtual dom的形式重新渲染
- 然后收集差异也就是diff新的virtual dom和老的virtual dom的差异
- 最后把差异队列里的差异,比如增加节点、删除节点、移动节点更新到真实的DOM上
# 什么是虚拟 DOM?
虚拟 DOM 是组成应用程序用户界面的实际HTML元素的内存表示形式。重新渲染组件时,虚拟 DOM 会将更改与其 DOM 模型进行比较,以创建要应用的更新列表。主要优点是它高效,仅需对实际 DOM 进行最小限度的更改,而不必重新渲染大块
# 你了解 Virtual DOM 吗?解释一下它的工作原理。
Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。
Virtual DOM 工作过程有三个简单的步骤。
每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。
然后计算之前 DOM 表示与新表示的之间的差异。
完成计算后,将只用实际更改的内容更新 real DOM。
# 虚拟DOM实现原理?
- 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
- 状态变更时,记录新树和旧树的差异
- 最后把差异更新到真正的dom中
# 区分Real DOM和Virtual DOM
Real DOM | Virtual DOM |
---|---|
1. 更新缓慢。 | 1. 更新更快。 |
2. 可以直接更新 HTML。 | 2. 无法直接更新 HTML。 |
3. 如果元素更新,则创建新DOM。 | 3. 如果元素更新,则更新 JSX 。 |
4. DOM操作代价很高。 | 4. DOM 操作非常简单。 |
5. 消耗的内存较多。 | 5. 很少的内存消耗。 |
# 对虚拟 DOM 的理解?虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟dom是对DOM的抽象,这个对象是更加轻量级的对DOM的描述。它设计的最初目的,就是更好的跨平台,比如node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟dom,因为虚拟dom本身是js对象。 在代码渲染到页面之前,vue或者react会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。在vue或者react内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
为什么要用 Virtual DOM:
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能 下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
- 真实DOM∶ 生成HTML字符串+ 重建所有的DOM元素
- Virtual DOM∶ 生成vNode+ DOMDiff+必要的dom更新
Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。
(2)跨平台 Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
# React diff 原理
- 把树形结构按照层级分解,只比较同级元素。
- 列表结构的每个单元添加唯一的 key 属性,方便比较。
- React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
- 合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty 到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制.
- 选择性子树渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。
# React diff 算法的原理是什么?
实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。
具体的流程如下:
- 真实的 DOM 首先会映射为虚拟 DOM;
- 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
- 根据 patch 去更新真实的 DOM,反馈到用户的界面上。
一个简单的例子:
import React from 'react'
export default class ExampleComponent extends React.Component {
render() {
if(this.props.isVisible) {
return <div className="visible">visbile</div>;
}
return <div className="hidden">hidden</div>;
}
}
2
3
4
5
6
7
8
9
这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个 div 节点。
<div class="visible">visbile</div>
当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。
diff算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化: 策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比) 这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比) 在组件比对的过程中:
- 如果组件是同一类型则进行树比对;
- 如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。
策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比) 元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。
# React key 是干嘛用的 为什么要加?key 主要是解决哪一类问题的
Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯一性。
在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系
注意事项:
- key值一定要和具体的元素—一对应到
- 尽量不要用数组的index去作为key。
- 永远不要试图在render的时候用随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销比不加key的情况下更糟糕。
# 虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么
虚拟DOM相对原生的DOM不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量DOM时,由于多了一层虚拟DOM的计算,虚拟DOM也会比innerHTML插入慢。它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。所以一定要根据具体的场景进行探讨。
在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
# React 与 Vue 的 diff 算法有何不同?
diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。
- 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
- 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
- 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。
以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current 与 workInProgress 两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,
因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。
# 为什么虚拟 dom 会提高性能?
- 虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能。
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。
# 虚拟DOM的优劣如何?
优点:
- 保证性能下限: 虚拟DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限
- 无需手动操作DOM: 虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率
- 跨平台: 虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
缺点:
- 无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化
# React16
# react 最新版本解决了什么问题,增加了哪些东西
React 16.x的三大新特性 Time Slicing、Suspense、 hooks
- Time Slicing(解决CPU速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得react能在性能极其差的机器跑时,仍然保持有良好的性能;
- Suspense (解决网络IO问题) 和lazy配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T异步但是同步的写法,这是最好的解决异步问题的方式;
- 提供了一个内置函数componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件;
(1)React16.8 加入hooks,让React函数式组件更加灵活,hooks之前,React存在很多问题:
- 在组件间复用状态逻辑很难;
- 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深;
- class组件的this指向问题;
- 难以记忆的生命周期;
hooks很好的解决了上述问题,hooks提供了很多方法
- useState 返回有状态值,以及更新这个状态值的函数
- useEffect 接受包含命令式,可能有副作用代码的函数。
- useContext 接受上下文对象(从 React.createContext返回的值)并返回当前上下文值,
- useReducer useState 的替代方案。接受类型为 (state,action)=> newState的reducer,并返回与dispatch方法配对的当前状态。
- useCalLback 返回一个回忆的memoized版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性 o useMemo 纯的一个记忆函数 o useRef 返回一个可变的ref对象,其Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个生命周期内保持不变。
- useImperativeMethods 自定义使用ref时公开给父组件的实例值
- useMutationEffect 更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发
- useLayoutEffect DOM改变后同步触发。使用它来从DOM读取布局并同步重新渲染
(2)React16.9
- 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
- 废弃 javascrip:形式的 URL。以javascript:开头的URL 非常容易遭受攻击,造成安全漏洞。
- 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。
- act()也支持异步函数,并且你可以在调用它时使用 await。
- 使用 <React.ProfiLer> 进行性能评估。在较大的应用中追踪性能回归可能会很方便
(3)React16.13.0
- 支持在渲染期间调用setState,但仅适用于同一组件
- 可检测冲突的样式规则并记录警告
- 废弃 unstable_createPortal,使用CreatePortal
- 将组件堆栈添加到其开发警告中,使开发人员能够隔离bug并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
# react16的错误边界(Error Boundaries)是什么
部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。
import React from 'react';
import ReactDOM from 'react-dom';
class ErrorBoundary extends React.Component{
constructor(props) {
super(props);
this.state={hasError:false};
}
componentDidCatch(err,info) {
this.setState({hasError: true});
}
render() {
if (this.state.hasError) {
return <h1>Something Went Wrong</h1>
}
return this.props.children;
}
}
class Page extends React.Component{
render() {
return (
<ErrorBoundary>
<Clock/>
</ErrorBoundary>
)
}
}
class Clock extends React.Component{
render() {
return (
<div>hello{null.toString()}</div>
)
}
}
ReactDOM.render(<Page/>,document.querySelector('#root'));
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# react16版本的reconciliation阶段和commit阶段是什么
- reconciliation阶段包含的主要工作是对current tree 和 new tree 做diff计算,找出变化部分。进行遍历、对比等是可以中断,歇一会儿接着再来。
- commit阶段是对上一阶段获取到的变化部分应用到真实的DOM树中,是一系列的DOM操作。不仅要维护更复杂的DOM状态,而且中断后再继续,会对用户体验造成影响。在普遍的应用场景下,此阶段的耗时比diff计算等耗时相对短。
# Fiber
# 你对Fiber架构了解吗
注:答案参考司徒正美大神的文章
# 什么是 React Fiber?
Fiber 是 React 16 中新的协调引擎或重新实现核心算法。它的主要目标是支持虚拟DOM的增量渲染。React Fiber 的目标是提高其在动画、布局、手势、暂停、中止或重用等方面的适用性,并为不同类型的更新分配优先级,以及新的并发原语。
React Fiber 的目标是增强其在动画、布局和手势等领域的适用性。它的主要特性是增量渲染:能够将渲染工作分割成块,并将其分散到多个帧中。
# Fiber架构相对于以前的递归更新组件有有什么优势
- 原因是递归更新组件会让JS调用栈占用很长时间。
- 因为浏览器是单线程的,它将GUI渲染,事件处理,js执行等等放在了一起,只有将它做完才能做下一件事,如果有足够的时间,浏览器是会对我们的代码进行编译优化(JIT)及进行热代码优化。
- Fiber架构正是利用这个原理将组件渲染分段执行,提高这样浏览器就有时间优化JS代码与修正reflow!
# 既然你说Fiber是将组件分段渲染,那第一段渲染之后,怎么知道下一段从哪个组件开始渲染呢?
- Fiber节点拥有return, child, sibling三个属性,分别对应父节点, 第一个孩子, 它右边的兄弟, 有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。
# 怎么决定每次更新的数量
- React16则是需要将虚拟DOM转换为Fiber节点,首先它规定一个时间段内,然后在这个时间段能转换多少个FiberNode,就更新多少个。
- 因此我们需要将我们的更新逻辑分成两个阶段,第一个阶段是将虚拟DOM转换成Fiber, Fiber转换成组件实例或真实DOM(不插入DOM树,插入DOM树会reflow)。Fiber转换成后两者明显会耗时,需要计算还剩下多少时间。
- 比如,可以记录开始更新视图的时间var now = new Date - 0,假如我们更新试图自定义需要100毫秒,那么定义结束时间是var deadline = new Date + 100 ,所以每次更新一部分视图,就去拿当前时间new Date<deadline做判断,如果没有超过deadline就更新视图,超过了,就进入下一个更新阶段
# 如何调度时间才能保证流畅
- 使用浏览器自带的api - requestIdleCallback,
- 它的第一个参数是一个回调,回调有一个参数对象,对象有一个timeRemaining方法,就相当于new Date - deadline,并且它是一个高精度数据, 比毫秒更准确
- 这个因为浏览器兼容性问题,react团队自己实现了requestIdleCallback【RIC】
# fiber带来的新的生命周期是什么
React 新版 生命周期
# 创建时
- constructor ->
- getDerivedStateFromProps(参数nextProps, prevState,注意里面this不指向组件的实例)->
- render ->
- componentDidMount
# 更新时
- getDerivedStateFromProps(这个时props更新才调用,setState时不调用这个生命周期, 参数nextProps, prevState) ->
- shouldComponentUpdate(setState时调用参数nextProps, nextState)->
- render->
- getSnapsshotBeforeUpdate(替换 componentWillUpdate)
- componentDidUpdate(参数prevProps, prevState, snapshot)
# 你是如何理解fiber的?【要点】
React Fiber 是一种基于浏览器的单线程调度算法.
React 16之前 ,reconcilation
算法实际上是递归,想要中断递归是很困难的,React 16 开始使用了循环来代替之前的递归.
Fiber
:一种将 recocilation
(递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。
# 你对 Time Slice的理解?
时间分片
- React 在渲染(render)的时候,不会阻塞现在的线程
- 如果你的设备足够快,你会感觉渲染是同步的
- 如果你设备非常慢,你会感觉还算是灵敏的
- 虽然是异步渲染,但是你将会看到完整的渲染,而不是一个组件一行行的渲染出来
- 同样书写组件的方式