面试常见问题,特地记录一下。
代码已经关联到github: 链接地址 觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
什么是虚拟DOM
与真实DOM对应,一个能表示真实DOM的对象。
react下的虚拟DOM
格式:
const vNode = {
key:null,
props:{
children:[{type:'span'}]
},
className:"",
onClick: () => {},
type:'div'
}
代码创建:
使用 React.createElement
或者 jsx
React.createElement('div',{className:"",onClick: () => {}},[
//child...
])
DOM操作慢?虚拟DOM快?
二者其实不能简单的概括,因为虚拟DOM最终还是会操作真实DOM。
- 真实DOM操作慢是对比于js原生的api,如数组操作。
- 任何基于DOM的库(React、Vue),因为都会操作DOM,所以不会比真实DOM快。
- 正常节点较少时,虚拟DOM会比真实的DOM渲染快(不是操作快,也就是不可交互时间短),而当节点太多时,如10W,则真实的DOM操作更快。
那为什么还会有人说DOM慢,虚拟DOM快呢?因为在某些情况下,虚拟DOM确实比真实DOM快。首先,我们要理解一个概念,JS计算比操作dom更快
- 减少DOM操作
- 合并DOM操作,减少DOM操作次数。(增加1000个节点,计算1000次,插入一次)
- 虚拟DOM借助DOM diff可以把多余的操作省略,减少DOM操作的范围(增加1000个节点,比较后,只有10个是新增的)
- 跨平台
虚拟DOM本质上是js对象,所以可以转化成如小程序的组件,安卓的视图等等。
react和JS控制原生DOM的差异
事件(如 click )
- js的点击事件是监听在对应dom上的,当然也可以通过事件委托给直接委托给父辈元素,
- react的点击事件,是监听在虚拟dom上的,而react的事件是合成事件,可以理解为委托到window的事件
属性(如class style)
- js是直接去修改对应的属性
- react则是通过props或者state的改变,触发重新渲染,domDiff,合并修改,修改属性。
DOM diff
策略
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
核心流程
一个函数,旧版(16之前)是为 patch
:
patches = patch(oldVNode,newVNode)
新版为核心函数为: reconcileChildFibers
:
reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {}
旧的fiber
对象与新的ReactElement
对象相比较,输出新的fiber
新版DOM diff 逻辑
构造Fiber树的方式。
比较顺序
- 开始之前,
markUpdateLaneFromFiberToRoot
,判断从该节点到root节点,哪些节点需要更新,设置更新通道childLane
- 标签(或者组件)属性 【memoizedProps!==pendingProps】,标记
didReceiveUpdate
为true,只有meno等组件需要。 - 父元素是否需要更新(
markUpdateLaneFromFiberToRoot
中得到的childLane
),如果不需要就直接跳过,往下遍历子或者兄弟元素 - 标签(或者组件)类型 【type和key是否一样】,是否复用组件
- 父元素比较完,往下比较子或者兄弟元素(通过workLoop,注意是通过类型链表的方式,这里会神奇的再次调用beginWork来走第2步)
比较逻辑
逻辑
- diff
- 比较props,如果不一样则标记
didReceiveUpdate
- 先看当前的Fiber节点是否需要更新,需要则直接进行调和,不需要则比较子节点,最后都返回子节点。
- 比较props,如果不一样则标记
- reconcileChildFibers
- 先判断传入的元素类型,是对象、数组还是字符串
- 不同元素类型分别比较
- 核心都是比较key和type,类型或者key不同直接替换(删除旧的stateNode节点,新建stateNode,注意fiber节点都是新的)
数组节点:
1. 第一次循环遍历两个数组的相同key部分,key一样就会复用,如果一开始的key就不一致,就会直接跳出循环
1. 第二次循环遍历两个数组的不同key部分,key一样就会复用
文本节点:只要旧的节点第一个不是文本节点就直接新建
- 如果相同则复用
- 完成本次比较,返回节点,同时会再次进行一遍步骤1,因为已经比较完了,所以这次会进入子节点或者兄弟节点的diff。
- 如果有下个节点就继续比较
- 没有就完成当前子树的遍历,
completeUnitOfWork
旧版DOM diff 逻辑
比较顺序
- 标签(或者组件)类型
- 标签(或者组件)属性
- 子元素递归
比较逻辑
- Tree diff
- 新旧两棵树逐层对比,找出哪些节点需要更新
- 如果是组件节点就看 Component diff
- 如果是标签节点就看 Element diff
- Component diff
- 如果是组件节点,先比较组件类型
- 类型不同直接替换(删除旧的)
- 如果相同只更新属性
- 深入组件 Tree diff
- Element diff
- 原生节点,看标签名
- 标签名不同直接替换,相同则只更新属性
- 深入标签后代 Tree diff
DOM diff例子(Key的作用)
-
两个子元素,删除后一个(不存在key的情况)。
标签类型和标签属性不变,不用更新;子元素从[1,2]变成了[2],1标签没变,但是children变了,更新内容(子元素2的内容放到了这边);子元素2不见了,删除对应dom。
-
两个子元素,删除后一个(存在key的情况)。
标签类型和标签属性不变,不用更新;子元素从[1,2]变成了[2],但是因为存在key,计算机知晓是 key:1
的元素删除了,2不变,所以会直接删除1,保留2.