一、状态机
redux用来帮助我们管理应用的状态,它本身和React并无关系,但是非常多的React应用都选择是用redux管理应用的状态。在实际的React项目中,使用redux和react-redux这两个工具。react-redux这个工具提供了一系列的简洁api,让我们更好地在React中使用redux。
redux的设计理念基于状态机的概念。当一个应用的状态较多,状态变化逻辑比较复杂时候,使用状态机来梳理逻辑会有助于开发者理解和实现。
状态机提出“状态”(state)和“操作”(action)的概念,
一个应用或者一个功能可能会处于不同的状态。
一个状态在进行操作后,可能会变到其他的状态,也可能维持当前状态不变。
按照状态机去理解和分析一个应用,会更清晰,有条理,而不会有无从下手的感觉。开发者只需要找出应用都有哪些状态和哪些操作,在进行某个操作时候,状态是如何变化的,就能实现这个应用了。
下面通过一个小例子理解什么是状态机。
这个状态机描述的是一个开关的逻辑,初始是“off”状态,经过“open”操作,状态变为“on”,而处于“on”状态时候,经过“close”操作,状态会变为“off”。
可以看到,状态机通过“状态”和“操作”两个概念,就可以将一个功能逻辑有条理地描述清楚。
二、Flux和Redux
Flux是Facebook用户建立客户端Web应用的前端架构, 它通过利用一个单向的数据流补充了React的组合视图组件,这更是一种模式而非正式框架。
Flux使用状态机的思想描述应用的状态变化,Flux架构图示如下:
【前端面试刷题网站:灵题库,收集大厂面试真题,相关知识点详细解析。】
Flux架构中,可能有多个store,每个组件可以拥有自己的store。
dispatcher是一个集中的分发器,当收到action时候将action分发到所有的store,关心这个action的store就会根据用户注册的处理方法对action处理,得到最新的状态,然后store会把最新的状态通知到view,view监听到状态更新后,用最新的状态重新渲染视图。
Flux的一个核心就是单向数据流,视图只能通过触发action改变状态,不能直接修改store。
Flux中的store用来存储状态并根据action计算新的状态。
Redux是实现了Flux思想的最著名的一个库。
Redux架构核心也是单向数据流,Redux架构中包含3大核心:actions、store和reducers。
视图触发Action,store接收到action之后调用reducer计算新的状态,然后更新状态并通知视图。
三、Redux的使用
什么时候需要Redux
如果页面很简单,或者虽然有多个页面,但是页面之间彼此独立,页面之间、组件之间不需要共享状态,那么引入Redux没有意义。
如果页面的组件之间跨层级通信或者页面的不同部分共享状态的情况很多,那么仅仅用状态提升来实现组件间通信就会非常杂乱,这时候就需要Redux来帮助管理应用的状态。
Redux的API介绍
本文的示例是写在基于create-react-app创建的项目中,如果希望自己把代码跑起来,可以先创建一个create-react-app项目,然后将代码复制到App.js中即可运行。其他复杂一些的例子,可以根据例子中说明的目录结构将代码复制到项目中即可。
使用redux管理应用的状态,redux会维护应用的状态,开发者需要将进行某种操作后,状态变化的逻辑(这个逻辑就是开发者需要实现的reducer方法)告诉redux,这样后面只需要触发操作,redux就会根据reducer来生成新的状态了。
概括地说,使用redux的步骤是:
编写reducer并传入createStore
监听store的数据,更新视图
触发action
createStore用来创建一个store,store是存储、监听和更新数据的一个js对象。
createStore接收一个函数(即reducer),reducer负责返回更新后的状态。
reducer接收当前的state和action,根据action计算新的状态,初始化时候,返回默认的状态。
创建好store后,通过store.getState()获取当前的状态;通过store.subscribe()来监听状态的改变,状态改变后,在回调中通过store.getState()来获取最新的数据;store.dispatch用来进行状态的更新。
dispatch方法接收一个action参数。action是一个对象,约定这个对象中包含一个key为"type",value为字符串的属性,这个属性表示触发的action的类型,reducer函数根据这个action来计算新的state。
当开发者调用store.dispatch(action)之后,redux会调用reducer,将当前的state和action传入到reducer中,得到更新的状态后,redux将会用这个最新的状态替换掉当前的state,然后调用store.subscribe()注册的回调。
下面通过一个例子解释redux的主要api的使用。
这里例子实现了一个开关的功能,开关的状态维护在redux中,调用createStore并传入reducer生成store。开关组件通过store.dispatch触发action,改变开关状态,通过store.subscribe监听状态改变,更新界面,通过store.getState()获取状态。
import React from 'react';
import {createStore} from 'redux';
const defaultState = {
// 开关默认关闭状态
isSwitchOn: false
};
function reducer(state = defaultState, action) {
switch(action.type) {
case 'open':
return {isSwitchOn: true};
case 'close':
return {isSwitchOn: false};
default:
return state;
}
}
const store = createStore(reducer);
class App extends React.PureComponent {
state = {
isOpen: store.getState().isSwitchOn
};
componentDidMount() {
// 监听store数据变化的回调
store.subscribe(() => {
this.setState({isOpen: store.getState().isSwitchOn});
});
}
switchButton() {
const currentIsSwitchOn = store.getState().isSwitchOn;
const action = currentIsSwitchOn ? {type: 'close'} : {type: 'open'};
// 触发action,更新state
store.dispatch(action);
}
render() {
return (
<div>
<button onClick={this.switchButton}>
{this.state.isOpen ? '关闭' : '打开'}
</button>
</div>
);
}
}
export default App;
通过代码可以简单总结redux基本用法:
实现reducer
创建store:const store = createStore(reducer)
通过store注册状态更新的回调:stroe.subscribe(callback)
通过store获取当前的状态:store.getState()
通过store触发action,从而改变状态:store.dispatch(action)
四、reducer合并
通常在一个React应用中只有一个store,负责所有的状态管理,如果项目复杂,所有的action->newState都通过一个reducer来处理,会让reducer代码臃肿混乱、不易维护。所以redux支持开发者对reducer进行拆分。
reducer拆分的步骤为:
- 首先实现多个reducer。
- 然后在createStore时候,用combineReducers将多个reducer包装。
- 获取数据时候,按照combineReducers传入的对象结构进行访问。
下面示例代码展示了reducer合并的用法,示例实现了一个可以改变颜色的按钮和一个开关。按钮和开关是不同的功能,因此我们用不同的reducer实现其逻辑。
import React from 'react';
import {createStore, combineReducers} from 'redux';
const defaultSwitch = {
// 开关默认关闭状态
isSwitchOn: false,
};
const defaultButton = {
color: ''
};
function switchReducer(state = defaultSwitch, action) {
switch(action.type) {
case 'open':
return {isSwitchOn: true};
case 'close':
return {isSwitchOn: false};
default:
return state;
}
}
function buttonReducer(state = defaultButton, action) {
switch(action.type) {
case 'changeColor':
return {...state, color: action.color}
default:
return state;
}
}
const store = createStore(combineReducers({
switch: switchReducer,
button: buttonReducer
}));
class App extends React.PureComponent {
state = {
isOpen: store.getState().switch.isSwitchOn,
color: store.getState().button.color
};
componentDidMount() {
// 监听store数据变化的回调
store.subscribe(() => {
this.setState({
isOpen: store.getState().switch.isSwitchOn,
color: store.getState().button.color
});
});
}
switchButton = () => {
const {switch: {isSwitchOn}} = store.getState();
const action = isSwitchOn ? {type: 'close'} : {type: 'open'};
// 触发action,更新state
store.dispatch(action);
};
changeButtonColor = () => {
store.dispatch({type: 'changeColor', color: 'red'});
};
render() {
return (
<div>
<button onClick={this.switchButton}>
{this.state.isOpen ? '关闭' : '打开'}
</button>
<button style={{color: this.state.color}} onClick={this.changeButtonColor}>改变按钮颜色</button>
</div>
);
}
}
export default App;
需要注意的是,在调用dispatch时候,不同reducer都会处理这个action,因此,需要避免不同reducer的action的冲突。
五、中间件(middleware)
在使用redux管理状态时候,可能会对触发action的过程,即dispatch有一些扩展的需求,redux提供了中间件机制来让开发者可以扩展dispatch。
比如,我们希望每次更新状态时候可以把更新之前的状态、action和更新之后的状态打印出来,这样可以方便地对应用进行调试和排查。
再比如,我们希望支持异步action,即dispatch可以传一个函数,函数中可以进行异步操作,异步更新状态(这个需求在调用接口获取数据的业务场景很常见)。
中间件实际做的操作就是改造了dispatch方法。
通过调用applyMiddleware,并传入createStore作为第二个参数来使用中间件。
示例代码演示了如何使用redux-logger和redux-thunk两个中间件。
redux-logger在每次触发状态改变后,就会打印更新之前的状态、action和更新之后的状态。
redux-thunk改造store.dispatch,让store.dispatch可以接收对象或者函数,函数参数为原dispatch和store.getState,在函数中可以执行异步操作,并调用原dispatch来更新状态。
示例代码展示了一个简单的请求数据并展示的功能,注意这里dispatch的action并不是想之前示例中的一个对象,而是一个方法,这个方法接收dispatch和getState参数(这两个参数是redux原生的api),并且方法中进行了dispatch操作,这样就很方便地进行了异步的action。之所以store.dispatch能够接收一个方法作为参数是因为我们使用了redux-thunk中间件。
import React from 'react';
import {createStore, applyMiddleware} from 'redux';
import {createLogger} from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
const loggerMiddleware = createLogger();
const defaultState = {
// init loading fetched
status: 'init',
length: 0
};
function reducer(state = defaultState, action) {
switch(action.type) {
case 'start':
return {...state, status: 'loading'};
case 'end':
return {status: 'fetched', length: action.length};
default:
return state;
}
}
// 经过redux-thunk中间件改造,dispatch可以传一个方法
function start() {
return function (dispatch, getState) {
if (getState().status === 'init') {
dispatch({type: 'start'});
fetch('https://cnodejs.org/api/v1/topics')
.then(response => {
response.json().then(res => {
dispatch({type: 'end', length: res.data.length});
});
});
}
}
}
const store = createStore(
reducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);
class App extends React.PureComponent {
state = {
status: store.getState().status,
length: store.getState().length
};
componentDidMount() {
// 监听store数据变化的回调
store.subscribe(() => {
this.setState({
status: store.getState().status,
length: store.getState().length
});
});
}
onStartButtonClick() {
store.dispatch(start());
}
render() {
return (
<div>
<button onClick={this.onStartButtonClick}>
{
{
init: '开始',
loading: '请等待...',
fetched: '请求结束'
}[this.state.status]
}
</button>
<div>数据长度:{this.state.length}</div>
</div>
);
}
}
export default App;
六、react-redux
通过前面示例,我们在React项目中使用redux时候,每次都需要监听store变化并绑定到组件自身的state,并且在获取store中的数据时候和触发action时候都需要写一些样板代码(store.subscribe、store.getState())。为了让我们在React项目中更简洁地使用redux,我们可以引入react-redux这个工具。
react-redux相当于一个辅助的工具,让我们在React项目中使用redux时候可以写更简洁的代码。
下面我们来看如何使用react-redux来让我们的代码更加简洁。
react-redux中有两个主要的API:
Provider // 包裹根组件,并传入store属性,store属性值为redux的store,该组件用于给组件中增加store的context,这样组件树种的子组件就可以通过consumer来访问store,当然开发者不需要直接获取store,而是通过connect来方便地对store进行访问和监听。
connect(mapState, mapDispatch) // 用来包裹开发的UI组件,该组件可以根据传入的参数监听store中的属性,并注入到UI组件中。
react-redux中有两个概念:UI组件和容器组件。
使用connect返回的高阶组件包装的组件,即我们实现的组件是UI组件,connect返回的是一个包含容器组件的高阶组件,容器组件负责监听store变化,并将开发者关注的数据通过props注入到UI组件中。
connect是一个方法,它接收的前两个参数是mapStateToProps和mapDispatchToProps,它返回一个高阶组件,用高阶组件包裹UI组件后,UI组件中就能在props中访问store的数据,并通过dispatch触发action、修改数据了。
mapStateToProps用于将store中的数据映射到UI组件的props中。mapStateToProps的参数是store,返回的结果是一个对象,对象中的数据将会被传入到UI组件的props中。mapStateToProps返回的数据决定了组件需要监听的数据,虽然store中存储了很多数据,但是只要对某个组件(例如A)使用connect包装时候,mapStateToProps的返回值中不包括它不关心的数据,那么其他数据改变后,不会触发A的render。
mapDispatchToProps,如果该参数为空,那么将默认将dispatch传入到UI组件的props中。如果不为空,则props中不会被传入dispatch方法,而是会将mapDispatchToProps返回的结果处理后传入到props中。mapDispatchToProps返回的结果是一个对象,key是将会挂到props上的方法名,value是执行的方法,一般在执行方法中会调用store的dispatch方法触发action,store的dispatch方法会在mapDispatchToProps方法的参数中传入。
connect方法所做的工作可以简单描述为,使用consumer获取了context中的store,并返回高阶组件,这个高阶组件包装UI组件后,返回一个容器组件,容器组件根据connect传入的参数监听感兴趣的数据,并在数据变化时候更新UI组件。
index.js代码:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Provider} from 'react-redux';
import { createStore } from 'redux';
const defaultState = {
// 开关默认关闭状态
isSwitchOn: false
};
function reducer(state = defaultState, action) {
switch(action.type) {
case 'open':
return {isSwitchOn: true};
case 'close':
return {isSwitchOn: false};
default:
return state;
}
}
const store = createStore(reducer);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
App.js代码:
该示例没有传入mapDispatchToProps,关于mapDispatchToProps参数可以看后面的例子
// App.js
import React from 'react';
import {connect} from 'react-redux';
class App extends React.PureComponent {
switchButton = () => {
const action = this.props.isSwitchOn ? {type: 'close'} : {type: 'open'};
// 触发action,更新state
this.props.dispatch(action);
};
render() {
return (
<div>
<button onClick={this.switchButton}>
{this.props.isSwitchOn ? '关闭' : '打开'}
</button>
</div>
);
}
}
const mapStateToProps = ({isSwitchOn}) => ({isSwitchOn});
export default connect(mapStateToProps)(App);
七、redux实践
请确保在了解redux实践之前先理解上述的redux和react-redux的基本使用。
在React项目中使用redux时候通常都会用到redux和react-redux两个库,使用redux管理状态,使用react-redux来更方便地访问、监听和改变数据。
在使用了redux的项目中,reducer代码和action代码的组织因人而异,推荐尽量把项目中所有的reducer和action代码放在一起,便于维护。
一般actionType都会定义为常量,而非直接写字符串,这样在项目规模较大,状态较多时候更利于项目维护。
dispatch的action是一个对象,这个对象中除了必需的type字段外,很可能会有其他的字段,为了减少样板代码,通常都使用一个方法返回action对象,这个方法称为action creator。
下面是一个参考的目录结构
src
└── store // store目录用于存放redux状态管理相关代码
├── actionCreators // 项目中所有的action creator
│ ├── article.js
│ └── user.js
├── actionTypes.js // 项目中所有action type 定义的常量
├── reducers // 项目中所有的reducer
│ ├── article.js
│ └── user.js
└── store.js // 导出store
在React中使用redux的一个实践步骤描述如下:
- 统一维护redux相关代码:
实现actionTypes
实现actionCreators
实现reducers
实现store,加载中间件
- 在入口使用Provider注入store
- 使用connect包裹组件,监听时间,并获取store的属性,进行数据访问和改变
按照这种实践实现的一个示例如下:
关键模块的目录结构:
src
├── App.js
├── index.js
└── store
├── actionCreators
│ ├── article.js
│ └── user.js
├── actionTypes.js
├── reducers
│ ├── article.js
│ └── user.js
└── store.js
先看入口文件的代码,入口将store注入到了组件树中
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {Provider} from 'react-redux';
import store from './store/store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
再看下store相关的代码
actionTypes.js就是定义了action type的常量字符串。
// store/actionTypes.js
export const CHANGE_TITLE = 'change_title';
export const CHANGE_NAME = 'change_name';
export const CHANGE_AGE = 'change_age';
actionCreators目录中的模块定义了项目中的action creator
// store/actionCreators/
// article.js
import {
CHANGE_TITLE
} from '../actionTypes'
export const changeTitle = title => {
return {
type: CHANGE_TITLE,
title
};
};
// user.js
import {
CHANGE_NAME, CHANGE_AGE
} from '../actionTypes';
export const changeName = name => {
return {
type: CHANGE_NAME,
name
};
};
export const changeAge = age => {
return {
type: CHANGE_AGE,
age
};
};
reducers目录中的模块定义了reducer
// store/reducers/
// article.js
import {
CHANGE_TITLE
} from '../actionTypes';
const defaultArticleState = {
title: '新年快乐!',
};
const articleReducer = (state = defaultArticleState, action) => {
switch (action.type) {
case CHANGE_TITLE:
// reducer返回的结果会被作为新的状态,因此需要保留之前状态的字段
return {...state, title: action.title};
default:
return state;
}
};
export default articleReducer;
// user.js
import {
CHANGE_NAME, CHANGE_AGE
} from '../actionTypes';
const defaultUserState = {
name: 'Jam',
age: 18
};
const userReducer = (state = defaultUserState, action) => {
switch (action.type) {
case CHANGE_NAME:
return {...state, name: action.name};
case CHANGE_AGE:
return {...state, age: action.age};
default:
return state;
}
};
export default userReducer;
store.js中根据reducer,并引入一些常用中间件,然后创建并导出store。
// store/store.js
import {createStore, applyMiddleware, combineReducers} from 'redux';
import {createLogger} from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import user from './reducers/user';
import article from './reducers/article';
const loggerMiddleware = createLogger()
export default createStore(
combineReducers({user, article}),
applyMiddleware(
thunkMiddleware,
loggerMiddleware
)
);
在App.js中使用状态管理,可以看到组件被connect包裹后可以访问到store中的状态,并可以通过mapDispatchToProps映射后的方法改变状态。
// App.js
import React from 'react';
import {connect} from 'react-redux';
import {changeName, changeAge} from './store/actionCreators/user';
import {changeTitle} from './store/actionCreators/article';
class User extends React.Component {
state = {
text: ''
};
onInputChange = e => {
this.setState({text: e.target.value})
};
changeUserName = () => {
this.props.changeName(this.state.text);
};
changeUserAge = () => {
this.props.changeAge(+this.state.text);
};
render() {
return (
<div>
<input value={this.state.text} onChange={this.onInputChange} />
<div>用户姓名:{this.props.name}</div>
<div>用户年龄:{this.props.age}</div>
<button onClick={this.changeUserName}>修改用户姓名</button>
<button onClick={this.changeUserAge}>修改用户年龄</button>
</div>
);
}
}
class Article extends React.Component {
state = {
text: ''
};
onInputChange = e => {
this.setState({text: e.target.value})
};
changeArticleTitle = () => {
this.props.changeTitle(this.state.text);
};
render() {
return (
<div>
<input value={this.state.text} onChange={this.onInputChange} />
<div>文章标题:{this.props.title}</div>
<button onClick={this.changeArticleTitle}>修改文章标题</button>
</div>
);
}
}
const WrappedUser = connect(
({
user: {name, age}, article: {title}
}) => ({name, age, title}),
dispatch => ({
changeName: name => dispatch(changeName(name)),
changeAge: age => dispatch(changeAge(age))
})
)(User);
const WrappedArticle = connect(
({
article: {title}
}) => ({title}),
dispatch => ({
changeTitle: title => dispatch(changeTitle(title))
})
)(Article);
class App extends React.PureComponent {
render() {
return (
<div>
<WrappedUser />
<WrappedArticle />
</div>
);
}
}
export default App;
八、redux生态
- 中间件
- reselect,减少redux state改变时候无关属性计算
- redux-saga,异步actions管理
- react-router-redux
- devTools状态跟踪
- redux热重载
九、redux的实现
Redux有3大核心,actions、store和reducer。Redux维护一个state,接受用户定义的reducer,用于根据action计算新的state,当用户触发action后,Redux调用reducer计算新的state,更新state,然后将新的state发布给订阅者。
这节讲一下redux的简单实现:createStore这个API的实现,先来看下createStore的使用。
const defaultState = {
// 开关默认关闭状态
isSwitchOn: false
};
function reducer(state = defaultState, action) {
switch (action.type) {
case 'open':
return {isSwitchOn: true};
case 'close':
return {isSwitchOn: false};
default:
return state;
}
}
const store = createStore(reducer);
const unsubscribe = store.subscribe(() => {
console.log('state', store.getState());
});
store.dispatch({type: 'open'});
unsubscribe();
我们看到createStore
接收一个方法reducer作为参数,返回一个对象store,store有3个方法
- getState,获取当前状态。
- subscribe,接收一个回调作为参数,订阅状态改变,它返回一个函数,用来解除订阅。
- dispatch,接收一个action对象作为参数,触发action之后会依据reducer计算新的状态,并通知订阅者。
根据上面的列举,我们用TypeScript写一个简单的redux
interface IStore {
subscribe: (cb: Function) => void,
getState: () => Object,
dispatch: (action: Object) => void
}
const createStore = (reducer: Function): IStore => {
let state = reducer();
const listeners = [];
const subscribe = cb => {
listeners.push(cb);
return () => {
const index = listeners.indexOf(cb);
listeners.splice(index, 1);
};
};
const getState = () => state;
const dispatch = action => {
state = reducer(action);
listeners.forEach(listener => listener());
};
return {
subscribe, getState, dispatch
};
};
但是在这个简单的实现中有几个问题没有考虑到:
- reducer应该是一个纯函数,但是用户如果在reducer中调用getState、subscribe、dispatch怎么办?
- 如果用户连续调用解绑函数怎么办?
- 如果用户在dispatch过程中,即在订阅者中做绑定和解绑的操作怎么办?
对于上面的问题,redux的解决方案是这样的:
- 设置标志位:
isDispatching
,用reducer计算新state时候置为true,在计算完再置为false。然后在getState、subscribe、dispatch方法中判断,如果isDispatching
为true,则抛出错误。 - 设置标志位:
isSubscribed
,它标识一个订阅者是否正在订阅中,订阅后置为true,解绑后置为false,如果用户连续调用解绑,redux判断isSubscribed
为false,就直接return了。 - redux设置了
currentListners
和nextListners
两个数组,这两个数组就是为了防止执行listners时候还添加和移除监听器。在添加和移除时候都新建一个区别于currentListners
的数组:nextListeners
,在nextListners
添加或删除listener,当dispatch时候再将currentListners
和nextListners
同步。
redux的createStore就是实现了上面提到的API并且完美解决了上面提到的问题。
阅读redux代码,可以理解的更深刻:redux-github、redux createStore源码。