- Published on
【笔记】- React DOM Diff总结
- Authors
- Name
- McDaddy(戣蓦)
DOM diff的核心就是新旧virtual dom
的属性对比,通过在内存中做数据对比而非直接操作DOM,提升了渲染的效率,直到最后对比完成再统一更新DOM。而diff算法就是如何高效得完成这个内存数据结构的对比。 如果没有diff算法对比完所有节点将花费O(n3)的复杂度。
前置知识
vdom
- virtual dom
即虚拟dom节点,其中包括几个主要属性
type
- 表示节点类型,主要分三种,1. 纯文字节点 2. 原生tag 如div
3. 自定义的组件, 其中又分函数组件和类组件props
- 表示传入的propschildren
- 表示此节点的子节点集合
renderVdom
- 指的是执行过render
或函数组件函数返回的vdom,这个vdom和上面的区别是,它指代一个自定义组件执行完渲染函数之后的vdom
前置方法
findDOM
- 通过vdom来找到当前渲染的真实dom中对应于这个vdom的真实dom节点
createDOM
- 通过传入一个vdom和父真实节点,创建出一个真实的dom节点
流程总结
从根节点开始,传入一个
compare
函数,接受4个参数,分别是父挂载点的dom(parentDOM
),旧的vdom
,新的vdom
以及旧节点的下一个兄弟DOM节点nextDOM
(真实可能不存在),而这个函数将被递归执行考虑新旧对比节点的5种情况
- 新旧节点都为空,啥都不需要做
- 新为空,旧不为空,表示此节点需要被移除,通过
findDOM
找到此节点的真实dom,然后调用parent.removeChild
来删除这个节点,同时调用节点的componentWillUnmount
- 新不为空,旧为空,表示此节点是新增的,通过
createDOM
来创建真实的dom节点,如果nextDOM
存在,调用parent.insertBefore
来插入到nextDOM
之前,否则,调用parentDOM.appendChild
来添加这个子节点。同时调用新节点的componentDidMount
- 两者都不为空,但是
type
不同,即节点需要被替换,通过findDOM找到老的真实dom, 通过createDOM
创建出新的节点。parentDOM.replaceChild
替换节点,分别调用新旧节点的componentDidMount
和componentWillUnmount
- 两者都不为空,且type相同,开始对比
props
和children
在
type
相同的前提下,根据type不同类型,更新props
分3种情况- 如果是纯文本节点,直接替换
DOM.textContent
- 原生
tag
, 更新props,也分3种情况style
属性的更新,需要逐一更新到dom.style
属性上on
开头的事件属性,如onClick
,将事件注册到document
上,做合成事件
- 普通属性,直接赋值
- 自定义组件,分2种情况
- 类组件
- 复用老vdom的
class instance
,不要重新创建。 - 调用
componentWillReceiveProps
- 触发
classInstance
的updater
的emitUpdate
方法 (决定到底是批量更新还是立即更新) - 执行
getDerivedStateFromProps
,通过nextProps
得到新的state
- 执行更新 先执行
shouldComponentUpdate
- 如果返回true, 执行
componentWillUpdate
- 执行自定义的
render
方法,返回新的renderVdom
- 执行
getSnapshotBeforeUpdate
,计算一个更新前dom的计算结果,将来传给componentDidUpdate
- 将老的
renderVdom
和新的renderVdom
做对比,此处递归返回流程的第一步 - 执行
componentDidUpdate
- 复用老vdom的
- 函数组件
- 函数组件没有实例,重新执行函数得到新的
renderVdom
- 将老的
renderVdom
和新的renderVdom
做对比,此处递归返回流程的第一步
- 函数组件没有实例,重新执行函数得到新的
- 类组件
- 如果是纯文本节点,直接替换
对比
children
精读《DOM diff 原理详解》如果就一个儿子,那么就直接将这个独子走一遍上面的流程
是一个列表,需要进行队列diff对比,核心是尽量得复用原有的节点,不额外销毁和新建节点
- 列表的每一项都需要一个key,否则这一步就不存在了
- React采用的是仅右移策略,即对元素发生的位置变化,只会将其移动到右边,那么右边移完了,其他位置也就有序了。
- Vue会采用一种双指针法
先从头部遍历,跳过未变化的点
从尾部开是遍历,跳过未变化的点,这个过程结束后,剩下的就是变化的点,如果首尾重合就是没有变化的列表
此时如果Old列表重合了,New没有,则是中间插入了新元素
如果Old没有重合,New重合了,就是删除了元素
此时剩下的都是变化的点,如Old为[c,d,e,f], New为[e,d,c,h]
创建一个Old的Map
{ c: 0, d: 1, e: 2, f: 3 }
后面的数字是index遍历New, 如果Old中有New中没有,就删除。反之,如果Old中没有,New有,就标记新增
如果新旧都有,就取出Old中的index,得到一个新的Map
{ e:2, d: 1, c: 0, f: 999 }
, 这样就知道了哪些点是可以复用的,接下来要做的就是移动位置如何做到高效移动?找到一个最长升序子序列作为基准不动,剩下的元素按顺序插入这个列表,这样可以做到最少的插入次数