likes
comments
collection
share

react 状态管理之 react-redux 使用与实现原理

作者站长头像
站长
· 阅读数 47

什么是 react-redux

可以看到 redux 实际上只是维护了内部的一个 state, 它并没有与视图层相关的一些代码, 那我们如何在每个组件里面获取 state 的值, 并且在 state 改变之后重新 render 呢?那么可以借助 react-redux 来做。

react-reduxRedux 官方的 React UI 绑定层。它让 React 组件从 Redux 存储中读取数据,并将操作分派到存储以更新状态。

使用

以下面的例子为例我们将实现一个简单的计数器以及一个点击add todo按钮在列表中增加一项的功能,效果如下:

react 状态管理之 react-redux 使用与实现原理

代码如下:

index.tsx:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './store'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

store.ts:

import { createStore, combineReducers } from '../packages/redux'

export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';

export const increment = () => ({
  type: INCREMENT
})

export const decrement = () => ({
  type: DECREMENT
})

interface CounterState {
  value: number
}

const counterInitState: CounterState = {
  value: 0,
}

const counterReducer = (state = counterInitState, action: any): CounterState => {
  switch (action.type) {
    case INCREMENT:
      return { value: state.value + 1 };
    case DECREMENT:
      return { value: state.value - 1 };
    default:
      return state;
  }
}

interface TodoState {
  todos: string[]
}

const todoInitState: TodoState = {
  todos: []
}

const ADD_TODO = 'ADD_TODO'

export const addTodo = () => ({ type: ADD_TODO });

const todoReducer = (state = todoInitState, action: any): TodoState => {
  switch (action.type) {
    case ADD_TODO:
      return { todos: ['Use Redux'] };
    default:
      return state;
  }
}

const rootReducer = combineReducers({
  counter: counterReducer,
  todo: todoReducer
})

export const store = createStore(rootReducer);


App.tsx:

import React from 'react';
import logo from './logo.svg';
import styles from './App.module.css';
import { useDispatch, useSelector } from 'react-redux';
import { addTodo, decrement, increment } from './store';

function App() {
  const dispatch = useDispatch();

  const state: any = useSelector((state) => state)

  const handleDecrement = () => {
    dispatch(increment())
  }

  const handleIncrement = () => {
    dispatch(decrement())
  }

  const handleAddTodo = () => {
    dispatch(addTodo())
  }

  return (
    <div className={styles.App}>
      <header className={styles['App-header']}>
        <img src={logo} className={styles['App-logo']} alt="logo" />
        <div>
          <div className={styles.row}>
            <button
              className={styles.button}
              aria-label="Decrement value"
              onClick={handleDecrement}
            >
              -
            </button>
            <span className={styles.value}>{state.counter.value}</span>
            <button
              className={styles.button}
              aria-label="Increment value"
              onClick={handleIncrement}
            >
              +
            </button>
          </div>

          <div className={styles.row}>
            <ul>
              todos:
              {
                state.todo.todos.map((item: any) => (<li key={item}>{item}</li>))
              }
            </ul>

            <button
              className={styles.button}
              aria-label="Add todo"
              onClick={handleAddTodo}
            >
              add todo
            </button>
          </div>
        </div>
      </header>
    </div>
  );
}

export default App;

react-redux 通过 react context 在子组件中注入 store, 使得在每个组件中通能拿到 store, 从而获取数据, 派发 action。 所以使用 react-redux 主要有以下步骤:

原理与实现

介绍完了使用方法,下面来实现 react-redux 的核心逻辑。

首先要想在层级嵌套很深的组件中传递数据,首选的是react提供的context接口,react-redux同样是使用context来传递store以及dispatch等方法。

context

首先来看react-redux中的context,它实际上就是向下传递 storesubscription

ReactReduxContext.ts:

import React from 'react';

interface ReactReduxContext {
  store: any;
  subscription: any
}

export default React.createContext<ReactReduxContext>(null as unknown as ReactReduxContext);

Provider

首先看我们用来包裹项目中根组件AppProvider组件,Provider 组件的props接收一个store, 该 store 即为在redux中调用createStore函数创建的store

Provider.tsx:

import React from 'react';
import ReactReduxContext from './ReactReduxContext'
import Subscription from './utils/Subscription';

export default function Provider({ store, children }: any) {
  const subscription = new Subscription(store);

  return (
    // react-redux 内部也是用了 context 来在组件之间传递数据
    <ReactReduxContext.Provider value={{ store, subscription }}>
      {children}
    </ReactReduxContext.Provider>
  );
}

其中这个 Subscription 是一个发布订阅, 用于在 store.state 改变之后重新 render 组件

Subscription.ts:

export default class Subscription {
  listeners: Function[];
  unsubscribe: Function;

  constructor(store: any) {
    this.listeners = [];
    this.unsubscribe = store.subscribe(() => this.notify());
  }

  subscribe(listener: Function) {
    this.listeners.push(listener);

    return () => {
      const index = this.listeners.indexOf(listener);
      this.listeners.splice(index, 1);
    };
  }

  notify() {
    this.listeners.forEach((listener) => listener());
  }
}

hooks

react 16.8版本加入hooks以后, 函数组件因为其优势已经成为react组件类型的主流,下面让我来看看react-redux中暴露的hooks实现原理

useDispatch

useDispatch的源码很简单就是要导出dispatch函数用以在函数组件中派发action,由于这里的 store 是一个单例,所以 useDispatch 返回的 dispatch 函数也永远指向同一个函数。

useDispatch.ts:

import { useContext } from 'react';
import ReactReduxContext from '../ReactReduxContext';

export default function useDispatch() {
  const { store } = useContext(ReactReduxContext);

  return store.dispatch;
}
useSelector

useSelector 可以让我们在组件中获取 store 中的值,并且在这些值改变时重新render组件。

import { useContext, useReducer, useLayoutEffect, useRef } from 'react';
import ReactReduxContext from '../ReactReduxContext';

function useSelectorWithStore(selector: Function, equalityFn: Function, store: any, subscription: any) {
  // 使用 useReducer 来让组件重新 render
  const [_, forceUpdate] = useReducer(x => x + 1, 0);
  
  // 使用 useRef 来记住上一次的计算值,从而和最新的计算值进行比较从而决定是否需要重新 render 组件
  const lastSelectedState = useRef();

  let selectedState = selector(store.getState());

  if (lastSelectedState.current !== undefined && !equalityFn(lastSelectedState.current, selectedState)) {
    selectedState = lastSelectedState.current;
  }

  useLayoutEffect(() => {
    lastSelectedState.current = selectedState
  })

  useLayoutEffect(() => {
    // 注册 store 变更的事件,在 store 变更之后比较前后两次 selector 函数的返回值是否相同,如果不同的话重新 render 组件
    const unsubscribe = subscription.subscribe(() => {
      const newSelectedState = selector(store.getState());
      if (equalityFn(lastSelectedState.current, newSelectedState)) {
        return;
      }
      forceUpdate();
      lastSelectedState.current = newSelectedState
    });
    return unsubscribe;
  }, [store, subscription]);

  return selectedState;
}

const refEquality = (a: any, b: any) => a === b

/**
 * useSelector 接收两个参数一个是 selector 函数用于计算你想要的 store 中的数据
 * 一个是 equalityFn, 这个函数用于在 store 中的数据发生变化时调用这个equalityFn函数根据返回值比较前后两次的计算值是否相同,如果不相同则重新 render 组件
 */
export default function useSelector(selector: Function, equalityFn = refEquality) {
  const { store, subscription } = useContext(ReactReduxContext);

  const selectedState = useSelectorWithStore(
    selector, equalityFn, store, subscription,
  );

  return selectedState;
}

useSelector 中使用一个 useReducer 来实现一个 forceUpdate 方法来重新 render 组件,并且通过 subscription.subscribe 方法来监听store的变更,并比较前后两次 selector 的计算值,如果不相同的话再重新render组件。

转载自:https://juejin.cn/post/7243241586569543740
评论
请登录