react源码解密6-渲染性能优化
时间:2026-4-5 22:02 作者:john 分类: 技术实现
前置准备
- jsx我们应该叫他什么。他本质上是createElement('div'或MyApp, props, xxxx), 我觉得他返回的应该叫组件树或elements树吧(这个树里既有div类型的节点,又有MyApp函数式组件类型的节点)
- 实际render的时候,才会执行上述elements树里面的组件函数或class,从而拿到每个element节点的真正的原子的vnode。此时的vnode才是原子的。这时候如果组织成树感觉才叫真正的vnode树或虚拟dom树。但此时就直接拿着vnode去生成真实dom了,貌似并没有把原子的vnode组织成树。
所以,感觉还是可以叫上述第一点里面的elements树就叫他虚拟dom树吧,不然也没有别的树了。 - 实际render(包括初始化或后续更新)的过程中,基于每个vnode生成了真实dom树。
之前遗留的疑问
-
如果jsx形成的elements虚拟dom树,如果某个click事件发生后。其中某个父组件和子组件里面都改变了自己组件内的state,也就意味着要触发组件重新render,尽管updateQUEUE会对update缓存等待统一处理但他并没有减少update次数,那么父组件和子组件都会各自调用一次render和update吗?这样岂不是父组件那次update就已经包含了子组件的update?这里是不是有性能浪费。
我实验了一下,的确是的:如果我子组件了点击了一下,子组件setState;同时由于父组件也监听了click于是事件也会冒泡给父组件触发,父组件也set了自己的state。于是子组件走一次update,父组件又走了一次update(会走一遍vnode挂载真实dom),而父组件里update的时候又把子组件render了一次,而且又把子组件额外重复调用render了一次(其实假设有domdiff的话,即使多render了一次,等真正对比的时候也会发现跟上次一样从而不更新真实dom吧..)。
而且关于这一点我发现一个严重bug:如果子组件和父组件都发生update,子组件自己update会使用原先那个组件实例,但父组件重新render的时候相当于重新执行实例化子组件,子组件的所有state变化都被重新重置了,无法位置之前子组件的状态了,这是个大bug。
https://yb.tencent.com/s/7R3FmdVaaSJa -
之前实现的源码我们只要某个组件要重新update,我们就会重新render虚拟dom并重新把这个位置的真实dom干掉,换成新虚拟dom对应的真实dom。这里是不是无脑干的话也太浪费了。是不是需要dom diff。是的。
dom更新的优化-dom diff
粗暴简单比较法
- 首先总体思路肯定是同一层级进行比较。
- 如果同一层级有多个,则从左到右依次比较。若相同类型则复用修改、若不同类型则替换(删旧的加新的)、若oldTree上没有则新增、若newTree没有则要删除。合计行为:不操作、删除老的、增加新的、替换(删老的加新的)、修改(这是最复杂的,因为类型一样所以要做修改)。
可以看到,如果没有key这种标记的话,我们能想到的其实就是直接对比。然而这样一对比就很容易出现浪费的情况。例如下图最简单的这种情形都有极大的浪费:

明明事实上只是做了一个顺序变换---明明是做移动操作即可,结果vnode对比的是时候得出的结论是“A和B是同一个标签类型,于是他就A改成B的各种属性和值,然后把B改成A的属性进行更新”非常消耗性能。
可见“简单比较法”好像就是缺了个“移动”这种动作。
高效算法
思路是:不动、移动、删除旧的、插入新的。好像这4个动作就能满足所有场景需求。但他需要你的节点上有key从而告诉算法是不是同一个节点。
不过好像还缺了个动作:如果类型一样而且key也一样,那他就是不动,不动的情况下可能属性或value值会有变化。
其实高效算法和普通算法不是冲突的,而是说在比较2个vnode的时候如果“都存在且类型都相同”才会用复杂算法去递归他的children孩子。
下面是当前基于简单+复杂算法的完整的dom diff算法的实现。难点就在于children列表那里如何获得一个最终操作真实dom 的actions数组。
export function updateDomTree(oldDom, newVNode, oldVNode) {
// const parent = oldDom.parentNode
// if (parent) {
// parent.removeChild(oldDom)
// doMount(newVNode, parent)
// }
// 实现虚拟dom算法(注意这里接收到的vNode里面依然会存在createElement(MyApp)这样的element,因为他还没有做mount所以还没有深度递归处理成最终vnode)
const typeMap = {
NO_OPERATE: !oldVNode && !newVNode,
ADD: !oldVNode && newVNode,
DELETE: oldVNode && !newVNode,
REPLACE: oldVNode && newVNode && oldVNode.type !== newVNode.type
}
let UPDATE_TYPE = Object.keys(typeMap).filter(key => typeMap[key])[0]
switch(UPDATE_TYPE) {
case 'NO_OPERATE':
break;
case 'DELETE':
oldDom.remove()
break;
case 'ADD':
oldDom.parentNode.appendChild(_createRealDom(newVNode))
break;
case 'REPLACE':
oldDom.remove()
oldDom.parentNode.appendChild(_createRealDom(newVNode))
break
default:
// 新老虚拟dom都存在,且类型相同。则要进行深度比较
deepDOMDiff(oldVNode, newVNode)
break;
}
}
// 深度比较
function deepDOMDiff(oldVNode, newVNode) {
const diffTypeMap = {
ORIGIN_NODE: typeof oldVNode.type === 'string',
CLASS_COMPONENT: typeof oldVNode.type === 'function' && oldVNode.type.REACT_CLASS,
FUNCTION_COMPONENT: typeof oldVNode.type === 'function',
TEXT: oldVNode.$$typeof === REACT_TEXT_TYPE
}
const DIFF_TYPE = Object.keys(diffTypeMap).filter(key => diffTypeMap[key])[0]
switch(DIFF_TYPE) {
case 'ORIGIN_NODE':
{ const currentDOM = newVNode.dom = findDomByVNode(oldVNode)
_setRealElementProps(currentDOM, newVNode.props)
updateChildren(currentDOM, oldVNode.props.children, newVNode.props.children)
break; }
case 'CLASS_COMPONENT':
updateClassComponent(oldVNode, newVNode)
break;
case 'FUNCTION_COMPONENT':
updateFunctionComponent(oldVNode, newVNode)
break;
case 'TEXT':
newVNode.dom = findDomByVNode(oldVNode)
newVNode.dom.textContent = newVNode.props.text
break
default:
break
}
}
function updateChildren(parentDOM, oldChildren, newChildren) {
oldChildren = Array.isArray(oldChildren) ? oldChildren : [oldChildren]
newChildren = Array.isArray(newChildren) ? newChildren : [newChildren]
let lastNotMovedIdx = -1 // 上一个不需要移动的节点在旧列表中的位置,这个标识很重要,他决定了碰到下一个可复用旧节点的时候到底是保持不动还是要移动。
let oldKeyChildMap = {}
oldChildren.forEach((oldVNode, index) => {
oldVNode.index = index
let oldKey = oldVNode?.key ?? index
oldKeyChildMap[oldKey] = oldVNode
})
const actions = []
newChildren.forEach((newVNode, idx) => {
newVNode.index = idx
let newKey = newVNode.key ?? idx
let oldVNode = oldKeyChildMap[newKey] // 其实这个map实现了能用o1的时间快速找到某个新vnode在旧列表中是否存在的能力。
if (oldVNode) {
// 找到了旧的vnode
deepDOMDiff(oldVNode, newVNode) // 先把子树递归处理完再来处理当前节点
if (oldVNode.index < lastNotMovedIdx) {
actions.push({
type: 'MOVE',
oldVNode,
newVNode,
idx
})
}
delete oldKeyChildMap[newKey]
lastNotMovedIdx = Math.max(lastNotMovedIdx, oldVNode.index)
}
else {
// 找不到旧的vnode,那就是新增
actions.push({
type: 'CREATE',
newVNode,
idx
})
}
// 开始操作
const VNodeToMove = actions.find(a => a.type === 'MOVE' && a.newVNode === newVNode)?.oldVNode
const VNodeToDelete = Object.values(oldKeyChildMap)
VNodeToMove.concat(VNodeToDelete).forEach(oldVNode => {
oldVNode.dom.remove()
})
actions.forEach(action => {
let todoDom = null
if (action.type === 'CREATE') {
todoDom = _createRealDom(newVNode)
}
else if (action.type === 'MOVE') {
todoDom = action.oldVNode.dom
}
const realDomChildren = parentDOM.childNodes
const childRefNode = realDomChildren[action.idx]
if (childRefNode) {
parentDOM.insertBefore(todoDom, childRefNode)
}
else {
parentDOM.appendChild(todoDom)
}
})
})
}
function updateClassComponent(oldVNode, newVNode) {
const classInstance = newVNode.classInstance = oldVNode.classInstance
classInstance.updater.launchUpdate() // 这里感觉就是交给update去处理(里面会再调用render和updateDomTree),而不是父组件完全走一遍mount过程---如果完全走一遍会导致子组件重新实例化成新实例。
}
function updateFunctionComponent(oldVNode, newVNode) {
let oldDOM = findDomByVNode(oldVNode)
if (!oldDOM) return
const { type, props } = newVNode
let newRenderVNode = type(props)
updateDomTree(oldVNode.oldRenderVNode, newRenderVNode, oldDOM)
newVNode.oldRenderVNode = newRenderVNode
}
缺陷
假设是从“a-b-c-d-e”变成“e-a-b-c-d”。 上述算法算出来的actions是:
- e不动
- a、b、c、d分别移动
这很明显不太对,最快的应该是e移动到最前面即可。
于是vue3利用了一种leetcode上的“最长递增子序列”原理,找出跟新列表相比下旧列表里面的“最长递增子序列”即:a-b-c-d。
于是他就会充分利用abcd不变,然后变其他的。
PureComponent和Memo
所谓memo其实就是对原来的函数组件做了个包裹,包裹成了一个新型的对象。然后被createElement创建的时候,这个vnode的type就变成了这个新型对象。
于是你在初始化mount和后期update更新的时候,react-dom里面都要针对这个新型的memo类型type做处理。例如对比props和state是否发生变化,没变化就不要触发重新update。