React的Context API及对旧版Context问题的思考

文档上说:Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
文档上说:Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
文档上说:不要仅仅为了避免在几个层级下的组件传递 props 而使用 context,它是被用于在多个层级的多个组件需要访问相同数据的情景。
官方文档最后说:请谨慎使用

总之看React官方文档对Context的态度着实是够纠结。目前Context已经有了新的APIreact16.3之前的 Context API 已经不建议使用。

16.3之前的 Context API 的不足

完整代码在这里

React中父子组件可以通过 props 自顶向下的传递数据。但是当组件深度嵌套时,手动逐级传递 prop 不仅书写起来很繁琐同时还会为夹在中间的组件引入不必要的 prop。这时只需要在外层组件上
通过添加 childContextTypesgetChildContext,声明要传递给子组件的 ContextReact会向下自动传递,子组件只要通过定义contextTypes就可以获取上层传递的数据。

类似这样就可以定义一个声明了Context 的组件

class UserContext extends React.Component {
  static childContextTypes = {
    user: PropTypes.shape({
      name: PropTypes.string.isRequired,
      age: PropTypes.number.isRequired,
      changeUserName: PropTypes.func.isRequired
    }).isRequired
  };
  constructor() {
    super(...arguments);
    this.state = {
      user: {
        name: "xingmu",
        age: 38,
        changeUserName: this.changeUserName
      }
    };
  }
  getChildContext() {
    return this.state;
  }

  changeUserName = () => {
    console.log("修改姓名");
    this.setState({
      user: {
        ...this.state.user,
        name: "daixingmu"
      }
    });
  };

  render() {
    return this.props.children;
  }
}

为了方便使用,添加一个名字为ContextHOC的高阶组件。

export default class ContextHOC extends React.Component {
  render() {
    return (
      <UserContext>
        {this.props.children}
      </UserContext>
    );
  }
}

现在来使用一下

export default class User extends React.Component {
  static contextTypes = {
    user: PropTypes.object,
  };

  render() {
    return (
      <div>
        <div>
          姓名:{this.context.user.name}
          <button onClick={this.context.user.changeUserName}>修改姓名</button>
        </div>
        <div>年龄:{this.context.user.age}</div>
      </div>
    );
  }
}

export default class UserWrapper extends React.Component {
  render() {
    return (
      <div>
        用户情况:
        <User />
      </div>
    );
  }
}

现在将UserWrapper渲染到界面上就可以看到显示效果了。
在这里插入图片描述
我们来看下下这个小例子的组件结构。

<ContextHOC>
	<UserContext>
	    <UserWrapper>
	        <User />
	    </UserWrapper>
    </UserContext>
</ContextHOC>

其中组件 UserContext 会通过 getChildContext 设置 Context,组件 User 通过 this.context 读取 UserContext传递的值。

当点击按钮更新name属性的时候,组件 UserContext 通过 setState 设置新的 Context 值同时触发组件UserWrapper的渲染。然后User的渲染被触发,并在自己的 render 方法中拿到更新后的Context

整个流程如官方文档一样正常。

现在我们将组件 UserWrapper 的继承从React.Component改为React.PureComponent

不了解React.PureComponent的可以自行搜索

import React from "react";
import User from "./User";

export default class UserWrapper extends React.PureComponent {
  render() {
    return (
      <div>
        用户情况:
        <User />
      </div>
    );
  }
}

然后看下效果。
在这里插入图片描述
可以看到,点击之后,触发了修改name的事件,可以界面上的name属性并没有被修改。这是为什么的呢?

当点击按钮更新name属性的时候,组件 UserContext 通过 setState 设置新的 Context 值同时触发组件UserWrapper的渲染。

因为这时UserWrapper组件继承了React.PureComponent,自动执行了shouldComponetUpdate检测到 stateprops 均未变化因此返回 false,无需重新渲染。

导致User也没有被触发渲染。因此最新的 Context 值也就没有机会渲染到页面上。

使用hack的方法绕过中间组件

通过上的分析,可以知道定义Context的组件和使用Context组件中间的组件,如果使用了React.PureComponent或者重写了shouldComponetUpdate的时候,可能会造成context值传递不下的问题。所以我在想是不是可以考虑绕过中间组件,通过发布订阅,在数据更新的时候,直接来更新目标组件??

定义一个Context内容的模板。

class ContextHack {
  constructor(data = {}) {
    this.data = data;
    this.events = [];
    this.subscribe = this.subscribe.bind(this);
  }
  subscribe(event) {
    let e = event;
    this.events.push(e);
    return () => {
      this.events = this.events.filter(item => {
        return item !== e;
      });
    };
  }
  setData(data) {
    this.data = {
      ...this.data,
      ...data
    };
    this.events.forEach(event => {
      event && event();
    });
  }
}

export default ContextHack;

然后修改UserContext组件

class UserContext extends React.Component {
  static childContextTypes = {
    user: childContextTypes.isRequired
  };
  constructor() {
    super(...arguments);
    this.contextHack = new ContextHack({
      name: "xingmu",
      age: 38,
      changeUserName: this.changeUserName
    });
  }
  getChildContext() {
    return { user: this.contextHack };
  }

  changeUserName = () => {
    console.log("修改姓名");
    this.contextHack.setData({
      name: "daixingmu"
    });
  };

  render() {
    return this.props.children;
  }
}

然后修改 User组件,这里代码不贴了,全部代码都在这里,做为对该问题思考的总结。

16.3新版API

新版 Context API 都由以下几部分组成:

  1. React.createContext 方法用于创建一个 Context 对象。该对象包含 ProviderConsumer两个属性,分别为两个 React 组件。
  2. Provider 组件。用在组件树中更外层的位置。它接受一个名为 valueprop,其值可以是任何 JavaScript 中的数据类型。
  3. Consumer 组件。可以在 Provider 组件内部的任何一层使用。它接收一个函数做为children。这个函数的参数是 Provider 组件接收的那个 value 的值,返回一个 React 元素。
let ThemeContext = React.createContext({
    style: {
        color: "green",
        background: "red",
    }
});

function Button({ theme, ...props }) {
    return (
        <button {...props} >我是按钮</button>
    );
}


function ThemedButton(props) {
    return (
        /*
        * Consumer会自动获取离其最近的Provider提供的value
        * */
        <ThemeContext.Consumer>
            {
                (context) => {
                    return <Button {...props} style={context.style} />;
                }
            }
        </ThemeContext.Consumer>
    );
}

function Toolbar(props) {
    return (
        <ThemeContext.Provider value={{
            style: {
                color: "red",
                background: "green",
            }
        }}>
            <ThemedButton />
        </ThemeContext.Provider>
    );
}

ReactDOM.render(
    <Toolbar />,
    document.getElementById("root")
);

这版 Context API 的几个特点:

  1. ProviderConsumer 必须来自同一次 React.createContext 调用。也就是说 NameContext.ProviderAgeContext.Consumer 是无法搭配使用的。
  2. React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
  3. Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。
  4. Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is=== 的行为不完全相同。具体细节请参考 Object.isMDN 文档页
  5. Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。

通过高阶函数简化

import { ThemeContext } from "./themeContext";
import React from "react";
export function withThemeContext(Component) {
    return function (props) {
        return (
            <ThemeContext.Consumer>
                {
                    (context) => {
                        return <Component {...props} style={context.style} />;
                    }
                }
            </ThemeContext.Consumer>
        );
    };
}

16.6 提供了便利的API

为了简化 Context API的使用,React 16.6的版本新提供了一个contextType来简化使用

import { ThemeContext } from "./themeContext";
import React from "react";

class Button extends React.Component {
    
    static contextType = ThemeContext;
    
    render() {
        return (
            <button onClick={this.context.change} style={this.context.style}>我是按钮3333</button>
        );
    }
}

export { Button };

通过这种方式,每个组件只能注册一个context对象。如果需要读取多个context的value值,参加Consuming Multiple Contexts.

hooks

再后来 hooks 提供了 useContextAPI 可以在函数组件中更方便的使用任意多个Context,感兴趣的可以去官方文档
,这里就不列举了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值