React 新特性讲解及实例(一)
source link: https://juejin.im/post/5d0043915188255e780b6309
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
本节主要讲解以下几个新的特性:
- Context
- ContextType
- lazy
- Suspense
- 错误边界(Error boundaries)
- memo
想阅读更多优质文章请猛戳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>
);
}
}
复制代码
运行效果:
先来看个例子:
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 = () => {}
...
讲了这么多,我们还没有讲到 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,欢迎 加群 互相学习。
我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!
关注公众号,后台回复福利,即可看到福利,你懂的。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK