理想是美好的,现实是骨感的。。。理论上来说,如果我将中间儿子这层改成了PureComponent,背景上7000个格子就不应该更新了,性能应该大幅提高才对。但是我测试后发现并没有什么用,这7000个格子还是更新了,什么鬼?其实这是PureComponent本身的一个问题:只进行浅比较。注意this.props[field] !== nextProps[field],如果this.props[field]是个引用对象呢,比如对象,数组之类的?因为他是浅比较,所以即使前后属性内容没变,但是引用地址变了,这两个就不一样了,就会导致组件的更新!
而在react-big-calendar里面大量存在这种计算后返回新的对象的操作,比如他在顶层Calendar里面有这种操作:
代码地址:
这行代码的意思是每次props改变都去重新计算状态state,而他的计算代码是这样的:
代码地址:
注意他的返回值是一个新的对象,而且这个对象里面的属性,比如localizer的计算方法mergeWithDefaults也是这样,每次都返回新的对象:
代码地址:
这样会导致中间儿子节点每次接受到的props虽然内容是一样的,但是因为是一个新对象,即使使用了PureComponent,其运行结果也是需要更新。这种操作在他的源码中大量存在,其实从功能角度来说,这样写是可以理解的,因为我有时候也会这么干。。。有时候某个属性更新了,不太确定要不要更新下面的组件,干脆直接返回一个新对象触发更新,省事是省事了,但是面对我们这种近万个组件的时候性能就崩了。。。
歪门邪道shouldComponentUpdate如果只有一两个属性是这样返回新对象,我还可以考虑给他重构下,但是调试了一下发现有大量的属性都是这样,咱也不是他作者,也不知道会不会改坏功能,没敢乱动。但是不动性能也绷不住啊,想来想去,还是在儿子的shouldComponentUpdate上动点手脚吧。简单的this.props[field] !== nextProps[field]判断肯定是不行的,因为引用地址变啦,但是他内容其实是没变,那我们就判断他的内容吧。两个对象的深度比较需要使用递归,也可以参考React diff算法来进行性能优化,但是无论你怎么优化这个算法,性能最差的时候都是两个对象一样的时候,因为他们是一样的,你需要遍历到最深处才能肯定他们是一样的,如果对象很深,这种递归算法不见得会比运行一遍render快,而我们面临的大多数情况都是这种性能最差的情况。所以递归对比不太靠谱,其实如果你对这些数据心里有数,没有循环引用什么的,你可以考虑直接将两个对象转化为字符串来进行对比,也就是
JSON.stringify(this.props[field]) !== JSON.stringify(nextProps[field])注意,这种方式只适用于你对props数据了解,没有循环引用,没有变化的Symbol,函数之类的属性,因为JSON.stringify执行时会丢掉Symbol和函数,所以我说他是歪门邪道性能优化。
将这个转化为字符串比较的shouldComponentUpdate加到背景格子的组件上,性能得到了明显增强,点击相应速度从7.5秒下降到了5.3秒左右。
按需渲染上面我们用shouldComponentUpdate阻止了7000个背景格子的更新,响应时间下降了两秒多,但是还是需要5秒多时间,这也很难接受,还需要进一步优化。按照我们之前说的如果还能阻止另外1399个事件的更新那就更好了,但是经过对他数据结构的分析,我们发现他的数据结构跟我们前面举的列表例子还不一样。我们列表的例子所有数据都在items里面,是否选中是item的一个属性,而react-big-calendar的数据结构里面event和selectedEvent是两个不同的属性,每个事件通过判断自己的event是否等于selectedEvent来判断自己是否被选中。这造成的结果就是每次我们选中一个事件,selectedEvent的值都会变化,每个事件的属性都会变化,也就是会更新,运行render函数。如果不改这种数据结构,是阻止不了另外1399个事件更新的。但是改这个数据结构改动太大,对于一个第三方库,我们又不想动这么多,怎么办呢?