[译]开发类 redux 库来理解状态管理

原文地址javascript

对于想要跳过文章直接看结果的人,我已经把我写的内容制做成了一个库:use-simple-state,无任何依赖(除了依赖 react ),只有3kb,至关轻量。html

  • 近几年,应咱们的 app 增加的须要,web 应用数量增加迅速,随之而来的还有复杂性。为了使增长的复杂性易于处理,应用某些新增的技巧和模式使得开发者能够更简单的处理以及帮助咱们创建更加健壮的应用。
  • 其中一个复杂性增加的主要领域是管理咱们应用的状态,所以为了不这种复杂性,开发者使用了包含更新和获取 app 状态的库。最著名的例子是 redux,它是 flux 模式的一种应用。
  • 一旦开发者开始学习使用像 redux 的库,他们可能不太了解库的内部运行机制,由于一开始这并不明显,即便很容易了解到这是更新一个全局可用的对象这样的一个概念。
  • 在这篇文章中,咱们将会从零开始为 react 应用创建一个咱们本身的状态管理解决方案。咱们的解决方案最初只有几行代码,逐渐增长更高级的特性,最终将相似于 redux。

基本概念

  • 任何状态管理工具只须要两件东西:对整个应用均可用的全局状态,和读取以及更新它的能力。只有这些,真的。
  • 这里展现一个状态管理的简单例子:
const state = {};

export const getState = () => state;

export const setState = nextState => {
  state = nextState;
};
复制代码
  • 上面的例子已经尽量的简单,但它仍然包含了全部的要素:

一个全局可用的用于展示咱们应用状态的值:state;java

读取状态的能力:getState;react

更新状态的能力:setState。git

  • 上面的例子对于咱们真实应用来讲太过简单,所以接下来咱们将要构建一个能让 react 可用的解决方案。首先咱们来重构咱们的例子,以让它在 react 中可用。

react 状态管理

  • 为了制做一个咱们以前解决方案的 react 版本,咱们须要应用两个 react 功能。第一个功能是普通经典类组件,也就是众所周知的有状态组件。
  • 第二个功能是 context API,它可让数据在整个 react 应用可用。context 有两部分:provider (生产者) 和 consumer (消费者),provider 就像它的名字所说的那样,给应用提供 context (data 数据),消费者意指当咱们读取 context 时,咱们就是消费者。
  • 能够这样理解 context:若是说 props 是显式的传送数据,那么 context 就是隐式的传送数据。

建造咱们本身的状态管理器

  • 如今咱们知道了须要哪些工具,如今只要把它们合在一块儿就能够了。咱们准备建立一个上下文环境来存放全局状态,而后把它的 provider 包裹在一个有状态组件中,而后用 provider 来管理状态。github

  • 首先,咱们使用 React.createContext 来建立上下文,它能够给咱们提供 provider 和 consumer。web

  • import { createContext } from 'react';
    
    const { Provider, Consumer } = createContext();
    复制代码
  • 接下来咱们须要用有状态组件包裹咱们的 provider,利用它进行应用状态的管理。咱们也应该把 consumer 导出为一个更加准确的名称。express

  • import React, { Component, createContext } from 'react';
    
    const { Provider, Consumer } = createContext();
    
    export const StateConsumer = Consumer;
    
    export class StateProvider extends Component {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      render () {
        return (
          <Provider value={this.state}> {this.props.children} </Provider>
        );
      }
    }
    复制代码
  • 在上面的例子中,StateProvider 是接收一个 state 来做为初始状态的组件,而且使组件树中当前组件下面的任何组件均可以访问到这个属性。若是没有提供 state,默认会有一个空对象代替。redux

  • 用咱们的 StateProvider 包裹住根组件:api

  • import { StateProvider } from './stateContext';
    import MyApp from './MyApp';
    
    const initialState = {
      count: 0
    };
    
    export default function Root () {
      return (
        <StateProvider state={initialState}> <MyApp /> </StateProvider>
      );
    }
    复制代码
  • 在咱们完成上述代码以后,就能够做为一个消费者从 MyApp 的任何地方得到应用的状态。在这里咱们会初始化咱们的状态为一个有一个 count 属性的对象,因此不管何时咱们想要当即获取应用的状态,咱们就能够从这里得到。

  • 消费者使用 render 属性 来传递上下文数据,咱们能够经过下面的一个函数做为 StateConsumer 的子组件的例子来查看。state 参数传递给函数用以展示咱们应用的当前状态,做为咱们的初始状态,state.count 为 0.

  • import { StateConsumer } from './stateContext';
    
    export default function SomeCount () {
      return (
        <StateConsumer> {state => ( <p> Count: {state.count} </p> )} </StateConsumer>
      );
    }
    复制代码
  • 关于 StateConsumer 咱们须要知道的很重要一点是在上下文中它会自动订阅状态的改变,所以当咱们的状态改变后会从新渲染以显示更新。这就是消费者的默认行为,咱们暂时还没作可以用到这个特性的功能。

更新状态

  • 目前为止咱们已经能够读取应用的状态,以及在状态改变时自动更新。如今咱们须要一种更新状态的方法,为了作到这一点咱们仅仅只须要在 StateProvider 里面更新状态。

  • 你以前可能已经注意到了,咱们给 StateProvider 传递了一个 state 属性,也就是以后会传递给组件的 state 属性。咱们将使用 react 内置的 this.setState 来更新:

  • export class StateProvider extends Component {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      render () {
        return (
          <Provider value={{ state: this.state, setState: this.setState.bind(this) }}> {this.props.children} </Provider>
        );
      }
    复制代码
  • 继续保持简单的风格,咱们只给上下文传递 this.setState,这意味着咱们须要稍微改变咱们的上下文传值,不仅是传递 this.state,咱们如今同时传递 statesetState

  • 当咱们用 StateConsumer 时能够用解构赋值获取 statesetState,而后咱们就能够读写咱们的状态对象了:

  • export default function SomeCount () {
      return (
        <StateConsumer> {({ state, setState }) => ( <> <p> Count: {state.count} </p> <button onClick={() => setState({ count: state.count + 1 })}> + 1 </button> <button onClick={() => setState({ count: state.count - 1 })}> - 1 </button> </> )} </StateConsumer>
      );
    }
    复制代码
  • 有一点要注意的是因为咱们传递了 react 内置的 this.setState 做为咱们的 setState 方法,新增的属性将会和已有的状态合并。这意味着若是咱们有 count 之外的第二个属性,它也会被自动保存。

  • 如今咱们的做品已经能够用在真实项目中了(尽管还不是颇有效率)。对 react 开发者来讲应该会以为 API 很熟悉,因为使用了内置的工具所以咱们没有引用任何新的依赖项。假如以前以为状态管理有点神奇,但愿如今咱们多少可以了解它内部的结构。

华丽的点缀

  • 熟悉 redux 的人可能会注意到咱们的解决方案缺乏一些特性:

    • 没有内置的处理反作用的方法,你须要经过 redux 中间件来作这件事
    • 咱们的 setState 依赖 react 默认的 this.setState 来处理咱们的状态更新逻辑,当使用内联方式更新复杂状态时将可能引起混乱,同时也没有内置的方法来复用状态更新逻辑,也就是 redux reducer 提供的功能。
    • 也没有办法处理异步的操做,一般由 redux thunk 或者 redux saga等库来提供解决办法。
    • 最关键的是,咱们没办法让消费者只订阅部分状态,这意味着只要状态的任何部分更新都会让每一个消费者更新。
  • 为了解决这些问题,咱们模仿 redux 来应用咱们本身的 actionsreducers,和 middleware。咱们也会为异步 actions 增长内在支持。以后咱们将会让消费者只监听状态内的子状态的改变。最后咱们来看看如何重构咱们的代码以使用新的 hooks api

redux 简介

免责声明:接下来的内容只是为了让你更容易理解文章,我强烈推荐你阅读 redux 官方完整的介绍。

若是你已经很是了解 redux,那你能够跳过这部分。

  • 下面是一个 redux 应用的数据流简化流程图:

redux-data-flow

  • 如你所见,这就是单向数据流,从咱们的 reducers 接收到状态改变以后,触发 actions,数据不会回传,也不会在应用的不一样部分来回流动。

  • 说的更详细一点:

  • 首先,咱们触发一个描述改变状态的 action,例如 dispatch({ type: INCREMENT_BY_ONE }) 来加1,同咱们以前不一样,以前咱们是经过 setState({ count: count + 1 })来直接改变状态。

  • action 随后进入咱们的中间件,redux 中间件是可选的,用于处理 action 反作用,并将结果返回给 action,例如,假如在 action 到达 reducer 以前触发一个 SIGN_OUT 的 action 用于从本地存储里删除全部用户数据。若是你熟悉的话,这有些相似于 express 中间件的概念。

  • 最后,咱们的 action 到达了接收它的 reducer,伴随而来的还有数据,而后利用它和已有的状态合并生成一个新的状态。让咱们触发一个叫作 ADD 的 action,同时把咱们想发送过去增长到状态的值也发送过去(叫作 payload )。咱们的 reducer 会查找叫作 ADD 的 action,当它发现后就会将 payload 里面的值和咱们现有的状态里的值加到一块儿并返回新的状态。

  • reducer 的函数以下所示:

  • (state, action) => nextState
    复制代码
  • reducer 应当只是处理 state 和 action ,虽然简单却很强大。关键是要知道 reducer 应当永远是纯函数,这样它们的结果就永远是肯定的。

actions + dispatch

  • 如今咱们已通过了几个 redux app 的关键部分,咱们须要修改 app 来模仿一些相似的行为。首先,咱们须要一些 actions 和触发它们的方法。

  • 咱们的 action 会使用 action 建立器来建立,它们其实就是能生成 action 的简单函数,action 建立器使得测试,复用,传递 payload 数据更加简单,咱们也会建立一些 action type,其实就是字符串常量,为了让他们能够被 reducer 复用,所以咱们把它存储到变量里:

  • // Action types
    const ADD_ONE = 'ADD_ONE';
    const ADD_N = 'ADD_N';
    
    // Actions
    export const addOne = () => ({ type: ADD_ONE });
    export const addN = amount => ({ type: ADD_N, payload: amount });
    复制代码
  • 如今咱们来作一个 dispatch 的占位符函数,咱们的占位符只是一个空函数,将会被用于替换上下文中的 setState 函数,咱们一会再回到这儿,由于咱们还没作接收 action 的 reducer 呢。

  • export class Provider extends React.PureComponent {
      static defaultProps = {
        state: {}
      };
    
      state = this.props.state;
    
      _dispatch = action => {};
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 复制代码

reducers

  • 如今咱们已经有了一些 action,只须要一些 reducer 来接收就行了。回到以前的 reducer 函数标记,它只是一个关于 action 和 state 的纯函数:

  • (state, action) => nextState
    复制代码
  • 知道了这个,咱们只须要传递组件的状态,而后在 reducer 里触发 action。对 reducer 来讲,咱们只想要一个对应上面标记的函数数组。咱们之因此使用一个数组是由于可使用数组的 Array.reduce 方法来迭代数组,最终生成咱们的新状态:

  • export class Provider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        const { reducers } = this.props;
        const nextState = reducers.reduce((state, reducer) => {
          return reducer(state, action) || state;
        }, this.state);
    
        this.setState(nextState);
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 复制代码
  • 如你所见,咱们所作的就是使用 reducer 来计算并得到新状态,而后就像以前所作的,咱们调用 this.setState 来更新 StateProvider 组件的状态。

  • 如今咱们只须要一个实际的 reducer:

  • function countReducer ({ count, ...state }, { type, payload }) {
      switch (type) {
        case ADD_N:
          return { ...state, count: count + payload };
        case ADD_ONE:
          return { ...state, count: count + 1 };
      }
    }
    复制代码
  • 咱们的 reducer 只是检查传入的 action.type,而后假如匹配到以后将会更新相对应的状态,不然就会在通过 switch 判断语句以后返回函数默认的 undefined。咱们的 reducer 和 redux 的 reducer 的一个重要的区别在当咱们不想更新状态时,通常状况下咱们会由于未匹配到 action type 而返回一个falsy 值,而 redux 则会返回未变化的状态。

  • 而后把咱们的 reducer 传进 StateProvider:

  • export default function Root () {
      return (
        <StateProvider state={initialState} reducers={[countReducer]}> <MyApp /> </StateProvider>
      );
    }
    复制代码
  • 如今咱们终于能够触发一些 action,而后就会观察到相对应的状态更新:

  • export default function SomeCount () {
      return (
        <StateConsumer> {({ state, dispatch }) => ( <> <p> Count: {state.count} </p> <button onClick={() => dispatch(addOne())}> + 1 </button> <button onClick={() => dispatch(addN(5))}> + 5 </button> <button onClick={() => dispatch(addN(10))}> + 10 </button> </> )} </StateConsumer>
      );
    
    复制代码

中间件

  • 如今咱们的做品已经跟 redux 比较像了,只须要再增长一个处理反作用的方法就能够。为了达到这个目的,咱们须要容许用户传递中间件函数,这样当 action 被触发时就会被调用了。

  • 咱们也想让中间件函数帮助咱们处理状态更新,所以假如返回的 null 就不会被 action 传递给 reducer。redux 的处理稍微不一样,在 redux 中间件你须要手动传递 action 到下一个紧邻的中间件,假如没有使用 redux 的 next 函数来传递,action 将不会到达 reducer,并且状态也不会更新。

  • 如今让咱们写一个简单的中间件,咱们想经过它来寻找 ADD_N action,若是它找到了那就应当把 payload 和当前状态里面的 count 加和并输出,可是阻止实际状态的更新。

function countMiddleware ({ type, payload }, { count }) {
  if (type === ADD_N) {
    console.log(`${payload} + ${count} = ${payload + count}`);
    return null;
  }
}
复制代码
  • 跟咱们的 reducer 相似,咱们会将中间件用数组传进咱们的 StateProvider

  • export default function Root () {
      return (
        <StateProvider state={initialState} reducers={[countReducer]} middleware={[countMiddleware]} > <MyApp /> </StateProvider>
      );
    }
    复制代码
  • 最终咱们会调用全部全部中间件,而后根据返回的结果决定是否应当阻止更新。因为咱们传进了一个数组,然而咱们须要的是一个单个值,所以咱们准备使用 Array.reduce 来得到咱们的值。跟 reducer 相似,咱们也会迭代数组依次调用每一个函数,而后将结果赋值给一个变量 continueUpdate

  • 因为中间件被认为是一个高级特性,所以咱们不想它变成强制性的,所以若是没有在StateProvider 里面找到 middleware 属性,咱们会将 continueUpdate 置为默认的 undefined。咱们也会增长一个 middleware 数组来做默认属性,这样的话 middleware.reduce 就不会由于没传东西而抛出错误。

  • export class StateProvider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: [],
        middleware: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        const { reducers, middleware } = this.props;
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, this.state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          const nextState = reducers.reduce((state, reducer) => {
            return reducer(state, action) || state;
          }, this.state);
    
          this.setState(nextState);
        }
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 复制代码
  • 如你所见在第13行,咱们会查看中间件函数的返回值。若是返回 null 咱们就会跳过剩下的中间件函数,continueUpdate 将为 null,意味着咱们会中断更新。

异步 action

  • 由于咱们想让咱们的状态管理器对真实生产环境有用,因此咱们须要增长对异步 action 的支持,这意味着咱们将能够处理像网络请求相似案例的通用任务。咱们借鉴下 Redux Thunk ,由于它的 API 很简单,直观并且有效。

  • 咱们所要作的就是检查是否有为被调用的函数被传递到 dispatch,若是找到的话咱们就会在传递 dispatchstate 时调用函数,这样就能够给用户所写的异步 action 执行的机会。拿这个受权 action 做为例子来看下:

  • const logIn = (email, password) => async dispatch => {
      dispatch({ type: 'LOG_IN_REQUEST' });
    
      try {
        const user = api.authenticateUser(email, password);
        dispatch({ type: 'LOG_IN_SUCCESS', payload: user });
      catch (error) {
        dispatch({ type: 'LOG_IN_ERROR', payload: error });
      }
    };
    复制代码
  • 在上面的例子中咱们写了一个叫作 logIn 的 action 建立器,不是返回一个对象,它返回一个接收 dispatch 的函数,这可让用户在一个异步 API 请求的前面和后面触发异步 action,根据 API 不一样的返回结果触发不一样的 action,这里咱们在发生错误时发送一个错误 action。

  • 作到这一点只须要在 StateProvider 里的 _dispatch 方法里检查 action 的类型是否是 function:

  • export class StateProvider extends React.PureComponent {
      static defaultProps = {
        state: {},
        reducers: [],
        middleware: []
      };
    
      state = this.props.state;
    
      _dispatch = action => {
        if (typeof action === 'function') {
          return action(this._dispatch, this.state);
        }
        
        const { reducers, middleware } = this.props;
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, this.state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          const nextState = reducers.reduce((state, reducer) => {
            return reducer(state, action) || state;
          }, this.state);
    
          this.setState(nextState);
        }
      };
    
      render () {
        return (
          <StateContext.Provider value={{ state: this.state, dispatch: this._dispatch }}> {this.props.children} </StateContext.Provider> ); } } 复制代码
  • 这里须要注意两点:咱们调用 action 为函数,传入 this.state,这样用户能够访问异步 action 中已有的状态,咱们也会返回函数调用的结果,容许开发者从他们的异步 action 中得到一个返回值从而开启更多的可能性,例如从 dispatch 触发的 promise 链。

避免没必要要的从新渲染

  • Redux 的一个常常被忽视的必要特性是它能在必须时才会对组件从新渲染(或者更准确的说是 React-Redux  — react 跟 redux 的绑定)。为了作到这一点,它使用了 connect 高阶组件,它提供了一个映射函数 — mapStateToProps — 仅仅只在关联的 mapStateToProps 的输出改变时(只映射从如今开始的状态)才会触发组件的从新渲染。若是不这样的话,那么每次状态更新都会让组件使用 connect 来订阅存储改变而后从新渲染。

  • 想一想咱们须要作的,咱们须要一种方法来存储 mapState 前面的输出,这样咱们就能够比较二者看看有没有差别来决定咱们是否须要继续向前和从新渲染咱们的组件。为了作到这一点咱们须要使用一种叫作记忆化的进程,跟咱们这行的许多事情同样,对于一个想到简单的进程来讲,这是一个重要的词,尤为是咱们可使用 React.Component 来存储咱们状态的子状态,而后仅在咱们检测到 mapState 的输出改变以后再更新。

  • 接下来咱们须要一种可以跳过没必要要的组件更新的方法。react 提供了一个生命周期方法 shouldComponentUpdate 可让咱们达到目的。它将接收到的 props 和 state 当作参数来让咱们同现有的 props 和 state 进行比较,若是咱们返回 true 那么更新继续,若是返回 false 那么 react 将会跳过渲染。

  • class ConnectState extends React.Component {
      state = this.props.mapState(this.props.state);
    
      static getDerivedStateFromProps (nextProps, nextState) {}
    
      shouldComponentUpdate (nextProps) {
        if (!Object.keys(this.state).length) {
          this.setState(this.props.mapDispatch(this.props.state));
          return true;
        }
    
        console.log({
          s: this.state,
          nextState: nextProps.mapState(nextProps.state),
          state: this.props.mapState(this.state)
        });
        
        return false;
      }
    
      render () {
        return this.props.children({ state: this.props.state, dispatch: this.props.dispatch });
      }
    }
    
    export function StateConsumer ({ mapState, mapDispatch, children }) {
      return (
        <StateContext.Consumer> {({ state, dispatch }) => ( <ConnectState state={state} dispatch={dispatch} mapState={mapState} mapDispatch={mapDispatch}> {children} </ConnectState> )} </StateContext.Consumer> ); } 复制代码
  • 上面只是对咱们接下来要作的事情的概述。它有了因此主要的部分:从咱们的 context 接收更新,它使用了 getDerivedStateFromPropsshouldComponentUpdate,它也接收一个 render 属性来做为子组件,就像默认的消费者同样。咱们也会经过使用传递的 mapState 函数来初始化咱们的消费者初始状态。

  • 如今这样的话,shouldComponentUpdate 将只会在接收到第一次状态更新以后渲染一次。以后它会记录传进的状态和现有的状态,而后返回 false,阻止任何更新。

  • 上面的解决方案中在 shouldComponentUpdate 内部也调用了 this.setState,而咱们都知道 this.setState 老是会触发从新渲染。因为咱们也会从 shouldComponentUpdate 里返回 true,这会产生一次额外的从新渲染,因此为了解决这个问题,咱们将使用生命周期 getDerivedStateFromProps 来获取咱们的状态,而后咱们再使用 shouldComponentUpdate 来决定基于咱们获取的状态是否继续更新进程。

  • 若是咱们检查控制台也能够看到全局的状态更新,同时咱们的组件阻止任何更新到 this.state 对象以致组件跳过更新:

  • redux-like-state-update

  • 如今咱们知道了如何阻止没必要要的更新,咱们还须要一个能够智能的决定咱们的消费者什么时候应当更新的方法。若是咱们想要递归循环传进来的 state 对象来查看每一个属性来看状态是否有改变,可是这对于帮助咱们理解有帮助却对性能不利。咱们没办法知道传进来的 state 对象层级有多深或者多复杂,若是条件永远不知足,那么递归函数将会无限期的执行下去,所以咱们准备限制咱们比较的做用域。

  • 跟 redux 相似,咱们准备使用一个比较函数。在这里意味着咱们在比较咱们的对象是否相等的属性的层级,意味着咱们只会比较一层。所以咱们将会检查咱们每一个新状态的顶层属性是否等于咱们现有状态的同名属性,若是同名属性不存在,或者它们的值不一样,咱们将会继续渲染,不然咱们就认为咱们的状态时相同的,而后阻止渲染。

  • function shallowCompare (state, nextState) {
      if ((typeof state !== 'object' || state === null || typeof nextState !== 'object' || nextState === null)) return false;
    
      return Object.entries(nextState).reduce((shouldUpdate, [key, value]) => state[key] !== value ? true : shouldUpdate, false);
    }
    复制代码
  • 首先咱们从检查是否两个 state 都是对象开始,若是不是那么咱们就跳过渲染。在初始检查以后咱们把现有的状态转化为一个键值对的数组,并经过将数组减小为单个布尔值来检查每一个属性的值与传进来的 state 对象的值。

  • 这是困难的部分,如今咱们想用咱们的 shallowCompare 函数,实际上只是调用并检查结果。若是它返回 true,咱们就返回 true 来容许组件从新渲染,不然咱们就返回 false 来跳过更新(而后咱们得到的状态被放弃掉)。咱们也想在 mapDispatch 存在的时候调用它。

  • class ConnectState extends React.Component {
      state = {};
    
      static getDerivedStateFromProps ({ state, mapState = s => s }) {
        return mapState(state);
      }
    
      shouldComponentUpdate (nextProps, nextState) {
        return shallowCompare(this.state, nextState);
      }
    
      render () {
        return this.props.children({
          state: this.state,
          dispatch: this.props.mapDispatch ? this.props.mapDispatch(this.props.dispatch) : this.props.dispatch
        });
      }
    }
    复制代码
  • 最后咱们须要传递一个 mapState 函数让咱们消费者以只匹配咱们的部分状态,这样咱们就会将它做为一个属性传给咱们的 StateConsumer

  • return (
      <StateConsumer mapState={state => ({ greeting: state.greeting })} mapDispatch={dispatch => dispatch}> // ... 复制代码
  • 如今咱们只订阅 greeting 里面的改变,所以假如咱们更新了组件里的 count 将会被咱们的全局状态改变所忽略而且避免了一次从新渲染。

快速回顾

  • 若是你已经作到了这一步,你就已经见到了如何开发一个带有 reducer 和 action 的 类 redux 的状态管理库。咱们也覆盖了更高级的特性,例如异步 action,中间件,以及如何让咱们只接收咱们想要的状态更新,从而避免咱们的消费者每次全局状态更新进而引发的从新渲染。
  • 尽管 redux 其实作的比咱们的解决方案要多得多,但愿这个方案有助于澄清一些核心概念,而 redux 一般被认为是一个更加高级的特性,但它的实现其实相对简单。
  • 想要对 redux 的内部了解更加完全,我强烈推荐你阅读它在 github 的源码
  • 咱们目前为止的解决方案已经有了真实项目所必须的工具和特性了。咱们能够在一个 react 项目中使用它,不须要使用 redux,除非咱们想要接入一些真正高级的功能。

Hooks

  • 若是你还没听过它,它正在快速变成 react 的下一个大特性。这里有一段来自官方描述的简单解释:

    Hooks 是一个让你没必要写 class 就可使用 state 和 react 其余特性的新功能。

  • hooks 提供给咱们高阶组件的全部能力,以及更加清晰和直观的 API 来渲染属性。

  • 咱们来看一个使用基本的 useState 钩子的例子来看看它们是如何工做的:

    import React, { useState } from 'react';
    
    function Counter () {
      const [count, setCount] = useState(0);
      return (
        <> <span> Count: {count} </span> <button onClick={() => setCount(count + 1)}> Increment </button> </> ); } 复制代码
  • 在上面的例子中,咱们经过给 useState 传递一个 0 来初始化新状态,它会返回咱们的状态:count,以及一个更新函数 setCount。若是你之前没见过的话,可能会奇怪 useState 是如何不在每次渲染时初始化,这是由于 react 在内部处理了,所以咱们无需担忧这一点。

  • 让咱们暂时先忘掉中间件和异步 action,用 useReducer 钩子来从新应用咱们的 provider,就像咱们正在作的同样,除了将 action 触发到一个得到新状态的 reducer,它就像 useState 同样工做。

  • 知道了这个,咱们只须要将 reducer 的逻辑从老的 StateProvider 拷贝到新的函数组件 StateProvider里就能够了:

    export function StateProvider ({ state: initialState, reducers, middleware, children }) {
      const [state, dispatch] = useReducer((state, action) => {
        return reducers.reduce((state, reducer) => reducer(state, action) || state, state);
      }, initialState);
    
      return (
        <StateContext.Provider value={{ state, dispatch }}> {children} </StateContext.Provider> ); } 复制代码
  • 能够如此的简单,可是当咱们想要保持简单时,咱们仍然没有彻底掌握 hooks 的全部能力。咱们也可使用 hooks 来把咱们的 StateConsumer 换为咱们本身的钩子,咱们能够经过包裹 useContext 钩子来作到:

    const StateContent = createContext();
    
    export function useStore () {
      const { state, dispatch } = useContext(StateContext);
      return [state, dispatch];
    }
    复制代码
  • 尽管以前当咱们建立咱们的上下文时使用了解构的 ProviderConsumer,可是此次咱们会将它存到咱们传递到 useContext 单个变量从而让咱们可不用 Consumer 就能够接入上下文。咱们也将它命名为咱们本身的 useStore 钩子,由于 useState 是一个默认的钩子。

  • 接下来咱们来简单地重构下咱们消费上下文数据的方法:

    export default function SomeCount () {
      const [state, dispatch] = useStore();
      return (
        <>
          <p>
            Count: {state.count}
          </p>
          <button onClick={() => dispatch(addOne())}>
            + 1
          </button>
          <button onClick={() => dispatch(addN(5))}>
            + 5
          </button>
          <button onClick={() => dispatch(addN(10))}>
            + 10
          </button>
        </>
      );
    }
    复制代码
  • 但愿这些例子能让你感觉到 hooks 是如何的直观、简单和有效。咱们减小了所需的代码数量,而且给了咱们一个友好、简单的 API 供使用。

  • 咱们也想让咱们的中间件和内置的异步 action 从新开始工做。为了作到这一点,咱们将咱们的 useReducer 包裹进一个自定义钩子,在咱们的 StateProvider 中被特殊的使用,而后简单地重用咱们老的状态组件的逻辑就行了。

  • export function useStateProvider ({ initialState, reducers, middleware = [] }) {
      const [state, _dispatch] = useReducer((state, action) => {
        return reducers.reduce((state, reducer) => reducer(state, action) || state, state);
      }, initialState);
    
      function dispatch (action) {
        if (typeof action === 'function') {
          return action(dispatch, state);
        }
    
        const continueUpdate = middleware.reduce((result, middleware) => {
          return result !== null ? middleware(action, state) : result;
        }, undefined);
    
        if (continueUpdate !== null) {
          _dispatch(action);
        }
      }
    
      return { state, dispatch };
    }
    复制代码
  • 在咱们的老的解决方案中,咱们想让中间件是可选的,因此咱们添加了一个空数组做为默认值,一样地此次咱们也使用一个默认的参数来替换默认属性。相似于咱们老的 dispatch 函数,咱们调用了中间件,而后若是 continueUpdate !== null 咱们就继续更新状态。咱们也不会改变处理异步 action 的方式。

  • 最终,咱们将 useStateProvider 的结果和它的参数到咱们的 provider,这咱们以前没怎么考虑:

  • export function StateProvider ({ state: initialState, reducers, middleware, children }) {
      return (
        <StateContext.Provider value={useStateProvider({ initialState, reducers, middleware })}> {children} </StateContext.Provider> ); } 复制代码
  • 完结!

然而...

  • 你可能已经注意到的一件事是咱们对 hooks 的应用可能没有办法跳过没必要要的更新。这是由于 hooks 是在函数组件体内被调用的,在这个阶段 react 无法摆脱渲染进程(在不适用一些技巧的前提下)。这没有必要担忧,react 团队已经考虑到这一点并且计划提供一种方法让咱们可以在函数组件内部终止更新。
  • 在函数组件内一旦咱们有官方提供的方法来让咱们摆脱渲染,我就会回到这里来更新这篇文章。与此同时,我用 hooks 实现的这个库还有消费者,这样咱们就能够访问此功能。

总结

  • 综上所述,咱们已经写完了状态管理的大部分功能,还逐步在它基础上增长了一些相似 redux 的功能,包括 action,reducer,中间件以及一种比较状态差别的方法来提高性能。咱们也看到了可使用新的 hooks API 能够如何简化咱们的代码。
  • 但愿你能从这篇文章中得到一些有用的东西,可以对一些高级概念有些许了解,同时可让咱们在使用一些工具时比咱们初次见到它时更加简单。

在开始的时候提到过,我写了一个库,Use Simple State,看完这篇文章以后,你能够在个人 github页面看到,我已经使用 hooks 最终实现了,包括几个新增的功能。

相关文章
相关标签/搜索