34

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

 4 years ago
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 提供了一种方式,能够让数据在组件树中传递而不必一级一级手动传递。

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

1

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

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

1

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

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

1

首先要有一个 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 值。

运行结果:

1

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

运行效果 :

16b490d99744f914?imageslim

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

运行结果:

16b490e1e6ddab48?imageslim

接下来在 App 中注释掉

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

在看运行效果:

1

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

const BatteryContext = createContext(90);
复制代码



1

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

运行后会发现:

1

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

...
<Suspense fallback={<div>Loading...</div>}>
  <About></About>
</Suspense>
...
复制代码

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

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

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

1

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

1

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

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

运行效果:

1

先来看个例子:

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

运行结果:

16b490f9517d209a?imageslim

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

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

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

运行效果:

16b490fd2a598a00?imageslim

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

运行效果:

16b49109b442d931?imageslim

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

运行效果:

16b4910e8fc53002?imageslim

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



16b49110278df11e?imageslim

最后,如果你喜欢这个系列的,肯请大家给个赞的,我将会更有的动力坚持写下去。

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

交流(欢迎加入群,群工作日都会发红包,互动讨论技术)

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

github.com/qq449245884…

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

1




About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK