从 Redux 源码谈谈函数式编程

摘要

在 React 的世界中,状态管理方案不下几百种,但其中最经典的,莫过于 Redux 。如果你想学习函数式编程,那么 Redux
源码就是最好的学习材料。考虑到不少小伙伴用的是 Vue ,本人争取让这篇文章写得通俗易懂,让没有接触过 React 技术栈的同学也能掌握
Redux 。

Redux 属于典型的“百行代码,千行文档”,其中核心代码非常少,但是思想不简单,可以总结为下面两点:

  • 全局状态唯一且不可变(Immutable) ,不可变的意思是当需要修改状态的时候,用一个新的来替换,而不是直接在原数据上做更改:
 let store = {
    foo: 1, bar: 2 };

 // 当需要更新某个状态的时候
 // 创建一个新的对象,然后把原来的替换掉
 store = {
    ...store, foo: 111 };

这点与 Vue 恰好相反,在 Vue 中必须直接在原对象上修改,才能被响应式机制监听到,从而触发 setter 通知依赖更新。

状态更新通过一个纯函数(Reducer)完成。纯函数(Pure function)的特点是:

  • 输出仅与输入有关;
  • 引用透明,不依赖外部变量;
  • 不产生副作用;

因此对于一个纯函数,相同的输入一定会产生相同的输出,非常稳定。使用纯函数进行全局状态的修改,使得全局状态可以被预测。

1. 需要了解的几个概念

在使用 Redux 及阅读源码之前需要了解下面几个概念:

Action

action 是一个普通 JavaScript 对象,用来描述如何修改状态,其中需要包含 type 属性。一个典型的 action 如下所示:

const addTodoAction = {
   
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}

Reducers

reducer 是一个纯函数,其函数签名如下:

/**
 * @param {State} state 当前状态
 * @param {Action} action 描述如何更新状态
 * @returns 更新后的状态
 */
function reducer(state: State, action: Action): State

reducer 函数的名字来源于数组的 reduce 方法,因为它们类似数组 reduce 方法传递的回调函数,也就是上一个返回的值会作为下一次调用的参数传入。

reducer函数的编写需要严格遵顼以下规则:

  • 检查reducer是否关心当前的action
  • 如果是,就创建一份状态的副本,使用新的值更新副本中的状态,然后返回这个副本
  • 否则就返回当前状态

一个典型的 reducer 函数如下:

const initialState = {
    value: 0 }

function counterReducer(state = initialState, action) {
   
  if (action.type === 'counter/incremented') {
   
    return {
   
      ...state,
      value: state.value + 1
    }
  }
  return state
}

Store

通过调用 createStore 创建的 Redux 应用实例,可以通过 getState() 方法获取到当前状态。

Dispatch

store 实例暴露的方法。更新状态的唯一方法就是通过 dispatch 提交 action 。store 将会调用 reducer 执行状态更新,然后可以通过 getState() 方法获取更新后的状态:

store.dispatch({
    type: 'counter/incremented' })

console.log(store.getState())
// {value: 1}

storeEnhancer

createStore 的高阶函数封装,用于增强 store 的能力。Redux 的 applyMiddleware 是官方提供的一个 enhancer 。

middleware

dispatch 的高阶函数封装,由 applyMiddleware 把原 dispatch替换为包含 middleware 链式调用的实现。Redux-thunk 是官方提供的 middleware,用于支持异步 action 。

2. 基本使用

学习源码之前,我们先来看下 Redux 的基本使用,便于更好地理解源码。

首先我们编写一个 Reducer 函数如下:

// reducer.js
const initState = {
   
  userInfo: null,
  isLoading: false
};

export default function reducer(state = initState, action) {
   
  switch (action.type) {
   
    case 'FETCH_USER_SUCCEEDED':
      return {
   
        ...state,
        userInfo: action.payload,
        isLoading: false
      };
    case 'FETCH_USER_INFO':
      return {
    ...state, isLoading: true };
    default:
      return state;
  }
}

在上面代码中:

  • reducer首次调用的时候会传入initState作为初始状态,然后switch…case最后的default用来获取初始状态
  • 在switch…case中还定义了两个action.type用来指定如何更新状态

接下来我们创建 store :

// index.js
import {
    createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

store 实例会暴露两个方法 getState 和 dispatch ,其中 getState 用于获取状态,dispatch 用于提交 action 修改状态,同时还有一个 subscribe 用于订阅store的变化:

// index.js

// 每次更新状态后订阅 store 变化
store.subscribe(() => console.log(store.getState()));

// 获取初始状态
store.getState();

// 提交 action 更新状态
store.dispatch({
    type: "FETCH_USER_INFO" });
store.dispatch({
    type: "FETCH_USER_SUCCEEDED", payload: "测试内容" });

我们运行一下上面的代码,控制台会先后打印:

{
    userInfo: null, isLoading: false } // 初始状态
{
    userInfo: null, isLoading: true } // 第一次更新
{
    userInfo: "测试内容", isLoading: false } // 第二次更新

3. Redux Core 源码分析

上面的例子虽然很简单,但是已经包含 Redux 的核心功能了。接下来我们来看下源码是如何实现的。

createStore

可以说 Redux 设计的所有核心思想都在 createStore 里面了。 createStore 的实现其实非常简单,整体就是一个闭包环境,里面缓存了 currentReducer 和 currentState ,并且定义了getState、subscribe、dispatch 等方法。

createStore 的核心源码如下,由于这边还没用到 storeEnhancer ,开头有些if…else的逻辑被省略了,顺便把源码中的类型注解也都去掉了,方便阅读:

// src/createStore.ts
function createStore(reducer, preloadState = undefined) {
   
  let currentReducer = reducer;
  let currentState = preloadState;
  let listeners = [];

  const getState = () => {
   
    return currentState;
  }

  const subscribe = (listener) => {
   
    listeners.push(listener);
  }

  const dispatch 

你可能感兴趣的:(javascript,Redux)