1. 首页

React 新特性讲解及实例(一)

本节主要讲解以下几个新的特性:

  • Context
  • ContextType
  • lazy
  • Suspense
  • 错误边界(Error boundaries)
  • memo

Context

定义:Context 提供了一种方式,能够让数据在组件树中传递而不必一级一级手动传递。

这定义读的有点晦涩,来看张图:

码农进阶题库,每天一道面试题 or Js小知识

假设有如上的组件层级关系,如果最底层的 Item 组件,需要最顶层的 Window 组件中的变量,那我们只能一层一层的传递下去。非常的繁琐,最重要的是中间层可能不需要这些变量。

有了 Context 之后,我们传递变量的方式是这样的:

码农进阶题库,每天一道面试题 or Js小知识

Item 可以直接从 Window 中获取变量值。

当然这种方式会让组件失去独立性,复用起来更困难。不过存在即合理,一定有 Context 适用场景。那 Context 是如何工作的呢。

码农进阶题库,每天一道面试题 or Js小知识

首先要有一个 Context 实例对象,这个对象可以派生出两个 React 组件,分别是 ProvierConsumer

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 值。

运行结果:

码农进阶题库,每天一道面试题 or Js小知识

当 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 的点击事件进减少 一 操作。

运行效果 :

JS中文网 - 前端进阶资源分享

同样,一个组件可能会消费多个 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 需要声明函数,语法稍微复杂些。

运行结果:

JS中文网 - 前端进阶资源分享

接下来在 App 中注释掉

// <BatteryContext.Provider></BatteryContext.Provider>

在看+ 每天一道面试题 or Js小知识 https://www.javascriptc.com/

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

可以看出,并没有报错,只是 battery 取不到值。这时候 createContext() 的默认值就派上用场了,用以下方式创建:


const BatteryContext = createContext(90);

码农进阶题库,每天一道面试题 or Js小知识

这个默认值的使用场景就是在 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;

运行后会发现:

码农进阶题库,每天一道面试题 or Js小知识

因为 App 渲染完成后,包含 About 的模块还没有被加载完成,React 不知道当前的 About 该显示什么。我们可以使用加载指示器为此组件做优雅降级。这里我们使用 Suspense 组件来解决。
只需将异步组件 About 包裹起来即可。


... <Suspense fallback={<div>Loading...</div>}> <About></About> </Suspense> ...

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个异步组件。


那如果 about 组件加载失败会发生什么呢?

上面我们使用 webpackChunkName 导入的名加载的时候取个一个名字 about,我们看下网络请求,右键点击 Block Request URL

码农进阶题库,每天一道面试题 or Js小知识

重新加载页面后,会发现整个页面都报错了:

码农进阶题库,每天一道面试题 or Js小知识

在实际业务开发中,我们肯定不能忽略这种场景,怎么办呢?

错误边界(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> ); } }

运行效果:

码农进阶题库,每天一道面试题 or Js小知识

memo

先来看个例子:


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 的值。

运行结果:

JS中文网 - 前端进阶资源分享

可以看出 count 值每变化一次, Foo 组件都会重新渲染一次,即使它没有必要重新渲染,这个是我们的可以优化点。

React 中提供了一个 shouldComponentUpdate,如果这个函数返回 false,就不会重新渲染。在 Foo 组件中,这里判断只要传入的 name 属性没有变化,就表示不用重新渲染。


class Foo extends Component { ... shouldComponentUpdate (nextProps, nextState) { if (nextProps.name === this.props.name) { return false } return true } ... }

运行效果:

JS中文网 - 前端进阶资源分享

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

运行效果:

JS中文网 - 前端进阶资源分享

点击按键后,本应该重新渲染的 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> ); ...

运行效果:

JS中文网 - 前端进阶资源分享

可以看到 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>; }) ...

运行效果

JS中文网 - 前端进阶资源分享

参考

  1. React 官方文档
  2. 《React劲爆新特性Hooks 重构去哪儿网》

系列推荐

作者:前端小智
链接:https://www.javascriptc.com/3368.html

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:React 新特性讲解及实例(一)

链接:https://www.javascriptc.com/3368.html

« 10 万人的大场馆如何“画座位”?
如何使用 Set 来提高代码的性能—ES6知识点»
Flutter 中文教程资源

相关推荐

QR code