react性能优化

我们考虑一下react优化应该在生命周期,装载阶段基本没有什么选择,当react组件第一次出现在dom树中的时候,无论如何也要渲染一次的,从这个组件往下的所有子组件,都要经历一次react组件的装载生命周期,因为这部分没什么可以省略的,所以没有什么性能优化的点;至于卸载阶段,就更没有什么可做了;所以值得关注的等阶段就在更新阶段了。

react的调和过程

在装载过程中,react通过render在内存中产生了一个树形结构,树上每一个节点都代表一个react组件或者原生的dom元素,这个树形结构就是所谓的虚拟dom。react根据这个虚拟dom来渲染产生浏览器中的dom树。

在装载过程结束后,用户就可以对网页进行交互,用户操作引发了界面的更新,网页中需要更新界面, React 依然通过 render 方法获得 个新的树形结构 Virtual DOM ,这时候当然不能完全和装载过程 样直接用 Virtual DOM 去产生 DOM 树,不然就和最原始的字符串模板一个做法 而且,在真实的应用中,大部分网页内容的更新都是局部的小改动,如果每个改动都是推倒重来,那样每次都重新完全生成 DOM 树,性能肯定不可接受。

实际上, React 在更新阶段很巧妙地对比原有的 Virtual DOM 和新生成的 Virtual DOM,找出两者的不同之处,根据不同来修改 DOM 树,这样只需做最小的必要改动

React 在更新中这个“找不同”的过程,就叫做 Reconciliation (调和)

找出两个树形结构的区别,从计算机科学的角度来说,真的不是 件快速的过程。按照计算机科学目前的算法研究结果,对比两个 个节点的树形结构的算法,时间复杂度是 O(N3),打个比方,假如两个树形结构上各有 100 节点,那么找出这两个树形结别的操作,需要 100 100 100 次操作,也就是 百万次当量的操作,假如有 千个节点,那么需要相当于进行相当于 1000 1000 1000 次操作,这是 亿次的操作当量,这么巨大数 的操作在强调快速反应的网页中是不可想象的,所以 React 不可能采用这样的算法、

React 实际采用的算法需要的时间复杂度是 O(N),因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比 O(N)时间复杂度更低的算法。

其实 React Reconciliation 算法并不复杂,当 React 要对比两个 Virtual DOM 的树形结构的时候,从根节点开始递归往下比对,在树形结构上,每个节点都可以看作一个这个节点以下部分子树的根节点 所以其实这个对比算法可以从 Virtual DOM 上任何一个节点开始执行。

React 先检查两个树形的根节点的类型是否相同,根据相同或者不同有不同处理方式

  1. 节点类型不同的情况

如果树形结构根节点类型不相同,那就意味着改动太大了,也不要去费心考虑是不是原来那个树形的根节点被移动到其他地方去了,直接认为原来那个树形结构已经没用,可以扔掉, 要重新构建新的 DOM 树,原有的树形上的 React 组件会经历“卸载”的生命周期。

也就 说,对于 Virtual DOM 树这是 个“更新”的过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程。

  1. 节点类型相同的情况

如果两个树形结构的根节点类型相同, React 就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。这时,有必要区分一下节点的类型,节点的类型可以分为两类: 一类是 DOM 元素类型,对应的就是 HTML 直接支持的元素类型,比如 div span ;另一类是 React件,也就是利用 React 库定制的类型;对于 DOM 元素类型, React 会保留节点对应的 DOM 元素,只对树形结构根节点上的属性和内容做一下比对,然后只更新修改的部分;如果属性结构的根节点不是 DOM 元素类型,那就只可能是 React 组件类型,那么React 做的工作类似,只是 React 此时并不知道如何去更新 DOM 树,因为这些逻辑还在React 组件之中, React 能做的只是根据新节点的 props 去更新原来根节点的组件实例,引发这个组件实例的更新过程,也就是按照顺序引发下列函数:

shouldComponentUpdate
componentWillReceiveProps
componentWillUpdate
render
componentDidUpdate 

在这个过程中,如果 shouldComponentUpdate 函数返回 false 的话,那么更新过程就此打住,不再继续 所以为了保持最大的性能,每个 React 组件类必须要重视 shouldComponentUpdate,如果发现根本没有必要重新渲染,那就可以直接返回 false;在处理完根节点的对比之后, React 的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。

上面只是虚拟dom的一种概括,我们来看看虚拟dom具体的算法,包括几个步骤:

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中

  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异

  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。

Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

这三步最难理解的应该就是第二步了,怎么比较两棵虚拟DOM树的差异?

  1. 在前端当中,你很少会跨越层级地移动DOM元素。所以 Virtual DOM 只会对同一个层级的元素进行对比

  2. 在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记

  3. 在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面

差异通常指以下几点:

  • 替换掉原来的节点,例如把div换成section

  • 移动、删除、新增子节点,调换dom顺序

  • 修改了节点的属性

  • 对于文本节点,文本内容可能会改变。

以下是常用的几种优化方式:

PureComponent

前面已经提过,性能优化通常在shouldComponentUpdate阶段,而PureComponent就是官方帮我们实现了shouldComponentUpdate,shouldComponentUpdate实现了浅比较,所以我们就不用自己去写了。

//shadowEqual

function shadowEqual(obj,newObj){
    if(obj === newObj){
        return true
    }

    const objKeys = Object.keys(obj)
    const newObjectKeys = Object.keys(newObj)
    if(objKeys.length !== newObjectKeys.length){
        return false
    }

    // 只需关注props中每一个是否相等,无需深入判断
    return objKeys.every(key => {
        return newObj[key] === obj[key]
    })
}

PureRender优化

PureRender指的是满足纯函数的特点,即条件的渲染总是被相同的props和state渲染从而得到相同的结果

  1. 直接为props设置对象和数组

每次调用react组件都会重新创建组件,那么就算传入的数组和对象没有改变,他们引用的地址也会变

<App style= {margin:10}>

这样设置prop,每次渲染style都是新对象,对于这种情况,我们可以直接提前赋值,将默认值保存为同一份引用

const appStyle =  {margin:10}
<App style=appStyle>
  1. 设置props方法通过事件绑定在元素上

前面已讲,现在不再赘述,可以观看前面的文章react中的事件

Immutable

js中的对象一般都是可变的,因为使用了引用赋值,新的对象引用了原始对象,改变新的对象将影响到原始的对象。。为了解决这个问题,一般是采用浅拷贝或深拷贝来解决问题,一般我们是用对象扩展符或者Object.assign来解决,但是当我们遇到很复杂的项目时,这样会造成cpu和内存的浪费。Immutable实现的原理是持久化的数据结构,也就是使用旧数据来创建新数据的时候,要保证旧数据同时可用且不变。同时为了避免深拷贝把所有节点都复制一遍带来的性能损耗,Immutable使用了结构共享,即如果对象中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

keys

试分析这种情况:

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

假如我们在最后面加一个li,react会直接在后面加一个li,但是一旦我们在最前面加一个li,react这回却不认了,它会把所有li删除然后重新创建新的4个li,这就很浪费性能了。

React 不会使用一个 O(N2)时间复杂度的算法去找出前后两列子组件的差别,默认情况下,在 React 的眼里,确定每一个组件在组件序列中的唯一标识就是它的位置,所以它也完全不懂哪些子组件实际上并没有改变,为了让 React 更加“聪明”,就需要开发者提供一点帮助。

如果在代码中明确地告诉 React 每个组件的唯一标识,就可以帮助 React 在处理这个问题时聪明很多,告诉 React 每个组件“身份证号”的途径就是 key 属性。

理解了 key 属性的作用,也就知道,在一列子组件中,每个子组件的 key 值必须唯唯一,不然就没有帮助 React 区分各个组件的身份,这并不是一个很难的问题,一般很容易给每个组件找到一个唯一的 id;但是这个 key 值只是唯一还不足够,这个 key 值还需要是稳定不变的,试想,如果key 值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导 React 做出错误判断,甚至导致错误的渲染结果。这就是为什么不推荐用数组的index来做key的原因了,虽然它是唯一的,但是它不够稳定,会变来变去。

reselect

在前面的例子中,都是通过优化渲染过程来提高性能,既然 React Redux 是通过数据驱动渲染过程,那么除了优化渲染过程,获取数据的过程也是一个需要考虑的优化点.

selector 实际上就是一个纯函数

selector(state) => some props

而纯函数是具有可缓存性的,即对于同样的输入参数,永远会得到相同的输出值。reselect 的原理就是如此,每次调用 selector 函数之前,它会判断参数与之前缓存的是否有差异,若无差异,则直接返回缓存的结果,反之则重新计算

reselect认为一个选择器的工作可以分为两个部分,把一个计算过程分为两个部分

  1. 从输入参数state抽取第一层结果,将这一层结果和之前抽取的第一层结果作比较,如果发现完全相同,就没有必要进行第二部分运算了。选择器直接把之前第二部分的运算结果返回就可以了。注意,这一部分做的"比较",就是js中的===操作符比较,如果第一层结果是对象的话,只有同意对象才会认为是相同

  2. 根据第一层结果计算出选择器需要返回的最终结果

显然,每次选择器函数被调用时,步骤一都会被执行,但步骤一的结果被用来判断是否可以使用缓存的结果,所以并不是每次都会调用步骤二的运算