前言
- 本文会从变量赋值 & 修改到 Flux 思想来阐述 Redux 究竟想要做什么
- 本文会做一个 Redux 的基本使用 & 基本实现
- 本文会从函数式编程的角度来进行阐述
从字面量赋值到 Flux 流思想
假设有这样一个 TodoList
的场景:我们需要有变量 list
表示当前的列表和 todoInput
来表示当前需要添加的数据。
直接使用变量进行修改:
在通常情况下,我们会考虑使用直接字面量赋值的方式来创建和修改这些变量的值:
js
let list = [];
let todoInput = '';
// ...
list = [...list, { /* something */ }];
todoInput = '打篮球';
上述的代码当然是不好的,因为它如下的弊端:
- 数据的离散型太强(内聚性差):没有办法在
TodoList
模块中进行统一的处理 - 数据修改不明确 :每次数据的修改都需要查看对应行的代码,链路模糊不清
- 数据的修改难以做其他的操作处理 :(e.g 中间操作(日志、劫持、连接 devtool)、后续操作(更新视图、配合其他模块操作))
下面将针对上述这些问题,进行逐一修改:
降低数据的离散程度
我们需要使用一个状态变量 state
来存储 TodoList
模块需要用到的数据:
js
const state = {
list: [],
todoInput: ''
};
state.list = [...state.list, { /* something */ }];
state.todoInput = '打篮球';
让数据修改变得更加明确
在每一次修改数据的时候,我们都需要使用对应的纯函数来进行修改(而不是直接字面量赋值)。
js
const state = {
list: [],
todoInput: ''
};
function addTodo(state, newTodo)
const newList = [
...state.list,
newTodo
];
state.todoList = newList;
}
function changeTodoInput(state, todoInput) {
state.todoInput = todoInput;
}
扩展操作处理
我们需要:
- 使用函数将 TodoList 这个模块包裹起来
- 将需要外界访问的内容抛出去,不需要外界访问的内容使用
_
前置命名声明在模块中 - 使用回调函数的方式来扩展 state 的操作
js
import _ from 'lodash';
function TodoList(initialState, middleware, callback) {
const _state = _.cloneDeep(initialState);
const _middleware = typeof middleware === 'function' ? middleware : (() => {});
const _callback = typeof callback === 'function' ? callback : (() => {});
function _addTodo(state, newTodo) {
const newList = [
...state.list,
newTodo
];
_middleware(state, newTodo);
state.todoList = newList;
_callback(state, newTodo);
}
function _changeTodoInput(state, todoInput) {
_middleware(state, todoInput);
state.todoInput = todoInput;
_callback(state, todoInput);
}
return {
getState() {
return _state;
},
// 返回 _addTodo 的偏函数
addTodo(newTodo) {
_addTodo(_state, newTodo);
},
// 返回 _changeTodoInput 的偏函数
changeTodoInput(todoInput) {
_changeTodoInput(_state, todoInput);
}
};
}
这样写,几乎把变量的操作问题解决了,但是方法操作上面还有问题需要修复:
- 方法操作需要每次都调用
_middleware
和_callback
, 可读性变差。这里我们封装一个面相切面操作的高阶函数来解决可读性差的问题
js
import _ from 'lodash';
function TodoList(initialState, middleware, callback) {
const _state = _.cloneDeep(initialState);
const _middleware = typeof middleware === 'function' ? middleware : (() => {});
const _callback = typeof callback === 'function' ? callback : (() => {});
function _addTodo(state, newTodo) {
// _middleware(state, newTodo);
// const newList = [
// ...state.list,
// newTodo
// ];
// state.todoList = newList;
// _callback(state, newTodo);
useExtendCallbacks(() => {
const newList = [
...state.list,
newTodo
];
state.todoList = newList;
}, {
middleware: _middleware,
callback: _callback
}, [state, newTodo]);
}
function _changeTodoInput(state, todoInput) {
// _middleware(state, todoInput);
// state.todoInput = todoInput;
// _callback(state, todoInput);
useExtendCallacks(() => {
state.todoInput = todoInput;
}, [state, todoInput]);
}
function useExtendCallacks(main, {
middleware,
callback
}, args) {
if (arguments[2] === undefined) {
args = arguments[1];
middleware = _middleware;
callback = _callback;
}
typeof middleware === 'function' && middleware.apply(null, args);
main();
typeof callback === 'function' && callback.apply(null, args);
}
return {
getState() {
return _state;
},
// 返回 _addTodo 的偏函数
addTodo(newTodo) {
_addTodo(_state, newTodo);
},
// 返回 _changeTodoInput 的偏函数
changeTodoInput(todoInput) {
_changeTodoInput(_state, todoInput);
}
};
}
-
每声明一个修改变量的方法,都需要从
TodoList
模块里面返回出去。当方法越来越多的时候,返回的方法也会越来越多,不利于统一管理操作。解决方法:这里我们需要使用一个【抽象函数】来统一管理所有的方法,通常这个【抽象函数】我们把它命名为
dispatch
, 意思是派发给对应的方法进行操作:
js
import _ from 'lodash';
function TodoList(initialState, middleware, callback) {
const _state = _.cloneDeep(initialState);
const _middleware = typeof middleware === 'function' ? middleware : (() => {});
const _callback = typeof callback === 'function' ? callback : (() => {});
function _addTodo(state, newTodo) {
// _middleware(state, newTodo);
// const newList = [
// ...state.list,
// newTodo
// ];
// state.todoList = newList;
// _callback(state, newTodo);
useExtendCallbacks(() => {
const newList = [
...state.list,
newTodo
];
state.todoList = newList;
}, {
middleware: _middleware,
callback: _callback
}, [state, newTodo]);
}
function _changeTodoInput(state, todoInput) {
// _middleware(state, todoInput);
// state.todoInput = todoInput;
// _callback(state, todoInput);
useExtendCallacks(() => {
state.todoInput = todoInput;
}, [state, todoInput]);
}
function useExtendCallacks(main, {
middleware,
callback
}, args) {
if (arguments[2] === undefined) {
args = arguments[1];
middleware = _middleware;
callback = _callback;
}
typeof middleware === 'function' && middleware.apply(null, args);
main();
typeof callback === 'function' && callback.apply(null, args);
}
return {
getState() {
return _state;
},
dispatch(type, ...payload) {
switch (type) {
case 'addTodo':
_addTodo(_state, ...payload);
break;
case 'changeTodoInput':
_changeTodoInput(_state, ...payload);
break;
default:
break;
}
}
};
}
执行到这里时,flux 流的思想就呼之欲出了!
Flux 是什么?
概述
Flux 是一种架构思想,专门用于解决软件的结构问题。它跟 MVC 架构是一类东西,但是更加简单和清晰。
Flux 的核心就是一个简单的约定:视图层组件不允许直接修改应用状态,只能通过 dispatch
方法来触发 action
。应用的状态必须独立 出来放到 store
里面统一管理,通过侦听 action 来执行具体的状态操作。
大致结构:
以 React 为例,Flux 架构的组件结构如下:
核心概念:
这里会有几个 Flux 思想对应的一些核心概念:
-
store
: 数据仓库,存放应用状态,并且通过dispatch
方法来触发action
。 -
state
: 数据仓库中真实存储的数据,可以通过getState
方法来获取。 -
action
: 动作,触发store
状态变更的操作,可以是用户操作、异步操作等,可以是一个对象,也可以是一个异步函数。 -
dispatch
: 触发action
的方法,可以大致理解为store
的setState
,但是它不会直接修改 store 仓库里面的数据。 -
view
:store
仓库中的数据对应需要渲染的视图
Flux 数据流是一个经典的单向的数据流动设计思想,而 Redux 就是参考 Flux 数据流设计的解决方案之一。
Redux 是什么?
Redux 概述
Redux 是基于 Flux 思想设计的 JavaScript 应用程序的状态容器,它为应用提供了可预测的状态管理。
Redux 官网
参考:www.reduxjs.cn/introductio...
Redux 做了什么?
注意:
- Redux 是一个全局状态管理的应用仓库,它的使用并不仅仅局限于 React 的应用,您可以在任何的 JavaScript 程序中使用 Redux
- 不是所有的状态都应该存储在 redux 中,虽然 redux 提供了全局状态管理,但是它并不是万能的,有些状态不应该存储在 redux 中,比如:组件的配置
props
,组件自身应有的状态state
/data
- 不是所有的全局状态都应该存储在 redux 中,Redux 中大量的数据存储会使 store 的行为越来越难以预测!(以 React 项目举例,我们可以使用 context API 和 自定义 hook 来存储我们应用中的全局数据。)
Redux 核心
下面我们将从零到一,创建一个 Redux 仓库并且使用它:
创建 store
在 Redux 中,我们可以使用 createStore
来创建一个 store:
js
const store = createStore(/* ... */);
直接使用 createStore 是无法创建 Redux 仓库的。因为 createStore 是一个高阶函数,它必须要接收一个修改 state 的函数作为参数,这个函数就是 Redux 中的 reducer
。
js
const store = createStore(reducer);
注意
- Redux 认为:使用
createStore
方法创建 store 需要遵循单一 store 原则 ------ 一个应用只能有一个 store !
创建 reducer
在 Redux 中,reducer 是一个纯操作的函数,它接收当前状态和 action 作为参数,返回一个新的状态。
js
// 这个 initialState 就是 redux 仓库一开始的值,它是由用户自己去定义的
import initialState from './state';
/**
* @function reducer
* @description reducer -> 通知 redux 仓库修改状态值的唯一方法
* @param {any} state redux 仓库的状态
* @param {{ type: string; [payload: string]: any }} action 一个带有 type 属性的对象,它表示要执行的操作
* @returns {} 返回一个新的 state(最好和旧的 state 的类型保持一致)
*/
function reducer(state = initialState, action) {
switch (action.type) {
case 'xxx':
return {
...state
};
default:
return state;
}
}
export default reducer;
作者观点
- 和很多人的理解不同,笔者认为 reducer 并不是一个纯函数,因为它一开始 state 的值取决于用户定义的 initialState,而 initialState 是一个对象,它可能包含一些初始值,也可能不包含。
- 虽然 reducer 不是一个纯函数,但是它里面的操作需要保证纯度(reducer 内部不应该有副作用)
使用 store
概述:
在 Redux 中,store 是一个对象,它抛出了一些 API 方法给到我们使用:
getState()
: 获取当前 Redux 仓库中的状态dispatch()
: 接收一个 action,并通知 Redux 仓库修改状态值subscribe()
: 接收一个函数,当 Redux 仓库状态改变时,会执行该函数;subscribe
函数执行完成后返回一个unsubscribe
函数 (unsubscribe
函数执行后取消subscribe
函数注册的 listener 回调)。
基本实现:
js
/* index.js */
import store from './store';
// 获取当前 Redux 仓库中的状态
const state = store.getState();
console.log('=== Redux 仓库中的状态 ===', state);
// 修改 Redux 仓库中的状态
store.dispatch({
type: 'ADD_TODO',
payload: '待办事项'
});
// 重新获取当前 Redux 仓库中的状态
const state = store.getState();
console.log('=== Redux 仓库中的状态 ===', state);
使用 store.subscribe
修改上面的例子
js
import store from './store';
const unsubcribe = store.subscribe(() => {
const state = store.getState();
console.log('=== Redux 仓库中的状态 ===', state);
});
store.dispatch({
type: 'ADD_TODO',
payload: '待办事项'
});
unsubcribe();
Redux 的基本实现
下面,笔者将使用自己的思路来实现一下 redux 的基本功能
代码结构:
perl
my-redux
|- creators
|- createStore.js # 创建 Redux 仓库的核心方法
|- shared
|- utils.js # 一些公共的工具库
|- configs.js # redux 中的一些默认配置
|- index.js # my-redux 库的出口
实现步骤
my-redux
从外往里面设计:
- 将
createStore
方法和 utils 中需要用到的方法统一导出
js
export { default as createStore } from './creators/createStore';
export {
// compose, combineReducers
} from './shared/utils';
- 实现一下将 shared 包里面的
utils.js
和configs.js
js
/* shared/utils.js */
export function isFunction(target) {
return typeof target === 'function';
}
export function isObject(target) {
return typeof target === 'object' && target !== null;
}
export function deepClone(origin, target) {
var toString = Object.prototype.toString;
var arrType = '[object Array]';
var tar = target || (toString(origin) === arrType ? [] : {});
for (var key in origin) {
if (origin.hasOwnProperty(key)) {
if (isObject(origin[key])) {
// do somthing
tar[key] = toString(origin[key]) === arrType ? [] : {};
deepClone(origin[key], tar[key]);
} else {
tar[key] = origin[key];
}
}
}
return tar;
}
export function compose() {
var fns = [];
for (var i = 0; i < arguments.length; i++) {
fns.push(arguments[i]);
}
return function (x) {
return fns.reduceRight(function (y, f) {
return f(y);
}, x);
}
}
js
/* shared/configs.js */
export const INIT_ACTION_TYPE = '@@INIT@@';
- 声明核心方法
createStore()
js
/* creators/createStore.js */
export function createStore(reducer) {
return {};
}
- 在
createStore
中,我们需要返回三个 API 方法:getState()
、dispatch()
、subscribe()
js
/* creators/createStore.js */
export function createStore(reducer) {
return {
getState,
dispatch,
subscribe
};
function getState() {}
function dispatch() {}
function subscribe() {}
}
- 初始化
createStore
中需要用到的变量
js
/* creators/createStore.js */
import { INIT_ACTION_TYPE } from '../shared/configs';
import { isFunction, isObject } from '../shared/utils';
export function createStore(reducer) {
if (!isFunction(reducer))
throw new Error('reducer must be a function');
let currentReducer = reducer;
let currentState = currentReducer(undefined, { type: INIT_ACTION_TYPE });
const subscribeListeners = [];
return {
getState,
dispatch,
subscribe
};
function getState() {}
function dispatch() {}
function subscribe() {}
}
- 实现一下
getState
方法
js
/* creators/createStore.js */
import { INIT_ACTION_TYPE } from '../shared/configs';
import { isFunction, isObject, deepClone } from '../shared/utils';
export function createStore(reducer) {
if (!isFunction(reducer))
throw new Error('reducer must be a function');
let currentReducer = reducer;
let currentState = currentReducer(undefined, { type: INIT_ACTION_TYPE });
const subscribeListeners = [];
return {
getState,
dispatch,
subscribe
};
function getState() {
return deepClone(currentState);
}
function dispatch() {}
function subscribe() {}
}
- 实现一下
dispatch
方法 (暂时不考虑中间件的情况)
js
/* creators/createStore.js */
import { INIT_ACTION_TYPE } from '../shared/configs';
import { isFunction, isObject, deepClone } from '../shared/utils';
export function createStore(reducer) {
if (!isFunction(reducer))
throw new Error('reducer must be a function');
let currentReducer = reducer;
let currentState = currentReducer(undefined, { type: INIT_ACTION_TYPE });
const subscribeListeners = [];
return {
getState,
dispatch,
subscribe
};
function getState() {
return deepClone(currentState);
}
function dispatch(action) {
const currentAction = action;
if (!isObject(currentAction))
throw new Error('action must be an object');
if (action.type !== 'string')
throw new Error('action.type must be a string');
currentState = currentReducer(currentState, currentAction);
subscribeListeners.forEach(listener => listener());
return currentAction;
}
function subscribe() {}
}
- 实现一下
subscribe
方法
js
/* creators/createStore.js */
import { INIT_ACTION_TYPE } from '../shared/configs';
import { isFunction, isObject, deepClone } from '../shared/utils';
export function createStore(reducer) {
if (!isFunction(reducer))
throw new Error('reducer must be a function');
let currentReducer = reducer;
let currentState = currentReducer(undefined, { type: INIT_ACTION_TYPE });
const subscribeListeners = [];
return {
getState,
dispatch,
subscribe
};
function getState() {
return deepClone(currentState);
}
function dispatch(action) {
const currentAction = action;
if (!isObject(currentAction))
throw new Error('action must be an object');
if (action.type !== 'string')
throw new Error('action.type must be a string');
currentState = currentReducer(currentState, currentAction);
subscribeListeners.forEach(listener => listener());
return currentAction;
}
function subscribe(listener) {
if (!isFunction(listener)) return;
if (subscribeListeners.includes(listener)) return;
subscribeListeners.push(listener);
const unsubscribe = () => {
subscribeListeners.splice(subscribeListeners.indexOf(listener), 1);
};
return unsubscribe;
}
}
像这样,Redux 的功能就基本上实现了