当业务变得复杂,状态也就是数据的管理和传递会变得混乱,redux就是帮助我们管理状态
redux的由来
2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。如果你的项目组件的数量和层级也变得越来越多,越来越深,此时组件间的数据通信就变得异常的复杂和低效,为了解决这个问题,引入了状态管理(redux)从而很好的解决多组件之间的通信问题。
安装redux-devtools
- 为了方便调试redux(可选安装),建议去谷歌商店安装redux-devtools
redux的三大原则(理解思考)
- 单一数据源
- 整个应用的 state 被储存在一棵对象结构中,并且这个对象结构只存在于唯一一个 store 中
- State 是只读的
- redux中的state只读的不可以直接修改
- 使用纯函数(reducer)来执行修改state
- 为了修改了state数据,redux定义了一个reducer函数来完成state数据的修改,reducer会接收先前的 state 和 action,并返回新的 state
Redux是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图。
自己实现一个Redux
基本的状态管理及数据渲染:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Redux principle 01</title>
</head>
<body>
<h1>redux principle</h1>
<div class="counter">
<span class="btn" onclick="dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
<span class="count" id="count"></span>
<span class="btn" id="add" onclick="dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
</div>
<script>
// 定义一个计数器的状态
const countState = {
count: 10
}
// 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
const changeState = (action) => {
switch(action.type) {
// 处理减
case 'COUNT_DECREMENT':
countState.count -= action.number
break;
// 处理加
case 'COUNT_INCREMENT':
countState.count += action.number
break;
default:
break;
}
}
// 定义一个方法用于渲染计数器的dom
const renderCount = (state) => {
const countDom = document.querySelector('#count')
countDom.innerHTML = state.count
}
// 首次渲染数据
renderCount(countState)
// 定义一个dispatch的方法,接收到动作之后,自动调用
const dispatch = (action) => {
changeState(action)
renderCount(countState)
}
</script>
</body>
</html>
创建createStore方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Redux principle 02</title>
</head>
<body>
<h1>redux principle</h1>
<div class="counter">
<span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
<span class="count" id="count"></span>
<span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
</div>
<script>
// 定义一个方法,用于集中管理state和dispatch
const createStore = (state, changeState) => {
// getState用于获取状态
const getState = () => state
// 定义一个监听器,用于管理一些方法
const listeners = []
const subscribe = (listener) => listeners.push(listener)
// 定义一个dispatch方法,让每次有action传入的时候返回render执行之后的结果
const dispatch = (action) => {
// 调用changeState来处理数据
changeState(state, action)
// 让监听器里的所以方法运行
listeners.forEach(listener => listener())
}
return {
getState,
dispatch,
subscribe
}
}
// 定义一个计数器的状态
const countState = {
count: 10
}
// 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
const changeState = (state, action) => {
switch(action.type) {
// 处理减
case 'COUNT_DECREMENT':
state.count -= action.number
break;
// 处理加
case 'COUNT_INCREMENT':
state.count += action.number
break;
default:
break;
}
}
// 创建一个store
const store = createStore(countState, changeState)
// 定义一个方法用于渲染计数器的dom
const renderCount = () => {
const countDom = document.querySelector('#count')
countDom.innerHTML = store.getState().count
}
// 初次渲染数据
renderCount()
// 监听,只要有dispatch,这个方法就会自动运行
store.subscribe(renderCount)
</script>
</body>
</html>
让changeState方法变为一个纯函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Redux principle 03</title>
</head>
<body>
<h1>redux principle</h1>
<div class="counter">
<span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
<span class="count" id="count"></span>
<span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
</div>
<script>
// 定义一个方法,用于集中管理state和dispatch
const createStore = (state, changeState) => {
// getState用于获取状态
const getState = () => state
// 定义一个监听器,用于管理一些方法
const listeners = []
const subscribe = (listener) => listeners.push(listener)
// 定义一个dispatch方法,让每次有action传入的时候返回render执行之后的结果
const dispatch = (action) => {
// 调用changeState来处理数据
state = changeState(state, action)
// 让监听器里的所有方法运行
listeners.forEach(listener => listener())
}
return {
getState,
dispatch,
subscribe
}
}
// 定义一个计数器的状态
const countState = {
count: 10
}
// 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
const changeState = (state, action) => {
switch(action.type) {
// 处理减
case 'COUNT_DECREMENT':
return {
...state,
count: state.count - action.number
}
// 处理加
case 'COUNT_INCREMENT':
return {
...state,
count: state.count + action.number
}
default:
return state
}
}
// 创建一个store
const store = createStore(countState, changeState)
// 定义一个方法用于渲染计数器的dom
const renderCount = () => {
const countDom = document.querySelector('#count')
countDom.innerHTML = store.getState().count
}
// 初次渲染数据
renderCount()
// 监听,只要有dispatch,这个方法就会自动运行
store.subscribe(renderCount)
</script>
</body>
</html>
合并state和changeState(最终版)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Redux principle 04</title>
</head>
<body>
<h1>redux principle</h1>
<div class="counter">
<span class="btn" onclick="store.dispatch({type: 'COUNT_DECREMENT', number: 10})">-</span>
<span class="count" id="count"></span>
<span class="btn" id="add" onclick="store.dispatch({type: 'COUNT_INCREMENT', number: 10})">+</span>
</div>
<script>
// 定义一个方法,用于集中管理state和dispatch, changeState改名了,专业的叫法是reducer
const createStore = (reducer) => {
// 定义一个初始的state
let state = null
// getState用于获取状态
const getState = () => state
// 定义一个监听器,用于管理一些方法
const listeners = []
const subscribe = (listener) => listeners.push(listener)
// 定义一个dispatch方法,让每次有action传入的时候返回reducer执行之后的结果
const dispatch = (action) => {
// 调用reducer来处理数据
state = reducer(state, action)
// 让监听器里的所有方法运行
listeners.forEach(listener => listener())
}
// 初始化state
dispatch({})
return {
getState,
dispatch,
subscribe
}
}
// 定义一个计数器的状态
const countState = {
count: 10
}
// 定一个方法叫changeState,用于处理state的数据,每次都返回一个新的状态
const reducer = (state, action) => {
// 如果state是null, 就返回countState
if (!state) return countState
switch(action.type) {
// 处理减
case 'COUNT_DECREMENT':
return {
...state,
count: state.count - action.number
}
// 处理加
case 'COUNT_INCREMENT':
return {
...state,
count: state.count + action.number
}
default:
return state
}
}
// 创建一个store
const store = createStore(reducer)
// 定义一个方法用于渲染计数器的dom
const renderCount = () => {
const countDom = document.querySelector('#count')
countDom.innerHTML = store.getState().count
}
// 初次渲染数据
renderCount()
// 监听,只要有dispatch,renderCount就会自动运行
store.subscribe(renderCount)
</script>
</body>
</html>
redux的模块化拆分
-
store通过reducer创建了初始状态
-
view通过store.getState()获取到了store中保存的state挂载在了自己的状态上
-
用户产生了操作,调用了actions 的方法
-
actions的方法被调用,创建了带有标示性信息的action
-
actions将action通过调用store.dispatch方法发送到了reducer中
-
reducer接收到action并根据标识信息判断之后返回了新的state
-
store的state被reducer更改为新state的时候,store.subscribe方法里的回调函数会执行,此时就可以通知view去重新获取state
Reducer必须是一个纯函数:
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。Reducer不是只有Redux里才有,之前学的数组方法reduce
, 它的第一个参数就是一个reducer
纯函数是函数式编程的概念,必须遵守以下一些约束。
-
不得改写参数
-
不能调用系统 I/O 的API
-
不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。
最好把 State 对象设成只读。要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变(immutable)的对象。
我们可以通过在createStore中传入第二个参数来设置默认的state,但是这种形式只适合于只有一个reducer的时候。
// State 是一个对象
function reducer(state = defaultState, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}
// State 是一个数组
function reducer(state = defaultState, action) {
return [...state, newItem];
}
划分reducer:
因为一个应用中只能有一个大的state,这样的话reducer中的代码将会特别特别的多,那么就可以使用combineReducers方法将已经分开的reducer合并到一起
注意:
- 分离reducer的时候,每一个reducer维护的状态都应该不同
- 通过store.getState获取到的数据也是会按照reducers去划分的
- 划分多个reducer的时候,默认状态只能创建在reducer中,因为划分reducer的目的,就是为了让每一个reducer都去独立管理一部分状态
容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)
展示组件通常会使用无状态组件
展示组件 | 容器组件 | |
---|---|---|
作用 | 描述如何展现(骨架、样式) | 描述如何运行(数据获取、状态更新) |
直接使用 Redux | 否 | 是 |
数据来源 | props | 监听 Redux state |
数据修改 | 从 props 调用回调函数 | 向 Redux 派发 actions |
调用方式 | 手动 | 通常由 React Redux 生成 |
使用react-redux
帮助开发者订阅state数据
可以先结合context
来手动连接react和redux。
react-redux提供两个核心的api:
-
Provider: 提供store
-
connect: 用于连接容器组件和展示组件
-
Provider
根据单一store原则 ,一般只会出现在整个应用程序的最顶层。
-
connect
语法格式为
connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)
一般来说只会用到前面两个,它的作用是:
- 把
store.getState()
的状态转化为展示组件的props
- 把
actionCreators
转化为展示组件props
上的方法
- 把
-
只要上层中有Provider
组件并且提供了store
, 那么,子孙级别的任何组件,要想使用store
里的状态,都可以通过connect
方法进行连接。如果只是想连接actionCreators
,可以第一个参数传递为null
| store
- | actions
- | state
- | reducer
- | index
- …
- | methods
- index.js
index文件
// combineReducers合并多个reducer
// import { createStore, applyMiddleware, compose, combineReducers } from 'redux'
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
/* // 导入多个reducer
import admin from './reudcer/admin'
import www from './reudcer/www'
// 合并多个reducer,返回一个合并后大的reducer
const reducer = combineReducers({
// key:value
// key 访问state前缀 命名空间 key名称随意 state n state.n state.名称.n
// value 单个的reducer函数
// admin: admin,
// www: www
admin,
www
}) */
import reducer from './reudcer'
const composeEnhancers = (typeof window !== "undefined" && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
export default createStore(
reducer,
composeEnhancers(applyMiddleware(thunk))
)
state
const data = {
}
export default data
reducer
- 主文件
// combineReducers合并多个reducer
import { combineReducers } from 'redux'
// 导入多个reducer
import admin from './admin'
import web from './web'
// 合并多个reducer,返回一个合并后大的reducer
const reducer = combineReducers({
// key:value
// key 访问state前缀 命名空间 key名称随意 state n state.n state.名称.n
// value 单个的reducer函数
/* admin: admin,
www: www */
admin,
// ....
})
export default reducer
- 子文件
import method from '../method/adminMethod'
import defaultState from '../state/movie'
const admin = (state = defaultState, action) => {
try {
return method[action.type](state, action.data)
} catch (error) {
return state
}
}
export default admin
method
const method = {
setName(state, data) {
return {
...state,
name: data
}
}
}
export default method
action
export const setName = data => ({
type: 'setName',
data
})
Redux异步
通常情况下,action只是一个对象,不能包含异步操作,这导致了很多创建action的逻辑只能写在组件中,代码量较多也不便于复用,同时对该部分代码测试的时候也比较困难,组件的业务逻辑也不清晰,使用中间件了之后,可以通过actionCreator异步编写action,这样代码就会拆分到actionCreator中,可维护性大大提高,可以方便于测试、复用,同时actionCreator还集成了异步操作中不同的action派发机制,减少编码过程中的代码量,思想就是加一层结构可以在过程中做一些事情。
常见的异步库:
- Redux-thunk
- Redux-saga
- Redux-effects
- Redux-side-effects
- Redux-loop
- Redux-observable
- …
基于Promise的异步库:
- Redux-promise
- Redux-promises
- Redux-simple-promise
- Redux-promise-middleware
- …
首先只有大型的项目才需要使用到异步库,之所以要使用是为了低耦合,复用我们的异步操作,否则我们可以直接在组件内进行请求,抽离action后,thunk因为写在action内的原因违背了redux的三大原则,因此saga就显得结构更加清晰而已,并非越难就越好,只要能够达到项目要求,使用什么技术都是可以的
saga的解决方案
thunk会需要修改action的内容,让action会产生副作用,saga有点类似于vue的action,将异步的抽离出来,集中处理了所有的异步操作。
action不易维护:
-
action的形式不统一
-
就是异步操作太为分散,分散在了各个action中
-
注册插件
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './root-reducer';
import { watchFetchSaga } from './saga/fetchData.saga';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(watchFetchSaga);
...
但是 saga 使用的仍然是普通的 action
// 这个 action 将由 saga 监听,并且出发 side effect 异步加载 api 操作
export const fetchData = () => ({
type: "START_FETCH_DATA"
});
// 这个 action 将由 saga 发出
export const fetchSuccess = data => ({
type: "REMOTE_DATA_RECEIVED",
payload: data
});
接下来就是注册 saga 相关 side effect 操作。下面的文件是 fetchData.saga.js
import { takeLatest, put } from "redux-saga/effects";
function* fetchDataSaga(action){
try {
const response = yield fetch(action.url);
const data = yield response.json()
yield put(fetchSuccess(data));
} catch (error) {
console.log(error);
}
}
export default function* watchFetchSaga(){
// saga 将监听此事件,takeLatest 表示仅仅只监听最新的此事件
yield takeLatest("START_FETCH_DATA", fetchDataSaga)
}