react

✍️ Tangxt ⏳ 2021-02-04 🏷️ Context API

06-React Context API

★课件

预习内容:https://reactjs.org/docs/context.html 中文翻译:https://react.docschina.org/docs/context.html#when-to-use-context

Context API

此图来源:怎样使用 React Context API - SegmentFault 思否

★四层函数传递值

Context 是 React 的 API,API 是什么?不用管它是什么,你直接用即可 -> API ,程序员经常说,但是就是没有人能清楚地解释它是什么,当然,你可以简单把它理解为「别人提供给你用的东西就叫 API」

Context 这个 API,解释起来相当复杂 -> 方方会从思想上给大家解惑

之前学习了「如何做组件间的通信?」 -> 与今天所讲的这个 Context API 有非常大的联系!

话又说回来,上一节留了一个悬念:react-redux 是如何实现一个标签就能让所有的元素都拥有一个方法呢?

1)在一个函数里边调一个函数(一共四层)

💡:需求 1:把一个变量传给 f4

传变量

实现:

传变量

我们不能在 f4 里边直接访问 let n 这个 n 变量 -> 因为它是在块级作用域 {} 里边的变量!

下一步 -> 使用 React 变形这个过程

2)使用 React

在写 JSX 代码的时候,请告诉自己我并不是在写 HTML,不然,你就是完全不懂 React -> 总之,经常使用那个 工具

一层一层地传 props

我们用 React 的形式把之前那个 f1~f4 的调用给演示了一遍,可以看到我们写标签实际上还是在调用函数!(不是马上调用,API 内部会调用它)

调用函数

问题:一层层地传 props 太麻烦了,如果我们只想 F4 组件拿到 n ,其它 F1~F3 不需要,那么我们能否把一层一层这样传的过程给省略掉呢?

回头看那个原生 JS 代码,答案是不行的,因为用 let 声明的变量 n{} 里边啊! f4 函数只能从 f3 那里拿到 n ……

问题来了:为啥不把写在 {} 里边的 n 提到 {} 外边去呢?

💡:写的 React 组件标签,必须是大写字母开头

大写开头

★用 Context API 传值

1)全局变量慎用

全局变量

如果用了全局变量,那么结果很有可能不是你想要的值!

比如:

let n = 100;

function f1() {
  console.log(1);
  f2();
}

function f2() {
  console.log(2);
  f3();
}

n = 50

function f3() {
  console.log(3);
  f4();
}

function f4() {
  console.log(4, n);
}

{
  setTimeout(() => {
    f1();
  })
  console.log("done");
}

n = 66

我们必须把代码看完,才能确定 n 的值是 66 ,而不是 f4 一执行就只看上边的代码……

总之,全局变量的 n 作用范围太广了……

问题来了:如何让 f4 拿到 n 的值,而且又不会因为其它因素影响到 n 的值呢?

我们可以做一个约定 -> 传一个局部的全局变量,这个变量大家都能访问,但是就不给别人访问 -> 不是一家人不进一家门

2)如何做一个局部的全局变量?

{
  let x = {}
  // 访问器
  window.setX = (key, value) => {
    x[key] = value
  }
} {
  window.setX('n', 100)
}

x 就是局部的全局变量,表示只在它自己那块作用域里边有用,但在其它地方就不能用了!

自己那块区域能用,看起来自己是全局变量,但在它的上一级 window 看来,它就是局部变量了!

{
  let context = {};
  window.setContext = (key, value) => {
    context[key] = value;
  };

  window.f1 = function() {
    console.log(1);
    f2();
  };

  function f2() {
    console.log(2);
    f3();
  }

  function f3() {
    console.log(3);
    f4();
  }

  function f4() {
    console.log(4, context["n"]);
  }
}

{
  window.setContext("n", 100);
  setTimeout(() => {
    f1();
  });
  console.log("done");
}

回过头来上边的代码是否达到了我们的需求:

接下来,看看 React 形式怎样的

3)Context API

文档:https://zh-hans.reactjs.org/docs/context.html

Context API

总之,该 API 就是为了解决「prop drilling」这个问题的!

prop drilling

之前我们是通过管家、EventHub 来管理数据的,而现在我们则通过 Context(局部的全局变量)来管理数据!


💡:何时使用 Context?

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据

这句话的言外之意就是「局部的全局变量」

关于官方文档,即便是英文好的看英文文档也不一定能搞懂,更不用说中文文档了……

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // Toolbar 组件接受一个额外的“theme”属性,然后传递给 ThemedButton 组件。
  // 如果应用中每一个单独的按钮都需要知道 theme 的值,这会是件很麻烦的事,
  // 因为必须将这个值层层传递所有组件。
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}

function Button(props) {
  return <button>{props.theme}</button>
}

如何看待这个例子? -> Toolbar、ThemedButton、Button 就是 F1~F3

使用 context , 我们可以避免通过中间元素传递 props

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext("light");
class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

function Button(props) {
  return <button>{props.theme}</button>;
}

对比之前的例子,我们可以看到:

视频教程里边的代码(react 16.5.2):

示例

可以看到与现在的 react 17.0.1 是有区别的……

问题来了:Context API 是如何做到的呢?

接下来,就把我们之前那个抽象一点的 F1~F4 代码改成是用 Context API 的

回顾需求:

4)如何使用 Context API 改写例子

依葫芦画瓢……

nContext

解释这个 nContext.Consumer

nContext. Consumer

Consumer 组件的实现其实是这样的:

Consumer

有些组件无法提前知晓它们子组件的具体内容。我们建议这些组件使用一个特殊的 children prop 来将他们的子组件传递到渲染结果中

总之,你把标签里边套函数 -> 把这个函数的返回值当作是子标签就行了!

➹:组合 vs 继承 – React

★标签里面居然可以传函数

1)你写的代码并不是 HTML

F1

💡:如果我们只写一个 {F1} 会怎样?

function F1() {
  return <div>F1</div>;
}
function App() {
  return <div>{F1}</div>;
}

会报错:

Warning: Functions are not valid as a React child. This may happen if you return a Component instead of <Component /> from render. Or maybe you meant to call this function rather than return it.

如果你这样:

function F1() {
  return <div>F1</div>;
}
function App() {
  return <div>{F1()}</div>;
}

相当于这样:

function F1() {
  return /*#__PURE__*/ React.createElement("div", null, "F1");
}

function App() {
  return /*#__PURE__*/ React.createElement("div", null, F1());
}

最后渲染的结果:

<div id="root" class="container">
  <div>
    <div>F1</div>
  </div>
</div>

💡: props.children

props.children

props.children 的返回值就是组件标签的所有内容

如果组件标签内容是函数,那意味着我们可以调用它!

所以就有了这样的代码:

function HHH(props) {
  console.log(props.children())
  return <div>{props.h}</div>;
}
function F1() {
  console.log('我被调用了')
  return <div>F1</div>
}
function App() {
  return <HHH h="hi">{F1}</HHH>;
}

log

既然函数被调用了,那我们总得传点信息意思意思一下才行……

进一步简化上边的代码:

function HHH(props) {
  let n = 100
  let res = props.children(n)
  return <div>{props.h}{res}</div>;
}
function App() {
  return <HHH h="hi">{(n)=><div>{n}</div>}</HHH>;
}

渲染结果:

<div id="root" class="container">
  <div>hi<div>100</div>
  </div>
</div>

上边的代码编译的结果是:

function HHH(props) {
  var n = 100;
  var res = props.children(n);
  console.log(res);
  return /*#__PURE__*/ React.createElement("div", null, props.h, res);
}

function App() {
  return /*#__PURE__*/ React.createElement(HHH, {
    h: "hi"
  }, function(n) {
    return /*#__PURE__*/ React.createElement("div", null, n);
  });
}

一个疑问: res 是虚拟 DOM,所以它的处理结果就是 HTML 元素?

准确来说,是使用 ReactDOM.render 就是在 render 虚拟 DOM

至此,你就应该明白:

Consumer

如何做一个接受函数的组件了……

虚拟 DOM

➹:React.createElement 和 ReactDOM.render 的简易实现

★更新 Context 里的值

1)子组件把 `n` 的值给变了

也就是把局部的全局数据给改变了……

💡:App 自己改数据

n update

💡: setState

setState

请忽视这个问题!这问题解释有错误!

💡:扩展运算符?

分析这个代码:

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      x: {
        n: 67,
        setN: () => {
          this.setState({
            x: {
              ...this.state.x,
              n: this.state.x.n + 1,
            },
          });
          console.log("执行完毕");
        },
      },
    };
  }
  render() {
    return (
      <div>
        <nContext.Provider value={this.state.x}>
          <F1 />
        </nContext.Provider>
      </div>
    );
  }
}

扩展运算符

每次 setN 都会把 x 的引用值给改了!

💡:Hooks API

Hooks API

★总结

总结

最初,我们是透过 props 来给那个 F4 传数据,之后我们使用了 Context API -> value 可以是对象,该对象可以有 store 属性,我们可以读写这个 store 属性 -> 也可以用 actionType+payload 那一套(把 Redux 那一套给照搬过来)

相较于 Redux,我们是否可以放弃使用 Redux 呢?毕竟它太啰嗦了……当然,Redux 还有其它功能……

react-redux 是早于 Context API 出现的,而 React 看到它这么好用,于是就抄过来了……

对比几种方案……才能更好地理解新方案到底做了什么……

★应用

以后讲了 Hooks ,就可以完全不用 class 了,class 可以凉凉了……

💡:在一个组件标签里边写组件标签?

class App extends React.Component {
  change = () => {
    if (this.state.theme === "green") {
      this.setState({ theme: "red" });
    } else {
      this.setState({ theme: "green" });
    }
  };
  constructor() {
    super();
    this.state = {
      theme: "green",
    };
  }
  render() {
    return (
      <themeContext.Provider value={this.state.theme}>
        <div className="App">
          <button onClick={this.change} className="button">换肤</button>
          <themeContext.Consumer>
            {(theme) => (
              <div>
                <Box theme={theme}>
                  <Button />
                </Box>
                <Box theme={theme}>
                  <Input />
                </Box>
              </div>
            )}
          </themeContext.Consumer>
        </div>
      </themeContext.Provider>
    );
  }
}

Box组件如何拿到内容?

function Box(props) {
  return <div className={`box ${props.theme}`}>{props.children}</div>;
}

类似 Vue 的 slot

代码:Demo

一句话总结今天的课: Context 是局部的全局变量

★了解更多

➹:Prop Drilling

➹:如何解读 react 16.3 引入的新 context api? - 知乎

➹:React 16.3 的 Context API 能否完全取代 Redux? - 知乎