写在开头
本片内容主要为本人在阅读redux官方文档中基础和进阶部分的学习笔记。由于本人能力有限,所以文章中可能会存在不合理的地方,欢迎指正,内容也会迭代更新。
本篇为介绍redux的第一篇,主要介绍了基本的使用方法。
前言
redux是一个JavaScript状态容器,主要提供状态管理。可以运行于服务端,客户端,原生应用。除了支持React外还支持其他的UI框架,体积小,只有2kb。
redux受Elm的启发,从Flux演变而来。简单容易上手。
redux由于行为可以预测,所以可以实现,日志打印,热加载,时间旅行,同构应用,录制和重放等,不需要开发参与。
开始使用
- 确认所开发的程序是否需要使用redux。对于较为简单的应用,完全没有必要为了使用redux而使用redux,redux会增加代码的复杂度。
- 参考资料你
安装
npm install redux --save
npm install redux-thunk --save
npm install redux-logger --save
npm install react-redux --save
npm install redux-devtools --save-dev
npm i redux redux-thunk redux-logger --save
// redux-thunk用于支持异步的action,redux-logger用于打印log,主要用于在开发阶段。react-redux用于和react结合使用
问什么要使用redux
- 使用JavaScript开发的单页应用逻辑复杂。有较多的状态(state)需要JavaScript去维护。例如:服务器返回数据,本低为持久化的数据,UI状态。或者分页等等。
- 将变化和异步混合在一起会增加程序的复杂性,使得状态难以被管理。React在视图层禁止异步不鼓励直接操作DOM,来解决这个问题。不过react中的数据(state)需要自己来进行处理,redux就是为了解决这个问题
- Redux试图让state的变化变得可预测。
redux的核心
使用state来存储数据,只能触发action通过reducer来更改state。
redux的三大原则
- 单一数据源:整个应用的state被存储在一颗object tree中,并且这个object只存在与唯一的一个store中
- state是只读的:改变state的唯一方法就是触发action,action是用于米哦啊书已发生时间的普通对象。
- 使用纯函数来执行修改:为了描述action如何改变state tree,需要编写 reducers
先前技术
redux生态系统
中间件
- redux-thunk — 用最简单的方式搭建异步
- action 构造器
- redux-promise — 遵从 FSA 标准的 promise 中间
件 - redux-axios-middleware — 使用 axios HTTP 客户端获取数据的 Redux 中间件
- redux-observable — Redux 的 RxJS 中间件
- redux-rx — 给 Redux 用的 RxJS 工具,包括观察变量的中间件
- redux-logger — 记录所有 Redux action 和下一次 state 的日志
- redux-immutable-state-invariant — 开发中的状态变更提醒
- redux-unhandled-action — 开发过程中,若 Action 未使 State 发生变化则发出警告
- redux-analytics — Redux middleware 分析
- redux-gen — Redux middleware 生成器
- redux-saga — Redux 应用的另一种副作用 model
- redux-action-tree — Redux 的可组合性 Cerebral-style 信号
- apollo-client — 针对 GraphQL 服务器及基于 Redux 的 UI 框架的缓存客户端
路由
- redux-simple-router — 保持 React Router 和 Redux 同步
- redux-router — 由 React Router 绑定到 Redux 的库
基础知识
Action
- action用于将数据传入store。本质是普通的JavaScript对象,必须含有type字段。
- 应该尽量减少在action中传递数据。
const ADD_TODO = 'ADD_TODO';
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
使用函数创建Action
const ADD_TODO = 'ADD_TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
Action创建函数可以是异步非纯函数。
在store里能直接通过store.dispatch()
调用dispatch()
方法。但是在redux应用中通常会使用connect()
帮助器来调用,启动bindActionCreators()
可以自动把多个action创建的函数绑定到dispatch()
方法上。
Reducer
设计state结构
- state范式化,不要存在嵌套把state比拟成数据库。进行适当的数据扁平化。参考normalizr
reducer
- reducer接受旧的satte和action,返回新的action
(previousState, action) => newState
reducer名字的由来是因为此函数与
Array.prototype.reduce(reducer,?initiaValue)
里的回调函数类型相似。
reducer禁止的操作
- 修改传入的参数
- 执行有副作用操作,Ajax请求,路由跳转等
- 调用非纯函数:Date.now()或Math.random()
慎记:只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
};
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({},state,newState)
,或者{...state,...newState}
- 在default情况下返回旧的state。
拆分reducer
注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
等价
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
combineReducers()
生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。
es6 使用import *
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const todoApp = combineReducers(reducers)
Store
职责
- 维持应用的state
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
创建store
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
let store = createStore(todoApp, window.STATE_FROM_SERVER)
let store = createStore(todoApp, window.STATE_FROM_SERVER,applyMiddleware())
发起Actions
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
console.log(store.getState())
)
// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
// 停止监听 state 更新
unsubscribe();
数据流
严格的单向数据流是 Redux 架构的设计核心
Redux 应用中数据的生命周期遵循下面 4 个步骤。
- 调用
store.dispatch(action)
。 - Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
- Redux store 保存了根 reducer 返回的完整 state 树。
进阶
异步Action
需求:发送异步的Action
- 一种通知reducer请求开始的action
- 一种通知reducer请求成功的action
- 一种通知action请求失败的action
实现
export const REQUEST_POSTS = 'REQUEST_POSTS';
export const RECEIVE_POSTS = 'RECEIVE_POSTS';
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT';
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。
dispatch(requestPosts(subreddit))
//可以多次dispatch
// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。
// 这个案例中,我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用 catch,因为会捕获
// 在 dispatch 和渲染中出现的任何错误,
// 导致 'Unexpected batch number' 错误。
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。
dispatch(receivePosts(subreddit, json))
)
}
}
引入thunk函数
import thunkMiddleware from 'redux-thunk'
import {createLogger} from 'redux-logger'
import {createStore, applyMiddleware} from 'redux'
import {selectSubreddit, fetchPosts} from './actions'
import rootReducer from './reducers'
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允许我们 dispatch() 函数
createLogger() // 一个很便捷的 middleware,用来打印 action 日志
)
);
store.dispatch(selectSubreddit('reactjs'));
store.dispatch(fetchPosts('reactjs')).then(
() => console.log(store.getState())
);
除了redux-thunk不是处理异步action的唯一方式。
- 你可以使用 redux-promise 或者 redux-promise-middleware 来 dispatch Promise 来替代函数。
- 你可以使用 redux-observable 来 dispatch Observable。
- 你可以使用 redux-saga 中间件来创建更加复杂的异步 action。
- 你可以使用 redux-pack 中间件 dispatch 基于 Promise 的异步 Action。
- 你甚至可以写一个自定义的 middleware 来描述 API 请求,就像这个 真实场景的案例 中的做法一样
异步数据流
默认情况下使用createStore()创建的只支持同步数据流,不过可以通过applyMiddleware()来增强createStore()以使其支持异步action。
redux-thunk或redux-promise通过封装store.dispatch()来实现对于异步的支持。使用middleware可以以自定义的方式解决dispatch的任何内容,并传递action给下一个middleware。支持Promise的middleware能够拦截Promise,然后为每个Promsie异步地dispatch一对begin/end。
需要确保middleware最后一个middleware开始dispatch action,这个action必须是一个普通代码。
Middleware
Redux middleware提供的是位于action被发现之后,到达reducer之前的扩展点。
Middleware 接收了一个 next() 的 dispatch 函数,并返回一个 dispatch 函数,返回的函数会被作为下一个 middleware 的 next(),以此类推。由于 store 中类似 getState() 的方法依旧非常有用,我们将 store 作为顶层的参数,使得它可以在所有 middleware 中被使用。
/**
* 让你可以发起一个函数来替代 action。
* 这个函数接收 `dispatch` 和 `getState` 作为参数。
*
* 对于(根据 `getState()` 的情况)提前退出,或者异步控制流( `dispatch()` 一些其他东西)来说,这非常有用。
*
* `dispatch` 会返回被发起函数的返回值。
*/
const thunk = store => next => action =>
typeof action === 'function' ?
action(store.dispatch, store.getState) :
next(action)
技巧
1.使用对象展开运算符
2.缩减样板代码
Actions
action需要拥有一个不变的type帮助reducer识别他们,因为string
是可序列化的,使用Symbol()
会使纪录和重载变得困难,所以推荐使用string
。
将每个action type定义为string
常量。
- 维护命名的一致性
- 避免冲突
- 及时追踪新功能的范围和实现
- 减少由于书写造成的错误的可能性
Action Creators
// event handler 里的某处
dispatch({
type: 'ADD_TODO',
text: 'Use Redux'
});
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
// event handler 里的某处
dispatch(addTodo('Use Redux'))
使用Action creators的优势。
例如,最多只允许三个todo
function addTodoWithoutCheck(text) {
return {
type: 'ADD_TODO',
text
};
}
export function addTodo(text) {
// Redux Thunk 中间件允许这种形式
// 在下面的 “异步 Action Creators” 段落中有写
return function (dispatch, getState) {
if (getState().todos.length === 3) {
// 提前退出
return;
}
dispatch(addTodoWithoutCheck(text));
}
}
Action creator 可以解耦额外的分发 action 逻辑与实际发送这些 action 的 components的耦合。
ActionCreators生成器
export function addTodo(text) {
return {
type: 'ADD_TODO',
text
}
}
export function editTodo(id, text) {
return {
type: 'EDIT_TODO',
id,
text
}
}
export function removeTodo(id) {
return {
type: 'REMOVE_TODO',
id
}
}
function makeActionCreator(type, ...argNames) {
return function(...args) {
let action = { type }
argNames.forEach((arg, index) => {
action[argNames[index]] = args[index]
})
return action
}
}
const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
export const addTodo = makeActionCreator(ADD_TODO, 'todo')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'todo')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')
异步Action Creators
中间件 让你在每个 action 对象 dispatch 出去之前,注入一个自定义的逻辑来解释你的 action 对象
中间件让我们能写表达更清晰的、潜在的异步 action creators。允许我们 dispatch 普通对象之外的东西,并且解释它们的值。比如,中间件能 “捕捉” 到已经 dispatch 的 Promises 并把他们变为一对请求和成功/失败的 action。
redux-thunk. “Thunk” 中间件让你可以把 action creators 写成 “thunks”,也就是返回函数的函数。 这使得控制被反转了: 你会像一个参数一样取得 dispatch ,所以你也能写一个多次分发的 action creator 。
export function loadPosts(userId) {
// 用 thunk 中间件解释:
return function (dispatch, getState) {
let { posts } = getState();
if (posts[userId]) {
// 这里是数据缓存!啥也不做。
return;
}
dispatch({
type: 'LOAD_POSTS_REQUEST',
userId
});
// 异步分发原味 action
fetch(`http://myapi.com/users/${userId}/posts`).then(
response => dispatch({
type: 'LOAD_POSTS_SUCCESS',
userId,
response
}),
error => dispatch({
type: 'LOAD_POSTS_FAILURE',
userId,
error
})
);
}
}
未避免多次重复代码,使用middleware进行处理。
Reducers生成器
写一个函数将 reducers 表达为 action types 到 handlers 的映射对象。例如,如果想在 todos reducer 里这样定义:
export const todos = createReducer([], {
[ActionTypes.ADD_TODO](state, action) {
let text = action.text.trim();
return [...state, text];
}
})
可以编写下面的辅助函数来完成:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action);
} else {
return state;
}
}
}
鉴于写法多种多样,Redux 没有默认提供这样的辅助函数
写在最后
谢谢阅读,欢迎点赞,
推荐阅读原版英文/中文文档。在可以灵活使用的基础上,下载源码进行阅读学习,相信会有很多的收获,加油 💪💪💪