Redux
redux 是一个为 JavaScript 应用设计的可预测的状态容器。
1. redux遵循三大原则
- 单一数据源:应用的 state存储在一个单一的 store 中
- 状态只读:仅可以通过发出一个 action(一个描述更改的对象)来改变状态
- 纯函数执行状态更改:需要编写纯函数 reducers 描述如何根据action 更改 state
2. redux 相关概念
action
扁平的 javascript 对象,是将数据从应用发送至store的载体,可通过store.dispatch()发送一个action。
action 中必须定义属性 type 以声明要执行的动作类型,type取值为字符串常量。
reducers
actions 描述发生了什么,reducers则指定应用是如何响应actions并更改状态的。
const todoReducer = (state=[], action)=> {
switch(action.type){
case 'add_todo':
return [...state, { text: action.text, completed: false }];
default:
return state;
}
}
实际应用中我们会按照业务逻辑根据模块定义不同的reducer,然后通过 combineReducers()组合,combineReducers()主要是生成一个函数,该函数可以根据key来筛选state的一部分数据并调用对应的reducer进行处理。
import { combineReducers } from 'redux'
const rootReducer = combineReducers({
todos: todoReducer,
users: userReducer,
})
store
store 用于组合 actions 和 reducers,主要有以下职责:
- 维护应用状态
- 允许通过
store.getState()获取状态 - 允许通过
store.dispatch(action)更新状态 - 可通过
store.subscribe(listener)注册监听器,state每次更新时都会调用listener - 可通过
store.subscribe(listner)返回的函数注销监听器
import { createStore } from 'redux';
const store = createStore(rootReducer, initialState)
// 注册监听函数
const unsubscribe = store.subscribe(()=> console.log(store.getState()))
store.dispatch({type: 'add_todo', text: 'cooking'})
// 停止监听 state 的更新
unsubscribe()
middleware
当我们需要做一些异步数据流的操作时,如请求数据或者日志打印,此时就需要用到 redux 中间件。
中间件可以拦截dispatch的一切东西,传递actions给下一个中间件,但是最后一个中间件的派发的action必须是一个扁平的object,用于 reducer 的处理。
我们通过使用applyMiddleware来进行 redux 的增强。
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // lets us dispatch() functions
loggerMiddleware // neat middleware that logs actions
)
)
3. redux 的一些原理
-
如何移除监听器?
// createStore(){} 内部定义的变量,用于存储监听器 let nextListeners = [] function subscribe(listener){ // 新增监听器 nextListeners.push(listener) // 调用返回的函数则可以移除监听器 return function unsubscribe() { const idx = nextListeners.indexOf(listener) nextListeners = nextListeners.slice(idx, 1) } } -
dispatch()调用 reducer 更新 state,然后依次执行监听器,最后返回传入的 action
function dispatch (action) { currentState = rootReducer(currentState, action) listeners.forEach(l => l()) return action } -
combineReducer()// 接口定义参考 interface ICombinedReducer { ( state: {}, action: {type: string; [key: string]: any} ) : object } // combineReducer 的接口形式 interface ICombineReducer { ( reducers: { [key: string]: function } ) : ICombinedReducer }传入的对象是所有 reducer 的集合 ,使用闭包保存该对象,返回值是一个函数,该函数作用就是针对传入的一个action把所有的 reducer 遍历一遍,汇总计算后的状态并返回,所以这里就需要在每个 reducer 中针对不需要处理的
action.type设置默认状态。// 源码抽离 function combineReducer ( reducers ){ const finalReducers = Object.keys(reducers).reduce((ret, key)=>{ ret[key] = reducers[key] return ret }, {}) return function combination(state={}, action){ // 对每个 action 调用所有的 reducer Object.keys(finalReducers).reduce((newState, key)=> { newState[key] = finalReducers[key](state[key], action) return newState }, {}) } } -
applyMiddleware(...middlewares)/** * 实际在 createStore 中使用时:enhancer(createStore)(reducer, preloadedState); * 此处的 enhancer = applyMiddleware(...middlewares) 其实就是用闭包把 middlewares 给保存起来 * 传入的 middleware 要求必须符合 `store => next => action => {}`的形式 */ export default function applyMiddleware(...middlewares) { // 此处的 args 即是指 reducer 和 initialState return createStore => (...args) => { // 创建 store , 此时 store = { dispatch, subscribe, getState, ...,} const store = createStore(...args); // 定义 dispatch ,中间件构建结束会更新dispacth的取值,构建中间件时调用就会抛错 let dispatch = () => { throw new Error('...'); }; // 用于存储 middleware chain let chain = []; // 定义传给 middleware 的 参数,对 dispatch 进行引用可以获取增强后的dispatch const middlewareAPI = { getState: store.getState, dispatch: (...args) => dispatch(...args) }; // middleware = ({getState, dispatch}) => next => action => {} // map之后: middleware = next => action => { /* 这里可以访问传入的 getState 和 dispatch */ } chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return { ...store, dispatch }; }; }这里最核心的代码即是
compose()的函数组合,先看一下compose的源码function compose(...funcs){ /* ... 省略边界处理相关代码.... */ return funcs.reduce((a,b) => (...args)=> a(b(...args))) }compose(chain)(store.dispatch)的执行分如下两步理解:- 计算
compose(chain),得到composedChain = (...args) => m1(m2(m3(m4(...args)))),作用是调用这个函数时,将 middleware chain中 的函数按照从右至左的顺序调用,最右侧的函数参数是传入参数,剩余函数的参数是前一个函数的返回值,此时每个中间件形如mw = next => action => {} - 计算
composedChain(store.dispatch)
// 1) 执行 m[len-1] // 入参为 next = store.dispatch // 返回值为函数 action => { /* 此处可以访问原始的 store.dispatch */ } // 2) 执行 m[len-2] // 入参为 next = action => { /* 这里是m[len-1]的函数体 */ } // 返回值为函数 action => { /* 此处若执行 next(action) 则代表调用 m[len-1] 处理 action */ } // .... // n) 执行 m[1] // 入参为 next = action => { /* 这里是m[2]的函数体 */ } // 返回值为函数 action => { /* m1 函数体,若调用 next(action) 则为调用 m2 处理 action */ } // 参考如下伪代码说明: dispatch = action => { /* m1 函数体 */ // do sth before call next next === action => { /* m2 函数体 */ next === action => { /* m3 函数体 */ next === action => { /* m4 函数体 */ next === store.dispatch } } } // do sth after call next }对照上述伪代码可以看出,中间件需要实现
next(action)的逻辑才可以保证链式调用,从前到后直到最后的中间件,然后再从后到前直到第一个中间件。 - 计算
Redux Thunk
Redux-Thunk 是一个 redux 中间件,用于异步处理。
Redux-Thunk 的主要作用是当传入的 action 是一个函数时,那就直接调用这个函数,并传入 dispatch 等参数,可以在这个函数内部调用 dispatch。
// 源码
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk
使用示例如下:
// store 已经配置好使用 thunk 中间件,具体如何配置可参考 redux middleware 部分
import store from './store.js'
function App(){
const [count, setCount] = useState(store.getState());
const handleAsyncDec = () => {
store.dispatch(dispatch => {
setTimeout(() => {
dispatch({ type: "INC" });
}, 1000);
});
};
useEffect(() => {
store.subscribe(() => {
setCount(store.getState());
});
}, []);
return (
<div>
<button onClick={handleAsyncDec}>异步+1</button>
<p>count: {count}</p>
</div>
);
}