react之所以可以快速更新dom,在于react可以对比虚拟dom,找到差异后,只更新改变的部分。
diff算法有很多,比如DFS算法O(n^3) > cito.js > kivi.jsO(n^2)
对于react,FB通过大胆的策略,满足了大多数的性能最大化,将O(n3)复杂度的问题成功的转换成了O(n),并且后面对于同级节点移动,牺牲一定的DOM操作,算法的复杂度也才打到O(max(M,N))。
react源码(0.3)实现:
updateMultiChild: function(nextChildren, transaction) {
if (!nextChildren && !this._renderedChildren) {
return;
} else if (nextChildren && !this._renderedChildren) {
this._renderedChildren = {}; // lazily allocate backing store with nothing
} else if (!nextChildren && this._renderedChildren) {
nextChildren = {};
}
var rootDomIdDot = this._rootNodeID + '.';
var markupBuffer = null; // Accumulate adjacent new children markup.
var numPendingInsert = 0; // How many root nodes are waiting in markupBuffer
var loopDomIndex = 0; // Index of loop through new children.
var curChildrenDOMIndex = 0; // See (Comment 1)
for (var name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {continue;}
// 获取当前节点与要渲染的节点
var curChild = this._renderedChildren[name];
var nextChild = nextChildren[name];
// 是否两个节点都存在,且类型相同
if (shouldManageExisting(curChild, nextChild)) {
// 如果有插入标示,之后又循环到了不需要插入的节点,则直接插入,并把插入标示制空
if (markupBuffer) {
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
markupBuffer = null;
}
numPendingInsert = 0;
// 如果找到当前要渲染的节点序号比最大序号小,则移动节点
/*
* 在0.3中,没有根据key做diff,而是通过Object中的key作为索引
* 比如{a,b,c}替换成{c,b,c}
* b._domIndex = 1挪到loopDomIndex = 1的位置,就是原地不动
a._domIndex = 0挪到loopDomIndex = 2的位置,也就是和c换位
*/
if (curChild._domIndex < curChildrenDOMIndex) { // (Comment 2)
this.enqueueMove(curChild._domIndex, loopDomIndex);
}
curChildrenDOMIndex = Math.max(curChild._domIndex, curChildrenDOMIndex);
// 递归更新子节点Props,调用子节点dom-diff...
!nextChild.props.isStatic &&
curChild.receiveProps(nextChild.props, transaction);
curChild._domIndex = loopDomIndex;
} else {
// 当前存在,执行删除
if (curChild) { // !shouldUpdate && curChild => delete
this.enqueueUnmountChildByName(name, curChild);
curChildrenDOMIndex =
Math.max(curChild._domIndex, curChildrenDOMIndex);
}
// 当前不存在,下个节点存在, 执行插入,渲染下个节点
if (nextChild) { // !shouldUpdate && nextChild => insert
this._renderedChildren[name] = nextChild;
// 渲染下个节点
var nextMarkup =
nextChild.mountComponent(rootDomIdDot + name, transaction);
markupBuffer = markupBuffer ? markupBuffer + nextMarkup : nextMarkup;
numPendingInsert++;
nextChild._domIndex = loopDomIndex;
}
}
loopDomIndex = nextChild ? loopDomIndex + 1 : loopDomIndex;
}
// 执行插入操作,插入位置计算方式如下:
// 要渲染的节点位置-要插入的节点个数:比如当前要渲染的节点index=3,当前节点只有一个,也就是index=1。
// 如<div>1</div>渲染成<div>1</div><div>2</div><div>3</div>
// 那么从<div>2</div>开始就开始加入buffer,最终buffer内容为<div>2</div><div>3</div>
// 那么要插入的位置为 3 - 1 = 2。我们以<div>1</div>为1,就是把buffer插入2的位置,也就是<div>1</div>后面
if (markupBuffer) {
this.enqueueMarkupAt(markupBuffer, loopDomIndex - numPendingInsert);
}
// 循环老节点
for (var childName in this._renderedChildren) {
if (!this._renderedChildren.hasOwnProperty(childName)) { continue; }
var child = this._renderedChildren[childName];
// 当前节点存在,下个节点不存在,删除
if (child && !nextChildren[childName]) {
this.enqueueUnmountChildByName(childName, child);
}
}
// 一次提交所有操作
this.processChildDOMOperationsQueue();
}
diff和dom更新关联图:
三个大胆策略详解:
tree diff
只会对同一层次的节点进行比较,如果节点不存在直接删除创建
Component diff
同一类型的组件继续tree diff比较,不同类型的组件直接删除重建。
element diff 或者叫list diff
三种方法:插入,移动,删除
-
INSERT_MARKUP插入,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
-
MOVE_EXISTING移动,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
-
REMOVE_NODE删除,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
举个例子:
state = {
testList:[
'll','ww','kk'
]
}
componentDidMount = () => {
this.backgroundDraw();
setTimeout( () => {this.setState({
bannerText:'我改变了',
testList:[
'kk','ll','ww'
]
})},5000)
}
//render两种情况
//一种key = {index}
<div className="strategy-position">
{
this.state.testList.map( (item,index) => <li key={index}>{item}</li>)
}
</div>
//一种key = {item}
<div className="strategy-position">
{
this.state.testList.map( (item,index) => <li key={item}>{item}</li>)
}
</div>
上面例子中第一种情况,在testList改变后:三个节点都会更新
第二种情况,会先删除,再移动,再插入
为什么会有上述两种情况的区别呢?
对于第一种情况:
key = {index}在更新前后是相同的,都是1,2,3
但是对比,key相同时,元素不同,则删除插入
对于第二种情况;
key = {item} key分别是 'll','ww','kk' ,更新后虚拟dom key分别是kk ll ww,在进行element diff时,发现kk元素节点在更新前后是相同的,无需创建,会进行移动
说明:
对于第一种情况,其实和没有key是一样的,除非其顺序和key保持一致;
针对这一现象,React 提出优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!
第二种情况的key和顺序无关,可以找到更新前后没有更改的元素节点。
总结:
-
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
-
React 通过分层求异的策略,对 tree diff 进行算法优化;
-
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
-
React 通过设置唯一 key的策略,对 element diff 进行算法优化;
-
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
-
建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
参考:https://www.jianshu.com/p/7b580d2e51d5
https://zhuanlan.zhihu.com/p/20346379