虚拟dom
刚入门react的话,可能会存在这样一个误区。就是react有虚拟dom在,他总是高效的,我修改应用的一个组件,其他组件不会重新渲染。 事实上,react每次update都会将整个app 重新渲染一遍,除非shouldComponentUpdate (以下简称SCU)返回false。也就是说默认情况下,只要修改应用的一部分,整个应用就会重新渲染。对,全部! 不过你也不必太担心 ,react使用了vdom来优化,使得不需要渲染的地方,只是执行了render(),而并没有patch到真实dom。
tips: 由于render频繁执行,所以不要在render中bind,统一放到construtor
我们先来看下 react的虚拟dom
react 和 vue都有vitual dom 机制,用来做dom diff,以减少实际操作dom的性能损失。 原理大概是这样的。
class App extends React.Component {
render() {
return <div>
<Child name="xiaoming" />
<Child name="xiaohu" />
<Child name="xiaosan" xiaosan={this.state.xiaosan} />
<Child name="xiaojin" />
</div>;
}
}
// 上面这个组件经过render会形成类似下面的数据结构
const vnode = [{
tag: 'div',
sel: '',
class: '',
children: [],
props: []
}, {
tag: 'div',
sel: '',
class: '',
children: [],
props: []
}]
// 然后将前一个前一次的vnode和这次的vnode比较
// 如果是可以比较,就打补丁(局部更新)
// 如果不可比较(跨层级,不同key),直接创建新节点删除旧节点
function domdiff(oldvnode, vnode) {
// 这就告诉我们尽量跨层级修改dom会让react不能优化,甚至会做一些无用的计算
// 所以尽量在同一层级修改
// 另外增加key会让dom diff更高效
if(sameVnode()) {
patch(vnode, oldvnode)
} else {
createEle(vnode)
delEle(oldvnode)
}
}
减少render
上面说了默认情况下,只要修改应用的一部分,整个应用就会重新渲染。 所以尽量不要将计算放在render中进行,复杂运算绝对要禁止!!!
我这里做了一个简单的demo。 演示了下如何优化render,如果想自己试试的话,可以clone到本地查看。
github地址:https://github.com/azl397985856/react-performance
可以看到上面的操作都是在父组件修改state,改变某一个子组件的props。 最上面的那种是什么都不做的情况下,默认所有组件都会render。中间那种通过手动写SCU。减少了不必要的render,但是这种做法代价昂贵,每一个组件都要这么写才可以避免不必要的render,而且简单对象还好比较,如果是复杂嵌套对象,根本就很难比较,甚至比较的时候会超过render时间得不偿失啊。
其实render时间是比较短的,就是将render走一遍,然后更新虚拟dom的过程(我希望你没有写什么复杂计算和无数层级)。
那么总结下如果优化react应用。
1.最常用的用法就是
shouldComponentUpdate(nextProps, nextState) {
// 组件还有什么属性你就继续添加, 另外state同理判断
// 因此请只传递component需要的props ,切勿一股脑的<Component {...props} />
return nextProps.name !== this.props.name || nextProps.xiaosan !== this.props.xiaosan;
}
项目数据扁平化,不扁平化带来的问题:
- 数据拷贝比较更耗时
- 获取数据的时候比较麻烦
2. 推荐做法
import pureRender from 'pure-render-decorator';
// 这种好处就是不要自己写代码判断
// 而且效率高
// 不好的地方就是修改state props的地方和原先代码有出入
@pureRender
class Child extends React.Component {
render() {
const { name, xiaosan } = this.props.payload;
return <div>
这里是第一层子节点
child-{name}
{xiaosan}
<ChildOfChild name="狗" />
</div>;
}
}
// 如果要修改state,需要这样的写法
this.setState({
payload: Immutable.set(Immutable(this.state.payload), 'xiaosan', '小伞你好')
});
diff最小化
diff最小化可以高效且正确的渲染数据。刚才简单说了下react vdom的原理。我们知道vdom是不会跨级比较的,并且在有key的情况下,会直接使用key,减少计算消耗。
举个栗子:
/*
* A simple React component
*/
class Application extends React.Component {
constructor(props) {
super(props);
this.state = {
flag: true
}
this.switch = this.switch.bind(this)
}
handleOk() {
console.log('ok');
}
handleCancel() {
console.log('cancel');
}
switch() {
console.log('switch');
this.setState({
flag: !this.state.flag
})
}
render() {
// button 是否加key 对渲染是有差别的,具体看下文
return (<div>
{
this.state.flag
? <button key="ok" onClick={this.handleOk}>确定
</button >
: <button key="cancel" onClick={this.handleCancel}>取消</button >
}
<button onClick={this.switch}>切换显示
</button >
</div>
)
}
}
/ * * Render the above component into the div#app * /
React.render(<Application / >, document.getElementById('app'));
加key,我们看到实际上是删除旧元素,添加新元素
不加key,实际上是替换了textContent等attr
看到区别了吗? 也就是说加不加key会导致react不同的做法。我们从代码上看下react dom diff。
代码摘自 vue 源码:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, elmToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !elmToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
if (sameVnode(elmToMove, newStartVnode)) {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
设置key和不设置key的区别:
不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx
中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
tips: dom上设置可被react识别的同级唯一key
,否则情况可能不会重新渲染。
DOM结构 的改变 =>
renderA: <div />
renderB: <span />
=> [removeNode <div />], [insertNode <span />
DOM属性的改变 =>
renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]
之前插入DOM =>
renderA: <div><span>first</span></div>
renderB: <div><span>second</span><span>first</span></div>
=> [replaceAttribute textContent 'second'], [insertNode <span>first</span>]
之前插入DOM,有key的情况
renderA: <div><span key="first">first</span></div>
renderB: <div><span key="second">second</span><span key="first">first</span></div>
=> [insertNode <span>second</span>]
由于依赖于两个预判条件,如果这两个条件都没有满足,性能将会大打折扣。
1、diff算法将不会尝试匹配不同组件类的子树。如果发现正在使用的两个组件类输出的 DOM 结构非常相似,你可以把这两个组件类改成一个组件类。
2、如果没有提供稳定的key(例如通过 Math.random() 生成),所有子树将会在每次数据更新中重新渲染。
动静分离
假设我们有一个下面这样的组件:
<ScrollTable width={300} color='blue' scrollTop={this.props.offsetTop} /> |
这是一个可以滚动的表格,offsetTop
代表着可视区距离浏览器的的上边界的距离,随着鼠标的滚动,这个值将会不断的发生变化,导致组件的 props 不断地发生变化,组件也将会不断的重新渲染。如果使用下面的这种写法:
<OuterScroll> <InnerTable width={300} color='blue'/> </OuterScroll> |
因为 InnerTable
这个组件的 props 是固定的不会发生变化,在这个组件里面使用pureRenderMixin
插件,能够保证 shouldComponentUpdate
的返回一直为 false
, 因此不管组件的父组件也就是 OuterScroll
组件的状态是怎么变化,组件 InnerTable
都不会重新渲染。也就是子组件隔离了父组件的状态变化。
通过把变化的属性和不变的属性进行分离,减少了重新渲染,获得了性能的提升,同时这样做也能够让组件更容易进行分离,更好的被复用。
最后说一个rendux小技巧
如果我们需要同时发送很多action,比如:
dispatch(action1)
dispatch(action2)
dispatch(action3)
可以减少不必要的计算,推荐用到redux-batched-actions
dispatch(batchActions[action1, action2, action3])
大家可以关注我的公众号获取更多资讯。
参考资料:
https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js
https://segmentfault.com/a/1190000006100489
https://juejin.im/entry/57621f7980dda4005f7332f3
http://taobaofed.org/blog/2016/08/12/optimized-react-components/