React 组件是一个返回 Virtual DOM Tree 的方法
function SomeComponent() {
return (
<div>
<span>..</span>
<Button>..</Button>
</div>
)
}
//* 上述代码会转换为一个 React Virtual DOM,就像如下所示
class VirtualDOMNode {
type: SomeComponent // 如果是内置的就会描述成 "div" 等等
children
update() {
const newVirtualDOM = this.type()
// ...
}
}
更新策略有很多,而最划算的更新策略就是在原来的基础上进行新增、删除等操作,而不是对整个DOM进行替换。
1. React 渲染函数执行时会生成一个树状结构
<div style={
xxx}>...</div>
// 上面的jsx会转换为
React.createElement('div', {
style: {
xxx},
...
})
createElement()
被调用之后将生成一个 Virtual DOM 节点
class ReactElement {
type: SomeComponent
props: {
children
}
}
Element 本身可以看做是对“数据”的描述,也就是元数据,它本身是没有行为的。
Element 可以看做是虚拟 DOM,因为它代表了真实的 DOM 结构。但是又因为它本身没有行为,所以要使它拥有像更新的能力,就得对该 Element 再进行一次封装 (FiberNode)。
class FiberNode {
type: SomeComponent // 函数组件本身,调用它的时候可以生成新的 Element,复制于 ElementNode
props: {
},
update() {
}
}
2. 更新
对于某个给定组件
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 应用这些更新。
从上述伪代码中可以看到所有的更新都依赖 diff,这就要求 diff 算法的效率必须足够高才能很好地支撑起整个项目。
FAQ
问:为什么不把更新方法放到 ElementNode 当中
答:因为组件的更新需要在特定的场景下,它可能是在浏览器端、Native等等;另外组件的更新涉及到特殊的算法,像 Fiber。在具体的场景下再封装具体的方法有利于代码的设计。
对于相同类型的节点
if (vDOMOld.type === vDOMNext.type) {
// ...
}
比如下述组件发生变化时
function Button({
text}) {
return <button>{
text}</button>
}
只需要替换属性即可
<Button text="点击" />
// 转换为
<Button text="click" />
对于不同类型的节点
当遇到不同类型的节点时,React 会直接替换而不是继续往下比