WdBly Blog

懂事、有趣、保持理智

WdBly Blog

懂事、有趣、保持理智

周维 | Jim

603927378@qq.com

react 生命周期(二)

react v16版本后的生命周期详解

image.png

更改

React v16.0推出 componentDidCatch

当组件发生错误时触发

componentDidCatch(error, info) { console.log(error, info); }

componentDidCatch会监听render()函数抛出的错误, 不能捕获自身事件处理程序的错误。

React v16.3 getDerivedStateFromProps

getDerivedStateFromProps在组件创建时和更新时的render方法之前调用,它应该返回一个对象来更新状态,或者返回null来不更新任何内容。

static getDerivedStateFromProps(props, state){ console.log(props, state); return state }

getDerivedStateFromProps是一个静态函数,在其中不能访问this

return null后props或者state的变更后,视图不会更新。

React v16.3 getSnapshotBeforeUpdate

被调用于render之后,可以读取但无法使用DOM的时候。它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。

React v16.4 getDerivedStateFromProps修改

image.png

在此版本后, 无论何种情况导致render, 在render前都会执行此钩子。

image.png

16前的钩子

  • componentWillReceiveProps
  • componentWillMount
  • componentWillUpdate

这三个钩子会在17版本被删除

推出新的生命周期钩子原因

在v16之前,react更新组件树的过程是同步的。

点击一个按钮 -> 事件处理函数 -> 得到新的state -> 调用setState -> 执行render方法 -> 计算对比虚拟DOM -> 更新render中的所有组件 -> 子组件更新 -> 子组件的生命周期方法 ->…

虽然react render有自己的优化策略, 如diff算法做更新对比, 所以一次setState最终实际更新的组件不会很多, 但是一旦我们的组件树过于庞大, 层次很深,最终的更新时间会很长,如果 JS 运算持续占用主线程,页面就没法得到及时的更新,或者在更新过程中伴随了用户的操作,用户的操作不能及时的响应,UI不能更新。

所以说在shouldComponentUpdate中判断是否更新组件是一个大的优化点。

组件的更新有两个阶段,第一个是找到所有更新,也就是是上方的流程, react遍历所有可能需要更新的子组件得到更新结果。 第二个是将更新结果渲染到DOM中去。

与这两个更新阶段相对应的组件生命周期(可能执行的)
第一阶段(Reconciliation Phase):

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • render

第二阶段(Commit Phase):

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

如果需要解决同步更新带来的体验问题, 则需要在第一步中寻找解决方案(主要的耗时过程)。
image.png

为了解决上述问题,react在v16中发布了React Fiber。

React Fiber是对核心算法的一次重新实现,React Fiber将上述耗时较长的第一阶段做分片处理,这时我们的第一阶段不再是同步的多调用栈的执行到底,而是分成了多个片段执行。

这里的分片是时间分片(time-slicing)

每执行完一个分片,React Fiber会将控制权移交给React协调模块,判断是否有更高优先级的任务, 如果有,则执行此任务,否则继续执行分片直至第一阶段完成。

理想情况下当有高优先级任务插队且执行完毕后,程序会从上一个分片结束的位置开始重新执行,也就是说如果在某个组件的componentWillMount执行完毕后,执行了紧急任务,应该接着执行此组件的render方法。 但实际处理中,React Fiber的callback会将未执行完毕的组件重新执行一次。也可以理解为以组件为单元作为了回归点。

一旦进入第二阶段的更新,即DOM的更新,此过程无法被高优先级任务打断。

也就是说我们的组将在更新过程中,第一阶段的钩子函数有可能会执行多次。那么在这种情况下,我们最好要保证在第一阶段的钩子函数都是纯函数。所以说在react v16.3后, 相当于精简了第一阶段的生命周期,且是static标记的静态函数。

纯函数:

  • 函数的返回结果只依赖于它的参数。
  • 函数执行过程里面没有副作用。

副作用 :只要是跟函数外部环境发生的交互就都是副作用 如:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个http请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM查询
  • 访问系统状态

React Fiber 类似于苹果的动画大法,我们看到了很流畅的开机动画,但是开机的总时长却变长了, React Fiber也是如此, 用户体验的提升的代价是组件总的更新时间变长,但相对于用户体验来说这也是微不足道的。

image.png

在线demo
https://claudiopro.github.io/react-fiber-vs-stack-demo/

实现

  • 需要实现一个React Fiber需要一个移交控制权的方法
  • 需要一个控制器来控制任务的移交
  • 需要一个数据结构来记录任务状态

window.requestIdleCallback()

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,使用此API可以在特定时机移交主线程的控制权。
高优先级的动画由window.requestAnimationFrame执行

调度器 (Scheduler)

调度器用于任务的分配,插入的任务也有类型和优先级之分,不同类型的任务处理方式不同。

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行
module.exports = { NoWork: 0, // No work is pending. SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects. AnimationPriority: 2, // Needs to complete before the next frame. HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive. LowPriority: 4, // Data fetching, or result from updating stores. OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible. };

Fiber 树

Fiber Reconciler 在阶段一进行 Diff 计算的时候,会生成一棵 Fiber 树。这棵树是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。

Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程

参考:
https://juejin.im/post/5ab7b3a2f265da2378403e57
https://segmentfault.com/a/1190000018250127?utm_source=tag-newest