redux 详解
action
Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 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。
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
样板文件使用提醒
使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。参照 减少样板代码 获取更多保持代码简洁的实践经验。
除了 type 字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议。
这时,我们还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 index 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。
{
type: TOGGLE_TODO,
index: 5
}
我们应该尽量减少在 action 中传递的数据。比如上面的例子,传递 index 就比把整个任务对象传过去要好。
最后,再添加一个 action type 来表示当前的任务展示选项。
{
type: SET_VISIBILITY_FILTER,
filter: SHOW_COMPLETED
}
Action 创建函数
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
在 Redux 中的 action 创建函数只是简单的返回一个 action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
这样做将使 action 创建函数更容易被移植和测试。
在 传统的 Flux 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
不同的是,Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。
dispatch(addTodo(text))
dispatch(completeTodo(index))
或者创建一个 被绑定的 action 创建函数 来自动 dispatch:
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))
然后直接调用它们:
boundAddTodo(text);
boundCompleteTodo(index);
store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。
Action 创建函数也可以是异步非纯函数。你可以通过阅读 高级教程 中的 异步 action 章节,学习如何处理 AJAX 响应和如何把 action 创建函数组合进异步控制流。因为基础教程中包含了阅读高级教程和异步 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
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
处理多个 action
还有两个 action 需要处理。就像我们处理 SET_VISIBILITY_FILTER 一样,我们引入 ADD_TODO 和 TOGGLE_TODO 两个 actions 并且扩展我们的 reducer 去处理 ADD_TODO.
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
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
}
]
})
default:
return 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
在前面的章节中,我们学会了使用 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)
createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
数据流
调用 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.' }
可以把 action 理解成新闻的摘要。如 “玛丽喜欢 42 号文章。” 或者 “任务列表里添加了’学习 Redux 文档’”。
你可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。
Redux store 调用传入的 reducer 函数。
Store 会把两个参数传入 reducer: 当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据:
// 当前应用的 state(todos 列表和选中的过滤器)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs.',
complete: false
}
]
}
// 将要执行的 action(添加一个 todo)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// reducer 返回处理后的应用状态
let nextState = todoApp(previousState, action)
注意 reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由你决定。Redux 原生提供 combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
下面演示 combineReducers() 如何使用。假如你有两个 reducer:一个是 todo 列表,另一个是当前选择的过滤器设置:
function todos(state = [], action) {
// 省略处理逻辑...
return nextState
}
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// 省略处理逻辑...
return nextState
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
})
当你触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
然后会把两个结果集合并成一个 state 树:
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
虽然 combineReducers() 是一个很方便的辅助工具,你也可以选择不用;你可以自行实现自己的根 reducer!
Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。
现在,可以应用新的 state 来更新 UI。如果你使用了 React Redux 这类的绑定库,这时就应该调用 component.setState(newState) 来更新。
react-redux
- provide
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
<Provider store={store}>
<App />
</Provider>
- @connect
import { connect } from "react-redux";
import { numAdd, numDel } from "../../action/index"; //引入action函数,触发什么操作,就根据操作怎样改变值
@connect((state) => ({ num: state }), { numAdd, numDel })
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from '../redux/actions'
@connect(
state=>({value: state.test}),
{increment,decrement}
)
class Counter extends Component {
onIncrease() {
this.props.increment(5);
}
onDecrement() {
this.props.decrement(3);
}
render() {
const { value } = this.props;
return (
<div>
Test: 测试{value}
<button onClick={this.onIncrease.bind(this)}>增加1</button>
<button onClick={this.onDecrement.bind(this)}>减少1</button>
</div>
);
}
}
export default Counter;
- connect
connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。
connect 方法传入的第一个参数是 mapStateToProps 函数,该函数需要返回一个对象,用于建立 State 到 Props 的映射关系。】
简而言之,connect 接收一个函数,返回一个函数。
第一个函数会注入全部的 models,你需要返回一个新的对象,挑选该组件所需要的 models。
import React, {Component} from 'react'
import {connect} from "react-redux";
class App extends Component {
render() {
const {value, onIncreaseClick, onReduceClick} = this.props
return (
<div>
<button style={{width: 40, height: 40}} onClick={onIncreaseClick}>+</button>
<text style={{padding: 40}}>{value}</text>
<button style={{width: 40, height: 40}} onClick={onReduceClick}>-</button>
</div>
);
}
}
const mapStateToProps = (state) => {
return {
value: state.count
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onIncreaseClick: () => dispatch({type: 'increase'}),
onReduceClick: () => dispatch({type: 'reduce'})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
- useSelector, useDispatch
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { numAdd, numDel } from "../../action/index"; //引入action函数,触发什么操作,就根据操作怎样改变值
const Test4 = () => {
const num = useSelector((state) => state);
const dispatch = useDispatch();
return (
<div>
<p style={{ color: "red" }}>点击次数{num}</p>
<button
onClick={() => {
dispatch(numAdd());
//setCount(store.getState());
}}
>
加一
</button>
<button
onClick={() => {
dispatch(numDel());
//setCount(store.getState());
}}
>
减一
</button>
</div>
);
};
export default Test4;
redux-thunk
首先,我们还是来看一下这个库的用法。redux-thunk 是作为 redux 的 middleware 存在的,用法和普通 middleware 的用法是一样的,注册 middleware 的代码如下:
import thunkMiddleware from ‘redux-thunk’
const store = createStore(reducer, applyMiddleware(thunkMiddleware))
注册后可以这样使用:
// 用于发起登录请求,并处理请求结果
// 接受参数用户名,并返回一个函数(参数为 dispatch)
const login = (userName) => (dispatch) => {
dispatch({ type: 'loginStart' })
request.post('/api/login', { data: userName }, () => {
dispatch({ type: 'loginSuccess', payload: userName })
})
}
store.dispatch(login('Lucy'))
可以看到,redux-thunk 主要的功能就是可以让我们 dispatch 一个函数,而不只是普通的 Object。后面我们会看到,这一点改变可以给我们巨大的灵活性。
redux-promise
不同的中间件都有着自己的适用场景,react-thunk 比较适合于简单的 API 请求的场景,而 Promise 则更适合于输入输出操作,比较 fetch 函数返回的结果就是一个 Promise 对象,下面就让我们来看下最简单的 Promise 对象是怎么实现的:
import { isFSA } from 'flux-standard-action';
function isPromise(val) {
return val && typeof val.then === 'function';
}
export default function promiseMiddleware({ dispatch }) {
return next => action => {
if (!isFSA(action)) {
return isPromise(action)
? action.then(dispatch)
: next(action);
}
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
};
}
它的逻辑也很简单主要是下面两部分:
先判断是不是标准的 flux action。如果不是,那么判断是否是 promise, 是的话就执行 action.then(dispatch),否则执行 next(action)。
如果是, 就先判断 payload 是否是 promise,如果是的话 payload.then 获取数据,然后把数据作为 payload 重新 dispatch({ …action, payload: result}) ;不是的话就执行 next(action)
结合 redux-promise 我们就可以利用 es7 的 async 和 await 语法,来简化异步操作了,比如这样:
const fetchData = (url, params) => fetch(url, params)
async function getWeather(url, params) {
const result = await fetchData(url, params)
if (result.error) {
return {
type: 'GET_WEATHER_ERROR', error: result.error,
}
}
return {
type: 'GET_WEATHER_SUCCESS', payload: result,
}
}
redux-saga
redux-saga 是一个管理 redux 应用异步操作的中间件,用于代替 redux-thunk 的。它通过创建 Sagas 将所有异步操作逻辑存放在一个地方进行集中处理,以此将 react 中的同步操作与异步操作区分开来,以便于后期的管理与维护。对于 Saga,我们可简单定义如下:
Saga = Worker + Watcher
redux-saga 相当于在 Redux 原有数据流中多了一层,通过对 Action 进行监听,从而捕获到监听的 Action,然后可以派生一个新的任务对 state 进行维护(这个看项目本身的需求),通过更改的 state 驱动 View 的变更。
saga 的应用场景是复杂异步。
可以使用 takeEvery 打印 logger(logger 大法好),便于测试。
提供 takeLatest/takeEvery/throttle 方法,可以便利的实现对事件的仅关注最近实践还是关注每一次实践的时间限频。
提供 cancel/delay 方法,可以便利的取消或延迟异步请求。
提供 race(effects),[…effects] 方法来支持竞态和并行场景。
提供 channel 机制支持外部事件。
function *getCurrCity(ip) {
const data = yield call('/api/getCurrCity.json', { ip })
yield put({
type: 'GET_CITY_SUCCESS', payload: data,
})
}
function * getWeather(cityId) {
const data = yield call('/api/getWeatherInfo.json', { cityId })
yield put({
type: 'GET_WEATHER_SUCCESS', payload: data,
})
}
function loadInitData(ip) {
yield getCurrCity(ip)
yield getWeather(getCityIdWithState(state))
yield put({
type: 'GET_DATA_SUCCESS',
})
}
总的来讲 Redux Saga 适用于对事件操作有细粒度需求的场景,同时它也提供了更好的可测试性,与可维护性,比较适合对异步处理要求高的大型项目,而小而简单的项目完全可以使用 redux-thunk 就足以满足自身需求了。毕竟 react-thunk 对于一个项目本身而言,毫无侵入,使用极其简单,只需引入这个中间件就行了。