- 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 如div3. 自定义的组件, 其中又分函数组件和类组件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 }, 这样就知道了哪些点是可以复用的,接下来要做的就是移动位置如何做到高效移动?找到一个最长升序子序列作为基准不动,剩下的元素按顺序插入这个列表,这样可以做到最少的插入次数