2-2-12 React原理基础 (DOM - DIFF )部分
Posted 沿着路走到底
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2-2-12 React原理基础 (DOM - DIFF )部分相关的知识,希望对你有一定的参考价值。
WHY DOM-DIFF?
我们将一个React函数看做是一个返回VirtualDOM Tree的方法:
function SomeComponent()
return <div>
<span>...</span>
<Button>...</Button>
</div>
上面的JSX会被转化成树状结构:
div
span
Button
当箭头函数的返回值发生变化的时候,比如(新增了某个节点):
function SomeComponent(a)
return <div>
<span>...</span>
<Button>...</Button>
a && <p>新增的节点</p>
</div>
我们可以有很多的策略完成这样的更新,比如把所有节点都删了,再重新构造。
当然,**最划算**的渲染策略,是在`div` 下新增一个`p` ,而不是将整个DOM替换。
React的调和算法帮助我们找到这种最快的更新途径。
伪代码
具体来说,React渲染函数的执行,会产生一个树状结构:
<div style=... />
// 会被转化为
React.createElement('div',
style :
...
)
`createElement` 后形成一个VirtualDOM节点
class ReactElement
type : SomeComponent
props :
children
Element可以看做“数据”的描述, 也就是元数据。 Element可以看做是虚拟DOM,代表了真实的DOM结构。因为每次渲染函数调用都生成一个新的Element,因此在Element之上还需要一个封装。
例如(伪代码):
class FiberNode
type : SomeComponent
props : ...
update(...)
同一个渲染函数的两次执行,究竟产生哪些DOM操作,需要由DOM-DIFF模块确定
具体来说,对于某个给定的组件:
function SomeComponent(...)
return <div>...</div>
当组件`SomeComponent`触发更新的时候,`react` 会这样处理:
... Fiber Context
let vDOMOld // 上一次调用SomeComponent产生的virtualDOM
……
update()
const vdomNext = SomeComponent(...)
const updates = domDiff(vDOMOld, vDOMNext)
vDOMOld = vDOMNext
apply(updates)
React更新产生虚拟DOM节点,然后通过diff算法比较两个DOM节点的差异,决定更新步骤,最后再向DOM应用这些更新。
在上面的程序中,箭头函数的返回值会被React记录下来,下次更新会尝试使用这个值继续比较。
这里有一个要求,domDiff算法的效率必须足够高,因为所有的更新都依赖它
DOM-DIFF细节
对于类型相同的节点
if ( vDOMOld.type === vDOMNext.type)
对于类型相同的节点,比如:
function Button(text)
return <button>text</button>
当发生变化的时候:
<Button text="点我" />
// 到
<Button text="请按键" />
只需要替换属性即可,React遇到这种情况,会帮用户替换掉属性。
不同类型的节点
当遇到不同类型的节点,React会直接替换。
<div>
<Counter />
</div>
// 到
<span>
<Counter />
</span>
子节点的处理
对于子节点, React会进行对比和替换。
<ul>
<li>first</li>
<li>second</li>
</ul>
// 到
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React会只insert第三个li。
但是对于下面这种乱序的情况,React会逐一替换:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
这是因为DOM-DIFF会用简单的算法,顺序比较,而不是用动态规划。在比较一个列表的时候,用最短编辑距离(动态规划)后,仍然有昂贵的DOM操作(插入、删除)等。而动态规划算法的复杂度有O(n^2)。
为了解决比较慢的问题,React引入了key来解决:
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
// 到
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
上面程序中,React会用key来进行比较,上面程序会考虑到key相同的情况,认为key相同的是同一个节点。 因此2014会是一个插入的节点。
伪代码
function* domDIFF(vDOM1, vDOM2)
if(!vDOM1)
yield new InsertUpdate(vDOM1, vDOM2)
return
if(vDOM1.type === vDOM2.type)
if(vDOM1.key === vDOM2.key)
yield new AttributeUpdate(vDOM1, vDOM2)
yield * domDIFFArray(vDOM1.children, vDOM2.children)
else
yield new ReplaceUpdate(vDOM1, vDOM2)
return
else
yield new ReplaceUpdate(vDOM1, vDOM2)
function toMap(arr)
const map = new Map()
arr.forEach(item =>
if(item.key)
map.set(item.key, item)
)
return map
function * domDiffArray(arr1, arr2)
if(!arr1 || !arr2)
yield new ReplaceUpdate(vDOM1, vDOM2)
return
const m1 = toMap(arr1)
const m2 = toMap(arr2)
// 需要删除的VDOM
const deletes = arr1.filter( (item, i) =>
return item.key ?
!m2.has(item.key)
: i >= arr2.length
)
for(let item of deletes)
yield new ReplaceUpdate(item, null)
// 需要Replace的VDOM
for(let i = 0; i <arr1.length; i++)
const a = arr1[i]
if(a.key )
if(m2.has(a.key))
yield * domDIFF(a, m2.get(a.key))
else
if(i < arr2.length)
yield * domDIFF(a, arr2[i])
// 需要Insert的VDOM
for(let i = 0; i <arr2.length; i++)
const b = arr2[i]
if(b.key)
if(!m1.has(b.key))
yield new InsertUpdate(i, b)
else
if(i >= arr1.length)
yield new InsertUpdate(i, arr[2])
class InsertUpdate
constructor(pos, to)
this.pos = pos
this.to = to
class ReplaceUpdate
constructor(from, to)
this.form = from
this.to = to
1
以上是关于2-2-12 React原理基础 (DOM - DIFF )部分的主要内容,如果未能解决你的问题,请参考以下文章
[react] 简要描述下你知道的react工作原理是什么?
[react] 简要描述下你知道的react工作原理是什么?
前端技能树,面试复习第 27 天—— React Diff 算法的原理,和 Vue 有什么区别 | 虚拟 DOM | key 的原理,为什么要用