在前面文章中写过 react-redux 在项目中的应用,现在来具体讲解一下 redux。
动机
随着前端框架的盛行,许多原来由后端完成的复杂逻辑和交互都转移到前端,前端应用变得越来越重要,单页应用日趋复杂,我们需要管理更多的 state (状态)。这些 state 可能包括服务器的响应、缓存的数据以及 UI 状态。管理不断变化的 state 非常困难,如果一个 model 的变化引起另一个 model 的变化,那么当页面的 view 变化时,就可能引起对应 model 以及另一个 model 的变化,甚至可能会引起另一个页面 view 的变化。这时,state 的变化就不受控制了。通过限制更新发生的时间和方式,redux 让 state 的变化变得可预测。
首先:什么是 redux 呢?
Redux 是 JavaScript 状态容器,提供可预测化的状态管理,可以和很多的界面库一同使用,例如:react,react-native 等等。同时Redux 是一个体小精悍的库,但它相关的内容和 API 都是精挑细选的,足以衍生出丰富的工具集和可扩展的生态系统。
Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。
安装:
npm install --save redux
redux 的三大原则
- 整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
- 为了描述 action 如何改变 state tree,需要编写 reducers。
设计思想
- Web 应用是一个状态机,视图与状态是一一对应的。
- 所有的状态,保存在一个对象里面。
基本概念和 API
Action
Action 把数据从应用传到 store。它是 store 数据的唯一来源。一般来说你可以通过 store.dispatch()
将 action 传到 store。可以理解为,Action 表示当前发生的事情。改变 State 的唯一的方法,就是使用 Action。它会运送数据到 store。
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本质上是一个 JavaScript 普通对象。action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作,是 action 的名称。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
上面代码中,Action 的名称是 ADD_TODO,携带的信息是 Build my first Redux app。
Action 创建函数
Action 创建函数 就是生成 Action 的方法。View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action 创建函数。
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
redux 中的 Action 创建函数 只是返回了一个 action。
下面是一个完整的 actions.js 文件
/*
* action 类型
*/
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
/*
* 其它的常量
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
/*
* action 创建函数
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
Reducer
Reducer 是一个纯函数,接收旧的 state 和 action,返回新的 state。也就是说,只要是同样的输入,必定得到同样的输出。
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
注意:永远不要在 reducer 里做如下操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如 Date.now() 或 Math.random()。
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
注意:
-
不要修改
state
。 使用Object.assign()
新建了一个副本。 - 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。
Reducer 合成
当一个功能的更新业务逻辑是独立的,可以把它拆分为一个单独 reducer,开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值,这就是 Reducer 合成,它是开发 Redux 应用最基础的模式。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
注意:
- todos 依旧接收 state,但它变成了一个数组!
- 每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
Redux 提供了combineReducers()
工具来做上面 todoApp 做的事情。
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
combineReducers()
所做的只是生成一个函数,这个函数来调用你所建的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。
下面来看一下完整的 reducers.js 文件
import { combineReducers } from 'redux'
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器
创建 store
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp, window.STATE_FROM_SERVER)
createStore() 的第一个参数是 reducer,第二个参数是可选的,用于设置 state 的初始状态。
工作流程
严格的单向数据流是 Redux 架构的设计核心。意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 用户发出 Action。
store.dispatch(action)
- Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State。
let nextState = todoApp(previousState, action)
- State 一旦有变化,Store 就会调用监听函数。
store.subscribe(listener);
- listener 通过 store.getState() 得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}