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',
  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

144737_ad16_2267438.png

 可以看到上面的操作都是在父组件修改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;
  }

 

项目数据扁平化,不扁平化带来的问题:

  1. 数据拷贝比较更耗时
  2. 获取数据的时候比较麻烦

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,我们看到实际上是删除旧元素,添加新元素

141522_BJjD_2267438.png

不加key,实际上是替换了textContent等attr

141632_Qohr_2267438.png

看到区别了吗?  也就是说加不加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])

 

大家可以关注我的公众号获取更多资讯。

151510_94Mj_2267438.jpg

参考资料:

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/

转载于:https://my.oschina.net/wanjubang/blog/873378

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值