React 全局状态管理的 3 种底层机制

253次阅读  |  发布于3年以前

现代前端框架都是基于组件的方式来开发页面。按照逻辑关系把页面划分为不同的组件,分别开发不同的组件,然后把它们一层层组装起来,把根组件传入 ReactDOM.render 或者 vue 的 $mount 方法中,就会遍历整个组件树渲染成对应的 dom。

组件都支持传递一些参数来定制,也可以在内部保存一些交互状态,并且会在参数和状态变化以后自动的重新渲染对应部分的 dom。

虽然从逻辑上划分成了不同的组件,但它们都是同一个应用的不同部分,难免要彼此通信、配合。超过一层的组件之间如果通过参数通信,那么中间那层组件就要透传这些参数。而参数本来是为了定制组件用的,不应该为了通信而添加一些没意义的参数。

所以,对于组件的通信,一般不会通过组件参数的层层传递,而是通过放在全局的一个地方,双方都从那里存取的方式。

具体的用于全局状态管理的方案可能有很多,但是他们的底层无外乎三种机制:props、context、state。

下面,我们分别来探究一下这三种方式是如何做全局状态的存储和传递的。

props

我们可以通过一个全局对象来中转,一个组件向其中存放数据,另一个组件取出来的方式来通信。

组件里面写取 store 中数据的代码比较侵入式,总不能每个用到 store 的组件都加一段这些代码吧。我们可以把这些逻辑抽成高阶组件,用它来连接(connect)组件和 store。通过参数的方式来把数据注入到组件中,这样,对组件来说来源是透明的。

这就是 react-redux 做的事情:

import { connect } from 'react-redux';

function mapStateToProps(state) {
    return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
    return bindActionCreators({ addTodo }, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)

此外,redux 还提供了中间件机制,可以拦截组件发送给 store 的 action 来执行一系列异步逻辑。

比较流行的中间件有 redux-thunk、redux-saga、redux-obervable,分别支持不同的方式来写组织异步流程,封装和复用异步逻辑。

类似的其他全局状态管理的库,比如 mobox、reconcil 等,也是通过 props 的方式注入全局的状态到组件中。

context

跨层组件通信一定要用第三方的方案么,不是的,react 本身也提供了 context 机制用于这种通信。

React.createContext 的 api 会返回 Provider 和 Consumer,分别用于提供 state 和取 state,而且也是通过 props 来透明的传入目标组件的。(这里的 Consumer 也可以换成 useContext 的 api,作用一样,class 组件用 Provider,function 组件用 useContext)

看起来和 redux 的方案基本没啥区别,其实最主要的区别是 context 没有执行异步逻辑的中间件。

所以 context 这种方案适合没有异步逻辑的那种全局数据通信,而 redux 适合组织复杂的异步逻辑。

案例代码如下:

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!
    </button>
  );
}

不知道大家有没有想过,props、state 改变了,重新渲染组件很正常,context 改变了,又是怎么触发渲染的呢?

其实 react 内部做了处理,如果改变了 context 的值,那么会遍历所有的子组件,找到用到 context 值的组件,触发它的更新。

所以,props、state、context 都能够触发重新渲染。

state

redux 和 context 的方案,一个是第三方的,一个是内置的,都是通过 props 来传入值或者通过 hooks 来取值,但它们都是组件外部的,而 state 是组件内部的,怎么通过 state 来做全局状态共享呢?

其实 class 组件的 state 做不到,但是 function 组件的 state 可以,因为它是通过 useState 的 hooks api 创建的,而 useState 可以抽离到自定义 hooks 里,然后不同的 function 组件里引入来用。

import React, { useState } from 'react';

const useGlobalState = (initialValue) => {
    const [globalState, setGlobalState] = useState(initialValue);
    return [globalState, setGlobalState];
}

function ComponentA() {
    const [globalState, setGlobalState] = useGlobalState({name: 'aaa'});

    setGlobalState({name: bbb});
    return <div>{globalState}</div>
}

function ComponentA() {
    const [globalState, setGlobalState] = useGlobalState({name: 'aaa'});

    return <div>{globalState}</div>
}

上面这段代码可以共享全局状态?

确实不可以,因为现在每个组件都是在自己的 fiber.memorizedState 中放了一个新的对象,修改也是修改各自的。

那把这两个 useState 的初始值指向同一个对象不就行了?

这样多个组件之间就可以操作同一份数据了。

上面的代码要做下修改:

let globalVal  = {
    name: ''
}

const useGlobalState = () => {
    const [globalState, setGlobalState] = useState(globalVal);

    function updateGlobalState(val) {
        globalVal = val;
        setGlobalState(val);
    }

    return [globalState, updateGlobalState];
}

这样,每个组件创建的 state 都指向同一个对象,也能做到全局状态的共享。

但这里有个前提,就是只能修改对象的属性,而不能修改对象本身。

总结

现在前端页面的开发方式是把页面按照逻辑拆成一个个组件,分别开发每一个组件,然后层层组装起来,传入 ReactDOM.render 或者 Vue 的 $mount 来渲染。

组件可以通过 props 来定制,通过 state 来保存交互状态,这些变了都会自动的重新渲染。除此之外,context 变了也会找到用到 contxt 数据的子组件来触发重新渲染。

组件之间彼此配合,所以难免要通信,props 是用于定制组件的,不应该用来透传没意义的 props,所以要通过全局对象来中转。

react 本身提供了 context 的方案,createContext 会返回 Provider 和 Consumer,分别用来存放和读取数据。在 function 组件中,还可以用 useContext 来代替 Provider。

context 虽然可以共享全局状态,但是却没有异步逻辑的执行机制,当有复杂的异步逻辑的时候,还是得用 redux 这种,它提供了中间件机制用于组织异步流程、封装复用异步逻辑,比如 redux-saga 中可以把异步逻辑封装成 saga 来复用。

context 和 redux 都支持通过 props 来注入数据到组件中,这样对组件是透明的、无侵入的。

其实通过 useState 封装的 自定义 hooks 也可以通过把初始值指向同一个对象的方式来达到全局数据共享的目的,但是是有限制的,只能修改对象的属性,不能修改对象本身。其实用这种还不如用 context,只是提一下可以这样做。

简单总结一下就是:context 和 redux 都可以做全局状态管理,一个是内置的,一个是第三方的,没有异步逻辑用 context,有异步逻辑用 redux。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8