Redux 入门摘要
2. 专注 Redux 核心概念和开发流程,希望可以通过 15-20 分钟的阅读,对 Redux 有比较全面的了解,可以快速上手。
Redux 入门介绍
Redux is a predictable state container for JavaScript apps.
Redux 是一个给JavaScript app使用的可预测的状态容器。
为什么需要 Redux?(动机)
JavaScript单页应用越来越复杂,代码必须管理远比以前多的状态(state)。这个状态包括服务端返回数据,缓存数据,本地创建的数据(未同步到服务器);也包括UI状态,如需要管理激活的路由,选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的状态是很难的。如果一个 model 可以更新另一个 model ,那么一个 view 也可以更新一个 model 并导致另一个 model 更新,然后相应地,可能导致另一个 view 更新 —— 你理不清你的 app 发生了什么,失去了对 state 什么时候,为什么,怎么变化的控制 。当系统变得 不透明和不确定,就很难去重现 bug 和增加 feature 了。
通过 限制何时以及怎么更新,Redux 试图让 state 的变化可以预测 。
这里可以配合阅读 You Might Not Need Redux : Redux 的引入并不一定改善开发体验,必须权衡它的限制与好处。
Redux本身很简单,我们下面首先阐述它的核心概念和三大原则。
核心概念
想象一下用普通 JavaScript 对象 来描述 app 的 state:
// 一个 todo app 的 state 可能是这样的: { todos: [{ text: 'Eat food', completed: true }, { text: 'Exercise', completed: false }], visibilityFilter: 'SHOW_COMPLETED' }
这个对象就像没有 setter 的 model,所以其它部分的代码不能随意修改它而造成难以复现的 bug 。
如果要改变 state ,我们必须 dispatch 一个 action。action 是描述发生了什么的普通 JavaScript 对象。
// 下面都是action: { type: 'ADD_TODO', text: 'Go to swimming pool' } { type: 'TOGGLE_TODO', index: 1 } { type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
强制 每个 change 都必须用 action 来描述,可以让我们清楚 app 里正在发生什么, state 是为什么改变的。最后,把 state 和 actions 联结起来,我们需要 reducer 。
reducer 就是函数,以之前的 state 和 action 为参数,返回新的 state :
// 关注 visibilityFilter 的 reducer function visibilityFilter(state = 'SHOW_ALL', action) { if (action.type === 'SET_VISIBILITY_FILTER') { return action.filter; } else { return state; } } function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action) }; }
以上就是 Redux 的核心概念,注意到我们并没有用任何 Redux 的 API,没加入任何 魔法。 Redux 里有一些工具来简化这种模式,但是主要的想法是描述如何根据这些 action 对象来更新 state。
三大原则
1. 单一数据源
整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
console.log(store.getState()) /* Prints { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] } */
2. State 是只读的
改变 state 的唯一方式是触发 (emit) action,action 是描述发生了什么的对象。
这确保了视图和网络请求等都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。
store.dispatch({ type: 'COMPLETE_TODO', index: 1 }) store.dispatch({ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_COMPLETED' })
3. 使用纯函数来执行修改
为描述 action 怎么改变 state tree,你要编写 reducers。
Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始你可能只需要一个 reducer ,但随着应用变大,你会需要拆分 reducer 。
以 todo app 为例迅速上手 Redux
1. 定义 actions
Action 就是把数据从应用(这些数据有可能是服务器响应,用户输入或其它非 view 的数据)发送到 store 的有效载荷。 它是 store 数据的唯一来源,你通过 store.dispatch(action)
来发送它到 store。
添加新 todo 任务的 action 是这样的:
const ADD_TODO = 'ADD_TODO' { type: ADD_TODO, text: 'Build my first Redux app' }
Action 本质上是 JavaScript 普通对象。Action 必须有一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块/文件来存放 action。
除了 type
字段外,action 对象的结构完全由你自己决定。但通常,我们希望减少 action 中传递的数据。
Action 创建函数 (action creator)
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
// 生成一个 ADD_TODO 类型的 action function addTodo(text) { return { type: ADD_TODO, text } }
2. Reducers
Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 state。而这正是 reducer 要做的事情。
设计 State 结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。最好可以在写代码之前想好 state tree 应该是什么形状的。
通常,这个 state tree 需要存放一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把数据与 UI 相关的 state 分开。
// todo app 的 state { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }
处理 Action
有了 state 结构后,我们可以来写 reducer 了。 reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState
保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
在高级篇里会介绍如何执行有副作用的操作。现在只需要记住 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
import { VisibilityFilters } from './actions' function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if(index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) }) default: return state } }
注意:
- 不要修改 state。 使用
Object.assign({}, ...)
新建了一个副本。 - 在
default
情况下返回旧的 state。 遇到未知的 action 时,一定要返回旧的 state。
我们看到,多个 action 下,reducer 开始变得复杂。是否可以更通俗易懂?这里的 todos
和 visibilityFilter
的更新看起来是相互独立的,我们可以尝试拆分到单独的函数里。
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 todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: case TOGGLE_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) default: return state } }
注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。
现在更进一步,把 visibilityFilter
独立出去。那么我们可以有个主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 不再需要知道完整的 initial state。初始时,如果传入 undefined
, 子 reducer 将负责返回它们(负责部分)的默认值。
// 彻底地拆分: 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) } }
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
当应用越来越复杂,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。
最后,Redux 提供了 combineReducers()
工具来做上面 todoApp 做的事情。可以用它这样重构 todoApp:
import { combineReducers } from 'redux'; const todoApp = combineReducers({ visibilityFilter, todos }) // 完全等价于 function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }
3. 创建 store
前面两小节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
再次强调一下 Redux 应用只有一个 单一 的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。
根据已有的 reducer 来创建 store 是非常容易的。在前面我们使用 combineReducers()
将多个 reducer 合并成为一个。现在我们将其导入,并传给 createStore()
。
import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
你可以把初始状态 intialState 作为第二个参数传给 createStore()
。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
发起 actions
现在我们已经创建好了 store ,可以验证一下:
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' // 打印初始状态 console.log(store.getState()) // 每次 state 更新时,打印日志 // 注意 subscribe() 返回一个函数用来注销监听器 let unsubscribe = store.subscribe(() => console.log(store.getState()) ) // 发起一系列 action store.dispatch(addTodo('Learn about actions')) store.dispatch(addTodo('Learn about reducers')) store.dispatch(addTodo('Learn about store')) store.dispatch(toggleTodo(0)) store.dispatch(toggleTodo(1)) store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) // 停止监听 state 更新 unsubscribe();
4. 数据流
严格的单向数据流 是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 调用
store.dispatch(action)
- Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
- Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener)
的监听器都将被调用;监听器里可以调用 store.getState()
获得当前 state。
搭配 React 一起使用
首先强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
尽管如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
安装 react-redux
Redux 自身并不包含对 React 的绑定库,我们需要单独安装 react-redux
。
Presentational and Container Components
绑定库是基于 容器组件和展示组件相分离 的开发思想。建议先读完这篇文章。
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
技术上讲,我们可以手动用 store.subscribe()
来编写容器组件,但这就无法使用 React Redux 做的大量性能优化了。一般使用 React Redux 的 connect()
方法来生成容器组件。(不必为了性能而手动实现 shouldComponentUpdate
方法)
设计组件层次结构
还记得前面 设计 state 根对象的结构 吗?现在就要定义与它匹配的界面的层次结构。这不是 Redux 相关的工作,React 开发思想在这方面解释的非常棒。
- 展示组件: 纯粹的UI组件,定义外观而不关心数据怎么来,怎么变。传入什么就渲染什么。
- 容器组件: 把展示组件连接到 Redux。监听 Redux store 变化并处理如何过滤出要显示的数据。
- 其它组件 有时很难分清到底该使用容器组件还是展示组件,并且组件并不复杂,这时可以混合使用。
实现组件
省略其它部分,主要讲讲容器组件一般怎么写。
import { connect } from 'react-redux'
// 3. connect 生成 容器组件
const ContainerComponent = connect(
mapStateToProps,
mapDispatchToProps
)(PresentationalComponent)
// 2. mapStateToProps 指定如何把当前 Redux store state 映射到展示组件的 props 中
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
// 1. mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
// 可以使用 Redux 的 bindActionCreators 把所有的暴露出来的 actionCreators 转成方法注入 props
export default ContainerComponent
connect
本身还是很明确的,指定我们注入哪些 data 和 function 到展示组件的 props ,给展示组件使用。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
API 探索
Redux API
1.
createStore(reducer, [preloadedState], enhancer)
创建一个 Redux store 来以存放应用中所有的 state。详情可见 Redux API,这里主要强调两点:
preloadedState
:初始时的 state。在同构中会用到,比如从一个session恢复数据。当 store 创建后,Redux 会 dispatch action(
{ type: ActionTypes.INIT })
) 到 reducer 上,得到初始的 state 来填充 store。所以你的初始 state 是preloadedState
在 reducers 处理ActionTypes.INIT
action 后的结果。 https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.jsenhancer
:如果有enhancer
,那么会首先得到增强的createStore
,然后再createStore(reducer, [preloadedState])
。https://github.com/reactjs/redux/blob/v3.6.0/src/createStore.js 可结合下面
applyMiddleware
一起看。2. middleware 与
applyMiddleware(...middlewares)
我们可以用 middleware 来扩展 Redux。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。
middleware 的函数签名是
({ getState, dispatch }) => next => action
。如下是两个 middleware:
applyMiddleware
返回一个应用了 middleware 后的 store enhancer。这个 store enhancer 的签名是createStore => createStore
,但是最简单的使用方法就是直接作为最后一个enhancer
参数传递给createStore()
函数。再来看下
applyMiddleware
:可以看到(假设
enhancerTest = applyMiddleware(A, B, C)
):dispatch
。dispatch
本质上是同步的,但我们可以通过 thunk 等延迟执行dispatch
。chain[index](dispatch) --> (action) => action
,即我们得到的dispatch
是一个层层嵌套的(action) => action
函数。next
是原本的dispatch
,剩下的都是被层层嵌套的(action) => action
函数,并且越右侧越嵌套在里面,所以当dispatch(action)
调用时,将会以下面顺序执行:A -> B -> C -> B -> A
。C
之前A/B
都只执行了next
之前的逻辑,之后各自完全执行。3.
combineReducers(reducers)
把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数。
真的很简单,从逻辑上来讲,就是:
核心就是干了上面的事,只是多了一些判断和检查。
4.
bindActionCreators(actionCreators, dispatch)
把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。
核心就是自动
dispatch
,这样我们可以在 react 组件里直接调用,Redux store 就能收到 action。React Redux API
1.
Provider
用法:
源码:
注意到,
Provider
应用了 React Context,子组件都可以去访问store
。2.
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
connect
的函数签名是([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) => (WrappedComponent) => ConnectComponent
,最后返回的onnectComponent
可以通过context
去访问store
。connect
API 比较复杂,这里主要讲下前两个参数。[mapStateToProps(state, [ownProps]): stateProps] (Function)
: 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。[mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function)
: 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,而且这个对象会与 Redux store 绑定在一起,其中所定义的方法名将作为属性名,合并到组件的 props 中。