13 min read

记一次升级 React

Why

React 目前最新的公开版本为 16.7,而 目前项目版本为 15.5

对比有如下重要变化:

新核心架构:Fiber (16.0)

异步渲染:周期性地对浏览器执行调度渲染工作的策略。

通过异步渲染,避免阻塞了主线程,实施更快地响应。

异步渲染能够将渲染任务划分为多块,这意味着几乎所有的行为都是同步发生的。

React 16.X 使用浏览器提供的 API 间歇性地检查当前是否还有其他任务需要完成,从而实现了对主线程和渲染过程的间接管理。

例如拖动、onChange 等在不考虑防抖情况,以及频繁 setState 的场景,相对于之前版本,有一定性能的提升。

这意味着 React 可以在更细的粒度上控制组件的绘制过程,从最终的用户体验来讲,用户可以体验到更流畅交互及动画体验。而因为异步渲染涉及到 React 的方方面面甚至未来,在 16.0 版本中 React 还暂时没有启用

GitHub - acdlite/react-fiber-architecture: A description of React’s new core algorithm, React Fiber > New Core Algorithm · Issue #6170 · facebook/react · GitHub

减少文件体积 (16.0)

React 16 对比 15.6.1

react 从20.7kb(gzip 后:6.9 kb)减至大小为 5.3 kb(gzip 后:2.2 kb)。
react-dom 从141 kb(gzip 后:42.9 kb)减至 103.7 kb(gzip 后:32.6 kb)。
react + react-dom 从 161.7 kb(gzip 后:49.8 kb)减至 109 kb(gzip 后:34.8 kb)

更好地 SSR (16.0)

What’s New With Server-Side Rendering in React 16 – Hacker Noon

Fragment (16.2)

我们编写组件常见模式是将一个组件返回多个元素。

为了包裹多个元素肯定写过的 divspan,为了不必要的嵌套,提出了 Fragment

FragmentsVue.js<template> 功能类似,做不可见的包裹元素。

Fragments 简写形式 <></>

新版 React 还支持直接返回数组

New LifeCycle (16.3)

官方团队在实现更好地 SSR 时发现有些现有的生命周期经常被开发者误用或者“巧妙”的使用,这些生命周期容易带来不好的实践。它们是:

  1. componentWillMount
  2. componentWillReceiveProps
  3. componentWillUpdate

这三个生命周期将在 17.0 版本之前,启用 Strict Mode 下 console 警告 ⚠️

并在 17.0 之后,必须加前缀 UNSAFE_ 才能正常使用。

未来版本将逐步去掉这三个生命周期。

React 16.3 一并带来了 两个新的生命周期:

  1. static getDerivedStateFromProps
  2. getSnapshotBeforeUpdate
class Example extends React.Component {
static getDerivedStateFromProps(props, state) {
// ...
}
}
  • static getDerivedStateFromProps 返回要更新的 state 内容,返回 null 表示没有 state 需要更新(为差异化更新 state,与现有 setState 方式一致;与 React Hooks 方式不同)
class Example extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
// ...
}
}

New Context API (16.3)

现有 Context API 一直被官方定义为实验性 API,有许多问题:

  1. 违反目前 React 组件设计。无法将使用 context 的子组件无缝移植到其他的根组件树种

  2. 在 Context 值更新后,顶层组件向目标组件 的 props 透传过程中,如果中间过程的某个组件的 shouldComponentUpdate 返回了 false,所以无法触发之后子组件的 reRender,导致无法得到新的 Context 值

而 新的 Context API 解决了以上的问题,并且采用声明式写法。小型复杂组件间适合采用 Context

React.memo (16.6)

类似于 Class 组件使用的 PureComponent

这是为 functional component 实现的过滤层,只有在 props 变化(浅比较)才会更新组件。

const MyComponent = React.memo(function MyComponent(props) {
// XXX
});

Lazy (16.6)

Lazy

const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<OtherComponent />
</div>
);
}

Suspense 用来添加一个 placeholder,在 lazy 化 component 加载之前显示

fallback 是懒加载组件载入过程中的一个过渡,可以放一些过渡效果或方法。

目前不能 SSR 使用

const OtherComponent = React.lazy(() => import("./OtherComponent"));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</section>
</Suspense>
</div>
);
}

Strict Mode (16.6)

Strict Mode – React

Hooks

重点!

好吧,目前 16.7 还不能用,但未来几个月就可以用上这一突破功能。

有关文档:Introducing Hooks – React

这次升级 React 的很大部分因素是 为了能使用到 Hooks

How

简要记录升级过程

升级 react 包

选择升级的 npm 包

react

react-dom

以及重点选择了几个有关 react 的包

推荐使用 npm-check 去选择需要更新的包

替换生命周期

总览: LifeCycle 变更

  • 未来将去除
    1. componentWillMount
    2. componentWillUpdate
    3. componentWillReceiveProps
  • 新生命周期
    1. static getDerivedStateFromProps
    2. getSnapshotBeforeUpdate

实施

  • componentWillMount ——> componentDidMount

    无痛变更

    componentWillMount 存在的 问题: 1. componentWillMount 中 setState 一定会等到首次 render 之后才执行 reRender 2. SSR 会 call componentWillMount twice 3. 组件已 mounted,组件设计原则 4. Fiber 架构 导致 调用 componentWillMount 次数不确定

  • componentWillUpdate ——> componentDidUpdate

    无痛变更

    componentWillMount 存在的问题差不多,也可能会多次执行

  • componentWillReceiveProps ——> static getDerivedStateFromProps + componentDidUpdate

这个可能是修改起来比较麻烦的一个了,项目中使用的机会比较多,一般用作:

  1. 根据 Props 变更与否来修改 State
  2. Props 改变时去调用一些 Func,或者做出一些不会修改 state 的操作

componentWillReceiveProps不同,getDerivedStateFromProps在 Mounting 阶段也会执行

针对第一种,更换方法:

componentWillReceiveProps ——> getDerivedStateFromProp

static getDerivedStateFromProps(nextProps, prevState) {
// ...
}

注意这是一个 static function,也就是说,在内部使用 this 是指向类的,而不是实例,所以 this 并不是指向实例,意味着无法使用 this.props/this.state

返回需要更新的 state(差异化更新,与现有 setState 方式一致,与 Hooks 不同)

或是 null :表示 state 不需要更新

// before
componentWillReceiveProps(nextProps) {
if (this.props.currentRow !== nextProps.currentRow) {
this.setState({
isScrollingDown: nextProps.currentRow > this.props.currentRow
})
}
}
// after
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.currentRow !== prevState.lastRow) {
return {
isScrollingDown: nextProps.currentRow > prevState.lastRow,
lastRow: nextProps.currentRow
}
}
// Return null to indicate no change to state.
return null
}

通过例子:

以往我们依赖比较 this.props.xxxnextProps.xxx 或者 nextProps.xxxthis.state.xxx是不行了。

getDerivedStateFromProps 中,需要将要比较的值也存到 state 当中,才能在之后通过 第二个参数 prevState 进行比较,比如 nextProps.xxx vs prevState.xxx

官方禁止了组件在 getDerivedStateFromProps中 去访问 this.props,强制让开发者去比较 nextPropsprevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。

返回值的机制和使用 setState 的机制是类似的 —— 只需要返回发生改变的那部分状态,其他的值会保留。

WHY: 设计 getDerivedStateFromProps 时,不加入一个 prevProps 参数,以方便比较

  1. 首次加载,调用 getDerivedStateFromProps 时,prevProps 参数是空值,这就依赖我们后面的代码考虑到这一条件
  2. 为了释放内存,保留之前的 props 是需要消耗内存的

针对第二种,使用 <code>compomentDidUpdate</code>:

基于 getDerivedStateFromProp 的执行次数不可预测性,以及静态方法。

所有需要执行的 side-effects要放在 compomentDidUpdate 中去执行

基本上在旧有 componentWillReceiveProps 中没有更新 state 的情况下,使用这个 methods 来代替是完全可行的,因为包括了我们需要用的到prevProps,并且 componentDidUpdate 内部可以访问 this.props,利用这两个我们可以作出对应比较

componentDidUpdate(prevProps, prevState) {
if (this.props.reqPending !== prevProps.reqPending && !this.props.reqPending) {
this.props.history.push('/next-page')
}
}

旧方法 componentWillReceiveProps 和新 getDerivedStateFromProps 方法都会增加组件的复杂性

强烈建议用这样派生状态前思考组件的设计模式。

官方建议:

You Probably Don’t Need Derived State – React Blog

image

在 16.4 修复了 16.3 版本关于 getDerivedStateFromProps 调用的时机,加入了 组件 自身触发setState 以及 forceUpate

建议:

  1. 保证无副作用 的 getDerivedStateFromProps
  2. 计算受控值时,将传入的 props 与先前更新好的 state 进行比较

    React v16.4.0: Pointer Events – React Blog
  • getSnapshotBeforeUpdate

    很少情况会用到。

将在 DOM 被更新前调用,此生命周期的返回值将作为第三个参数传递给componentDidUpdate

Context 替换

暂无替换,鉴于项目内使用到的地方比较杂,旧 Context API 方法在 17.0 之前还是可以正常使用

Pains

升级遇到的痛点

  1. 有很多组件在 componentWillReceiveProps 异步延时调用 setState …… ,而 getDerivedStateFromProps 中无法 异步更新 state ,所以做了大量工作去优化重写,实在优化不了的在 DidUpdate 去调用 setState(会造成多次渲染,但组件树不复杂的情况,忽略这些开销)

Dan 回复过一个有关问题 : getDerivedStateFromProps for asynchronous setState · Issue #1147 · reactjs/reactjs.org · GitHub

javascript - How to use React’s getDerivedStateFromProps with a setTimeout? - Stack Overflow

  1. 有很多子组件 state 初始值依赖于父组件,但却是在componentWillReceiveProps 方法中 去初始化,之后子组件 setState 替换掉 state 值。由于子组件 setState 不会调用 componentWillReceiveProps,但会调用 getDerivedStateFromProps,导致更新 state 值无效。

    解决:修改逻辑,加状态锁

  2. 之前很多逻辑只依赖于 componentWillReceiveProps ,现在用 getDerivedStateFromPropscomponentDidUpdate 去替换,导致逻辑分层,有些相似的逻辑要重复写(并不是说这里可以写个方法来重用,只是多了很多不必要的相似逻辑)

    期待未来版本 Hooks 将其统一。

  3. 有些组件的逻辑 严重依赖 componentWillReceiveProps ,而 getDerivedStateFromPropsmounting 阶段也会调用,需要根据具体情况修改逻辑…

  4. basicPopup 组件……

    此组件在 submodule,由于修改此组件,需要配合修改所有继承自它的 Popup 组件,而 这些组件并不全部在 teachingSite。也就是说,如果修改它,必须将依赖于它的所有项目升级 react 版本至 16.4 及其以上。暂时放弃此组件及其子类组件。(备注:在 17.0 之前,过去的生命周期可正常使用)

Regret

  1. basicPopup 及其子类组件还是有 未来将删去的 componentWillReceiveProps
  2. Context 目前还没有变更至新 Context