react需要深入了解的几个点
一、diff算法/原理
传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。
要知道修改dom是很费性能的,为此,react采用虚拟dom的方法,将很多状态的改变合并计算出需要更新的地方在进行渲染,但是虚拟dom没有一个强力的diff算法可不行,来看看react做了哪些神仙diff优化调整策略。
1.因为卡法中跨层级的dom节点移动惭怍很少,可以忽略不计,所以react只做同层diff
2.有同类名的组件会生成相似的树结构,有不同类名的组件生成不同结构的树
3.同层的一组节点,通过唯一的id\key区分
通过这几个策略,diff算法的复杂度就达到了O(n)—因为一次遍历就足够。
以这三个策略为前提,react分别对tree diff、component diff 以及 element diff进行了合理优化
1.tree diff
基于第一个策略,对节点树的diff,react只做同层的对比,发现某个节点不存在时,就把该节点及其子节点全部删除掉,也就是说,一次遍历就完成了tree diff。但是,偶尔也可能遇到跨层的节点移动情况,此时,react是这样处理的,依然同层比较,移走就删除该节点,在移动到的位置新建节点,都知道这种操作对性能消耗很大,所以react不推荐这样操作。
启示:写react项目时,保证稳定的树结构很重要,必要时可以用css隐藏节点而不是真正的删除节点。
2.component diff
如果是同类型组件,就依照原策略继续比较虚拟dom树,如果不是,该组件被判断为dirtyComponent,替换下组件所有子节点—删除并重新创建。
进行diff是要消耗时间的,如果你确切地知道某组件不会发生变化,可以使用shouldComponentUpdate()来判断是否需要diff。
3.element diff
而同一层级的元素进行diff时,提供了三种操作,移动、插入、删除,举例:
情况一:无新增或者删除
原节点—A、B、C、D
新节点—B、C、D、A
react希望开发者对同一组节点添加唯一key加以区分,这样只需要把A移动到末尾就好了,移动策略是这样的,新节点数组从左到右遍历,第一个B,在原节点中,且index为0,老节点数组B的index为1>0,不移动,同理,CD也不移动,直到A,在原节点中且index在新数组中是3,旧数组中是0<3,所以A进行移动。
情况二:有新增有删除
原节点—A、B、C、D
新节点—B、E、A、D
B和情况一相同,不移动,遇到E在原节点中不存在,则新建E节点添加到B后面,遇到A,可以认为A的index不考虑之前新增的节点E,认为A的index还是1,大于老数组A的index0,需要移动,然后是D,新节点中index是2小于老数组中index3,不更新。
总结:element diff希望开发者在同一组节点上添加key以区分节点,从左到右遍历新节点数组,判断是否在原节点数组中,在的话,判断index是否大于原节点index,大于则移动到新位置,否则不移动;不在则新建该节点,并放入新数组。
注意:新节点数组元素index可以认为不考虑新增节点。
但是,react的diff算法还是有一些问题的,比如:
原节点—A、B、C、D
新节点—D、A、B、C
按照上面的diff,D节点不移动,而ABC节点均要移动一次,很明显浪费了性能。
启示:尽量在react中避免把列表最后一个元素放到前面,列表元素过多或者渲染次数过多时性能肯定会下降很多。
二、setState原理
用过setState应该知道setState是“异步”的,加引号意思是说并不是所有情况都是异步的,比如原生js中,setTimeout\promise中就是同步的,那么先分析一下,为什么大多数情况要异步处理。
因为setState后要diff,然后你根据diff渲染页面,如果是同步的,那么下面的代码可能就是灾难性的:
1 | |
每次setState后都diff+渲染,项目可能直接就挂了,真实的react中,这样的代码,最后只会渲染出1,异步的原因清楚了,那么底层原理呢?
react内部是自己封装了一套事件机制的,代理了原生js事件,如onClick,onChange等,所以遇到原生js异步会失效。
异步怎么形成的呢,合成事件或者生命周期执行前后会调用pre和post钩子,pre钩子会调用batchedUpdate方法将isBatchingUpdates变量置为true(在批量更新),开启批量更新,而post钩子会将isBatchingUpdates置为false(结束批量更新),为true时把需要更新的组件添加到dirtyCompotent中,直到isBatchingUpdates为false,调用updateComponent更新组件。
讲到这,setTimeout和promise不是异步的原因也找到了,因为js事件循环的机制,主任务队列执行结束后才会执行异步任务队列中的任务,线程中延后执行,此时事务流已结束,isBatchingUpdates已经是false,自然也就是同步了。
注意:在生命周期或者合成事件中,多次调用setState会合并,比如:
1 | |