React新旧生命周期的理解
下面是在React新旧生命周期的学习中遇到和思考的问题:
- 废除了几个生命周期,为什么要这样做?
- 新增的生命周期解决了什么问题?
- Fiber 为什么会影响到生命周期?
- Vue是如果做到精确的组件更新的?
废弃了哪几个生命周期?
废弃的生命周期如下:
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
新增了哪几个生命周期?
新增的生命周期如下:
- static getDerivedStateFromProps(静态方法)
- getSnapshotBeforeUpdate
为什么要改变生命周期?
被废弃的三个生命周期函数都是在render之前,Fiber的出现将VDOM拆分成一个个小任务,因为Fiber算法是异步渲染,并且其中高优先级任务会打断现有任务,导致函数多次执行。
下面分析下它们被废弃的原因
componentWillMount
- 如果使用服务端渲染的话,WillMount会在服务端和客户端各自执行一次,这会导致请求两次(官方推荐用constructor代替WillMount)
- 在使用Fiber架构之后,由于任务可中断,WillMount可能会被执行多次(Fiber算法是异步渲染,异步渲染很可能因为高优先级任务的出现而打断现有任务导致componentWillMount就可能执行多次)
- 节省的时间非常少,跟其他的延迟情况相比,这个优化可以使用九牛一毛的形容(为了这么一点时间而一直不跟进技术的发展,得不偿失),并且render函数是肯定比异步数据到达先执行,白屏时间并不能减少
componentWillReceiveProps
通常会拿componentWillReceiveProps
和getDerivedStateFromProps
做对比,但是它们还是有很多区别的,
包括触发阶段, 参数和可访问的数据都有很大的差异.
两者的差异:
componentWillReceiveProps
是静态方法- 触发机制不同
componentWillReceiveProps
在接收到新的参数时触发getDerivedStateFromProps
每次组件被重新渲染前被调用
- 工作方式不同
componentWillReceiveProps
参数接收新的propsgetDerivedStateFromProps
参数接收新的props和组件当前的state,返回新的state对象或者null不操作
componentWillReceiveProps 被废弃的原因:
主要是性能问题。
props每次改变都会导致componentWillReceiveProps
被调用,而且这个调用不会像setState一样被合并,如果有异步请求还可能导致请求阻塞。
相反getDerivedStateFromProps
虽然是在render之前被调用,但是react中大部分更新(render)都是setState触发,setState的操作会被transaction合并,因此触发的频率不会非常频繁。
componentWillUpdate
与 componentWillReceiveProps 类似,许多开发者也会在 componentWillUpdate 中根据 props 的变化去触发一些回调。但不论是 componentWillReceiveProps 还是 componentWillUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类似,componentDidUpdate 也不存在这样的问题,一次更新中 componentDidUpdate 只会被调用一次,所以将原先写在 componentWillUpdate 中的回调迁移至 componentDidUpdate 就可以解决这个问题。
另外一种情况则是我们需要获取DOM元素状态,但是由于在fiber中,render可打断,可能在WillMount中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决。
与WillMount不同的是, getSnapshotBeforeUpdate会在最终确定的render执行之前执行,也就是能保证其获取到的元素状态与didUpdate中获取到的元素状态相同。
React Fiber 是如何更新管控的?
下面的内容整理自:React Fiber 是如何实现更新过程可控的
React Fiber
更新过程的可控主要体现在下面几个方面:
- 任务拆分
- 任务挂起、恢复、终止
- 任务具备优先级
任务拆分
在React Fiber
机制中会将递归遍历 VDOM 的任务拆成若干个小任务,每个节点代表一个小任务,利用时间分片(Time Slicing)执行一个或多个颗粒度小的任务,过程如下:
- 将 VDOM 拆分成若干小任务
- 在每个时间分片中执行一个或者多个任务
- 每个小任务完成后,才生成下一个小任务,如果没有下个任务了,则代表本次 DOM Diff 操作完成
挂起、恢复、终止
首先提下两颗 Fiber 树:
workInProgress tree
:当前正在执行更新的 Fiber 树currentFiber tree
:表示上次渲染构建的 Filber 树
在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,收集副作用。同时也会复用和 currentFiber 对应的节点对象,减少新创建对象带来的开销。也就是说无论是创建还是更新,挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务,大致过程如下:
挂起
当第一个小任务完成后,先判断这一帧是否还有空闲时间,没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。
恢复
在浏览器渲染完一帧后,判断当前帧是否有剩余时间(RequestIdleCallback
),如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段,这样完美的解决了调和过程一直占用主线程的问题。
Fiber 将 VDOM 生成链表数据格式,每个任务其实就是在处理一个 FiberNode 对象,每个 FiberNode 对象又会生成下一个需要处理的 FiberNode 任务
终止
其实并不是每次更新都会走到提交阶段。当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。
任务具备优先级
React Fiber 除了通过挂起,恢复和终止来控制更新外,还给每个任务分配了优先级。具体点就是在创建或者更新 FiberNode 的时候,通过算法给每个任务分配一个到期时间(expirationTime)。在每个任务执行的时候除了判断剩余时间,如果当前处理节点已经过期,那么无论现在是否有空闲时间都必须执行改任务。
任务在执行过程中顺便收集了每个 FiberNode 的副作用,将有副作用的节点通过 firstEffect、lastEffect、nextEffect 形成一条副作用单链表。
知识点补充
浏览器绘制一帧会经历哪几个过程?
- 接受输入事件
- 执行事件回调
- 执行 RAR(RequestAnimationFrame)
- 计算样式,更新布局
- 绘制页面(渲染)
- 空闲阶段(执行 RquestIdelCallback)
最后一步的 RquestIdelCallback 事件不是每一帧结束都会执行,只有在一帧的 16ms 中做完了前面 6 件事且还有剩余时间,才会执行。
RquestIdelCallback 没有执行结束不会进入一下帧的执行,所以 RquestIdelCallback 执行时间最好不要超过 30ms,否则浏览器得不到控制权,会影响下一帧的渲染,导致页面卡顿和事件响应不及时。
js 执行栈理解
如果页面逻辑复杂,函数的执行栈调用太深
会导致浏览在下次帧率绘制之前无法得到控制权
如果这时候有在做动画的操作,会导致不能及时绘制下一帧而让用户觉得卡顿的体验
同时事件响应是在每一帧开始时执行,所以事件响应也会延迟。
React Fiber
之前React
通过原生执行栈递归遍历 VDOM,如果页面太过复杂,会导致函数执行栈太深无法跟上浏览器帧率的刷新,从而影响动画和事件的执行
时间分片和链表
时间分片指的是一种将多个粒度小的任务放入一个时间切片(一帧)中执行的一种方案,在 React Fiber 中就是将多个任务放在了一个时间片中去执行。
在 React Fiber 中用链表遍历的方式替代了 React 16 之前的栈递归方案。
链表与顺序结构数据的对比:
优点:
- 操作更高效,比如顺序调整、删除,只需要改变节点的指针指向就好了
- 不仅可以根据当前节点找到下一个节点,在多向链表中,还可以找到他的父节点或者兄弟节点
缺点
- 比顺序结构数据更占用空间,因为每个节点对象还保存有指向下一个对象的指针
- 不能自由读取,必须找到他的上一个节点
Vue更新
了解React的Fiber任务调度后,发现无论如何,在React中子组件都会被重新render一遍,借住react提供的生命周期方法,可以用memo和shouldComponentUpdate这些方法进行组件的优化,那么Vue是如何做到只更新当前组件而不影响子组件的呢?
依赖收集实现组件颗粒度更新
与React不同的是,Vue中每个组件都会有自己渲染的 Watcher,负责掌控当前组件的更新,组件会先收集当前的data和props值作为依赖,当更新发生时,触发当前组件和有更新依赖的子组件更新。
关于Vue更详细的组件如何更新强烈推荐这篇文章:为什么说 Vue 的响应式更新比 React 快?