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

本节主要讲解如下几个新的特性:前端

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

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!react

Context

定义:Context 提供了一种方式,可以让数据在组件树中传递而没必要一级一级手动传递。webpack

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

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

有了 Context 以后,咱们传递变量的方式是这样的:web

Item 能够直接从 Window 中获取变量值。数组

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

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

Provider 接收一个 value 属性,这个组件会让后代组件统一提供这个变量值。固然后代组件不能直接获取这个变量,由于没有途径。因此就衍生出 Consumer 组件,用来接收 Provier 提供的值。ide

一个 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>
    );
  }
}
复制代码

运行效果:

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

运行结果:

能够看出 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>;
})
...
复制代码

运行效果

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

参考

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

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

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

github.com/qq449245884…

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

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

相关文章
相关标签/搜索