单页面应用的开发,一个项目需要管理的状态越来越多,服务器的响应,客户端缓存数据,本地生成未持久化的。ui状态都需要保存,单单的一个变量不足以满足我们的业务。状态管理器由此而生
解决的问题
三大原则
单一数据源
整个应用的state应该储存在object tree中,并且只有一个store。
state是只读的
immutable state
改变state的方式就是派发action,一个action是描述了已经发生事件的对象,用来描述具体的动作
action由reducer接收,根据不同的动作处理不同的state
纯函数修改state
为了对应不同的action动作而决定不同的处理方式,需要编写不同的reducers
reducer只是一个纯函数,接收一个old state和action,返回state。随着应用的扩大应该合理的拆分reducer,控制他们的调用顺序.
记录每一次的动作类型,可以在这记录错误日志,这对前端线上代码很友好,不会导致线下复现不了而苦恼
工作流程
根据上图可以看出,views 发出action, 由store分配给reducers去处理,返回一个新的state,重新通知views去更新UI。
下面列出最经典的 Todo List案例
const initState = {
counter: 0
};
function counter(state = initState, action) { // reducer
switch (action.type) {
case "INCREMENT":
return {
counter: state.counter + 1
};
case "DECREMENT":
return {
counter: state.counter - 1
};
default:
return state;
}
}
// 或者传入第二个参数 initState
const store = createStore(counter, applyMiddleware(thunk));
// mian.js
<Context.Provider value={Store}>
<App />
</Context.Provider>
// context.js
export default function (Component) {
return (props) => {
const store = React.useContext(Context);
return <Component {...props} store={store} />;
};
}
//app.js
export default HocContainer(
({store}) => {
const [state, setState] = React.useState(0);
function getState() {
let { counter } = store.getState();
setState(counter);
}
React.useEffect(() => {
return store.subscribe(getState);
}, []);
const inc = () => {
store.dispatch({ type: "INCREMENT" });
};
const dnc = () => {
store.dispatch({ type: "DECREMENT" });
};
return (
<div>
<p>{state}</p>
<button onClick={inc}> 加加 </button>
<button onClick={dnc}>减减</button>
</div>
);
}
)
api解读
createStore
用来创建一个车store对象,
- 接收一个reducer。返回一个新的state tree
- preloadedState, 初始化的state。
- enhancer 一个增强器,用来强化store createor。可以通过自定义的复合函数改变store接口
@return Store
- getState 返回当前最新的state
- dispatch 接收一个action负责传递给store,最后交给reducer去处理。
- subscribe 订阅store ,每当store由变化的时候会被通知到每一个订阅者
replaceReducer 替换当前store用来计算的reducer。 一个高级的api,只有需要代码分割的,在加载一些独立的reducer的时候才可能用到它。
combineReducers
随着应用的复杂,我们可以把很多个reducer拆分成多个单独的函数,每个函数独立管理state的一部分。
该函数的作用就是和并多个reducer函数,最终可以为合并后的reducer调用store方法。就像是下面这样const mergeR = resucers => (states, action) => Object.entries(reducers).reduce((state, reduce) => { let [redName, fn] = reduce; let prevState = state[redName]; state[redName] = fn(prevState, action) return state }, states)
如果参数了es6对象的增强写法,默认是以reducer的name属性命名的。
注意点。如果当前的reducer未能匹配到action时, 必须把state原封不动的返回
- 永远不能返回undefined,如果由,该方法会爆出异常
- 如果传入state就是undefined,应该es6的默认参数语法来设置默认初始state很容易,也可以手动检查第一个车参数是否未undefined
bindActionCreators
该方法用来增强actions的,类似于这样const bindActions = (objects, dispatch) => { for (let [key, fn] of Object.entries(objects)) { objects[key] = (...args) => dispatch(fn(...args)); } return objects }
compose
需要合成的多个函数。预计每个函数都接收一个参数。它的返回值将作为一个参数提供给它左边的函数,以此类推。
例外是最右边的参数可以接受多个参数,因为它将为由此产生的函数提供签名
参数从右向左依次调用,参数是收一个函数的返回值const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)))
applyMiddleware
中间件加载组合的方式,像下面这样,可以用来加载中间件,会执行以下操作,把需要的中间件传入即可
接收用来执行的中间件,返回一个函数,会被createStore接收并把createStore传入,在此返回一个函数用来接收创建从库的参数。把当前仓库的方法合并成一个新对象,对当前函数的参数遍历执行,新的对象传入,得到内层函数
const store = createStore(counter, applyMiddleware(thunk));
具体实现
createStore
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
}
createStore(reducer, null, applyMiddleware(...))
函数开始会对参数进行校验。把enhancer处理掉。所以像第八行这样也可以
enhancer有值,会进行一下操作
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState)
}
所以说有了中间件,dispattch在enhancer内部已经改写,在applyMiddleware增强函数里
let isDispatching = false
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function subscribe(listener) {
if (isDispatching) {
throw new Error(`抛出异常` )
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(`抛出异常` )
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(`抛出异常` )
}
if (typeof action.type === 'undefined') {
throw new Error(`抛出异常` )
}
if (isDispatching) {
throw new Error(`抛出异常` )
}
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
}
首先在内部维护了一个车 isDispatch
的变量,在初始化未false。在更改 currentState
时被改成了true。后续执行”通知”操作,就是subscribe的订阅操作。所以isDispatch
可以理解为“锁”。
那为什么要上锁呢?
在执行当前reducer时会上锁,避免在执行过程中被再次调用dispatch,从而避免套娃时dispatch,导致的死循环。
因为在reducer中调用了dispatch, 而dispath返回来又会执行reducer…..整活
current - nextListener
每一次执行订阅前都会确保,二者指向不同的引用,操作的都是nextListener,而current相当于辅助了next数组。都是为了后面执行通知操作的稳定性。
因为在后面循环通知的过程中,如果直接操作next数据,当数组的长度发生了变化,通过操作就挂了,所以复制一份,指向不同的引用,在执行中,就不会和之前的操作有任何影响。(但我自己试了一下,没有发生这种情况)
而在unsubscribe卸载函数里,调用了该函数也是删除某一个订阅者,如果当前是在更新阶段,在更新过程中执行卸载操作显然是不合理的,而应该在更新之后或者更新前去执行卸载操作。所以用到之前的锁。
applyMiddleware
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// if (typeof enhancer !== 'undefined') {
//return enhancer(createStore)(reducer, preloadedState)
//}
const store = createStore(...args)
let dispatch = () => {
throw new Error(`抛出异常`)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
在第六行首先创建了一个store,参数为之前传入的reducer。
而下面的dispatch,的异常抛出,是不希望在执行中间件的时候被执行派发操作。因为禁止套娃💚
而该函数的参数就是我们的中间件。执行所有的中间件,传入对应的参数。传出API。
这里以redux-thunk为例
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
dispatch = compose(...chain)(store.dispatch)
所以在中间件内部可以拿到getState,和dispath,返回的函数中,next方法就是11行传入的dipatch。compose成一个dispatch。而执行之后得到的dispath是compose函数之后的函数,所以dispatch每次执行的时候,都会执行中间件中的函数逻辑,
if (typeof action === "function") {
return action(dispatch, getState);
}
console.log(next); // function dispatch(action) { }
console.log(action); // {type: "INCREMENT"}
return next(action);
}
通过测试可以看到,next就是dispatch函数,是compose函数执行之后注入的 store.dispatch
方法。它可能是最原始的dispatch方法,也可能是其他中间件覆盖过的方法。 而传入进去middlewareAPI在此被放入我们的函数action中。在我们用到dispatch的时候,dispatch已经被compse函数执行后被覆盖掉了。
compose
组合,从头到尾的执行一组函数。 接受初始值,前一个函数的返回值作为下一个函数的参数
const compose = (...fns) => fns.reduce((a, b) => (...args) => a(b(...args)))