react在大型企业级项目中用的是非常多的, 但凡一聊到react, redux又必定是跑不掉的, 作为react的衍生库, redux相当可观的解决了数据的问题, 有很多同学可能只会用, 也可能还有一部分同学用都用的不太明白, 我们不如换一种角度, 来看看redux他的一个大致实现思路, 或许可以帮助你更好的去应用redux
redux说来说出无非也就是四个核心知识点:
针对每个知识点, redux都提供了对应辅助的功能, 所以在本篇博客中, 我们大致分为如下几块:
- 手写
createStore
- 手写
bindActionCreators
- 手写
combineReducers
- 手写
applyMiddleware
在此之前, 我们先来做一些准备工作
我们新建一个redux
目录, 然后创建几个文件和index.js
, 并在index.js
中对其他文件的默认导出再做一次导出
目前大致的目录结构如下
|--redux
|-- index.js 作为出口, # 做默认导出用
|-- createStore.js # 用于实现createStore的逻辑
|-- bindActionCreators.js # 用于实现redux的bindActionCreators功能
|-- combineReducers.js # 用于实现redux的combineReducers功能
|-- applyMiddleware.js # 用于实现redux的applyMiddleware功能
|-- compose.js # 用于实现redux的compose函数
|-- utils.js # 主要存放一些工具方法
// index.js
export { default as createStore } from "./createStore.js";
export { default as bindActionCreators } from "./bindActionCreators";
export { default as combinReducers } from "./combinReducers";
export { default as applyMiddleware } from "./applyMiddleware";
export { default as compose } from "./compose";
同时我们需要书写一些工具函数供我们使用
// utils.js
// 检测一个对象是否为一个plain object
export const isPlainObject = (obj) => {
if( obj === undefined || Object.getPrototypeOf(obj) !== Object.prototype ) return false;
return true;
}
// 用于生成一个随机字符串
export const getRandomStr = (length) => {
if(typeof length !== "number") length = 6;
return Math.random().toString(36).substr(2, length).split("").join(".");
}
// 在redux中, 为了时刻验证用户的reducer是否满足要求, redux都会在特定的时机去调用
// 传递进来的reducer, 只不过触发的action是一些特殊的type值, 所以我们将这些特殊type
// 值放到utilTypes对象中, 以便后续可以正常使用
export const utilTypes = {
INIT: () => ({
type: `@@redux/INIT${getRandomStr()}`
}),
UNKOWN: () => ({
type: `@@redux/UNKOWN${getRandomStr()}`
})
}
手写createStore
其实要写这些原理, 我们可以从一些现象去着手, 我们可以先来看看附着在createStore
上的功能点有哪些需要我们去实现:
-
createStore
可以接受3个参数:- reducer: 作为第一个参数, 该参数也是
createStore
的必填参数, 主要含义为传递进来的根据action来处理数据仓库的一个函数 - defaultState: 第二个参数, 可以不填, 该参数的含义是表示整个数据仓库的默认值
- enhancer: 渐进增强的中间件应用函数, 配置了该函数过后, 根据所传递的中间件将改变整个redux仓库disptach的行为
- reducer: 作为第一个参数, 该参数也是
-
createStore
会返回一个对象, 对象结构如下:- dispatch: 用于修改当前redux数据仓库数据的函数, dispatch分发了一个action以后, 会去执行reducer, reducer根据对应的action来对仓库的数据进行更改
- subcribe: 用于订阅每次仓库的值发生改变以后要执行的回调
- replaceReducer: 很简单, 用于替换掉当前的reducer函数
- getState: 用于得到当前的redux仓库状态
- Symbol("observerable"): 这个跟esnext有关, 用于生成一个符合发布订阅模式的方法(这篇博文不做探究, 因为我们几乎可以说从来不用他, 但是他的实现原理还比较复杂, 是redux为了应对将来的场景而预设的)
-
当仓库期间,
redux
会自动触发一个特殊的action
来确保你传入的reducer
是符合reducer
标准的
OK, 知道了这个
createStore
他存在的形式和他的功能, 我们就可以根据自己的逻辑思维一点一点的去实现他
// 引入两个工具函数, 在编码的时候会用到
import { utilTypes, isPlainObject } from "./utils";
// 正儿八经的createStore函数
export default (reducer, defaultState, enhancer) => {
// 首先reducer作为必填项, 如果出现错误, 是不能容忍的
if(typeof reducer !== "function") throw new TypeError("Expected the reducer to be a function");
// 我们知道, 我们有的时候不会传递defaultState, 而是直接将第二个参数书写为enhancer
// 这样redux也能够识别, 所以我们可以做一个简单的校验(当然redux做的校验更细节)
if( typeof defaultState === "function" && enhancer === undefined ) enhancer = defaultState;
// 这里为什么要把这两哥们提出来在内部用变量保存哈, 主要是因为我们后续要对
// 这些东西进行替换或者修改操作, 这样写可能更直观一点
let curReducer = reducer; // 当前的reducer
let curState = defaultState; // 当前的状态
let listeners = []; // 后续用来存放监听回调函数的数组
if( typeof enhancer === "function" ) {
// 这里要做一些中间件的操作, 要跟后续的applyMiddleware有关, 我们先不做处理
return;
}
// 能够走到这里, 代表用户没有传递enhancer, 所以我们就按照最基本的流程去做
// 其实就是给createStore赋予能够返回一个对象, 对象里有几个函数的能力
const dispatch = () => {};
const replaceReducer = () => {};
const subcribe = () => {};
const getState = () => {};
return {
dispatch,
replaceReducer,
subcribe,
getState
}
}
这个时候, 我们去自己测试一些这个createStore
函数, 我相信他一定会给你一个如同官方redux
的一个对象(只是缺失了Symbol(observerble))
, 但是这个我们是不写的)
接下来我们要做的, 就是一步一步的完善其中的各个函数, 同时处理好一些小细节
dispatch函数和getState, replaceReducer的实现
我们知道, dispatch
的功能就很简单, 他会接受一个action作为参数, 然后在内部触发reducer
并将action
传递给reducer
从而达到修改仓库中的值的目的
// createStore.js
...
const dispatch = () => {
// dispatch第一步就是要判断action的合法性:
// 1. 必须是一个平面对象: 原型指向Object.prototype
// 2. 必须含有一个type属性
if( !isPlainObject(action) || action["type"] === undefined ) {
// 代表没通过验证, 直接报错
throw new TypeError("action must be a plain object with property 'type'");
}
console.warn("curState", curState);
// 过了验证以后, 我们就要触发reducer了
curState = curReducer(curState, action);
// 我们知道, 我们修改了状态以后, 还要做一件事情, 就是将listeners中的所有监听
// 回调函数依次执行
if( listeners.length === 0 ) return;
listeners.forEach(cb => cb());
};
...
const replaceReducer = (reducer) => {
// replaceReducer也非常简单
// 只是在赋值之前我们需要对reducer进行一层校验
if(reducer(undefined, utilTypes.INIT) === undefined) return;
curReducer = reducer;
};
const getState = () => {
// 顺便将getState也写了, 因为他实在是easy了
return curState;
};
...
return {
dispatch,
replaceReducer,
subcribe,
getState
}
...
这个时候我们之前说过, redux会在一开始就验证reducer的合法性, 会自动触发一个action, 所以我们经常利用这个来做初始化, 但是我们要记住, 一旦
createStore
中使用了默认值, 则单个reducer的默认值会直接失效
// createStore.js
...
if( typeof enhancer === "function" ) {
// 这里要做一些中间件的操作, 要跟后续的applyMiddleware有关, 我们先不做处理
return;
}
// 所以这里我们需要补上一些代码, 用来测试reducer的合法性
const result = curReducer(defaultState, utilTypes.INIT); // 触发一个init type
if( result === undefined ) {
throw new TypeError("reducer cannot return undefined");
}
...
ok, 这个时候我们的dispatch函数就写完了, 经过测试也能够达到我们想要的功能
subcribe的功能实现
subcribe主要是用来传递监听函数的, 当状态改变的时候, 会运行监听的函数
...
const subcribe = (listener) => {
if(typeof listener !== "function") return;
listeners.push(listener);
// subcribe还会返回出去一个函数, 用于接触侦听
return () => {
let index = listeners.findIndex(listener);
if( index === -1 ) return;
listeners.splice(index, 1);
}
}
...
手写bindActionCreator
bindActionCreator
也比较简单, 他的主要功能如下:
-
bindActionCreator
接收2个参数- actionCreator / actionCreatorSet: action的创建函数, 也可以是action创建函数的一个对象集合
- dispatch: 对应的仓库dispatch函数
-
bindActionCreator
会直接返回一个对应的函数或者函数集合, 当我们调用该函数的时候, 会自动进行dispatch, 而不需要我们再手动操作了
想到这里, 其实这个函数蛮简单的, 无非就是做个高阶函数套一层, 你给我一个函数, 我给你一个新的函数, 新的函数里自动帮你调用了dispatch
// bindActionCreator.js
// 用于帮助我们创建一个可以自动dispatch的action creator
const getAutoDispatchActionCreator = (actionCreator, dispatch) => (...args) => dispatch(actionCreator(...args));
export default (actionCreator, dispatch) => {
console.log(actionCreator);
// 同样我们先做一个兼容性的处理
if(!(typeof actionCreator !== "function" || typeof actionCreator !== "object")) {
throw new TypeError("bindActionCreators expected an object or a function");
}
// 如果dispatch没有传, 我们直接将源对象返回出去
if(typeof dispatch !== "function") return actionCreator;
// 如果actionCreator就是一个函数, 不是对象, 我们就直接调用上面的函数
if(typeof actionCreator === "function") {
return getAutoDispatchActionCreator(actionCreator, dispatch);
}
const autoDispatchCreators = {};
// 如果actionCreator是一个对象, 那我们需要返回一个新的对象, 对象里的每一个函数, 都映射成一个新的函数
for(const prop in actionCreator) {
if(typeof actionCreator[prop] !== "function") {
throw new TypeError("bindActionCreators expected an object or a function");
}
autoDispatchCreators[prop] = getAutoDispatchActionCreator(actionCreator[prop]);
}
return autoDispatchCreators;
}
手写combineReducers
combineReducer我们要出现他的两个现象:
- 接受一个函数对象作为参数, 也可以接收单独的一个函数作为参数(虽然这没有多大的意义)
- 会返回一个函数reducer, 返回的函数可以接受action来改变状态
combineReducers也好说, 他其实就是把多个reducer聚合到一起, 我们知道一个store只能有一个reducer, 这个reducer就是一个函数, 该函数最终要返回一个新的状态, 那么我们怎么把这个多个reducer聚合到一个最终的reducer中呢?
// combineReducers.js
import { utilTypes } from "./utils";
export default (reducers) => {
// 如果传进来的reducers就是一个函数, 直接就返回出去
if( typeof reducers === "function" ) return reducers;
// 对reducers进行校验
const (key in reducers) {
let curReducer = reducers[key]; // 拿到当前reducer
// 当拿到reducer的时候, 这里要确认一下reducer的合法性
// redux是直接进行了两次redux的合法性校验
let passFlag = true;
const arr = [utilTypes.INIT, utilTypes.UNKOWN];
arr.forEach(el => {
let result = curReducer(el());
if(result === undefined) passFlag = false;
})
if( passFlag === false ) {
// 代表校验没有通过, 直接报错
throw new TypeError("reducer cannot return undefined");
}
}
// 这个时候我要做的就是返回出去一个函数, 这个函数就是传递给store的reducer
return (state = {}, action) => {
const lastState = {};
// 当我拿到一个action的时候, 我要做的事情也非常简单, 我直接将传进来的reducers每个都走一遍
for( const key in reducers ) {
// 这里传值的时候要注意了, 当我们执行reducer的时候, 一定要记得是将
// 属于他的状态传递进去, 而不是将整个状态传递
// 比如: 总的状态是{ loginInfo: {}, userInfo: {} };
// 我们reducers[key]可以拿到login的reducer, 这时候我们要传的就是
// loginInfo的reducer
lastState[key] = reducers[key](state[key], action);
}
// 这个时候lastReducers其实就是已经处理好之后的state对象了
// 这里可能有些同学看不太明白, 其实我们知道action他是一个对象,
// 如果我传给第一个reducer, 他里面的reducer没有该action的type值
// 那么他会直接返回默认值出来, 如果他有, 那么他会将对应的执行结果返回
// 所以我让每个reducer都走一次, 这样就可以做到一个聚合的效果, 有的话
// 就修改了, 没有的话会返回默认值, 这个一定要记得
return lastState
}
}
手写applyMiddleware
这个才是重头戏, applyMiddleware算是redux源码中比较复杂和绕的地方了, 在看这块的时候, 你必须确保你对redux中间件的运行机制(洋葱模型)摸清楚了, 不然你一定会绕不出来, 同时你还需要掌握一点函数组合(compose)的知识
在写applyMiddleware之前, 我们需要做一点准备工作, 之前不是留了一个compose文件, 我们先来把他搞定
compose文件中导出了一个方法, 该方法的基本功能如下:
- 接受多个函数作为参数
- 返回一个新的函数, 该函数执行的时候会将之前所有函数的功能进行组合, 并将最后的结果返回
// compose.js
const compose = (...funcs) => {
if(!funcs || !funcs instanceof Array) return (...args) => args;
return (...args) => funcs.reduce((fst, sec) => fst(sec(...arg));
}
// 这里我怕同学看不懂, 我再举个例子说明一下这个compose函数式干什么的
function add(n) {
return n + 2;
}
function mult(n) {
return n * n;
}
const func = compose(add, mult);
const result = func(3);
console.log("result", result); // 这里会输出11, 他会先把3给到mult函数执行, 然后将mult函数的执行结果给到add执行
export default compose;
ok, 有了上面的铺垫, 我们再来康康applyMiddleware都做了什么事:
- 接收不限个数的中间件处理函数(中间件处理函数会返回一个dispatch创建函数)作为参数,
- 返回一个新的函数enhancer, enhancer会接受一个createStore函数作为参数, enhancer函数执行完毕以后还会返回一个函数, 该函数就是真正用来初始化仓库的函数了
- 在初始化仓库函数的内部, 会执行createstore方法得到store, 同时通过调用中间件来修改store的dispatch方法, 将所有的中间件都注册完毕以后, 将经过处理后的dispatch返回出去
再次注意: 因为这是原理博客, 我就没有过多的去介绍关于redux中间件的规范, 以及整个redux的中间件模型这块的概念了, 这块务必是要掌握的, 否则你一定不知道我在说什么
// appliMiddleware.js
import compose from "./compose";
export default (...middlewares) => {
return function enhanced(createStore) {
return (reducer, defaultState) => {
const store = createStore(reducer, defaultState);
// 这个dispatch是我们最终要传递出去覆盖掉store的dispatch的函数
// 为什么要覆盖哈: 因为我们需要将这个dispatch函数进行反复的中间件包装增强
// 至于一开始这个函数是什么其实不重要, 你可以直接就写为空函数, 也可以等于store.dispatch
// 只是官方写了报错函数, 所以我这里也跟着写了
let dispatch = () => throw new Error("u can't use dispatch now");
// 因为我们知道每个中间件处理函数执行以后都会返回一个dispatch创建函数的数组
// 什么叫dispatch创建函数, 就是该函数执行以后还是会返回一个函数, 该函数就是完全符合dispatch的格式
// 所以我们可以拿到目前所有中间件处理函数的dispatch创建函数
const dispatchProducers = middlewares.map(el => el(store));
// 这个时候我们就要进行compose组合了, 其实我们只需要拿到最后一个dispatch创建函数的返回结果
// 最后一个dispatch创建函数创建出来的dispatch就是我们想要的经过了多重增幅的dispatch
// 当我们经过compose组合以后会返回一个最终的dispatch创建函数, 然后我们将
// store.dispatch作为参数给到这个dispatch创建函数, 他就可以帮我们生成一个dispatch
dispatch = compose(dispatchProducers)(store.dispatch);
return {
...store,
dispatch
}
}
}
}
到了这里, applyMiddleware就写完了, 但是可能很多同学还是不太明白, 我们来看看中间件的规范是什么样的
const middleware = (store) => (next) => (action) => {
// store: 就是整个仓库的store, 这个store里的dispatch就是最原始的dispatch
// 所以我们在上面使用map映射的时候要讲store传递进去, 他这样做的原因是因为
// 假设你这个中间件很霸道, 完全不想去增强别人已经增强过的dispatch, 你可以选择增强
// 最原始的dispatch(也就是store.dispatch), 这是为了给开发者更多灵活性
// next: 这个next其实就是被上一个中间件处理过后的dispatch, 他为什么叫next
// 你仔细想想, 中间件的compose组合是从最后一个开始包的, 但是执行的时候是从第一个开始执行的
// a(b(args)), 你说b的下一个是不是a, 但是执行的时候一定是a先执行的, 他是这么个意思
// 你在适当的时机可以将你的action交给下一个中间件继续进行包装, 当next没有的时候, next就是store.dispatch了, 这里很细节, 一定要好好看
next(action);
}
这个中间件模型一定要很清楚, 否则你这里绝壁看不懂
OK, 这时候我们之前还留了个尾巴, 我们需要去处理一下createStore中传递了enhancer的情况
// createStore
...
if( typeof enhancer === "function" ) {
return enhancer(createStore)(reducer, defaultState); // 第一个参数是createStore, 然后第二个参数就是reducer和defaultState
}
...
好了, 至此我们已经将redux的四个核心功能都摸透了, 其实他还有一些东西, 但是都是边边角角的, 我这里就不提了, 希望这篇博客能够对你有帮助, 也希望可以得到大手子的指教, see you~