虚拟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',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,实际上是替换了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,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,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,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,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,oldStartVnode.elm) newStartVnode = newCh[++newStartIdx] } } } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm,refElm,newStartIdx,newEndIdx,insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm,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/