本节主要讲解以下几个新的特性:
- Context
- ContextType
- lazy
- Suspense
- 错误边界(Error boundaries)
- momo
想阅读更多优质文章请猛戳 GitHub 博客, 一年百来篇优质文章等着你!
Context
定义:Context 提供了一种方式,能够让数据在组件树中传递而不必一级一级手动传递。
这定义读的有点晦涩,来看张图:
假设有如上的组件层级关系,如果最底层的 Item
组件,需要最顶层的 Window
组件中的变量,那我们只能一层一层的传递下去。非常的繁琐,最重要的是中间层可能不需要这些变量。
有了 Context 之后,我们传递变量的方式是这样的:
Item 可以直接从 Window 中获取变量值。
当然这种方式会让组件失去独立性,复用起来更困难。不过存在即合理,一定有 Context 适用场景。那 Context 是如何工作的呢。
首先要有一个 Context 实例对象,这个对象可以派生出两个 React 组件,分别是 Provier 和 Consumer。
Provider 接收一个 value
属性,这个组件会让后代组件统一提供这个变量值。当然后代组件不能直接获取这个变量,因为没有途径。所以就衍生出 Consumer 组件,用来接收 Provier
提供的值。
一个 Provider 可以和多个消费组件有对应关系。多个 Consumer 也可以嵌套使用,里层的会覆盖外层的数据。
因此对于同一个 Context 对象而言,Consumer 一定是 Provier 后代元素。
创建 Contect 方式如下:
const MyContext = React.createContext(defaultValue?);
来个实例:
import React, {createContext, Component} from 'react';
const BatteryContext = createContext();
class Leaf extends Component {render() {
return (
<BatteryContext.Consumer>
{battery => <h1>Battery: {battery}</h1>
}
</BatteryContext.Consumer>
);
}
}
// 为了体现层级多的关系,增加一层 Middle 组件
class Middle extends Component {render() {return <Leaf />}
}
class App extends Component {render () {
return (<BatteryContext.Provider value={60}>
<Middle />
</BatteryContext.Provider>
)
}
}
export default App;
上述,首先创建一个 Context 对象 BatteryContext
,在 BatteryContext.Provider 组件中渲染 Middle 组件,为了说明一开始我们所说的多层组件关系,所以我们在 Middle
组件内不直接使用 BatteryContext.Consumer
。而是在 其内部在渲染 Leaf
组件,在 Leaf 组件内使用 BatteryContext.Consumer 获取BatteryContext.Provider 传递过来的 value
值。
运行结果:
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
来个实例:
...
class App extends Component {
state = {battery: 60}
render () {const {battery} = this.state;
return (<BatteryContext.Provider value={battery}>
<button type="button"
onClick={() => {this.setState({battery: battery - 1})}}>
Press
</button>
<Middle />
</BatteryContext.Provider>
)
}
}
...
首先在 App 中的 state 内声明一个 battery
并将其传递给 BatteryContext.Provider 组件,通过 button 的点击事件进减少 一 操作。
运行效果:
同样,一个组件可能会消费多个 context
,来演示一下:
import React, {createContext, Component} from 'react';
const BatteryContext = createContext();
const OnlineContext = createContext();
class Leaf extends Component {render() {
return (
<BatteryContext.Consumer>
{
battery => (
<OnlineContext.Consumer>
{online => <h1>Battery: {battery}, Online: {String(online)}</h1>
}
</OnlineContext.Consumer>
)
}
</BatteryContext.Consumer>
);
}
}
// 为了体现层级多的关系,增加一层 Middle 组件
class Middle extends Component {render() {return <Leaf />}
}
class App extends Component {
state = {
online: false,
battery: 60
}
render () {const {battery, online} = this.state;
console.log('render')
return (<BatteryContext.Provider value={battery}>
<OnlineContext.Provider value={online}>
<button type="button"
onClick={() => {this.setState({battery: battery - 1})}}>
Press
</button>
<button type="button"
onClick={() => {this.setState({online: !online})}}>
Switch
</button>
<Middle />
</OnlineContext.Provider>
</BatteryContext.Provider>
)
}
}
export default App;
同 BatteryContext 一样,我们在声明一个 OnlineContext
,并在 App state 中声明一个 online
变量, 在 render
中解析出 online
。如果有多个 Context 的话,只要把对应的 Provier 嵌套进来即可,顺序并不重要。同样也加个 button 来切换 online
的值。
接着就是使用 Consumer,与 Provier 一样嵌套即可,顺序一样不重要,由于 Consumer 需要声明函数,语法稍微复杂些。
运行结果:
接下来在 App 中注释掉
// <BatteryContext.Provider></BatteryContext.Provider>
在看运行效果:
可以看出,并没有报错,只是 battery
取不到值。这时候 createContext() 的默认值就派上用场了,用以下方式创建:
const BatteryContext = createContext(90);
这个默认值的使用场景就是在 Consumer 找不到 Provier 的时候。当然一般业务是不会有这种场景的。
ContextType
...
class Leaf extends Component {render() {
return (
<BatteryContext.Consumer>
{battery => <h1>Battery: {battery}</h1>
}
</BatteryContext.Consumer>
);
}
}
...
回到一开始的实例,我们在看下 Consuer
里面的实现。由于 Consumer 特性,里面的 JSX 必须是该 Consumer 的回返值。这样的代码就显得有点复杂。我们希望在整个 JSX 渲染之前就能获取 battery
的值。所以 ContextType
就派上用场了。这是一个静态变量,如下:
...
class Leaf extends Component {
static contextType = BatteryContext;
render() {
const battery = this.context;
return (<h1>Battery: {battery}</h1>
);
}
}
...
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext()
创建的 Context 对象。这能让你使用 this.context
来消费最近 Context
上的那个值。你可以在任何生命周期中访问到它,包括 render
函数中。
你只通过该 API 订阅单一 context。如果你想订阅多个,就只能用较复杂的写法了。
lazy 和 Supense 的使用
React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
首先声明一个 About 组件
import React, {Component} from 'react'
export default class About extends Component {render () {return <div>About</div>}
}
然后在 APP 中使用 lazy
动态导入 About
组件:
import React, {Component, lazy, Suspense} from 'react'
const About = lazy(() => import(/*webpackChunkName: "about" */'./About.jsx'))
class App extends Component {render() {
return (
<div>
<About></About>
</div>
);
}
}
export default App;
运行后会发现:
因为 App 渲染完成后,包含 About 的模块还没有被加载完成,React 不知道当前的 About
该显示什么。我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense
组件来解决。
只需将异步组件 About
包裹起来即可。
...
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
...
fallback
属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense
组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense
组件包裹多个异步组件。
那如果 about 组件加载失败会发生什么呢?
上面我们使用 webpackChunkName
导入的名加载的时候取个一个名字 about
,我们看下网络请求,右键点击 Block Request URL
重新加载页面后,会发现整个页面都报错了:
在实际业务开发中,我们肯定不能忽略这种场景,怎么办呢?
错误边界(Error boundaries)
如果模块加载失败(如网络问题),它会触发一个错误。你可以通过 错误边界 技术来处理这些情况,以显示良好的用户体验并管理恢复事宜。
如果一个 class 组件中定义了 static getDerivedStateFromError()
或 componentDidCatch()
这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError()
渲染备用 UI,使用 componentDidCatch()
打印错误信息。
接着,借用 错误边界,我们来优化以上当异步组件加载失败的情况:
class App extends Component {
state = {hasError: false,}
static getDerivedStateFromError(e) {return { hasError: true};
}
render() {if (this.state.hasError) {return <div>error</div>}
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<About></About>
</Suspense>
</div>
);
}
}
运行效果:
momo
先来看个例子:
class Foo extends Component {render () {console.log('Foo render');
return null;
}
}
class App extends Component {
state = {count: 0}
render() {
return (
<div>
<button onClick={() => this.setState({count: this.state.count + 1})}>Add</button>
<Foo name="Mike" />
</div>
);
}
}
例子很简单声明一个 Foo 组件,并在 APP 的 state 中声明一个变量 count,然后通过按钮更改 count 的值。
运行结果:
可以看出 count
值每变化一次,Foo
组件都会重新渲染一次,即使它没有必要重新渲染,这个是我们的可以优化点。
React 中提供了一个 shouldComponentUpdate
,如果这个函数返回 false
, 就不会重新渲染。在 Foo 组件中,这里判断只要传入的 name
属性没有变化,就表示不用重新渲染。
class Foo extends Component {
...
shouldComponentUpdate (nextProps, nextState) {if (nextProps.name === this.props.name) {return false}
return true
}
...
}
运行效果:
Foo
组件不会重新渲染了。但如果我们传入数据有好多个层级,我们得一个一个的对比,显然就会很繁琐且冗长。其实 React 已经帮我们提供了现层的对比逻辑就是 PureComponent
组件。我们让 Foo
组件继承 PureComponent
...
class Foo extends PureComponent {render () {console.log('Foo render');
return null;
}
}
...
运行效果同上。但它的实现还是有局限性的,只有传入属性本身的对比,属性的内部发生了变化,它就搞不定了。来个粟子:
class Foo extends PureComponent {render () {console.log('Foo render');
return <div>{this.props.person.age}</div>;
}
}
class App extends Component {
state = {
person: {
count: 0,
age: 1
}
}
render() {const {person} = this.state;
return (
<div>
<button
onClick={() => {
person.age ++;
this.setState({person})
}}>
Add
</button>
<Foo person={person}/>
</div>
);
}
}
在 App 中声明一个 person
,通过点击按钮更改 person
中的 age
属性,并把 person
传递给 Foo
组件,在 Foo
组件中显示 age
。
运行效果:
点击按键后,本应该重新渲染的 Foo
组件,却没有重新渲染。就是因为 PureComponent
提供的 shouldComponentUpdate
发现的 person 本身没有变化,才拒绝重新渲染。
所以一定要注意 PureComponent
使用的场景。只有传入的 props
第一级发生变化,才会触发重新渲染。所以要注意这种关系,不然容易发生视图不渲染的 bug。
PureComponent
还有一个陷阱,修改一下上面的例子,把 age
的修改换成对 count
,然后在 Foo 组件上加一个回调函数:
...
return (
<div>
<button
onClick={() => {this.setState({count: this.state.count + 1})
}}>
Add
</button>
<Foo person={person} cb={() =>{}}/>
</div>
);
...
运行效果:
可以看到 Foo
组件每次都会重新渲染,虽然 person
本身没有变化,但是传入的内联函数每次都是新的。
解决方法就是把内联函数提取出来,如下:
...
callBack = () => {}
<Foo person={person} cb={this.callBack}/>
...
讲了这么多,我们还没有讲到 memo
, 其实我们已经讲完了 memo
的工作原理了。
React.memo
为高阶组件。它与 React.PureComponent
非常相似,但它适用于函数组件,但不适用于 class 组件。
我们 Foo
组件并没有相关的状态,所以可以用函数组件来表示。
...
function Foo (props) {console.log('Foo render');
return <div>{props.person.age}</div>;
}
...
接着使用 memo
来优化 Foo
组件
...
const Foo = memo(function Foo (props) {console.log('Foo render');
return <div>{props.person.age}</div>;
})
...
运行效果
最后,如果你喜欢这个系列的,肯请大家给个赞的,我将会更有的动力坚持写下去。
参考
- React 官方文档
- 《React 劲爆新特性 Hooks 重构去哪儿网》
交流
干货系列文章汇总如下,觉得不错点个 Star,欢迎 加群 互相学习。
https://github.com/qq44924588…
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复 福利,即可看到福利,你懂的。