记一次升级 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)
我们编写组件常见模式是将一个组件返回多个元素。
为了包裹多个元素肯定写过的 div
或 span
,为了不必要的嵌套,提出了 Fragment
Fragments
与 Vue.js
的 <template>
功能类似,做不可见的包裹元素。
Fragments 简写形式 <></>
新版 React 还支持直接返回数组
New LifeCycle (16.3)
官方团队在实现更好地 SSR 时发现有些现有的生命周期经常被开发者误用或者“巧妙”的使用,这些生命周期容易带来不好的实践。它们是:
componentWillMount
componentWillReceiveProps
componentWillUpdate
这三个生命周期将在 17.0 版本之前,启用 Strict Mode
下 console 警告 ⚠️
并在 17.0 之后,必须加前缀 UNSAFE_
才能正常使用。
未来版本将逐步去掉这三个生命周期。
React 16.3 一并带来了 两个新的生命周期:
static getDerivedStateFromProps
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,有许多问题:
违反目前 React 组件设计。无法将使用 context 的子组件无缝移植到其他的根组件树种
在 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)
Hooks
重点!
好吧,目前 16.7 还不能用,但未来几个月就可以用上这一突破功能。
有关文档:Introducing Hooks – React
这次升级 React 的很大部分因素是 为了能使用到 Hooks
How
简要记录升级过程
升级 react 包
选择升级的 npm 包
react
react-dom
以及重点选择了几个有关 react 的包
推荐使用
npm-check
去选择需要更新的包
替换生命周期
总览: LifeCycle 变更
- 未来将去除
-
componentWillMount
-
componentWillUpdate
-
componentWillReceiveProps
-
- 新生命周期
-
static getDerivedStateFromProps
-
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
这个可能是修改起来比较麻烦的一个了,项目中使用的机会比较多,一般用作:
- 根据
Props
变更与否来修改State
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 不需要更新
// beforecomponentWillReceiveProps(nextProps) { if (this.props.currentRow !== nextProps.currentRow) { this.setState({ isScrollingDown: nextProps.currentRow > this.props.currentRow }) }}
// afterstatic 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.xxx
与 nextProps.xxx
或者 nextProps.xxx
与 this.state.xxx
是不行了。
在 getDerivedStateFromProps
中,需要将要比较的值也存到 state 当中,才能在之后通过 第二个参数 prevState
进行比较,比如 nextProps.xxx vs prevState.xxx
官方禁止了组件在
getDerivedStateFromProps
中 去访问 this.props,强制让开发者去比较nextProps
与prevState
中的值,以确保当开发者用到getDerivedStateFromProps
这个生命周期函数时,就是在根据当前的props
来更新组件的state
,而不是去做其他一些让组件自身状态变得更加不可预测的事情。
返回值的机制和使用 setState 的机制是类似的 —— 只需要返回发生改变的那部分状态,其他的值会保留。
WHY: 设计
getDerivedStateFromProp
s 时,不加入一个prevProps
参数,以方便比较
- 首次加载,调用 getDerivedStateFromProps 时,prevProps 参数是空值,这就依赖我们后面的代码考虑到这一条件
- 为了释放内存,保留之前的 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
在 16.4 修复了 16.3 版本关于 getDerivedStateFromProps
调用的时机,加入了 组件 自身触发setState
以及 forceUpate
建议:
- 保证无副作用 的 getDerivedStateFromProps
- 计算受控值时,将传入的 props 与先前更新好的 state 进行比较
React v16.4.0: Pointer Events – React Blog
getSnapshotBeforeUpdate
很少情况会用到。
将在 DOM 被更新前调用,此生命周期的返回值将作为第三个参数传递给componentDidUpdate
Context 替换
暂无替换,鉴于项目内使用到的地方比较杂,旧 Context API 方法在 17.0 之前还是可以正常使用
Pains
升级遇到的痛点
- 有很多组件在
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
有很多子组件
state
初始值依赖于父组件,但却是在componentWillReceiveProps
方法中 去初始化,之后子组件setState
替换掉state
值。由于子组件setState
不会调用componentWillReceiveProps
,但会调用getDerivedStateFromProps
,导致更新state
值无效。
解决:修改逻辑,加状态锁之前很多逻辑只依赖于
componentWillReceiveProps
,现在用getDerivedStateFromProps
同componentDidUpdate
去替换,导致逻辑分层,有些相似的逻辑要重复写(并不是说这里可以写个方法来重用,只是多了很多不必要的相似逻辑)期待未来版本
Hooks
将其统一。有些组件的逻辑 严重依赖
componentWillReceiveProps
,而getDerivedStateFromProps
在mounting
阶段也会调用,需要根据具体情况修改逻辑…basicPopup
组件……
此组件在submodule
,由于修改此组件,需要配合修改所有继承自它的Popup
组件,而 这些组件并不全部在teachingSite
。也就是说,如果修改它,必须将依赖于它的所有项目升级 react 版本至 16.4 及其以上。暂时放弃此组件及其子类组件。(备注:在 17.0 之前,过去的生命周期可正常使用)
Regret
- basicPopup 及其子类组件还是有 未来将删去的
componentWillReceiveProps
- Context 目前还没有变更至新 Context