Read Redux
XXHolic opened this issue · comments
目录
引子
之前看过一些对于 redux 源码的解读,现在想按照自己的想法写一写。
从 2018.04.18 Redux v4.0.0 之后就没有发过大的版本了,现在处于一个很稳定的阶段。
下面是查看代码所处分支、时间、commit 的信息:
- 代码分支——master
- commit 时间——2019.06.17
- commit content——removed distinctState() filter (#3450)
- commit hash——beb1fc29ca6ebe45226caa3a064476072cd9ed26
基本概念
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。可以和很多框架配合使用。
Redux 三大原则:
- 单一数据源(Single source of truth):整个应用的
state
被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个store
中。 - State 是只读的(State is read-only):唯一改变 state 的方法就是触发
action
,action
是一个用于描述已发生事件的普通对象。 - 使用纯函数来执行修改(Changes are made with pure functions):为了描述 action 如何改变 state tree ,你需要编写
reducer
。
Action
描述有事情发生,它把数据从应用传到 store
的有效载荷。它是 store
数据的唯一来源。
Action 创建函数
就是生成 action
的方法。
Reducers
指定了应用状态的变化如何响应 action
并发送到 store
。
action
描述“发生了什么”,使用 reducer
来根据 action
更新 state
,Store
就是把它们联系到一起的对象。
详细介绍见官方文档。
下面结合官方文档中的例子去看源码。
主要实现
在文档 基础(Basic Tutorial)
一章中,先提供了一个纯 JavaScript 的例子,这里取其中一部分完整的逻辑如下。
/*
* actions.js 文件
*/
// action 类型
export const ADD_TODO = "ADD_TODO";
// action 创建函数
export function addTodo(text) {
return { type: ADD_TODO, text };
}
/*
* reducers.js 文件
*/
const initialState = {
todos: []
};
function todosFun(state = [], action) {
switch (action.type) {
case "ADD_TODO":
return [...state, { text: action.text, completed: false }];
default:
return state;
}
}
function todoApp(state = initialState, action) {
return {
todos:todosFun(state.todos, action)
};
}
export default todoApp;
/*
* index.js 文件
*/
import { createStore } from 'redux';
import { addTodo } from "./actions";
import todoApp from './reducers';
let store = createStore(todoApp);
// 每次 state 更新时,打印日志
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
// 发起 action
store.dispatch(addTodo('Learn about redux'))
// 停止监听 state 更新
unsubscribe();
从例子里面可以发现,action
和 reducer
都是很常见的函数和对象,在 index.js 文件里面才出现了跟 redux 相关的调用,首先看下 createStore(todoApp)
做了什么。
API 文档中说是创建一个 Redux store 来以存放应用中所有的 state,有三个参数依次是:
- reducer:是一个方法,这个方法接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。
- preloadedState:初始 state。
- enhancer:是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。
在 createStore(todoApp)
中用到了第一个参数,传参也符合文档描述要求。看了源码后,发现除了文档说的作用,还有其它的作用。
先列出 createStore
方法中主要组成
function createStore(reducer, preloadedState, enhancer) {
// 主要变量
var currentReducer = reducer
var currentState = preloadedState
var currentListeners = []
var nextListeners = currentListeners
var isDispatching = false
// 方法
function ensureCanMutateNextListeners() {} // 对 currentListeners 浅复制
function getState() {} // 获取当前 state
function subscribe(listener) {} // 添加监听
function dispatch(action) {} // 分发 action ,这是唯一能够让 state 改变的方式。
function replaceReducer(nextReducer) {} // 替换当前 reducer
function observable() {} // 可观察/反应库的互操作点
// 主要执行逻辑
dispatch({
type: ActionTypes.INIT
})
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
其中 ActionTypes.INIT
并不是 createStore
的内部变量,找到后是这样的:
/**
* 这些是 Redux 内部保留的私有 action 类型
*/
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
export default ActionTypes
差不多齐了,可以发现在 createStore
执行后除了对外提供一些常见的接口,主要就执行了一次 dispatch
, 触发了一次 state 初始化操作,那么接着就是看下 dispatch
方法里面做了什么。除去检查和容错处理代码,主要的逻辑如下:
function dispatch(action) {
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
处理的逻辑:
- 将分发的状态置为 true ,表示正在分发 action。
- 设置当前 state,其中
currentReducer
就是一开始传给createStore
的参数todoApp
,也就是reducers.js
文件里面导出的方法todoApp
,这时传入的ActionTypes.INIT
在程序中没有匹配,返回了默认的initialState
。 - 检查是否有监听程序,如果有,就执行相关监听程序。
初始化的过程就这样结束了。这个函数里面就是 redux 的主要实现逻辑,简单来说就是将我们自己编写的 action
和 reducer
,通过 dispatch
方法一一对应起来,每次想要改变 state
,必须要通过 dispatch
方法。例如 index.js
文件里面后面执行的语句:
store.dispatch(addTodo('Learn about redux'))
addTodo
是 action 创建函数,返回的结果是 { type: 'ADD_TODO', 'Learn about redux' }
,通过 dispatch
方法,将这个结果传递了 reducers.js
里面的 todoApp
方法接收,进行相应的处理后,返回了新的 state
值 {todos:[{text: 'Learn about redux', completed: false}]}
。
更加详细的代码和注释见源文件 createStore.js。
其它 API 实现
下面针对其它 API 的实现介绍一下。
combineReducers
使用:combineReducers(reducers)
作用:是把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。
参数:reducers 是一个对象,它的值(value)对应不同的 reducer 函数。
列出主要相关代码:
// 错误处理和提示
function getUndefinedStateErrorMessage(key, action) {}
// 错误处理和提示
function getUnexpectedStateShapeWarningMessage(inputState,reducers,action,unexpectedKeyCache) {}
// 对 reducer 的检查、判断以及错误提示
function assertReducerShape(reducers) {}
export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)
let shapeAssertionError
try {
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}
let hasChanged = false
const nextState = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}
combineReducers 方法主要逻辑是:
- 获取 reducers 中属性值是方法的属性,并单独提取出属性名形成数组 finalReducerKeys。
- 对数据 reducers 数据正确性进行检查,例如是否有值等等。
- 返回一个函数,当执行 dispatch 的时候,根据 finalReducerKeys 数组循环所有的 reducer 函数并执行,得到最新的 state。
applyMiddleware
使用:applyMiddleware(...middlewares)
作用:扩展 Redux 的一种方式,可以让包装 store 的 dispatch 方法来达到想要的目的,例如使用 redux-thunk
中间件来实现异步 action。
返回值:是一个函数
首先来看下官网在 高级(Advanced Tutorial)
中讲解 异步 Action(Async Actions)
使用的示例:
import thunk from 'redux-thunk'
const store = createStore(
rootReducer,
applyMiddleware(
thunk
)
)
上面有提到 createStore
方法的第二个参数是初始的 state,这里怎么传了一个函数?原因是在 createStore
对参数做了下面的判断:
export default function createStore(reducer, preloadedState, enhancer) {
// 之前代码省略
// 如果第 2 个参数是函数 且 第 3 个参数 undefined,就把第 2 个参数值赋给第 3 个参数
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
// 如果 enhancer 有效,执行 enhancer 方法并返回结果。
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
// 之后代码省略
}
这样就相当于是执行了
applyMiddleware(thunk)(createStore)(reducer, preloadedState)
再来看 applyMiddleware
方法做了什么处理,下面是源码:
import compose from './compose'
/**
* @param {...Function}
* @returns {Function}
*/
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 正常的进行初始化
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
// 每个 middleware 需要传入的参数
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) //这里 dispatch 是一个引用,后面修改了这里也会影响
}
// 传入的参数通过 ... 转换放入 middlewares 数组中,统一执行传入的函数
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 返回按照传入参数顺序的调用函数,如果是多个,会返回一个嵌套的函数
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
以示例中 thunk
为例,看下最终的 dispatch 是什么样子。下面是 redux-thunk
源码。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
// es5 的语法形式
function createThunkMiddleware(extraArgument) {
return function (_ref) {
var dispatch = _ref.dispatch,
getState = _ref.getState;
return function (next) {
return function (action) {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
};
};
}
按照上面的逻辑,将这个函数传入其中:
const chain = [thunk(middlewareAPI)];
dispatch = thunk(middlewareAPI)(store.dispatch);
// 将其展开
dispatch = action => {
if (typeof action === 'function') {
return action(dispatch, getState, undefined);
}
return store.dispatch(action);
};
当示例中调用同步的 action 的时候 store.dispatch(selectSubreddit('reactjs'))
,实际上执行的是 redux 自己原本的 dispatch 方法。下面看下异步的 action 。
// actions.js
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
// index.js
store
.dispatch(fetchPosts('reactjs'))
.then(() => console.log(store.getState())
)
异步的 action 返回的是一个函数,当 dispatch
时,就会执行这个函数,这个函数返回的是一个 Promise
,里面有相关的请求处理,等结果返回后,还是调用 redux
最基本的 dispatch
方法触发 state 更新。
compose
从右到左组合函数。下面是源码:
export default function compose(...funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
就像注释里面的举例,将 compose(f,g,h)(...args)
转换为 f(g(h(...args)))
。
思考
看完 Redux 的主要逻辑后,一开始觉得有点惊讶,它好的地方除了文档上的描述的优点,还有什么?后来想到里面强调的一个“纯函数”的概念,去了解后才感觉到其中妙的地方。
纯函数是这样一种函数,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
纯函数带来的好处:
- 可移植性
- 可测试性
- 合理性
- 并行代码
再来看一下在学校里面函数的概念:
函数是不同数值之间的特殊关系:每一个输入值返回且只返回一个输出值。。
纯函数就是数学上的函数。这一点让我想到平时编程的时候,好像也写过类似 y=x+1
的程序,但 Redux 在实现的时候不仅运用这种**,在整体使用方式上,个人觉得也是遵循了这种**, Action、Reducer 分别相当于函数中的 x、y,Redux 相当于函数的对应关系。
回想起之前工作中碰到使用 MVC 模式,其中掺杂了不同的相互调用,数据状态混乱,排查问题耗时麻烦。现在想想如果也采用这种函数的**,也是可以在一定程度上,达到状态清晰的效果。还有就是对抽取公共方法的思考,为了兼容各种情况,对于相同的输入可能得到不同的结果,这种方式是否合理,使用纯函数的方式是否更好。
把简单的**巧妙的运用和扩展,这是个人感觉 Redux 最妙的地方。
参考资料
🗑️
看了一个纪录片《地平线系列之:阻止男性自杀》,上映的时间是 2019.9,里面有些信息:
- 在英国,四分之三的自杀者是男性
- 全世界每 40 秒就有一人自杀身亡
- 自杀会传染,尤其是身边有自杀成功案例
- 在英国每一起自杀平均会造成 150 万英镑的损失
在**这似乎是个很忌讳的问题。