react 渲染优化
虚拟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,我们看到实际上是删除旧元素,添加新元素 不加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的区别: tips:dom上设置可被react识别的同级唯一 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() 生成),所有子树将会在每次数据更新中重新渲染。 动静分离假设我们有一个下面这样的组件:
这是一个可以滚动的表格,
因为 通过把变化的属性和不变的属性进行分离,减少了重新渲染,获得了性能的提升,同时这样做也能够让组件更容易进行分离,更好的被复用。 最后说一个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/ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |