手写Redux/React-Redux核心API

1,966 阅读9分钟

前言

这篇文章会带大家梳理一下React、React-Redux的原理,以及核心API的最简实现。适合已经使用过redux,但对原理不大了解的读者。

简介

在开始之前先简单介绍一下这两个库,以及他们的区别

Redux

Redux是一个Javascript状态管理库。他的做的事情非常简单: 使用一个对象去描述应用的状态state,并只能通过action配合reducer去修改state。同时提供了一个订阅服务,在状态被修改时会通知所有订阅者。

  • state tree

    存放应用状态的状态树。

  • action
    可以将它理解为改变状态的一个指令,通常是一个这样的对象{type: 'ADD',payload: 1}type字段告诉reducer该怎么去改变store中的状态,payload则是携带的数据。

  • reducer
    一个返回新状态的纯函数,它接收一个action,通过判断其中type字段对全局状态进行不同的操作,返回一个全新的状态树。

Redux核心部分仅负责状态维护和订阅通知, 是与框架完全无关的。

React-Redux

React ReduxReact 的官方 Redux UI 绑定库,帮助用户在组件中拿到Redux中存放的状态以及更新状态的操作。

实现Redux

基本使用

我们先通过这个DEMO看一下如何使用Redux

import { createStore } from 'redux'
const preloadedState = {
  count: 0,
};
// reducer
const reducer = (state, action) => {
  switch (action.type) {
    case "INCREASE":
      return { ...state, count: state.count + 1 };
    case "DECREASE":
      return { ...state, count: state.count - 1 };
    case "SET_COUNT":
      return { ...state, count: action.payload };
    default:
      return state;
  }
};
// action
const actions = {
  increment: () => ({ type: "INCREASE" }),
  decrement: () => ({ type: "DECREASE" }),
  setCount: (count) => ({ type: "SET_COUNT", payload: count }),
};
const store = createStore(reducer, preloadedState);

const state = store.getState() // 获取状态树
store.subscribe(() => console.log('store变啦',store.getState())); // 订阅状态变化
store.dispatch(actions.setCount(233)); // 触发状态变化
store.dispatch(actions.increment()); // 触发状态变化

可以看到Redux包含了以下方法

  • createStore 接收一个默认状态树和reducer函数并返回一个store,store中包含以下方法

  • store.getState 获取状态

  • store.dispatch 通过派发action去修改状态

  • store.subscribe 订阅状态的变化

createStore

getState

getState非常简单,我们返回函数中定义的state即可。

function createStore(reducer, preloadedState) {
  let state = preloadedState;  // 定义初始的状态树
  
  const getState = () => state; // 返回状态树
  
  return { getState };
}

dispach

这是一个修改状态的函数,通过前面的介绍我们知道state不应该被直接修改,而是每次需要更新时就通过actionreducer来获得一个新的state tree。

const newState = reducer(oldState,action)

所以dispatch需要做的就是接收一个action,通过reducer生成新的state 并替换旧的 state

function createStore(reducer, preloadedState) {
 let state = preloadedState;  // 定义初始的状态树
 
 const getState = () => state; // 返回状态树
 
 const dispatch = (action) => {
   // 获得新的状态树
   state = reducer(state, action);
   
   // 这里还需要通知所有订阅者
 };
 return { getState, dispatch };
}

subscribe

我们已经实现的状态的创建、获取、修改,但都只是对闭包内变量的操作,外界并不能感知到。subscribe 需要提供一个通知外接订阅者状态更新了的这么一个功能。

这里我们可以通过实现一个简易的发布订阅模式来达到这个效果。在函数内部再维护一个listeners 数组,里面存放订阅者的回调函数。在每次状态变化(dispatch被调用)时,就执行所有回调。

 function createStore(reducer, preloadedState) {
 let state = preloadedState;  // 定义初始的状态树
 
 const getState = () => state; // 返回状态树
 
 const listeners =  [];  
 // 订阅
 const subscribe = (fn) => {
   listeners.push(fn);
   // 取消订阅
   return () => {
     const index = listeners.find((item) => item === fn);
     listeners.splice(index, 1);
   };
 };
 const dispatch = (action) => {
   // 获得新的状态树
   state = reducer(state, action);
   // 通知所有订阅者
   listeners.forEach((fn) => fn());
 };
 return { getState , dispatch, subscribe };
}

至此我们已经实现了一个乞丐版本的redux,我们来测试一下

const preloadedState = {/** ... */};
const reducer = (state, action) => {/** ... */};
const actions = {/** ... */};

const store = createStore(reducer, preloadedState);
console.log('初始状态' , store.getState())

const unsubscribe = store.subscribe( // 订阅
   () => console.log('我知道state变啦',store.getState())
); 
store.dispatch(actions.setCount(233)); 
store.dispatch(actions.increment());

unsubscribe(); // 取消订阅
store.dispatch(actions.increment());  // 这次更新不会通知到我

image.png

combineReducers

随着应用变得越来越复杂,我们就可以考虑将 reducer 函数拆分成多个单独的函数,每个函数负责独立管理state的一部分。

Redux就有几个扩展api用来增强reducerdispatchcombineReducers可以将多个reducer合并成一个最终的reducer函数。 applyMiddleware可以让我们自己包装 store 的 dispatch来增强功能,比如实现异步action。

这里只介绍combineReducers, 该方法接收一个对象,通过为传入对象的reducer命名不同的 key 来控制返回 state key 的命名。

// 合并后的reducer
const rootReducer = combineReducers({potato: potatoReducer, tomato: tomatoReducer})
// 相应的,state的结构就得是
const rootState = { potato: {}, tomato: {} }

const store = createStore(rootReducer, rootState)

实现思路就是通过传入对象的key去关联reducerstate中的部分状态,当接收到action时,需要执行所有reducer获得新的状态,然后将这些状态重新组装成一个新的状态树。

export default function combineReducers(reducers) {
    const reducerKeys = Object.keys(reducers)
    // 返回合并后的reducer函数
    return function combinedReducer(state = {}, action) {
      // 新的state
      const nextState = {}
      // 遍历执行所有的reducers
      for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]   // 代表state的key
        const reducer = reducers[key] // key对应的reducer
        // 当前key对应state的 旧值
        const prevKeyState = state[key]
        // 执行reducer,获得当前key对应state的新值
        const nextKeyState = reducer(prevKeyState, action)
        // 组装最终的state
        nextState[key] = nextKeyState
      }
      return nextState
    }
  }

实现React-Redux

基本使用

和前面一样,我们先看个DEMO

import store from './store'
import { Provider,connect } from 'react-redux'
function App() {
  return (
    <Provider store={store}>
      <Header />
      <Main />
      <Footer />
    </Provider>
  )
}
// 映射state到props
const mapState = (state) => ({ count: state.count });
// 映射dispatch到props
const mapDispatch = (dispatch) => ({
  increment: () => dispatch({ type: "increment" }),
});

const Header = connect(mapState)((props) => {
  const { count } = props;
  return <header> header: {count}</header>;
});
const Main = connect(mapState,mapDispatch)((props) => {
  const { count, increment } = props;
  return (
    <main>
      Main: count:{count} <button onClick={increment}>increase </button>
    </main>
  );
});
function Footer(props) {
  return <footer>footer 没用到状态</footer>;
}

可以看到 react-redux 提供了一个Provider组件,用于将redux中创建的store透传给所有的子组件。

以及一个connect函数,它可以让被包裹的组件直接通过props访问store中那些被‘map’的状态和dispatch函数。这里的map可以理解为映射关系。

  • mapStateToProps函数可以让你控制store中哪些状态会被映射到组件的props中。
  • mapDispatchToProps让你可以自行封装dispatch函数并映射到props中。

默认情况下,如果都不传的话组件会拿到整个storedispatch函数。

connect(mapStateToProps, mapDispatchToProps)(MyComponent)

Context

使用ReactcreateContext方法直接创建一个context即可

import React from 'react'
const ReduxContext = React.createContext(null)
export default ReduxContext

Provider

Provider 接收store 并通过 ReduxContext 传给所有子组件

import React from 'react'
import ReduxContext from "./Context";
const Provider = (props: any) => {
  const { store, children, ...rest } = props;
  // 把store透传给所有子组件
  return (
    <ReduxContext.Provider value={store} {...rest}>
      {children}
    </ReduxContext.Provider>
  );
};
export default Provider;

connect

先通过connect的调用方式去推断一下connect函数的结构。

connect(mapStateToProps, mapDispatchToProps)(MyComponent)

首先它接收mapStateToProps,mapDispatchToProps 这两个函数,并且又返回了一个函数,该函数接收一个组件,并且能让组件中的props获得参数。

function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(component) {
   // 这里需要向组件的props中注入参数
  };
}

那么怎么才能在wrapWithConnect函数中拿到store,并传给组件的props呢? store是通过context透传的,而只有在组件中才能消费context,所以很明显需要通过高阶组件(HOC)来实现这个功能。

function connect(mapStateToProps?: any, mapDispatchToProps?: any) {
  return function wrapWithConnect(component) {
    // 包裹组件,主要为了获取组件上下文
    const HOC = (props) => {
      const { dispatch, getState, subscribe } = useContext(AppContext);
      const state = getState();
      function childPropsSelector(state) {
        // 注入到props中的状态
        // 可能是整个store, 也可能是被mapState 返回的几个特定状态
        const stateProps = mapStateToProps ? mapStateToProps(state) : state;
        // 注入到props中改变状态的函数
        // 可以是原本的dispatch,也可以是mapDispatch 返回的特定更新状态的函数
        const dispatchProps = mapDispatchToProps
          ? mapDispatchToProps(dispatch)
          : dispatch;
        return { ...stateProps, ...dispatchProps, ...props };
      }
       // 获得最终子组件的的props
      const actualChildProps = childPropsSelector(state);
      return React.createElement(component, actualChildProps, props.children);
    };
    return HOC;
  };
}

通过一层高阶组件的包裹,我们可以从context中拿到store的信息,并经过一顿处理传给被包裹的组件。

接下来我们需要实现组件的更新,在react中更新组件的方式是通过setState,每次传入一个新的值就能触发更新。

通过store提供的的subscribe方法,我们可以知道状态在什么时候被dispatch,被改变了。

  // 强制更新组件
  const [, forceUpdate] = useState({});
  useEffect(() => {
    const unsubscribe = subscribe(() => {
      forceUpdate({});
    });
    return unsubscribe;
  }, []);

这里还需要做一个小优化。现在的代码即使dispatch后的状态没有改变,组件中也会收到通知并触发组件的render

// 记录上一次渲染时的state
const preStateRef = useRef({});
preStateRef.current = state

// 订阅store
useEffect(() => {
const unsubscribe = subscribe(() => {
  // 对比新的state和旧的state
  const state = getState();
  if (!isShadowEqual(preStateRef.current, state)) {
    forceUpdate({});
  }
});
return unsubscribe;
}, []);

通过一个ref去记录旧的state,并在每次render之前对新旧state做一次浅层次的对比,就可以减少不必要的重渲染。当然真正的场景下不会只有这么简单的判断,具体可以查看源码react-redux 源码

最终的实现代码如下:

import React, { useContext, useEffect, useRef, useState } from "react";
import ReduxContext from "./Context";

function connect(mapStateToProps, mapDispatchToProps) {
  return function wrapWithConnect(component) {
    const HOC = (props) => {
      const { dispatch, getState, subscribe } = useContext(ReduxContext);
      const state = getState();
      const [, forceUpdate] = useState({});
      function childPropsSelector(state) {
        const stateProps = mapStateToProps ? mapStateToProps(state) : state;
        const dispatchProps = mapDispatchToProps
          ? mapDispatchToProps(dispatch)
          : dispatch;
        return { ...stateProps, ...dispatchProps, ...props };
      }
      // 最终子组件的props
      const actualChildProps = childPropsSelector(state);

      // 记录上一次渲染时的state
      const preStateRef = useRef({});
      preStateRef.current = state;

      // 订阅store
      useEffect(() => {
        const unsubscribe = subscribe(() => {
          // 对比新的state和旧的state
          const state = getState();
          if (!isShadowEqual(preStateRef.current, state)) {
            forceUpdate({});
          }
        });
        return unsubscribe;
      }, []);

      return React.createElement(component, actualChildProps, props.children);
    };
    return HOC;
  };
}

function isShadowEqual(origin: any, next: any) {
  if (Object.is(origin, next)) {
    return true;
  }
  if (
    origin &&
    typeof origin === "object" &&
    next &&
    typeof next === "object"
  ) {
    if (
      [...Object.keys(origin), ...Object.keys(next)].every(
        (k) =>
          origin[k] === next[k] &&
          origin.hasOwnProperty(k) &&
          next.hasOwnProperty(k)
      )
    ) {
      return true;
    }
  }
  return false;
}
export default connect;

Hooks API

除了使用connectReact-Redux还支持使用hook的方式去访问store

  • useStore 拿到store的对象
  • useSelector 使用选择器函数从state中提取数据
  • useDipatch 拿到dispatch函数
import React from "react";
import { useSelector, useStore, useDispatch } from "react-redux";
const CounterComponent = () => {
  const store = useStore()
  const count = useSelector((state) => state.counter);
  const dispatch = useDispatch()
  return (
      <div>
          {count}
          <button onClick={() => dispatch({type: 'inc'})}>increase</button>
      </div>
  );
};

访问store需要拿到context,而这里的调用都不需要我们自己传入context,所以需要事先拿到到这个context对象。

import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
  const useSelector = (selector) => {}
  return useSelector
}
export default createSelectorHook(context);

拿到context之后,就可以通过React.useContext访问到store了,接下来处理方式就和connect类似

import React, { useEffect, useState } from 'react';
import context from './Context'
function createSelectorHook(context) {
  const useReduxContext = () => React.useContext(context)
  const useSelector = (selector) => {
    const { getState,subscribe } = useReduxContext()
    const state = getState()
    const selectedState = selector(state);
     // 强制更新组件
     const [, forceUpdate] = useState({});
      // 订阅store
      useEffect(() => {
        const unsubscribe = subscribe(() => {
          // 这里省略的state的浅层对比
          forceUpdate({});
        });
        return unsubscribe;
      }, []);
    return selectedState;
  }
  return useSelector
}
export default createSelectorHook(context);

其他两个API也是类似的思路,这里不再赘述。hooks的方式更适在函数组件中使用,不需要事前给组件包一层,更加的方便。而connect更适合类组件,也可以通过@connect 装饰器简化这一层包裹的过程。

结语

本文通过一些的代码示例,手写实现了ReactReact-Redux的一些常用函数,实现参考了源码但是简化了很多优化和错误判断的步骤,重点在于梳理出实现功能的大致思路。 如有描述不正确的地方,欢迎大家指正!