react 渲染优化

前端之家收集整理的这篇文章主要介绍了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;
  }

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

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

不加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/

猜你在找的React相关文章