手写 redux

我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!

redux 是什么

  • redux 是一个用来帮助我们管理应用状态的工具库
  • 它以集中式 store 的方式对整个应用的状态进行集中管理
  • 它制定了一套数据更新的规则,使得应用中的状态只能以可预测的方式进行更新

redux 的数据流向

  • redux 初始化一个 store 来集中管理视图页面的状态
  • store 中的 reducer 定义了更新状态的规则
  • 页面渲染时,可以直接在 store 中获取状态
  • 当用户在页面视图中进行一些操作,想要更新数据时,就需要 dispatch 一个 action 来更新数据,action 是一个纯对象,其中包含 type 与 payload
  • reducer 函数会接收 action 和更新之前的 state,并根据 action 来更新 state,最后返回更新之后的 state
  • 最后 redux 在 state 更新之后,触发页面更新渲染

redux 的使用

  • 下面是 store 的代码
// redux
// import { applyMiddleware, combineReducers, createStore } from "redux";
// import logger from "redux-logger";
// import thunk from "redux-thunk";
// import promise from "redux-promise";

import {
  createStore,
  applyMiddleware,
  combineReducers,
  logger,
  thunk,
  promise,
  promiseFull,
} from "../my-redux";

const countReducer = (state = 100, action) => {
  const { type, payload = 1 } = action;

  switch (type) {
    case "ADD":
      return state + payload;
    default:
      return state;
  }
};

const countReducer2 = (state = { num: 1 }, action) => {
  const { type, payload = 1 } = action;

  switch (type) {
    case "ADD2":
      return { ...state, num: state.num + payload };
    default:
      return state;
  }
};

export const store = createStore(
  countReducer,

  // 合并多个 reducer
  // combineReducers({
  //   count: countReducer,
  //   count2: countReducer2,
  // }),
  applyMiddleware(promise, thunk, logger)
);
复制代码
  • 下面是 redux 的使用 demo 的代码
import React, { Component } from "react";
import { store } from "./store";

export class MyReduxPage extends Component {
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  
  componentWillUnmount() {
    this.unsubscribe();
  }
  
  handleAdd = () => store.dispatch({ type: "ADD", payload: 100 });
  
  handleAsyncAdd = () => {
    // 用 setTimeout 模拟 ajax 请求
    // 
    // 1、直接在组件中写 ajax 请求
    // 非常不优雅
    // setTimeout(() => {
    //   store.dispatch({ type: "ADD", payload: 100 });
    // }, 1000);
    
    // 2、将数据请求函数抽出去
    // 让 store.dispatch 直接调用,这样就可以将数据请求与视图渲染分离开
    store.dispatch((dispatch) => {
      setTimeout(() => {
        dispatch({ type: "ADD", payload: 100 });
      }, 1000);
    });
  };
  
  handlePromiseAdd = () => {
    store.dispatch({
      type: "ADD",
      payload: Promise.resolve(100),
    });
  };
  
  setNum = () => {
    store.dispatch({ type: "ADD2", payload: 10 });
  };
  
  render() {    
    return (
      <div>
        <h2>my-redux-demo</h2>
        <button onClick={this.handleAdd}>ADD:{store.getState()}</button>
        <br />
        <button onClick={this.handleAsyncAdd}>
          async-ADD:{store.getState()}
        </button>
        <br />
        <button onClick={this.handlePromiseAdd}>
          promise-ADD:{store.getState()}
        </button>
        
        <br />
        
        {/* 下面是 combineReducers 的使用 */}
        {/* <button onClick={this.handleAdd}>ADD:{store.getState().count}</button>
        <button onClick={this.setNum}>
        ADD2:{store.getState().count2.num}
        </button> */}
      </div>
    );
  }
}
复制代码

实现redux

实现 createStore

export function createStore(reducer) {
  let currentState; // 用于存放 store 的数据
  let listeners = []; // 用于存放监听的回掉

  const getState = () => currentState;

  const dispatch = (action) => {
    // 执行 reducer
    currentState = reducer(currentState, action);

    // 执行监听的回掉
    listeners.forEach((listener) => listener());
  };

  // 注册监听回掉
  const subscribe = (newListener) => {
    listeners.push(newListener);

    // 返回一个移除监听回掉的方法
    return () => {
      listeners = listeners.filter((listener) => listener !== newListener);
    };
  };

  // 解决 currentState 没有初始值的问题
  dispatch({ type: `${Date.now()}` });

  return {
    getState,
    dispatch,
    subscribe,
  };
}
复制代码
  • createStore 是一个函数

    • 它接收一个,定义了 store 中数据更新规则的 reducer 函数
    • 然后返回一个对象,其中包括getStatedispatchsubscribe 等与 store 进行交互的方法
  • getState 是用来获取 store 数据的方法

  • dispatch 是用来提交数据更新到 store 的方法,其接受一个 action 纯对象

    • action 对象包含了数据更新的类型 type,以及数据更新的参数 payload
    • 执行 reducer 函数,并将当前的 state 与 action 作为参数传入,即可完成 store 数据的更新
  • 数据更新之后如何让视图进行更新?

    • 视图如何更新只有视图知道
      • 所以可以将视图的更新函数作为回掉函数注册到 store 中
      • 当数据更新时,调用视图更新的回掉函数即可完成视图的更新
    • redux 的解决方案就是
      • 在 store 中定义一个存放监听函数的数组 listeners
      • 提供一个 subscribe 函数用于注册视图更新的回掉函数
        • 该 subscribe 函数接收一个监听回掉
        • 返回一个注销该监听回掉的函数
      • 只要页面通过 dispatch 提交数据更新,就可以遍历 listeners,并执行每一个监听函数
const dispatch = (action) => {
  // 执行 reducer
  currentState = reducer(currentState, action);

  // 执行监听的回掉
  listeners.forEach((listener) => listener());
};

// 注册监听回掉
const subscribe = (newListener) => {
  listeners.push(newListener);

  // 返回一个移除监听回掉的方法
  return () => {
    listeners = listeners.filter((listener) => listener !== newListener);
  };
};
复制代码
  • 上面 redux demo 的代码中
    • 在组件挂载时,完成试图更新回掉的注册,然后将注销视图更新的函数保存在组件实例上的 unsubscribe 属性中
    • 在组件卸载前,执行 unsubscribe 以注销视图更新回掉
  componentDidMount() {
    this.unsubscribe = store.subscribe(() => {
      this.forceUpdate();
    });
  }
  
  componentWillUnmount() {
    this.unsubscribe();
  }
复制代码
  • 组件中通过 store.getState 方法即可获取 store 中存储的数据
  • 每次 button 被点击时就会调用 handleAdd 方法,该方法中调用 store.dispatch 提交数据更新
...

handleAdd = () => store.dispatch({ type: "ADD", payload: 100 });

<button onClick={this.handleAdd}>ADD:{store.getState()}</button>

...
复制代码

实现 applyMiddleware

  • 上面的例子实现了 redux 最基本的使用,dispatch 提交数据更新时,提交一个为普通对象的 action,但这只能满足一般的同步数据更新
  • 如何提交数据的异步更新?
    • 如下面代码所示
      • 方案一:直接将数据请求写在组件内的某个方法中,在请求拿到结果之后调用 store.dispatch 提交数据更新,这样可以实现需求,但是数据请求与视图混在一起写,极其不优雅
      • 方案二:将数据请求封装成一个函数,将该函数作为参数传给 store.dispatch 进行调用,这样就将数据请求与视图渲染分离开了
  handleAsyncAdd = () => {
    // 用 setTimeout 模拟 ajax 请求
    // 
    // 1、直接在组件中写 ajax 请求
    // 非常不优雅
    // setTimeout(() => {
    //   store.dispatch({ type: "ADD", payload: 100 });
    // }, 1000);
    
    // 2、将数据请求函数抽出去
    // 让 store.dispatch 直接调用,这样就可以将数据请求与视图渲染分离开
    const request = (dispatch) => {
      setTimeout(() => {
        dispatch({ type: "ADD", payload: 100 });
      }, 1000);
    }
    
    store.dispatch(request);
  };
复制代码
  • 但是上面也说到了,普通的 dispatch 只支持接收普通对象作为参数,那么如何让 store.dispatch 支持函数作为参数呢?
    • redux 的解决方案是:
      • 应用中间件的概念,给 dispatch 的执行过程加上中间件
      • 添加特定作用的中间件之后,就相当于增强 dispatch 的特定能力
  • 下面是 redux-thunk 中间件的代码
// 实现 redux-thunk 中间件
//
// 接收 storeApi,返回一个函数
export const thunk = ({ dispatch }) => {
  return function thunkDispatchWarp(next) {
    console.log("thunkDispatchWarp exec, next: ", next.name);

    return function thunkDispatch(action) {
      console.log("thunkDispatch exec", action);

      if (typeof action === "function") {
        return action(dispatch);
      }

      next(action);
    };
  };
};
复制代码
  • 如何添加中间件呢?
    • 下面则是仿照 redux 实现了用于应用中间件的 applyMiddleware 函数
export const applyMiddleware = (...middlewares) => {
  return (createStore) => (reducer) => {
    const store = createStore(reducer);
    // 未增强的 dispatch
    let dispatch = store.dispatch;

    // 增强 dispatch 后
    // 调用一次 dispatch, 所有中间件函数会被依次调用,最后执行 store.dispatch
    // 每个中间件函数都是可以进行读写 store 的操作,因此这里定义一个用于操作 store 的对象
    const storeApi = {
      getState: store.getState,
      // 用匿名箭头函数包了一层,它里面的 dispatch 就被固定下来,即为上面声明的 dispatch 变量
      // 下面 27 行代码, dispatch 变量被赋值为 增强之后的 dispatch 函数
      // 因此不管哪个中间件中使用 storeApi.dispatch 都相当于调用增强的 dispatch
      dispatch: (action) => dispatch(action),

      // 下面两种方式是等价的,都是直接引用的 store 里面的原始 dispatch,不符合要求
      // dispatch: store.dispatch,
      // dispatch,
    };

    // chain 里面是:每个中间件接收 storeApi 执行后的结果,也是一个函数
    // middleware 是一个三层嵌套函数形成的闭包,每层函数依次接收的参数是 storeApi, next, action
    const chain = middlewares.map((middleware) => middleware(storeApi));

    // 这里就是增强的 dispatch,供外部使用
    dispatch = compose(...chain)(dispatch);

    return {
      ...store,
      dispatch,
    };
  };
};

const compose = (...func) => {
  if (!func.length) return (arg) => arg;

  if (func.length === 1) return func[0];

  return func.reduce(
    (a, b) =>
      (...arg) =>
        a(b(...arg))
  );
};
复制代码
  • 修改 createStore 函数,使其第二个参数为 applyMiddleware 的执行结果
export function createStore(reducer, enhancer) {
  // 不使用中间件时,dispatch 只能接受一个普通对象
  // 使用中间件时,则可以增加 dispatch 的能力
  // 那么可以将一个接收中间件的 applyMiddleware 函数的执行结果
  // 当作 createStore 的第二个参数传入,也就是 enhancer,它也是一个函数
  if (enhancer) {
    // 这里就是如果传入 enhancer,就返回增强了 dispatch 的 store
    // 否则就还是按照原来的逻辑走
    return enhancer(createStore)(reducer);
  }
  
  ...
}
复制代码
  • 修改 store 的创建
    • 在 createStore 的第二个参数上,传入 applyMiddleware 接收了三个中间件之后的执行结果
...

export const store = createStore(
  countReducer,
  applyMiddleware(promise, thunk, logger)
);

...
复制代码
  • applyMiddleware 接收了中间件执行之后,返回了一个函数 enhancer
  • enhancer 也是一个函数,其应用了函数式编程中柯里化的思想
    • 接收一个,参数为 createStore 的函数
    • 返回一个,接收一个参数为 reducer 函数的函数
  • enhancer 最后执行完成之后,返回的对象中的 dispatch 就是增强之后的 dispatch

实现 combineReducers

  • 上面实现的功能都是基于一个 reducer 的,那么多个 reducer 的情况该如何处理呢?
  • redux 提供的 combineReducers 函数就是用来将多个 reducer 进行聚合
/**
 *
 * combineReducers 接收 reducer 的映射关系
 *
 * 返回合并后的 总 reducer
 *
 */
export const combineReducers = (reducerDict) => {
  // 返回是合并之后的总 reducer,所以也会接收 prevState 和 action
  return (prevState = {}, action) => {
    const nextState = {};

    let hasChanged = false;

    for (const key in reducerDict) {
      const reducer = reducerDict[key];
      nextState[key] = reducer(prevState[key], action);

      hasChanged = hasChanged || nextState[key] !== prevState[key];
    }

    hasChanged =
      hasChanged ||
      Object.keys(nextState).length !== Object.keys(prevState).length;

    return hasChanged ? nextState : prevState;
  };
};
复制代码
  • combineReducers 接收 reducer 的映射关系,返回一个函数,这个函数就是聚合之后的总 reducer
  • 实现也很简单
    • 按照 reducer 的映射关系,取出每个 reducer 函数并都进行执行
    • 然后将执行后的结果按照 reducer 的映射关系生成一个数据对象
    • 最后判断是否有 reducer 改变了 store 中的数据
      • 是则返回这个新的数据对象
      • 否则返回之前的数据对象

代码地址

おすすめ

転載: juejin.im/post/7069220743992573965
おすすめ