由于React只是DOM的一个抽象层,不是一个完整的Web应用的解决方案。为了开发大型的Web应用,我们还需要有一个好的架构来实现代码结构、组件间的通信。Redux应运而生
一、什么是Redux
Redux是一个JavaScript状态容器,提供可预测化的状态管理。Redux除了可以和React一起使用外,还支持其他的界面库。Redux的开发理念源自于2014年Facebook所提出的flux
,并将flux
的核心思想与函数式编程结合在一起而成。Redux是一个JavaScript状态容器,提供可预测化的状态管理。Redux除了可以和React一起使用外,还支持其他的界面库。Redux的开发理念源自于2014年Facebook所提出的flux
,并将flux
的核心思想与函数式编程结合在一起而成。
1、Redux的设计动机
随着前端应用的日趋复杂,JavaScript需要管理比任何时候都来得多的state(状态),这些状态有服务器响应数据、缓存数据、本地暂存数据、UI状态等等,正是由于状态之间彼此具有关联性、且状态是不断变化的,所以要管理好这些状态成为了一件复杂的事。再加上一些前端领域的新需求,如更新调优、服务端渲染、路由跳转前的数据请求等,都在增加着数据状态处理的复杂度。
虽然React解放了手动操作DOM的过程,但是如何处理state这个问题依然遗留了出来,而这也便是Redux设计出来的原因:让state的变化变得可预测、统一管理应用中的state,并提供易用的调试功能
2、三大原则
使用Redux,需要记住三大原则:
2-1、单一数据源
整个应用的state只存储在一个object tree中,且这个object tree也只存在于唯一的store中。这样子做的好处在于:
- 对于同构应用,无需编写更多的代码的情况下能够将状态序列化并注入到客户端中
- 方便调试,如可以把应用的状态保存在本地,加快开发速度
2-2、state是只读的
唯一改变state的方法是触发action
,action
是一个用于描述已发生事件的普通对象。
这样子可以确保视图和网络请求都不能直接修改state,能做的就是表达想修改的意图。而所有的修改都会被集中化、依次处理,从而不会出现条件竞争
的问题,而action
就是一个对象,从而可以被日志打印、序列化、存储、后期调试、回放
2-3、使用纯函数执行修改
action
只是一种描述,而修改state tree的过程则交由一个纯函数进行,而这个纯函数叫做reducer
reducer接受先前的state和action,然后返回新的state。在应用初始时可以只有一个reducer,不过随着应用的变大,可以拆分为多个reducer,然后分别独立地操作state tree的不同部分。
具体来说,Redux适用于多交互、多数据源的场景,即:
1)某个组件的状态,需要共享
2)某个状态需要在任何地方都能可以拿到
3)一个组件需要改变全局状态
4)一个组件需要改变另一个组件的状态
Redux的设计思想主要总计为两句话:
1)Web应用是一个状态机,视图与状态是一一对应的
2)所有的状态(state),保存在一个对象里(称之为store)
二、安装
可以用npm安装稳定版:
npm install --save redux
多数情况下,还可以使用React绑定库和开发者工具:
npm install --save react-redux
npm install --save-dev redux-devtools
三、基本概念
Redux中涉及的主要概念有:Store
、State
、Action
、Reducer
,解释如下:
1、store
store是Redux中保存数据的仓库,它的主要职责有:
1)维持应用的state
2)提供getState()
方法获取state
3)提供dispatch(action)
方法更新state
4)通过subscribe(listener)
注册监听器,subscribe(listener)
,方法返回的函数可以注销监听器
创建一个store,可以使用createStore()
方法,如:
import { createStore } from 'redux';
const store = createStore(reducer, defaultState);
createStore()
的第一个参数用于传入一个reducer
,而第二个参数则用于设置state的初始状态(不过第二个参数是可选的)。
2、state
state则是应用的状态,redux中,store对象包含了所有的数据,如果我们想得到某个时间点的数据,就需要对store生成快照,而这种特定时间点所采集的数据集合,就是state。获取当前时刻的state,采用getState()
方法,示例如下:
import { createStore } from 'redux';
const store = createStore(reducer);
const state = store.getState();
在Redux中,一个state对应于一个view,只要state相同,view就相同。即它们是一一对应的:
state <------> view
3、action
在Redux中,用户不能直接修改state,如果我们需要修改state,则需要通过action和reducer。action其实是一种消息,来发出通知表示state应该要发生变化了。一个action的示例如:
const ADD_TODO = 'ADD_TODO'
const action = {
type: ADD_TODO,
title: 'Learn Redux'
}
这里需要注意的是:
1)一个action中应该包含有一个type
,表示动作名称(在大型应用中,推荐采用常量并分离到一个单独的文件中,如actionTypes.js
)
2)除了type
外,action
中的其他属性都是随意的,其他的属性称之为载荷(payload
)
3)应该尽量减少在action
中传递的数据
action创建函数
我们可以定义一个函数用来生成action,避免写出太多的样板代码,如:
const ADD_TODO = 'ADD_TODO';
function addTodo(title, isCompleted) {
return {
type: ADD_TODO,
title,
isCompleted
}
}
const action = addTodo('Learn Redux', false);
这样子做的好处是更方便测试和具有更高的可移植性。
而有了action之后,我们可以使用store.dispatch()
来分发这个action,如:
store.dispatch(addTodo('Learn Redux', false));
在store
里,会直接通过store.dispatch()
来调用dispatch()
方法。不过多数情况下,我们会采用react-redux
提供的connect()
来帮助调用。此外,还有辅助函数bindActionCreators()
,可以自动把多个action创建函数绑定到dispatch()
方法上。
4、reducer
action
的职责是描述事情的发生,却没有指明事情发生后应用如何更新state。而reducer
做的事情正是接收action
,然后给出新的state
,即:
(previousState, action) => newState
而之所以称为reducer
,是因为它将被传递给Array.prototype.reduce(reducer, initialValue?)
方法,所以reducer
应该是一个纯函数,它里面不应该包含有以下操作:
- 修改传入参数
- 执行有副作用的操作,如API请求、路由跳转
- 调用非纯函数,如
Date.now()
、Math.random()
纯函数即:只要传入的参数相同,返回计算得到的下一个state就一定相同。没有特殊情况、没有副作用、没有API请求、没有变量修改,只单纯执行计算。
Redux首次执行时,其state为undefined
,所以此时可以返回应用的初始状态:
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
return state
}
// 也可以用ES6的写法简写todoApp,如
function todoApp(state = initialState, action) {
return state
}
接下来,可以拓展todoApp
,进行处理过程:
function todoApp(state = initialState, action) {
const { type } = action
switch (type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
// 这里也可以用ES7简写为:
// const newState = { visibilityFilter: action.filter }
// { ...state, ...newState }
default:
return state
}
}
注意点:
1)不能修改state,所以这里使用Object.assign()
,第一个参数传入的是一个新对象
2)在default的情况下返回旧的state
,即遇到未知的action时,一定要返回旧的state
1、处理多个action
处理多个action并不复杂,在reducer里多添加几个处理分支即可,如:
function todoApp(state = initialState, action) {
const { type } = action
switch (type) {
case SET_VISIBILITY_FILTER:
const newState = { visibilityFilter: action.filter }
return { ...state, ...newState }
case ADD_TODO:
const newState = {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
}
return { ...state, ...newState }
case TOGGLE_TODO:
const newState = {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
}
return { ...state, ...newState }
default: return state
}
}
2、拆分reducer
我们会发现以上的reducer看起来很冗长,那么有没有办法可以减少冗长呢?拆分示例如下:
function todos(state = [], action) {
const { type } = action
switch (type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return {
...state,
visibilityFilter: action.filter
}
case ADD_TODO:
case TOGGLE_TODO:
return {
...state,
todos: todos(state.todos, action)
}
default:
return state
}
}
即,现在todoApp()
接收的state是完整的整个state tree
,而todos()
接收的则是state tree
中的一部分(todos
),这种模式便是reducer
合成:把一部分状态传给子reducer,由子reducer自己决定如何更新部分数据。而这也是开发Redux应用最基础的模式。
以下是完整的做reducer合成
的例子:
1)首先,可以专门抽出一个reducer来管理visibilityFilter
,如:
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
2)然后,开发一个函数来作为主reducer
,主reducer
调用多个子reducer
分别处理state中的一部分数据,然后再把这些数据合成一个大的单一对象。主reducer不需要设置初始化时完整的state,如果初始时传入undefined,那么子reducer将负责返回它们的默认值
// 子reducer,负责todos部分的状态
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 {
...todo,
completed: !todo.completed
}
}
return todo
})
default:
return state
}
}
// 子reducer,负责visibilityFilter部分的状态
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
// 主reducer
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibility, action),
todos: todos(state.todos, action)
}
}
可见,每个reducer
只负责全局state
中它负责的一部分,所以每个reducer
中state
参数都不同,对应的也是它独自管理的那部分state
数据
4、combineReducers()
可以使用Redux提供的combineReducers()
工具来做todoApp
所做的事情,如下:
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
5、store.subscribe()
store允许使用store.subscribe()
方法设置监听函数,一旦state发生了变化,就自动执行该函数。如:
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
如此一来,当state发生了变化,那么listener函数就会被自动执行。如何应用这个函数呢?我们可以使用它来实现view的自动渲染,如在react中,我们可以把组件的render()
方法或者setState()
方法放入listener中,就可以实现view的自动渲染了。
subscribe方法会返回一个函数,调用该函数,就能注销这个监听器了:
const unsubscribe = store.subscribe(/* ... */);
unsubscribe();
四、搭配React
Redux和React之间没有关系,Redux可以支持React、Angular、Ember、jQuery甚至纯javascript,不过Redux的最佳搭配仍然是React或者Deku
1、安装ReactRedux
Redux默认是不带React绑定库的,所以需要单独安装:
npm install --save react-redux
2、容器组件和展示组件
ReduxReact有个开发思想:容器组件和展示组件相分离
推荐的做法是:只在最顶层组件里使用Redux,其余内部组件仅仅是展示性的,故数据都可以通过props传入:
+-----------------+------------------------+---------------------------+
| | 容器组件 | 展示组件 |
+-----------------+------------------------+---------------------------+
| 位置 | 最顶层,路由处理 | 中间和子组件 |
+-----------------+------------------------+---------------------------+
| 能感知Redux? | 是 | 否 |
+-----------------+------------------------+---------------------------+
| 读取数据 | 从Redux获取state | 从props获取数据 |
+-----------------+------------------------+---------------------------+
| 修改数据 | 向Redux派发actions | 从props调用回调函数 |
+-----------------+------------------------+---------------------------+
在复杂应用中,可以有多个容器组件,虽然也可以嵌套使用容器组件,但应该尽可能地使用传递props的形式
3、设计组件层次结构
现在我们以设计一个todoApp为例:
我们要显示一个todo项的列表,在一个todo项被点击后,会增加一条删除线并标记completed。
还能够显示用户新增一个todo字段,并在footer里显示一个 显示全部/已完成/未完成 的切换按钮
根据这个需求,可以设计出以下的结构:
AddTodo
新增任务,有输入框和按钮onAddClick(text: string)
当按钮被点击时调用的回调函数
TodoList
用于显示todos列表todos: Array
以{ text, completed }
形式显示的todo项数组onTodoClick(index: number)
当todo项被点击时调用的回调函数
Todo
一个todo项text: string
completed: boolean
onClick()
Footer
一个允许用户改变todo过滤器的组件filter: string
当前的过滤器为:SHOW_ALL
、SHOW_COMPLETED
、SHOW_ACTIVE
onFilterChange(nextFilter: string)
而以上全都是展示组件:不关心数据来源和变化,传入什么就渲染什么。
接下来,让我们编写相应的React组件,如下:
components/AddTodo.js
import React, { findDOMNode, Component, PropTypes } from 'react'
export default class AddTodo extends Component {
render() {
return (
<div>
<input type="text" ref="input" />
<button onClick={e => this.handleClick(e)}>Add</button>
</div>
)
}
handleClick(e) {
const node = findDOMNode(this.refs.input)
const text = node.value.trim()
this.props.onAddClick(text)
node.value = ''
}
}
AddTodo.propTypes = {
onAddClick: PropTypes.func.isRequired
}
components/Todo.js
import React, { Component, PropTypes } from 'react'
export default class Todo extends Component {
render() {
return (
<li
onClick={this.props.onClick}
style={{
textDecoration: this.props.completed
? 'line-through'
: 'none',
cursor: this.props.completed
? 'default'
: 'pointer'
}}>
{this.props.text}
</li>
)
}
}
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}
components/TodoList.js
import React, { Component, PropTypes } from 'react'
import Todo from './Todo'
export default class TodoList extends Component {
render() {
return (
<ul>
{this.props.todos.map((todo, index) =>
<Todo {...todo} key={index} onClick={
() => this.props.onTodoClick(index)
} />
)}
</ul>
)
}
}
TodoList.propTypes = {
onTodoClick: PropTypes.func.isRequired,
todos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
}).isRequired).isRequired
}
components/Footer.js
import React, { Component, PropTypes } from 'react'
export default class Footer extends Component {
renderFilter(filter, name) {
if (filter === this.props.filter) {
return name
}
return (
<a href="javascript:;" onClick={e => {
e.preventDefault()
this.props.onFilterChange(filter)
}}>{name}</a>
)
}
render() {
return (
<p>
Show:
{' '}
{this.renderFilter('SHOW_ALL', 'All')}
{', '}
{this.renderFilter('SHOW_COMPLETED', 'Completed')}
{', '}
{this.renderFilter('SHOW_ACTIVE', 'Active')}
</p>
)
}
}
Footer.propTypes = {
onFilterChange: PropTypes.func.isRequired,
filter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
}
在未和redux关联的情况下,容器组件containers/App.js
可以编写如下:
import React, { Component } from 'react'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'
export default class App extends Component {
render() {
return (
<div>
<AddTodo onAddClick={text => console.log('add todo', text)} />
<TodoList
todos={[
{ text: 'Task1', completed: true },
{ text: 'Task2', completed: false }
]}
onTodoClick={todo => console.log('todo clicked', todo)}
/>
<Footer filter="SHOW_ALL" onFilterChange={filter =>
console.log('filter change', filter)
} />
</div>
)
}
}
现在,我们要把这个APP连接到Redux,从而能够dispatch actions和获取store里的状态
首先,我们需要获取从之前安装好的react-redux
提供的Provider
,然后将根组件包装在<Provider>
里,如:
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import todoApp from './reducers'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
接下来,可以使用ReactRedux提供的connect()
方法将包装好的组件连接到Redux。注意:我们应该尽量只做一个顶层的组件,或者route处理,虽然可以将任一组件connect()
到store中,但是应该避免这么做否则会导致数据流难以追踪。
任何一个被connect()
包装好的组件都可以得到一个dispatch()
方法作为组件的props,以及得到全局state中所需的任何内容。connect()
接收参数selector
,从而从组件中筛选出需要的props,改造后的containers/App.js
如下:
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import {
addTodo, completeTodo, setVisibilityFilter, VisibilityFilters
} from '../actions'
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'
import Footer from '../components/Footer'
class App extends Component {
render() {
// 通过调用 connect() 注入
const { dispatch, visibleTodos, visibilityFilter } = this.props
return (
<div>
<AddTodo onAddClick={text => dispatch(addTodo(text))} />
<TodoList
todos={this.props.visibleTodos}
onTodoClick={index => dispatch(completeTodo(index))}
/>
<Footer
filter={visibilityFilter}
onFilterChange={nextFilter =>
dispatch(setVisibilityFilter(nextFilter))
}
/>
</div>
)
}
}
App.propTypes = {
visibleTodos: PropTypes.arrayOf(PropTypes.shape({
text: PropTypes.string.isRequired,
completed: PropTypes.bool.isRequired
})),
visibilityFilter: PropTypes.oneOf([
'SHOW_ALL',
'SHOW_COMPLETED',
'SHOW_ACTIVE'
]).isRequired
}
function selectTodos(todos, filter) {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(todo => todo.completed)
case VisibilityFilter.SHOW_ACTIVE:
return todos.filter(todo => !todo.completed)
}
}
function select(state) {
return {
visibleTodos: selectTodos(state.todos, state.visibilityFilter),
visibilityFilter: state.visibilityFilter
}
}
// 包装component。注入 dispatch 和 state
export default connect(select)(App)
五、小结
1、数据流
Redux架构的设计核心是严格的单向数据流
:应用中的所有数据都遵循相同的生命周期,从而可以让应用变得更加可预测且容易理解。这也同时鼓励了数据的范式化,避免使用多个且独立的无法互相引用的重复数据。
Redux的工作流程,如下图所示
说明:
1)首先,调用store.dispatch(action)
,action即为一个描述“发生了什么”的普通对象,如:
{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs' }
可以在任何地方调用store.dispatch(action)
(如组件中、XHR回调中、定时器中)
2)然后,store自动调用Reducer,并传入两个参数:当前state和收到的action,reducer会返回新的state:即nextState = someReducer(previousState, action)
// 当前应用的state
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs',
complete: false
}
]
}
// 将要执行的action
let action = {
type: 'ADD_TODO',
text: 'Understand the flow'
}
let nextState = todoApp(previousState, action)
仍然需要注意的是:reducer是纯函数,其状态是可预测的,相同的输入必然得到相同的输出。那些带有副作用的操作,如API调用、路由跳转,需要在dispatch action之前发生
3)所有订阅store.subscribe(listener)
的监听器在状态变更时都会被调用。在listener中,它可以使用store.getState()
获得当前状态,如果使用的是react,那么就会触发重新渲染得到新的view,如:
function listener() {
let newState = store.getState();
component.setState(newState);
}