好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

react context优化四重奏教程示例

一、前言

我们在使用react的过程中,经常会遇到需要跨层级传递数据的情况。props传递数据应用在这种场景下会极度繁琐,且不利于维护,于是context应运而生

官方解释: Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props

二、用法

在正文之前,先简单介绍一下context的三种消费方法:

1.通过 Consumer 来消费上下文

const globalContext = React.createContext();
class TestUseContextSon1 extends React.Component {
  render() {
    return (
      <globalContext.Consumer>
        {(value) => {
          return <div>{value.num}</div>;
        }}
      </globalContext.Consumer>
    );
  }
}
export default class TestUseContext extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      num: 2,
    };
  }
  render() {
    return (
      <globalContext.Provider value={{ num: this.state.num }}>
        <TestUseContextSon1 />
      </globalContext.Provider>
    );
  }
}

2.通过静态变量 contextType 来消费上下文

const globalContext = React.createContext();
class TestUseContextSon2 extends React.Component {
  static contextType = globalContext;
  render() {
    return <div>{this.context.num}</div>;
  }
}
export default class TestUseContext extends React.Component {
  ...省略...
  render() {
    return (
      <globalContext.Provider value={{ num: this.state.num }}>
        <TestUseContextSon2 />
      </globalContext.Provider>
    );
  }
}

3.通过hooks useContext 来消费上下文

const globalContext = React.createContext();
const TestUseContextSon3 = (props) => {
  const con = useContext(globalContext);
  return <div>{con.num}</div>;
};
export default class TestUseContext extends React.Component {
  ...省略...
  render() {
    return (
      <globalContext.Provider value={{ num: this.state.num }}>
        <TestUseContextSon3 />
      </globalContext.Provider>
    );
  }
}

比较:

Consumer 既可以在类组件中使用,也可以在函数组件中使用 contextType 只能在类组件中使用 useContext 只能在函数组件中使用

三、缺点

这里有一个例子:

import React, { useState } from "react";
const globalContext = React.createContext();
const Son1 = () => {
  return <div>Son1</div>;
};
const Son2 = () => {
  const value = useContext(globalContext);
  return <div>Son2---{value.num}</div>;
};
export const Demo = () => {
  const [value, setValue] = useState({ num: 1 });
  return (
    <globalContext.Provider value={value}>
      <Son1 />
      <Son2 />
    </globalContext.Provider>
  );
};

当我们改变 value 值时,会导致 Son1 、 Son2 都发生重渲染,但这与我们的初衷相悖,造成了额外的开销,我们期望做到的是 Son1 不执行, Son2 重新渲染。在较长的一段时间内,我都认为是使用了 context 导致 Provider 下面的子组件发生了重渲染。网上也有很多解释没有说清楚,容易误导人。

实际情况是 value 的变化导致了 Son1 、 Son2 发生重渲染。如下示例: 即使我们不使用· context ,当value发生变化时, Son1 、 Son2 也会重渲染。

const Son1 = () => {
  return <div>Son1</div>;
};
const Son2 = () => {
  return <div>Son2</div>;
};
export const Demo = () => {
  const [value, setValue] = useState({ num: 1 });
  return (
    <Son1 />
    <Son2 />
  );
};

那么问题来了,我们使用context的时候,必然要向 <globalContext.Provider value={value}> Provider的value中传入一个状态,但是当状态改变时又不可避免的造成 Provider 下的所有子组件重新渲染,我们期望只有消费了上下文的子组件重新渲染,那么有什么方法能够避免这种额外的开销吗?

四、context优化

我们知道,所有消费了context的地方,只要 Provider 的 value 值发生变化,都会发生重渲染.只要我们有什么办法能够避开父组件状态发生变化,引起的子组件状态发生变化,那就可以减少很多不必要的开销。

一重奏--使用PureComponent

const globalContext = React.createContext();
class TestUseContextSon2 extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {};
  }
  render() {
    console.log("TestUseContextSon2----render");
    return (
      <globalContext.Consumer>
        {(value) => {
          console.log("Consumer----handle");
          return <div>{value.num}</div>;
        }}
      </globalContext.Consumer>
    );
  }
}
const TestUseContext = () => {
  const [value, setValue] = useState({ num: 1 });
  return (
      <globalContext.Provider value={value}>
        <button onClick={() => setValue({ num: value.num + 1 })}>
          点击
        </button>
        <TestUseContextSon2 />
      </globalContext.Provider>
  );
}

初始化的时候,两个console各执行一遍

点击按钮之后, TestUseContextSon2----render 没有打印, Consumer----handle 打印,达到预期结果。

二重奏--使用shouldComponentUpdate

此处由于作者比较任性,省略100字,基本效果其实和 PureComponent 一致,不做过多描述。

三重奏--使用React.memo

React.memo 既可以用于函数组件,也可以用于类组件

const globalContext = React.createContext();
const TestUseContextSon3 = React.memo(function (props) {
  console.log("TestUseContextSon3----render");
  return (
      <globalContext.Consumer>
        {(value) => {
          console.log("Consumer----handle");
          return <div>{value.num}</div>;
        }}
      </globalContext.Consumer>
    );
});
const TestUseContext = () => {
  const [value, setValue] = useState({ num: 1 });
  return (
      <globalContext.Provider value={value}>
        <button onClick={() => setValue({ num: value.num + 1 })}>
          点击
        </button>
        <TestUseContextSon3 />
      </globalContext.Provider>
  );
}

点击按钮之后, TestUseContextSon2----render 没有打印, Consumer----handle 打印,达到预期结果。 那如果我们使用useContext来消费上下文呢?

const TestUseContextSon4 = React.memo(function (props) {
  const con = useContext(globalContext);
  console.log("TestUseContextSon4----render");
  return &lt;div&gt;{con.num}&lt;/div&gt;;
});

点击按钮之后, TestUseContextSon4----render 打印,也就是说当我们使用 useContext 来消费上下文的时候,整个函数组件会重新执行。而 Consumer 仅仅只是局部执行,这意味更少的性能消耗。

四重奏--Provider再封装+props.children

上面所述的三种方法都存在一个弊端,Provider的直接下级组件都需要用 memo 、 PureComponent 、 shouldComponentUpdate 处理,才能屏蔽掉父级状态变化带来的影响,那么有没有一种更方便的方式呢?

代码如下:

/** 主题 */
const ThemeContext = React.createContext({ theme: "red" });
const ThemeProvider = (props) => {
  const [theme, setTheme] = useState({ theme: "red" });
  console.log("ThemeProvider-----", theme.theme);
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {props.children}
    </ThemeContext.Provider>
  );
};
const Son1 = function (props) {
  const { setTheme } = useContext(ThemeContext);
  return <button onClick={() => setTheme({ theme: "blue" })}>改变主题</button>;
};
const Son2 = function (props) {
  const { theme } = useContext(ThemeContext);
  console.log("Son2----", theme.theme);
  return <div>主题----{theme.theme}</div>;
};
const Son4 = function (props) {
  console.log("Son4---没有使用上下文");
  return <div>没有使用上下文</div>;
};
export default class ContextChildren extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <Son1 />
        <Son2 />
        <Son4 />
      </ThemeProvider>
    );
  }
}

在上面这段代码中, <Son1 /> 、 <Son2 /> 、 <Son3 /> 并没有直接放到 ThemeContext.Provider 组件下面,而是将该组件再次封装成 ThemeProvider 组件,并将状态管理也放在 ThemeProvider 组件中,然后通过 props.children 来引入组件子节点。

效果如下:

当我们点击按钮时,打印如下:

点击按钮, setTheme 执行,状态由 { theme: "red" } 变为 { theme: "blue" } ,引起 ThemeProvider 组件重新执行,打印 ThemeProvider----- blue ,组件 Son2 由于消费了上下文,重新执行,打印 Son2---- blue

那么问题来了,为什么没有打印 Son4 呢?我们没有使用memo、PureComponent等处理 Son4 组件,但是它确实不会重新执行。

出现这种现象,其实是 props.children 引起的, props.children 指向一个对象,这个对象中存放着 <Son1 /> 、 <Son2 /> 、 <Son3 /> 执行的结果, ThemeProvider 执行的时候, props.children 指向的对象没有发生变化,只有当 ContextChildren 组件重新渲染的时候, <Son1 /> 、 <Son2 /> 、 <Son3 /> 才会重新执行,由于我们将状态放置于 ThemeProvider 组件中,所以 ContextChildren 组件不会重新渲染, <Son1 /> 、 <Son2 /> 、 <Son3 /> 也就不会重新执行,所以 Son4---没有使用上下文 没有打印。

那如果将 ThemeProvider 组件改成这样呢?

const ThemeProvider = (props) => {
  const [theme, setTheme] = useState({ theme: "red" });
  console.log("ThemeProvider-----", theme.theme);
  const content = React.Children.map(props.children, (child) => {
    return child;
  });
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {content}
    </ThemeContext.Provider>
  );
};

Son4 依然没有执行

再改一下:

const ThemeProvider = (props) => {
  const [theme, setTheme] = useState({ theme: "red" });
  console.log("ThemeProvider-----", theme.theme);
  const content = React.Children.map(props.children, (child) => {
    return React.cloneElement(child);
  });
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {content}
    </ThemeContext.Provider>
  );
};

我们使用 React.cloneElement api克隆一下child

Son4 执行了,我想这是因为克隆之后指向发生变化,导致组件重新执行

总结

本文简单介绍了一下context的几种用法,以及如何来屏蔽父级状态变化(provider的value一般是和父级组件状态挂钩的)导致未消费上下文的子组件重新渲染导致的额外开销。

以上就是react context优化四重奏教程示例的详细内容,更多关于react context 优化教程的资料请关注其它相关文章!

查看更多关于react context优化四重奏教程示例的详细内容...

  阅读:33次