React新旧生命周期的理解

下面是在React新旧生命周期的学习中遇到和思考的问题:

  • 废除了几个生命周期,为什么要这样做?
  • 新增的生命周期解决了什么问题?
  • Fiber 为什么会影响到生命周期?
  • Vue是如果做到精确的组件更新的?

废弃了哪几个生命周期?

废弃的生命周期如下:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

新增了哪几个生命周期?

新增的生命周期如下:

  • static getDerivedStateFromProps(静态方法)
  • getSnapshotBeforeUpdate

为什么要改变生命周期?

被废弃的三个生命周期函数都是在render之前,Fiber的出现将VDOM拆分成一个个小任务,因为Fiber算法是异步渲染,并且其中高优先级任务会打断现有任务,导致函数多次执行。

下面分析下它们被废弃的原因

componentWillMount

  • 如果使用服务端渲染的话,WillMount会在服务端和客户端各自执行一次,这会导致请求两次(官方推荐用constructor代替WillMount)
  • 在使用Fiber架构之后,由于任务可中断,WillMount可能会被执行多次(Fiber算法是异步渲染,异步渲染很可能因为高优先级任务的出现而打断现有任务导致componentWillMount就可能执行多次)
  • 节省的时间非常少,跟其他的延迟情况相比,这个优化可以使用九牛一毛的形容(为了这么一点时间而一直不跟进技术的发展,得不偿失),并且render函数是肯定比异步数据到达先执行,白屏时间并不能减少

componentWillReceiveProps

通常会拿componentWillReceivePropsgetDerivedStateFromProps做对比,但是它们还是有很多区别的,
包括触发阶段, 参数可访问的数据都有很大的差异.

两者的差异:

  • componentWillReceiveProps是静态方法
  • 触发机制不同
    • componentWillReceiveProps 在接收到新的参数时触发
    • getDerivedStateFromProps 每次组件被重新渲染前被调用
  • 工作方式不同
    • componentWillReceiveProps 参数接收新的props
    • getDerivedStateFromProps 参数接收新的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 快?