WHY DOM-DIFF?
我们将一个React函数看做是一个返回VirtualDOM Tree的方法:
function SomeComponent() {
return
...
}
上面的JSX会被转化成树状结构:
div
span
Button
当箭头函数的返回值发生变化的时候,比如(新增了某个节点):
function SomeComponent({a}) {
return
...
{a && 新增的节点
}
}
我们可以有很多的策略完成这样的更新,比如把所有节点都删了,再重新构造。
当然,**最划算**的渲染策略,是在`div` 下新增一个`p` ,而不是将整个DOM替换。
React的调和算法帮助我们找到这种最快的更新途径。
伪代码
具体来说,React渲染函数的执行,会产生一个树状结构:
// 会被转化为
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 ...
}
当组件`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算法的效率必须足够高,因为所有的更新都依赖它
对于类型相同的节点
if ( vDOMOld.type === vDOMNext.type)
对于类型相同的节点,比如:
function Button({text}) {
return
}
当发生变化的时候:
// 到
只需要替换属性即可,React遇到这种情况,会帮用户替换掉属性。
不同类型的节点
当遇到不同类型的节点,React会直接替换。
// 到
子节点的处理
对于子节点, React会进行对比和替换。
- first
- second
// 到
- first
- second
- third
React会只insert第三个li。
但是对于下面这种乱序的情况,React会逐一替换:
- Duke
- Villanova
- Connecticut
- Duke
- Villanova
这是因为DOM-DIFF会用简单的算法,顺序比较,而不是用动态规划。在比较一个列表的时候,用最短编辑距离(动态规划)后,仍然有昂贵的DOM操作(插入、删除)等。而动态规划算法的复杂度有O(n^2)。
为了解决比较慢的问题,React引入了key来解决:
- Duke
- Villanova
// 到
- Connecticut
- Duke
- Villanova
上面程序中,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) {
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