Blog
Blog copied to clipboard
教你如何实现一个简易的 redux.js 和 redux 中间件
首先我们要弄清楚 reduxjs 的思想、作用是什么,这样我们才能开始下一步的构思。在我看来 reduxjs 核心就是一种单一数据源的概念,数据存储在一个函数的 state 变量中,只能通过固定的方法去修改和获取 dispatch()、getState()。
在 SPA 应用中,reduxjs 被广泛使用。对数据进行统一管理、实现数据共享,通常组件和组件之间、页面和页面之间可以数据共享。在 react 开发中,我经常将共用的数据和异步请求数据存放在 state 中。通过 props 的形式存在,只要在一个组件中对数据源进行了修改,其他共享的组件都会及时得到更新和渲染UI界面。
现在我们知道了关于 redux 的关键思想和用途,接下来我们一步一步实现它。我会按照下面这个列表的顺序给大家详细说明:
- createStore
- reduce、combineReducers
- applyMiddleware
- 中间件原理
- 改造后的 dispatch
- redux 应用 demo
createStore()
function createStore(reducer, initState) {
// 声明一个初始化用的 action
const INIT_ACTION = undefined;
// 绑定监听事件的集合
const listeners = [];
// 这就是我们一直说的那个【数据源】
// 参数 initState 可以有,也可以没有。一般情况下不需要传递
let state = initState ? initState : {};
function dispatch(action) {
// action 必须是一个纯对象,不能是其他的类型
if (Object.prototype.toString.call(action) === '[object Object]') {
throw new Error('Actions must be plain objects');
}
// 注意:这里是最终还是通过调用 reducer 方法
state = reducer(state, action);
// 遍历 listeners
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
}
}
// 获取 state 数据
function getState() {
return state;
}
// 绑定监听事件
function subscription(listener) {
listeners.push(listener);
// 取消监听,将事件从 listeners 中移除
return function() {
const idx = listeners.indexOf(listener);
if (idx >= 0) {
listeners.splice(idx, 1);
}
}
}
// 这是啥意思了,其实这是在调用 createStore() 时,就初始化了一个 state
dispatch(INIT_ACTION);
// 通过对象,将这些内部函数传递到外部。不要怀疑,这就是一个典型的闭包
return {
dispatch,
getState,
subscription,
};
}
从 createStore 方法中我们可以看出来,其实他就是 js模块。利用了局部变量和闭包的特性,将 state 隐藏起来,只能通过闭包的形式进行访问和修改。
reduce、combineReducers
首先 reduce 它是一个函数,我们可以自己定义。我们可以把我们的项目想像成如下的一个场景,修改用户的信息:
function userName(state = {}, action = {}) {
switch (action.type) {
case 'name':
return { ...state, name: action.data };
case 'age':
return { ...state, age: action.data };
case 'sex':
return { ...state, sex: action.data };
// 必须设置 default,直接返回 state
default:
return state;
}
}
如果我们的项目中只需要这一种交互场景,那么定义 userName() 就够了。这个时候 我们把 userName 传递给 createStore
const { getState } = createStore(userName);
// 返回的是一个 {}
console.log(getState());
上面的代码在执行 createStore(userName) 时,内部执行一次 dispatch(INIT_ACTION) ,从而在 dispatch 方法内部调用了 userName({}, undefined)。所以打印的结果是一个空对象。
如果交互场景比较多的时候呢,一个 reducer 肯定不够用啊,那么这个时候我们可能会定义多个类似 userName 这个的 reducer 函数,所以我们还需要定义一个工具函数 combineReducers,将多个 reducer 函数组合成一个 reducer 函数。
function combineReducers(reducers) {
const keys = Object.keys(reducers);
const finallyKeys = [];
for (let i = 0; i < keys.length; i++) {
if (typeof reducers[keys[i]] !== 'function') throw Error('reducer must be a function');
finallyKeys.push(keys[i]);
}
// 看,最后返回的还是一个 function
return function(state = {}, action) {
let hasChange = false;
const newState = {};
// 遍历所有的 reducer 函数
finallyKeys.forEach(key => {
// 获取这个 reducer 函数对应的 state。注意它可能是一个 undefined
// 没错,在 createStore() 中执行 dispatch(INIT_ACTION),这个时候 prevState_key 可能就是一个 unudefined
const prevState_key = state[key];
const reducer = reducers[key];
// 调用该 reducer,返回一个新的 state
const nextState_key = reducer(prevState_key, action);
// 注意这里,如果 reducer 函数返回的是一个 undefined。那么这里就会报错了
// 所以我们在定义 reducer 函数时,应该有一个限制:如果没有匹配到 action 的 type 。应该默认返回 previous state。
if (typeOf nextState_key === 'undefined') {
throw Error('to ignore an action, you must explicitly return the previous state');
}
// 当 reducer 执行完成时,会在 newState 上添加一个新属性,属性值就是 nextState_key
// 其实,从这个地方我们就应该可以猜测到,最终得到的 state【数据源】,它的结果应该和我们传入的 reducers 结构是一样的
newState[key] = nextState_key;
hasChange = hasChange || nextState_key !== prevState_key;
});
return hasChange ? newState : state;
}
}
结合之前的 createStore,我们看看下面的 demo:
function menu(state = {}, action = {}) {
switch (action.type) {
case 'home':
return { ...state, home: action.data };
case 'list':
return { ...state, list: action.data };
case 'detail':
return { ...state, detail: action.data };
default:
return state;
}
}
const reducer = combineReducers({ userName, menu });
const { getState } = createStore(userName);
// 返回的是一个 { userName: {}, menu: {} }
// 这里和我们传递给 combineReducers() 中的参数的结构是一致的。
console.log(getState());
上面的 reducer 是 userName, menu 的一个组合体,所以每次调用 dispatch(action) 时,都会遍历所有的 reducers。还有一个很重要的地方就是,每个 reducer 函数在没有匹配到 action.type 时,必须把 reducer() 的参数 state 作为返回值,否则就报错。
applyMiddleware
reduxjs 还有一个非常厉害的功能,就是可以利用中间件,做很多事情。比如说,我们比较常用的 redux-thunk、redux-logger 等。
// 这里先不考虑参数为空的情况
function compose() {
const middleware = [...arguments];
// 这里利用了redux 高阶函数
// 第一次执行时,将 middleware 中的第一个和第二个元素赋值给 a、b。然后将返回的结果函数 fn 赋值给 a。
// 第二次执行时,a 就是上一次的执行结果,这个时候将 middleware 中的第三个元素赋值给 b。然后将返回的结果函数 fn 赋值给 a。
// 第三次,第四次。依次类推。。。
return middleware.reduce(function(a, b) {
return function fn () {
return a(b.apply(null, arguments));
}
});
}
function applyMiddlyWare(createStore) {
return function(reducer) {
// 接收中间件作为参数
return function(...middlewares) {
const { dispatch, getState, subscription } = createStore(reducer);
// 将 dispatch 赋值给变量 _dispatch
let _dispatch = dispatch;
const disp = (...args) => {
_dispatch(...args);
}
// 将上面定义 disp 内部函数,传递给每一个中间件函数
// 所以上面的 disp 就构成了一个闭包
const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
// 这里又对变量 _dispatch 进行了赋值。这里理解可能有点绕,后面再详细介绍
// 注意这里是一个科里化函数的调用, 参数 dispatch 是原始,没有进过改造的
_dispatch = compose(...chain)(dispatch);
return {
dispatch: _dispatch,
getState,
subscription,
}
}
}
}
到这里为止,reduxjs 就基本实现了。但是我们的探讨还没有结束,继续往下看
从上面的代码我们可以看出来,applyMiddlyWare 函数其实就是对 createStore 的一层封装,最终输出的 dispatch 是经过中间件改造过的。现在我们来看看这个 dispatch 到底是什么,它和我们传入的中间件有什么关系???
中间件原理
const chain = middlewares.map(middleware => middleware({ dispatch: disp, getState }));
_dispatch = compose(...chain)(dispatch);
上面的两行代码,先遍历执行中间件,再将变量 chain 传递给 compose 函数。所以我们应该可以猜测到,表达式 middleware({ dispatch: disp, getState }) 应该返回一个函数,不然 compose 中的 reduce 就没有办法执行了。
这里还要考虑到中间件执行的策略,所有的中间件必须串联起来,挨个往下执行。所以中间件应该还应该接收另一个中间件作为参数。所以现在我们可以大致的猜测到一个中间件应该是这样的:
function middleware({ dispatch, getState }) {
return function (nextMiddleware) {
return function () {
// 这里应该先执行一些任务,然后再去执行下一个中间件
...
nextMiddleware();
}
}
}
这个时候其实中间件的模型还不够完整,少了一些东西。少了什么了,就是 action 呀!applyMiddlyWare 函数通过中间件对 dispatch 进行改造。所以还是要接收 action 才能对 state 进行修改。所以这下我们清楚了
function middleware({ dispatch, getState }) {
return function (nextMiddleware) {
return function (action) {
// 在调用 nextMiddleware 之前可以进行一些操作
console.log(1111);
// 必须将 action 传递给下一个中间件
const result = nextMiddleware(action);
// 在调用 nextMiddleware 之后可以进行一些操作
console.log(222);
return result;
}
}
}
改造后的 dispatch 具体是个啥
现在我们清楚了中间件的模型了,可以来专门研究一下 applyMiddlyWare 函数返回的 dispatch 是啥玩意了
function compose() {
const middleware = [...arguments];
return middleware.reduce(function(a, b) {
return function fn () {
return a(b.apply(null, arguments));
}
});
}
function one(next) {
console.log('one');
return function one_(action) {
console.log('这是中间件one,你可以在这里做很多事情', action);
return next(action)
}
}
function two(next) {
console.log('two');
return function two_(action) {
console.log('这是中间件two,你可以在next调用之前做一些事情', action);
const result = next(action);
console.log('这是中间件two,也可以在next调用之后做一些事情', action);
return result;
}
}
function three(next) {
console.log('three');
return function three_(action) {
console.log('这是中间件three,你可以在这里做很多事情', action);
return next(action)
}
}
// 可以把它当作 createStore 函数返回的 dispatch 方法
function dispatch(action) {
console.log(action);
}
// 我这么写,大家应该可以理解哈。因为 compose 函数接收到的其实是 middleware({ dispatch, getState }) 返回的结果
// 所以这里的 one, two, three 可以理解为是 middleware({ dispatch, getState }) 返回的结果
// 这里只是做一个简单的 demo,用不到 dispatch, getState。
var disp = compose(one, two, three)(dispatch);
我们把 compose(one, two, three)(dispatch) 这段代码用我们自己的代码实现一下,大致就是下面这样的效果:
var fn = (function(one, two, three) {
var first = function() {
return one(two.apply(null, arguments));
};
var next = function() {
return first(three.apply(null, arguments));
};
return next
})(one, two, three);
var disp = fn(dispatch);
-
当调用
fn(dispatch)时,three.apply(null, dispatch)开始执行,返回一个three_函数。继续往下执行。 -
first(three_)开始执行,然后执行two.apply(null, three_),two执行完成,返回一个two_函数。继续往下执行。 -
one(two_)开始执行,并返回一个one_函数,这个函数最终作为fn(dispatch)执行的最终结果,并赋值给变量disp。
disp(action) 执行时,先调用 one_(action) 然后是 two_(action) 最后是 three_(action)。注意最后一个中间件接收的参数不是中间件参数了,而是原始的 dispatch 方法。所以会在最后一个中间件中执行 dispatch(action),从而调用 rducer 函数修改数据源【state】。
执行 disp({data: 1200, type: 'username'})这段代码,看下打印的结果是啥
这下我们就非常清楚了,原来经过 applyMiddlyWare 改造后输出的 dispatch 方法,在调用时,会挨个执行每一个传入 applyMiddlyWare 函数的中间件,并在最后一个中间件中调用原始的 dispatch() 方法。
最后自己实现一个 reduxjs 的应用
中间件定义
// 中间件1
function thunk ({dispatch, getState}) {
return function (next) {
return function(action) {
if (typeof action === 'function') {
action({dispatch, getState});
} else {
return next(action);
}
}
}
}
// 中间件2
function dialog ({dispatch, getState}) {
return function (next) {
return function(action) {
console.log('prevstate:', getState());
const result = next(action);
console.log('nextstate:', getState());
return result;
}
}
}
effects 方法定义
// 模拟用户http请求
function getUserName(name) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'name', data: name})
}, 0);
}
}
function getUserAge(age) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'age', data: age})
}, 0);
}
}
function getUserSex(sex) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'sex', data: sex})
}, 0);
}
}
function getHome(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'home', data: value})
}, 0);
}
}
function getList(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'list', data: value})
}, 0);
}
}
function getDetail(value) {
return ({dispatch}) => {
setTimeout(() => {
dispatch({type: 'detail', data: value})
}, 0);
}
}
初始化 state, 绑定到 DOM
// userName, menu 直接复制前面的代码
var reducer = combineReducers({ userName, menu });
var { dispatch, getState, subscription } = applyMiddlyWare(store)(reducer)(thunk, dialog);
console.log(getState(), 'initState');
const name_button = document.querySelector('.name');
const age_button = document.querySelector('.age');
const sex_button = document.querySelector('.sex');
const home_button = document.querySelector('.home');
const list_button = document.querySelector('.list');
const detail_button = document.querySelector('.detail');
const addListener = document.querySelector('.addListener');
const removeListener = document.querySelector('.removeListener');
name_button.onclick = function() {
dispatch(getUserName('shenxuxiang'))
};
age_button.onclick = function() {
dispatch(getUserAge('29'))
};
sex_button.onclick = function() {
dispatch(getUserSex('man'))
};
home_button.onclick = function() {
dispatch(getHome('home_page'))
};
list_button.onclick = function() {
dispatch(getList('list_page'))
};
detail_button.onclick = function() {
dispatch(getDetail('detail_page'))
};
let removeListen;
addListener.onclick = function() {
removeListen = subscription(function() {
console.log('我们添加了一个事件监听器', getState())
})
};
removeListener.onclick = function() {
removeListen && removeListen();
};