Before you’re going to hate it, then you’re going to love it.
Concurrent Render(贯穿 16)
在 18 年的 JSConf Iceland 上, Dan 神提到 Concurrent Render 涉及到 CPU 以及 IO 这两方面。
Time Slicing 对应解决左侧的问题, Suspense 对应解决了右侧的问题。它们共同要解决的是的提升用户体验, 在更多的场景下都可以做到可交互
。而 Fiber 架构是上述两者的基石。Time Slicing
在 16 之前的版本的渲染过程可以想象成一次性潜水 30 米, 在这期间做不了其它事情(Stack Reconciler);
痛点概括:
- 一次性渲染到底
- 中途遇到优先级更高的事件无法调整相应的顺序
接着拿上面的潜水例子为例, 现在变为可以每次潜 10 米, 分 3 个 chunk 进行; chunk 和 chunk 之间通过链表连接; chunk 间插入优先级更高的任务, 先前的任务被抛弃。
开启 Fiber 后, 获取异步数据的方法应放到 render 后面的生命周期钩子里(phase 2 阶段)进行, 因为 render 前面的生命周期钩子(phase 1 阶段)会被执行多次
注意: 并没有缩短原先组件的渲染时间(甚至还加长了), 但用户却能感觉操作变流畅了。
requestIdleCallback(): 借力此 api, 浏览器能在空闲的时间处理低优先级的事。Suspense(16.6, 16.8, 16.9)
Suspense 意思是能暂停当前组件的渲染, 当完成某件事以后再继续渲染。code splitting
(16.6, 已上线): 文件懒加载。在此之前的实现方式是 react-loadable并发模式
(16.8, 2019 年 Q2 季度): 并发模式下,Suspense
提供一个时间参数, 若小于这个值则不进行loading
加载, 若超过这个值则进行loading
加载;data fetching
(16.9 版本, 2019 年中): 可以控制等所有数据都加载完再呈现出数据;一种简单的预加载思路, 可参考 preload
import React, { lazy, Suspense } from 'react'
const OtherComponent = lazy(() => import('./OtherComponent'))
function MyComponent() {
return (
<Suspense fallback={<div>loading...</div>}>
<OtherComponent />
</Suspense>
)
}
const OtherComponentPromise = import('./OtherComponent');
const OtherComponent = React.lazy(() => OtherComponentPromise);
render 新增的返回类型
在 React16 版本中 render() 增加了一些返回类型, 到目前为止支持的返回类型如下:- React elements.
- Arrays and fragments.
- Portals.
- String and numbers.
- Booleans or null.
render
其中 render() 支持返回 Arrays 能让我们少写一个父节点, 如下所示:render() 支持返回数组的特性类似 Fragments(16.2), 使用 Fragments 可以不用写 key。
const renderArray = () => [
<div key="A">A</div>
<div key="B">B</div>
]
Portals(传送门)
将 React 子节点渲染到指定的节点上
案例: 实现一个 Modal 组件, demo
另外关于 Portals 做到冒泡到父节点的兄弟节点这个现象, demo, 我想可以这样子实现: 如果组件返回是 Portal 对象, 则将该组件的父组件的上的事件 copy 到该组件上。其实并不是真的冒泡到了父节点的兄弟节点上。Error Boundaries
React 16 提供了一个新的错误捕获钩子componentDidCatch(error, errorInfo)
, 它能将子组件生命周期里所抛出的错误捕获, 防止页面全局崩溃。demo
componentDidCatch 并不会捕获以下几种错误- 事件机制抛出的错误(事件里的错误并不会影响渲染)
- Error Boundaries 自身抛出的错误
- 异步产生的错误
- 服务端渲染
服务端渲染
服务端渲染一般是作为最后的优化手段, 这里浅显(缺乏经验)谈下 React 16 在其上的优化。
在 React 16 版本中引入了React.hydrate()
, 它的作用主要是将相关的事件注水
进html
页面中, 同时会比较前端生成的html
和服务端传到前端的html
的文本内容的差异, 如果两者不一致将前端产生的文本内容替换服务端生成的(忽略属性)。支持自定义属性
在 React 16 版本中, 支持自定义属性(推荐data-xxx
), 因而 React 可以少维护一份 attribute 白名单, 这也是 React 16 体积减少的一个重要因素。Context(16.3、16.6)
Context 相当于是用组件化的方式使用 global, 使用其可以共享认证的用户、首选语言(国际化)等一些全局的信息, 而不必通过组件一层层传递。
以下是比较冗余的传递:在
<Page riderId={riderId} />
// ... which renders ...
<RiderDetail riderId={riderId} />
// ... which renders ...
<RiderLevel riderId={riderId} />
// ... which renders ...
<Avatar riderId={riderId} />
Context
之前可以传递<Avatar>
本身(Component Composition 的思想), 写法如下:接着是使用
function Page(props) {
const avatar = <Avatar riderId={props.riderId} />
return <RiderDetail avatar={avatar} />
}
<Page riderId={riderId} />
// ... which renders ...
<RiderDetail avatar={avatar} />
// ... which renders ...
<RiderLevel avatar={avatar} />
// ... which renders ...
{ props.avatar }
Context
书写的例子, 写法如下:
const RiderContext = React.createContext(1) // 这里为默认值
function Page(props) {
const riderId = props.riderId
return (
<RiderContext.Provider value={riderId}>
<RiderDetail />
</RiderContext.Provider>
)
}
function RiderDetail() {
return <RiderLevel />
}
class RiderLevel extends React.Component {
static contextType = RiderContext
render() {
return <Avatar avatar={this.context} />;
}
}
新的生命周期(16.3)
在未来 17 的版本中, 将移除的生命周期钩子如下:componentWillMount()
: 移除这个 api 基于以下两点考虑:- 服务端渲染: 在服务端渲染的情景下, componentWillMount 执行完立马执行 render 会导致 componentWillMount 里面执行的方法(获取数据, 订阅事件) 并不一定执行完;
- Concurrent Render: 在 fiber 架构下, render 前的钩子会被多次调用, 在 componentWillMount 里执行订阅事件就会产生内存泄漏;
迁移思路, 将以前写在componentWillMount
的获取数据、时间订阅的方法写进componentDidMount
中;componentWillReceiveProps(nextProps)
: 移除这个 api 基于如下考虑:- 语义不太契合逻辑
- phase1 阶段会多次调用
新的钩子getDerivedStateFromProps()
更加纯粹, 它做的事情是将新传进来的属性和当前的状态值进行对比, 若不一致则更新当前的状态。另外关于 componentWillReceiveProps() 在 15 里大量使用的一个场景: 比如切换 tab 时都要重新获取当前页面的数据, 之前通常会这么做:
getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.riderId !== prevState.riderId) {
return {
riderId: nextProps.riderId
}
}
// 返回 null 则表示 state 不用作更新
return null
}
在 16 中可以使用 memoize 来代替, 写法如下:
componentWillReceiveProps(nextProps) {
if (nextProps.riderId !== this.props.riderId) {
fetchData(nextProps.riderId)
}
}
import memoize from "memoize-one"
class Demo extends React.Component {
fetchDataDemo = memoize((riderId) => fetchData(riderId))
componentDidUpdate() {
fetchDataDemo(this.props.riderId)
}
}
componentWillUpdate()
: 目前将其理解为和componentWillMount
一样的情况
在 React 16.3 的版本中, 新加入了两个生命周期:getDerivedStateFromProps(nextProps, prevState)
: 更加语义化, 用来替代componentWillMount()
和componentWillReceiveProps(nextProps)
;getSnapshotBeforeUpdate(prevProps, prevState)
: 可以将该钩子返回的结果传入 componentDidUpdate 的第三个参数中, 从而达到 dom 数据统一。用来替代 componentWillUpdate();
具体 demo 可见 Update on Async RenderingReact.memo(16.6)
React.memo
是一个高阶组件, 它使无状态组件拥有有状态组价中的shouldComponentUpdate()
以及PureComponent
的能力。
const MyComponent = React.memo(function MyComponent(props) {
...
})
Hooks(16.7)
在 React 16.7 之前, React 有两种形式的组件, 有状态组件(类)和无状态组件(函数)。Hooks 的意义就是赋能先前的无状态组件, 让之变为有状态。这样一来更加契合了 React 所推崇的函数式编程。
接下来梳理 Hooks 中最核心的 2 个 api,useState
和useEffect
useState
useState 返回状态和一个更新状态的函数使用 Hooks 相比之前用 class 的写法最直观的感受是更为简洁
const [count, setCount] = useState(initialState)
function App() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
)
}
useEffect(fn)
在每次 render 后都会执行这个钩子。可以将它当成是componentDidMount
、componentDidUpdate
、componentWillUnmount
的合集。因此使用 useEffect 比之前优越的地方在于: