测不准

V1

2022/06/09阅读:22主题:红绯

react学习(十) React 中的 context

react学习(十) React 中的 context

在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。

如果在你的项目中使用主题,基本是每个组件都需要;或者你在项目中使用多语言,也是每个组件都需要支持,这都是典型的可以通过 context 操作的例子

使用示例

我们实现一个多个组件,共享同一个颜色的示例,通过按钮点击切换颜色,如下:

我们有五个部分,外层的 pannel 组件,header 组件,title 组件,main 组件还有 content 组件。我们在随便一层组件中执行 color 切换函数,因为 setColor 方法已经通过 context 传递进去了。样式很简单,代码如下:

// src/index.js
import React from "react";
import ReactDOM from "react-dom";

// 创建上下文对象
const ColorContext = React.createContext()

const style = {
  margin: '5px',
  padding: '5px'
}
// 我们使用不同的方式创建组件

function Title() {
  return (
     // 函数组件使用方式,children 是一个函数
    <ColorContext.Consumer>
      {
        (contextValue) => {
          return <div style={{border: `5px solid ${contextValue.color}`}}>title</div>
        }
      }
    </ColorContext.Consumer>
  )
}


class Header extends React.Component {
  // 类组件绑定静态方法,默认给实例绑定 context 属性
  static contextType = ColorContext
  render() {
    // 也可以使用 comsumer 组件
    return (
      <div style={{border: `5px solid ${this.context.color}`}}>header <Title /></div>
    )
  }
}

function Content() {
  return (
    <ColorContext.Consumer>
      // 内部是函数形式
      {
        (contextValue) => {
          return (
            <>
            // 我们在这里控制颜色的改变
            <div style={{border: `5px solid ${contextValue.color}`}}>Content</div>
            <button onClick={() => {contextValue.changeColor('red')}}>red</button>
            <button onClick={() => {contextValue.changeColor('green')}}>green</button>
            </>
          )
        }
      }
    </ColorContext.Consumer>
  )
}
class Main extends React.Component {
  // 类组件获取方式
  static contextType = ColorContext

  render() {
    return (
      <div style={{border: `5px solid ${this.context.color}`}}>main <Content /></div>
    )
  }
}
class Panel extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      color: 'black'
    }

  }

  changeColor = (color) => {
    this.setState({color})
  }
  render() {
    const contextValue = {
      color: this.state.color,
      changeColor:this.changeColor
    }
    return (
      // 通过value向下传递
      <ColorContext.Provider value={contextValue}>
        <div style={{...style, width: '250px', border: `5px solid ${this.state.color}`}}>
          Panel
          <Header />
          <Main />
        </div>
      </ColorContext.Provider>
    )
  }
}

由上面可知,Provider 组件是一个提供数据的,数据存放在 value 中。Consumer 组件和 contextType 是消费数据的。而组件我们之前也实现过,更具不同的类型, 单独使用方法处理。

实现

定义类型:

// src/constants.js
export const REACT_PROVIDER = Symbol('react.provider')
// context 和 consumer 都是 context 类型,小伙伴们可自行打印官方的库查看
export const REACT_CONTEXT = Symbol('react.context')

React 中有个 createContext 方法:

// src/react.js
// 我们的写法效仿的是我们使用官方库打印出来的结果
function createContext() {
  const context = {
    $$typeof: REACT_CONTEXT,
    _currentValue: undefined, // 值是绑定在 context 中的 _currentValue 属性上
  }
  // 这里使用了递归引用,你中有我我中有你
  context.Provider = {
    $$typeof: REACT_PROVIDER,
    _context: context
  }
  
  context.Consumer = {
    $$typeof: REACT_CONTEXT,
    _context: context
  }
  return context
}


const React = {
  ...
  createContext
}

对于内容渲染我们要分两种情况考虑,一个是直接渲染的情况,一个是更新渲染的情况。第一种是 createDOM 中判断处理,第二种是在 updateElement 中处理,当然还有 forceUpdate 更新方法。

mount 处理

// src/react-dom.js  createDOM

...
if (type && type.$$typeof === REACT_PROVIDER) {
  return mountProviderComponent(vdom)  
else if (type && type.$$typeof === REACT_CONTEXT) {
  return mountContextComponent(vdom)
}
...


function mountProviderComponent(vdom) {
  const {props, type} = vdom
  const context = type._context // Provider._context
  context._current = props.value // 通过value属性提供值
  const renderVdom = props.chidlren
  vdom.oldRenderVdom = renderVdom
  return createDOM(renderVdom) // 递归处理子,这里限制了一个根子节点
}

funtion mountContextComponent(vdom) {
  const {props, type} = vdom
  const context = type._context// Consomer._context
  const renderVdom = props.children(context._currentValue) // consumer 组件子是一个函数,这里的值上一步provider 已经赋值了,引用类型
  vdom.oldRenderVdom = renderVdom
  return createDOM(renderVdom)
}

类组件需要处理 contextType 静态方法

// src/react-dom.js  mountClassComponent
...
if (type.contextType) {
  // 添加 context 属性
  classInstance.context = type.contextType._currentValue
}
...

这里可能有朋友有疑问,为什么 type 一会这样,一会这么判断。这是 babeljsx 解析的结果,typeof type === string, 就是我们正常的 html 标签。如果是函数类型的话,可能是类组件或者函数组件。在这里 type 就指代的 ProviderConsumer 对象,需要具体情况具体分析。

这里一个 ColorContext 只能处理颜色的逻辑,如果还有其他的共享逻辑怎么办呢?我们可以对 ProviderConsumer 进行多层嵌套,使用方法是一样的。因为子也是递归处理,再根据类型找到对应的处理函数。如果使用的组件在不同的页面,我们需要把 ColorContext 进行导出,文件中自行引入。

update 处理

react 更新函数即 diff 对比,同级对比,类型一样的话在比对子,同样需要对类型进行判断

// src/react-dom. updateElement
...
if (oldVdom.type.$$typeof === REACT_PROVIDER) {
  updateProviderComponent(oldVdom, newVdom)  
else if (oldVdom.type.$$typeof === RERACT_CONTEXT) {
  updateContextComponent(oldVdom, newVdom)
}
...


function updateProviderComponent(oldVdom, newVdom) {
  const currentDOM = findDOM(oldVdom)
  const parentDOM = currentDOM.parentNode
  // 下面的操作和 mount 类型,只是加了对比
  const {type, props} = newVdom
  const context = type._context
  const renderVdom = props.children
  context._currentValue = props.value
  compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom)
  newVdom.oldRenderVdom = renderVdom
}

function updateContextComponent(oldVdom, newVdom) {
  const currentDOM = findDOM(oldVdom)
  const parentDOM = currentDOM.parentNdoe
  const {type, props} = newVdom
  const context = type._context
  const renderVdom = props.children(context._currentValue)
  compareTwoVdom(parentDOM, oldVdom.oldRenderVdom, renderVdom)
  newVdom.oldRenderVdom = renderVdom
}

点击触发 changeColor 方法,促使 render 函数重新执行,我们要在 forceUpdate 中也判断类组件的字段

// src/Component.js

forceUpdate() {
  ...
  let oldDOM = findDOM(oldREnderVdom)
  // 更新时重新获取 context, 可能value 值更新了
  if (this.constructor.contextType) {
    this.context = this.constructor.contextType._currentValue
  }
  ...
}

我们自己实现的效果如下:

本节也是代码为主,但是中间穿插文字的描述,我相信大家可以理解 context 的机制和产生的原因。下一下小节我们学习下 react 中的高阶组件。

分类:

前端

标签:

React.js

作者介绍

测不准
V1