#yyds干货盘点#Redux 源码与函数式编程

Posted 尼羲

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点#Redux 源码与函数式编程相关的知识,希望对你有一定的参考价值。

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

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

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

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

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

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

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

基本概念

​在使用 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 。

基本使用

学习源码之前,我们先来看下 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 // 第二次更新

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 = (action) =>
currentState = currentReducer(currentState, action);

for (let i = 0; i < listeners.length; i++)
const listener = listeners[i];
listener();


return action;


dispatch( type: "INIT" );

return
getState,
subscribe,
dispatch

createStore 的调用链路如下:

  • 首先调用 createStore 方法,传入 reducer 和 preloadState 。preloadState 代表初始状态,假如不传那么 reducer 必须要指定初始值;
  • 将 reducer 和 preloadState 分别赋值给 currentReducer 和 currentState 用于创建闭包;
  • 创建 listeners 数组,这其实就是基于发布订阅模式,listeners 就是发布订阅模式的事件中心,也是通过闭包缓存;
  • 创建 getState 、subscribe 、dispatch 等函数;
  • 调用 dispatch 函数,提交一个 INIT 的 action 用来生成初始state,在 Redux 源码中,这里的 type 是一个随机数;
  • 最后返回一个包含 getState 、subscribe 、dispatch 函数的对象,即 store 实例;

那么很显然,外界无法访问到闭包的值,只能通过getState函数访问。

为了订阅状态更新,可以使用 subscribe 函数向事件中心 push 监听函数(注意 listener 是允许副作用存在的)。

当需要更新状态时,调用 dispatch 提交 action 。在 dispatch 函数中调用 currentReducer(也就是 reducer 函数),并传入 currentState 和 action ,然后生成一个新的状态,传给 currentState 。在状态更新完成后,将订阅的监听函数执行一遍(实际上只要调用 dispatch ,即使没有对 state 做任何修改,也会触发监听函数)。

如果有熟悉面向对象编程的小伙伴可能会说,createStore里面做的事情可以封装到一个类里面。确实可以,本人用 TypeScript 实现如下(发布订阅的功能不写了):

type State = Object;
type Action =
type: string;
payload?: Object;

type Reducer = (state: State, action: Action) => State;

// 定义 IRedux 接口
interface IRedux
getState(): State;
dispatch(action: Action): Action;


// 实现 IRedux 接口
class Redux implements IRedux
// 成员变量设为私有
// 相当于闭包作用
private currentReducer: Reducer;
private currentState?: State;

constructor(reducer: Reducer, preloadState?: State)
this.currentReducer = reducer;
this.currentState = preloadState;
this.dispatch( type: "INIT" );


public getState(): State
return this.currentState;


public dispatch(action: Action): Action
this.currentState = this.currentReducer(
this.currentState,
action
);
return action;



// 通过工厂模式创建实例
function createStore(reducer: Reducer, preloadState?: State)
return new Redux(reducer, preloadState);

你看,多有意思,函数式编程和面向对象编程竟然殊途同归了。

applyMiddleware

applyMiddleware 是 Redux 中的一个难点,虽然代码不多,但是里面用到了大量函数式编程技巧,本人也是经过大量源码调试才彻底搞懂。

首先要能看懂这种写法:

const middleware =
(store) =>
(next) =>
(action) =>
// ...

上面的写法相当于:

const middleware = function(store) 
return function(next)
return function(action)
// ...


其次需要知道,这种其实就是函数柯里化,也就是可以分步接受参数。如果内层函数存在变量引用,那么每次调用都会生成闭包。

说到闭包,有些同学马上就想到内存泄漏。但实际上闭包在平时项目开发中非常常见,很多时候我们不经意间就创建了闭包,但往往都被我们忽略了。

闭包一大作用就是缓存值,这和声明一个变量在赋值的效果是类似的。而闭包的难点就在于,变量是显式声明,而闭包往往是隐式的,什么时候创建闭包,什么时候更新了闭包的值,很容易被忽略。

可以这么说,函数式编程就是围绕闭包展开的。在下面的源码分析中,会看到大量闭包的例子。

applyMiddleware 是 Redux 官方实现的 storeEnhancer ,实现了一套插件机制,增加 store 的能力,例如实现异步 Action ,实现 logger 日志打印,实现状态持久化等等。

export default function applyMiddleware<Ext, S = any>(
...middlewares: Middleware<any, S, any>[]
): StoreEnhancer< dispatch: Ext >

applyMiddleware 接受一个或多个 middleware 实例,然后再传给createStore:

import  applyMiddleware, createStore  from "redux";
import thunk from "redux-thunk"; // 使用 thunk 中间件
import reducer from "./reducer";

const store = createStore(reducer, applyMiddleware(thunk));

createStore 入参中只接受一个 storeEnhancer ,如果需要传入多个,可以使用 Redux Utils 中的 compose 函数将它们组合起来。

compose 函数在后面会介绍

​看上面的用法,可以猜测 applyMiddleware 肯定也是个高阶函数。之前说到 createStore 前面有些if..else逻辑因为没用到 storeEnhancer 所以被省略了。这边我们一起来看下。

首先看 createStore 的函数签名,实际上是可以接受 1-3 个参数。其中 reducer 是必须要传递的。当第二个参数为函数类型,会识别为 storeEnhancer。如果第二个参数不是函数类型,则会识别为 preloadedState ,此时还可以再传递一个函数类型的 storeEnhancer :

function createStore(reducer: Reducer, preloadedState?: PreloadedState | StoreEnhancer, enhancer?: StoreEnhancer): Store

可以看到源码中参数校验的逻辑:

// src/createStore.ts:71
if (
(typeof preloadedState === function && typeof enhancer === function) ||
(typeof enhancer === function && typeof arguments[3] === function)
)
// 传递两个函数类型参数的时候,抛出异常
// 也就是只接受一个 storeEnhancer
throw new Error();

当第二个参数为函数类型,将它作为 storeEhancer 处理:

// src/createStore.ts:82
if (typeof preloadedState === function && typeof enhancer === undefined)
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined

接下来是一个比较难的逻辑:

// src/createStore.ts:87
if (typeof enhancer !== undefined)
// 如果使用了 enhancer
if (typeof enhancer !== function)
// 如果 enhancer 不是函数就抛出异常
throw new Error();


// 直接返回调用 enhancer 之后的结果,并没有往下继续创建 store
// enhancer 肯定是一个高阶函数
// 先传入了 createStore,又传入 reducer 和 preloadedState
// 说明很有可能在 enhancer 内部再次调用 createStore
return enhancer(createStore)(
reducer,
preloadedState
)

下面我们来看一下 applyMiddleware 的源码,为便于阅读,把源码中的类型注解都去掉了:

// src/applyMiddleware.ts
import compose from ./compose;

function applyMiddleware(...middlewares)
return (createStore) => (reducer, preloadedState) =>
const store = createStore(reducer, preloadedState);
let dispatch = () =>
throw new Error();


const middlewareAPI =
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)

const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

return
...store,
dispatch


可以看到这里代码并不多,但是出现了一个函数嵌套函数的情形:

const applyMiddleware = (...middlewares) =>
(createStore) =>
(reducer, preloadedState) =>
// ...

分析一下源码中的调用链路:

  • 调用 applyMiddleware 时,传入中间件实例,返回 enhancer 。从剩余参数的用法看出,支持传入多个 middleware ;
  • 由createStore调用 enhancer ,分两次传入 createStore 和 reducer 、preloadedState ;
  • 内部再次调用 createStore ,这次由于没有传 enhancer ,所以直接走创建 store 的流程;
  • 创建一个经过修饰的 dispatch 方法,覆盖默认 dispatch ;
  • 构造 middlewareAPI ,对 middleware 注入 middlewareAPI ;
  • 将 middleware 实例组合为一个函数,再向 middleware 传递默认的 store.dispatch 方法;
  • 最后返回一个新的 store 实例,此时 store 的 dispatch 方法经过了 middleware 修饰;

这里涉及到 compose 函数,是函数式编程范式中经常用到的一种处理,创建一个从右到左的数据流,右边函数执行的结果作为参数传入左边,最终返回一个以上述数据流执行的函数:

// src/compose.ts:46
export default function compose(...funcs)
if (funcs.length === 0)
return (arg) => arg

if (funcs.length === 1)
return funcs[0]

return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)

通过这边的代码,我们不难推断出一个中间件的结构:

function middleware( dispatch, getState ) 
// 接收 middlewareAPI
return function(next)
// 接收默认的 store.dispatch 方法
return function(action)
// 接收组件调用 dispatch 传入的 action
console.info(dispatching, action);
let result = next(action);
console.log(next state, store.getState());
return result;


看到这里,我想大多数读者都会有两个问题:

  1. 通过 middlewareAPI 获取的 dispatch 函数和 store 实例最终暴露的 dispatch 函数都是经过修饰的吗;
  2. 为了防止在创建 middleware 的时候调用 dispatch ,applyMiddleware 给新的 dispatch 初始化为一个空函数,且调用会抛出异常,那么这个函数究竟在何时被替换掉的;

大家可以先试着思考一下。

说实话,本人在阅读源码的时候也被这两个问题困扰,大多数技术文章也都没有给出解释。没办法,只能通过调试源码来找答案。经过不断调试,终于搞清楚了,middlewareAPI 的 dispatch 函数本身其实就是以闭包形式引入的,这个闭包可能没多少人能看得出来:

// 定义新的 dispatch 方法
// 此时是一个空函数,调用会抛出异常
let dispatch = () =>
throw new Error();

// 定义 middlewareAPI
// 注意这里的 dispatch 是通过闭包形式引入的
const middlewareAPI =
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)

// 对 middleware 注入 middlewareAPI
// 此时在 middleware 中调用 dispatch 会抛出异常
const chain = middlewares.map(middleware middleware(middlewareAPI));

然后下面这段代码其实做了两件事,一方面将 middleware 组合为一个函数,注入默认 dispatch 函数,另一方面将新的 dispatch 初始的空函数替换为正常可执行的函数。同时由于 middlewareAPI 的 dispatch 是以闭包形式引入的,当 dispatch 更新之后,闭包中的值也相应更新:

// 将 dispatch 替换为正常的 dispatch 方法
// 注意闭包中的值也会相应更新,middleware 可以访问到更新后的方法
dispatch = compose(...chain)(store.dispatch);

也就是说,createStore 生成的实例暴露的 dispatch 和 middleware 获取的都是修饰后的 dispatch ,并且应该是长这样:

function(action) 
// 注意这里存在闭包
// 可以获取到中间件初始化传入的 dispatch、getState 和 next
// 如果你打断点,可以在 scope 中看到闭包的变量
// 同时注意这里的 dispatch 就是这个函数本身
console.info(dispatching, action);
let result = next(action);
console.log(next state, store.getState());
return result;

以上是关于#yyds干货盘点#Redux 源码与函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# 更高级别的抽象---函数式思想

#yyds干货盘点#swagger定位问题⽅式

#yyds干货盘点#CSS的clamp()函数实现响应式布局

#yyds干货盘点#还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

#yyds干货盘点#还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

#yyds干货盘点#three.js源码解读-EventDispatcher