随着前端应用的发展,JavaScript 需要管理比以前更多的状态(state),这些状态包括服务器响应,缓存数据,UI状态等。状态管理的复杂程度主要体现在数据的变化和异步,在开发中要追踪数据的传递流程,保持界面与数据的一致性,异步状态的及时更新。redux是一个状态容器,提供可预测化的状态管理。
- 为什么需要
redux
以react为例(后面的示例也是react),在react中,数据的传递是单向的,正常的使用没有问题,但是当组件变得复杂后就变得难以管理了,某一个子组件的数据来源可能是其父组件的父组件,这样数据就传递了3层,而且中间层级的组件很可能不需要用到这样的数据,假如子组件的部分交互需要更新父组件的某些数据,那么父组件的回调函数也需要一级一级的传递下去,如果层级再多点那么多余的数据传递就变得更像明显了。
上图左边便是传统的单项数据流,而右边是redux的数据传递。在redux中,有一个store进行数据的管理和消息的分发,其中的数据是全局唯一的,负责整个APP的状态数据。在这个基础下,所有的组件都可以从store中获取数据,也可以发送消息更新store中的数据,不需要组件间的数据传递。
- 核心概念
在 redux 中 store 中保存着全局的状态数据(本质上是一个 JavaScript 对象),通过发送 action(携带动作类型和具体数据的普通对象)描述将要进行的数据更新类型和具体数据,reducer 在接受到数据后更新相应的state,然后再根据store的订阅情况进行界面更新。
- 三大原则
- 单一数据源
整个应用的state被存在一个Object Tree中,并且这个Object Tree在存在与唯一的store。
- state 是只读的
唯一改变state的方法是触发action,action是一个描述事件的普通对象。
- 使用纯函数进行修改
reducer进行数据修改,并且reducer必须是一个纯函数,接受state和action,返回一个新的state。
- action
action是一个普通的JavaScript对象。
{type: 'ADD_TODO',text: 'redux',id: Data.now,}
type 字段描述事件行为,其他字段为需要更新的数据。
一般在使用action时会搭配action createor使用,action creater是一个返回action的函数。
function addTodo(text) {return {type: 'ADD_TODO',text: 'redux',id: Data.now,}}
- reducer
reducer是一个纯函数,接收state和action并返回一个新的state。如果没有可以匹配的type就返回state本身。
const todosReducer = (state = [], action) => {switch (action.type) {case 'ADD_TODO':return [...state,{text: action.text,completed: false,id: Date.now(),}];case 'TOGGLE_TODO':return state.map((todo) => {if (todo.id === action.id) {return {...todo,completed: !todo.completed,}}return todo;});default:return state;}};const {createStore} = Redux;const store = createStore(todosReducer);
一个APP中可以有多个reducer,reducer也可以调用其他reduce更新state。
const todoReducer = (state, action) => {switch (action.type) {case 'ADD_TODO':return {text: action.text,completed: false,id: Date.now(),};case 'TOGGLE_TODO':return {...state,completed: state.id === action.id ? !state.completed : state.completed,}}};const TodosReducer = (state = [], action) => {switch (action.type) {case 'ADD_TODO':return [...state,todoReducer(undefined, action),];case 'TOGGLE_TODO':return state.map((todo) => {return todoReducer(todo, action);});default:return state;}};const {createStore} = Redux;const store = createStore(todosReducer);
如上述代码,将todo的操作拆分到todoReducer中,todosReducer调用todoReducer更新todo的数据内容。
如果是使用多个reducer组合可以使用combineReducers实现。
const visibilityFilter = (state = 'all', action) => {switch (action.type) {case 'SET_VISIBILITY_FILTER':return action.filter;default:return state;}};const {createStore, combineReducers} = Redux;const store = createStore(combineReducers({todos: todosReducer,visibilityFilter: visibilityFilter}));
- store
store是将action和reucer连接在一起的对象,在redux中,发送action和监听刷新都是store的职责,
getState获取当前状态;dispatch发送action;subscribe监听状态更新,调用返回的函数可以取消监听;
// 发送actionstore.dispatch({type: 'ADD_TODO',text: input.value,});// 设置监听this.unsubscribe = store.subscribe(() => {// 监听回调})// 取消监听this.unsubscribe();// createStore的简易实现const createStore = (reducer) => {let state = undefined;let listeners = [];const getState = () => state;const dispatch = (action) => {state = reducer(state, action);listeners.forEach(listenter => listenter());};const subscribe = (listener) => {listeners.push(listener);return () => {listeners = listeners.filter(l => l !== listener)};};dispatch({});return {getState, dispatch, subscribe};};
- 中间件
在redux的状态更新过程中都是同步的,如果要处理异步消息怎么办呢?答案是中间件(middleware)。
中间件本质上就是对store进行增强,store发起action,在到达reducer之前进行拦截并做相应的处理(打印日志,网络请求等),然后再重新dispatch处理后的数据,使其到达reducrer进行状态更新。
// 简易的日志中间件const logger = (store, action) => {console.log('dispatch: ', action);store.dispatch(action);console.log('nextState: ', store.getState());};
如果有多个中间件进行使用,道理是一样的。
// 修改后的middleware实现const logger = store => next => action => {console.log('dispatching', action)let result = next(action)console.log('next state', store.getState())return result}const crashReporter = store => next => action => {try {return next(action)} catch (err) {console.error('Caught an exception!', err)Raven.captureException(err, {extra: {action,state: store.getState()}})throw err}}// middleware的应用实现function applyMiddleware(store, middlewares) {middlewares = middlewares.slice()middlewares.reverse()let dispatch = store.dispatchmiddlewares.forEach(middleware =>dispatch = middleware(store)(dispatch))return Object.assign({}, store, { dispatch })}
