redux
为什么引入redux
以react来说,state可以包含内部的状态以及业务数据,对于一个复杂的应用来说,state非常难以管理,一个model的变化可能引起另一个model的变化…依次下去,造成了难以维护的情况,别人很难一下子摸透你的state到底是怎么变得?所以需要引入一个东西来做数据管理,使数据变化变得清晰,redux做的就是这件事情。假如说react是士兵的话那么redux就是将军,来管理士兵的状态。
Flux与redux
Flux
Flux是facebook提出的一种架构思想,与react一样,强调的是单向数据流,由三大部分组成:
- store来保存数据,一个数据对应一个store,每一个store有一个监听器
- dispatcher,含有两个方法:
- register,注册事件
- dispatch,分发一个action使store变化
- view,与flux搭配不仅仅是react还可以是vue等,为当前页面数据的一个展现,与数据保持一致
flux并不是一个mvc框架,flux没有明确的contoller,但是却有着一个controller-view,类似于mvvm中的vm(view=vm(model))。对于react来说,也就是一个最外层的容器,store全部作为该容器组件的state(类似于
flux规定store,store对外不暴露任何修改数据的接口。
redux
facebook提供的flux包基本只有一个dispatcher的实现,意味着我们需要为对每一个store进行一次定义并且创建一个dispatcher实例,需要register一次,dispatch时也需要区分不同的store(听着就麻烦)。基于flux思想的redux为我们做了简化。
redux与Flux的区别
redux提倡的是单一数据源,也就是一个应用就有一个store,包含着许多的reducer,reducer根据传入的action来决定怎么改变当前状态。关于redux,大家可以直接去看文档,说的很清楚详细,下面来看一下redux的源码:
入口
index.js为redux的入口文件,暴露出来了redux所提供的API:
export { createStore, // 创建store combineReducers, // 合并reducer bindActionCreators, applyMiddleware, compose }
其中还有一个isCrushed函数,其作用就是做环境的检测,假如在非生产环境中使用了压缩的redux,则提出警告,判断方式也很简单:
isCrushed.name !== 'isCrushed' // 压缩后函数名字会变短
下面来一个一个的看redux暴露出API的实现:
compose
compose源自于函数式编程,redux将其用于用于store的增强,将传入的函数从右到左的执行,避免了层层嵌套,举个例子:
const x = 10, add = x => x + 10, sub = x => x - 20, sup = x => x * 10; // 原生方式嵌套实现 add( sup( sub( add(x) ) ) ); // 利用compose改进 const fn = compose(add, sup, sub, add); fn(x);
对比上面代码,利用compose会使代码变得清晰,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)) ) ); };
createStore
什么是store
redux强调的是单一数据源,把所有的state放入到一棵object tree中,这棵树只能唯一的存在于一个store中,也就是说redux强调整个应用只有一个store。store可以包含
解析依赖函数
该模块依赖了lodash的isPlainObject,该函数用来判断对象是否继承了自定义类,实现起来非常简单:
function isPlainObject(val) { // 非对象的情况直接返回false if (!isObjectLike(value) || baseGetTag(value) != '[object Object]') { return false } const proto = Object.getPrototypeOf(value) // 针对Object.create(null)创建出来的对象 if (proto === null) { return true } const Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor // prototype.constructor === Object return typeof Ctor == 'function' && Ctor instanceof Ctor && funcToString.call(Ctor) == objectCtorString }
主体函数
// 内部的action,用于reset export const ActionTypes = { INIT: '@@redux/INIT' }; /* * 创建store * reducer reducer函数 * preloadedState 初始状态 * enhancer 增强函数,对createStoren能力进行增强,如devtools */ export default function createStore(reducer, preloadedState, enhancer) { // 参数修正 if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState; preloadedState = undefined; } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('Expected the enhancer to be a function.'); } // 返回已经增强后的store return enhancer(createStore)(reducer, preloadedState); } if (typeof reducer !== 'function') { throw new Error('Expected the reducer to be a function.'); } // 记录当前值 let currentReducer = reducer; let currentState = preloadedState; // 监听store变化的监听器(一些回调函数) let currentListeners = []; let nextListeners = currentListeners; // 是否处于dispatch的过程中 let isDispatching = false; /**** 看下面文字解释 ****/ function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice(); } } // 给store新增一个监听器 function subscribe(listener) { if (typeof listener !== 'function') { throw new Error('Expected listener to be a function.'); } let isSubscribed = true; ensureCanMutateNextListeners(); nextListeners.push(listener); // 返回一个卸载方法 return function unsubscribe() { if (!isSubscribed) { return } isSubscribed = false; ensureCanMutateNextListeners(); const index = nextListeners.index(listener); nextListeners.splice(index, 1); }; } // 返回当前的状态 function getState() { return currentState; } function dispatch(action) { // 一些类型检测 if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } try { isDispatching = true; // 根据recuder来更新当前状态 currentState = currentReducer(currentState, action); } finally { isDispatching = false; } const listeners = currentListeners = nextListeners; // 发布事件 for (let i = 0; i < listeners.length; i++) { const listener = listeners; listener(); } return action; } // 更新当前reducer,重置store function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('Expected the nextReducer to be a function.'); } currentReducer = nextReducer; dispatch({ type: ActionTypes.INIT }); } // 初始化store dispatch({ type: ActionTypes.INIT }); // 与Rxjs这种交互,根本看不懂-- function observable() { ... } return { getState, subscribe, dispatch, replaceReducer, [$$observable]: observable }; };
需要注意的有以下几点:
- ##### 为什么需要两个数组来保存监听函数
观察源码可以发现,currentListeners与nextListeners存储的都是监听函数,这样做的目的是保证dispatch的过程不发生错误,加入使用一个队列的话,当执行过程中有监听函数被移除时,则会发生错误,如:当前监听函数队列为:[a, b, c],当执行回调函数a后将其移除,则队列发生改变[b, c],而利用for循环来执行回调,执行到i = 2时会抛出错误。
forEach比for差在:
ie8不兼容forEach (ie8 is out) forEach不能提前终止,但是在dispatch中无此问题 性能,这是forEach最大的问题,for要比forEach快的多的多(差不多一倍左右),查看v8代码也可以感觉出,forEach本质上做的还是for循环,每次loop进行一次判断和函数调用,自然速度会慢。
applyMiddleware
中间件,express与koa也就中间件,express中的中间件处理的请求到响应的这一过程,redux中的中间件处理的是从action发出到reducer接收到action这一过程,在这个过程中可以利用中间件进行写日志,处理异步操作等过程,作用就是来增强createStore,给其添加更多的功能。先来看下下面这个简化的例子:
const calc = (obj) => obj.value + 20; // 简单的模拟下stroe const obj = { calc }; // 加强fn函数 function applyMiddleware(fn, middlewares) { middlewares = middlewares.slice(); middlewares.reverse(); // 每次改变fn,一次扩展一个功能 let { calc } = obj; middlewares.forEach( middleware => calc = middleware(obj)(calc) ); return calc; } // arrow function is cool!!! const logger = obj => next => num => { console.log(`num is ${num.value}`); let result = next(num); console.log(`finish calc, result is ${result}`); return result; }; const check = obj => next => num => { console.log(`typeof num.value is ${typeof num.value}`); let result = next(num); return result; }; const fn = applyMiddleware(obj, [check, logger]); fn({ value: 50 });
在上面简单的例子中为calc做了两个功能扩展,执行起来就是一个高阶函数check->logger->calc,理解了这个再来看看redux的applyMiddleware的实现:
export default function applyMiddleware(...middlewares) { // 利用闭包保存下来了middlewares return (createStore) => (reducer, preloadadState, enhancer) => { const store = createStore(reducer, preloadadState, enhancer); let dispatch = store.dispatch; let chain = []; // middleware不会改变store,利用闭包保存 const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; // chain中的元素仍未函数 // 接受的参数为`next` => 下一个中间件 chain = middlewares.map(middleware => middleware(middlewareAPI)); // 本身的dispatch放在最后执行 // dispatch仍未函数,接受的参数为`action` // 返回的是一个高阶函数,需要注意的是中间件并不会改变原本的action // dispatch变成了一个升级版 dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch }; }; };
附一张图:
applyMiddleware代码虽少,但是却是最难理解的部分
redux-thunk
redux-thunk是一个十分剪短有用的中间件,尤其是在异步应用中,利用redux-thunk可以使action变为一个函数,而不仅仅是一个对象,并且在该函数中仍然可以触发dispatch:
// 让action可以成为函数 function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { // action为函数类型,执行action,dispatch结束 if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; }
combineReducers
combineReducers用来合并多个reducer:
assertReducerSanity来验证reducer
利用createStore文件中定义的ActionTypes来进行初始化state,也就是定义的reducer会在一开始执行一遍,而后对得到state进行检测,为undefined抛出异常,同时利用随机字符串测试,防止其处理type为@@redux/INIT而未处理default的情况。
combineReducers
将我们所写的reducer进行合并,返回一个函数,每次dispatch时,执行函数,遍历所有的reducer,计算出最终的state:
export default function combineReducers(reducers) { const reducerKeys = Object.keys(reducers) const finalReducers = {} // 有效reducer集合 // 验证reducer,必须是纯函数才有效 for (let i = 0; i < reducerKeys.length; i++) { const key = reducerKeys[i] if (NODE_ENV !== 'production') { if (typeof reducers[key] === 'undefined') { warning(`No reducer provided for key "${key}"`) } } if (typeof reducers[key] === 'function') { finalReducers[key] = reducers[key] } } const finalReducerKeys = Object.keys(finalReducers) let unexpectedKeyCache if (NODE_ENV !== 'production') { unexpectedKeyCache = {} } let sanityError try { // 检测reducer assertReducerSanity(finalReducers) } catch (e) { sanityError = e } return function combination(state = {}, action) { if (sanityError) { throw sanityError } // getUnexpectedStateShapeWarningMessage // 检测state类型是否为0继承对象 // 用于找出多余的redcuer并给出警告 if (NODE_ENV !== 'production') { const warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache) if (warningMessage) { warning(warningMessage) } } let hasChanged = false const nextState = {} // 每次发出一个action会遍历所有的reducer for (let i = 0; i < finalReducerKeys.length; i++) { const key = finalReducerKeys[i] // 保留之前的state与新生成的state做对比 const reducer = finalReducers[key] const previousStateForKey = state[key] const next StateForKey = reducer(previousStateForKey, action) // state为undefined抛出异常 if (typeof nextStateForKey === 'undefined') { const errorMessage = getUndefinedStateErrorMessage(key, action) throw new Error(errorMessage) } nextState[key] = nextStateForKey // 比较是否数据发生变化 hasChanged = hasChanged || nextStateForKey !== previousStateForKey } // 未发生变化返回之前的数据 return hasChanged ? nextState : state } }
bindActionCreators
这个基本上的作用不大,唯一运用的情况就是将其作为props传入子组件,对于子组件来说可以全然不知redux的存在。如果利用UI库的组件来操作页面状态,bindActionCreators是一个很好的选择,实现很简单:
function bindActionCreator(actionCreator, dispatch) { return (...args) => dispatch(actionCreator(...args)) }
bindActionCreators仅仅是遍历所有的actions返回一个键值对。