

#yyds干货盘点#Redux 源码与函数式编程
source link: https://blog.51cto.com/u_11365839/5470245
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

#yyds干货盘点#Redux 源码与函数式编程
推荐 原创Redux 属于典型的“百行代码,千行文档”,其中核心代码非常少,但是思想不简单,可以总结为下面两点:
- 全局状态唯一且不可变(Immutable) ,不可变的意思是当需要修改状态的时候,用一个新的来替换,而不是直接在原数据上做更改:
// 当需要更新某个状态的时候
// 创建一个新的对象,然后把原来的替换掉
store = { ...store, foo: 111 };
这点与 Vue 恰好相反,在 Vue 中必须直接在原对象上修改,才能被响应式机制监听到,从而触发 setter 通知依赖更新。
状态更新通过一个纯函数(Reducer)完成。纯函数(Pure function)的特点是:
- 输出仅与输入有关;
- 引用透明,不依赖外部变量;
- 不产生副作用;
因此对于一个纯函数,相同的输入一定会产生相同的输出,非常稳定。使用纯函数进行全局状态的修改,使得全局状态可以被预测。
在使用 Redux 及阅读源码之前需要了解下面几个概念:
Action
action 是一个普通 JavaScript 对象,用来描述如何修改状态,其中需要包含 type 属性。一个典型的 action 如下所示:
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 函数如下:
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() 方法获取更新后的状态:
console.log(store.getState())
// {value: 1}
storeEnhancer
createStore 的高阶函数封装,用于增强 store 的能力。Redux 的 applyMiddleware 是官方提供的一个 enhancer 。
middleware
dispatch 的高阶函数封装,由 applyMiddleware 把原 dispatch替换为包含 middleware 链式调用的实现。Redux-thunk 是官方提供的 middleware,用于支持异步 action 。
学习源码之前,我们先来看下 Redux 的基本使用,便于更好地理解源码。
首先我们编写一个 Reducer 函数如下:
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 :
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer);
store 实例会暴露两个方法 getState 和 dispatch ,其中 getState 用于获取状态,dispatch 用于提交 action 修改状态,同时还有一个 subscribe 用于订阅store的变化:
// 每次更新状态后订阅 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: true } // 第一次更新
{ userInfo: "测试内容", isLoading: false } // 第二次更新
Redux Core 源码分析
上面的例子虽然很简单,但是已经包含 Redux 的核心功能了。接下来我们来看下源码是如何实现的。
createStore
可以说 Redux 设计的所有核心思想都在 createStore 里面了。 createStore 的实现其实非常简单,整体就是一个闭包环境,里面缓存了 currentReducer 和 currentState ,并且定义了getState、subscribe、dispatch 等方法。
createStore 的核心源码如下,由于这边还没用到 storeEnhancer ,开头有些if...else的逻辑被省略了,顺便把源码中的类型注解也都去掉了,方便阅读:
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 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 中的一个难点,虽然代码不多,但是里面用到了大量函数式编程技巧,本人也是经过大量源码调试才彻底搞懂。
首先要能看懂这种写法:
(store) =>
(next) =>
(action) => {
// ...
}
上面的写法相当于:
return function(next) {
return function(action) {
// ...
}
}
}
其次需要知道,这种其实就是函数柯里化,也就是可以分步接受参数。如果内层函数存在变量引用,那么每次调用都会生成闭包。
说到闭包,有些同学马上就想到内存泄漏。但实际上闭包在平时项目开发中非常常见,很多时候我们不经意间就创建了闭包,但往往都被我们忽略了。
闭包一大作用就是缓存值,这和声明一个变量在赋值的效果是类似的。而闭包的难点就在于,变量是显式声明,而闭包往往是隐式的,什么时候创建闭包,什么时候更新了闭包的值,很容易被忽略。
可以这么说,函数式编程就是围绕闭包展开的。在下面的源码分析中,会看到大量闭包的例子。
applyMiddleware 是 Redux 官方实现的 storeEnhancer ,实现了一套插件机制,增加 store 的能力,例如实现异步 Action ,实现 logger 日志打印,实现状态持久化等等。
...middlewares: Middleware<any, S, any>[]
): StoreEnhancer<{ dispatch: Ext }>
applyMiddleware 接受一个或多个 middleware 实例,然后再传给createStore:
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 :
可以看到源码中参数校验的逻辑:
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
// 传递两个函数类型参数的时候,抛出异常
// 也就是只接受一个 storeEnhancer
throw new Error();
}
当第二个参数为函数类型,将它作为 storeEhancer 处理:
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
接下来是一个比较难的逻辑:
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 的源码,为便于阅读,把源码中的类型注解都去掉了:
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
}
}
}
可以看到这里代码并不多,但是出现了一个函数嵌套函数的情形:
(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 函数,是函数式编程范式中经常用到的一种处理,创建一个从右到左的数据流,右边函数执行的结果作为参数传入左边,最终返回一个以上述数据流执行的函数:
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))
)
}
通过这边的代码,我们不难推断出一个中间件的结构:
// 接收 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;
}
}
}
看到这里,我想大多数读者都会有两个问题:
- 通过 middlewareAPI 获取的 dispatch 函数和 store 实例最终暴露的 dispatch 函数都是经过修饰的吗;
- 为了防止在创建 middleware 的时候调用 dispatch ,applyMiddleware 给新的 dispatch 初始化为一个空函数,且调用会抛出异常,那么这个函数究竟在何时被替换掉的;
大家可以先试着思考一下。
说实话,本人在阅读源码的时候也被这两个问题困扰,大多数技术文章也都没有给出解释。没办法,只能通过调试源码来找答案。经过不断调试,终于搞清楚了,middlewareAPI 的 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 更新之后,闭包中的值也相应更新:
// 注意闭包中的值也会相应更新,middleware 可以访问到更新后的方法
dispatch = compose(...chain)(store.dispatch);
也就是说,createStore 生成的实例暴露的 dispatch 和 middleware 获取的都是修饰后的 dispatch ,并且应该是长这样:
// 注意这里存在闭包
// 可以获取到中间件初始化传入的 dispatch、getState 和 next
// 如果你打断点,可以在 scope 中看到闭包的变量
// 同时注意这里的 dispatch 就是这个函数本身
console.info('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK