1、前言
聊起React,就不得不提redux,虽然并不是每个项目中必须使用redux,但是作为一个新生代农民工(国家认证)
,必须对相关概念了解,这样使用的时候才能立马上手,最近的项目中我们没怎么使用redux,为了防止遗忘,就琢磨写下一些学习心得,做到温故知新。
2、传统MVC框架
先看张图
MVC
的全名是Model View Controller
,是模型(model)-视图(view)-控制器(controller)
的缩写,是一种软件设计典范。
V
即View视图
,是指用户看到并与之交互的界面
。
M
即Model模型是管理数据
,是很多业务逻辑都在模型中完成
。在MVC的三个部件中,模型拥有最多的处理任务。
C
即Controller控制器
,是指控制器接受用户的输入并调用模型和视图去完成用户的需求
。控制器本身不输出任何东西和做任何处理
。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
看似MVC框架的数据流很理想,请求先到Controller, 由Controller调用Model中的数据交给View进行渲染,但是在实际的项目中,又是允许Model和View直接通信的。然后就出现了这样的结果,如下图:
3、Flux
React
只是一个MVC中的V(视图层)
,只管页面中的渲染
,一旦有数据管理的时候,React
本身的能力就不足以支撑复杂组件结构的项目,在传统的MVC
中,就需要用到Model和Controller
。
Facebook对于当时世面上的MVC
框架并不满意,于是就有了Flux
, 但Flux
并不是一个MVC
框架,他是一种新的思想。
在2013年,Facebook让React
亮相的同时推出了Flux框架,React
的初衷实际上是用来替代jQuery
的,Flux
实际上就可以用来替代Backbone.js
,Ember.js
等一系列MVC
架构的前端JS框架。
其实Flux
在React
里的应用就类似于Vue
中的Vuex
的作用,但是在Vue
中,Vue
是完整的mvvm
框架,而Vuex
只是一个全局的插件。
- View: 视图层
- ActionCreator(动作创造者):视图层发出的消息(比如mouseClick)
- Dispatcher(派发器):用来接收Actions、执行回调函数
- Store(数据层):用来存放应用的状态,一旦发生变动,就提醒Views要更新页面
Flux的流程:
-
组件获取到store中保存的数据挂载在自己的状态上
-
用户产生了操作,调用actions的方法
-
actions接收到了用户的操作,进行一系列的逻辑代码、异步操作
-
然后actions会创建出对应的action,action带有标识性的属性
-
actions调用dispatcher的dispatch方法将action传递给dispatcher
-
dispatcher接收到action并根据标识信息判断之后,调用store的更改数据的方法
-
store的方法被调用后,更改状态,并触发自己的某一个事件
-
store更改状态后事件被触发,该事件的处理程序会通知view去获取最新的数据
4、Redux
4.1、redux的介绍
React 只是 DOM 的一个抽象层
,并不是 Web 应用的完整解决方案。有两个方面,它没涉及。
- 代码结构
- 组件之间的通信
2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。
2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
如果你不知道是否需要 Redux,那就是不需要它
只有遇到 React 实在解决不了的问题,你才需要 Redux
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。
不需要使用Redux的项目:
- 用户的使用方式非常简单
- 用户之间没有协作
- 不需要与服务器大量交互,也没有使用 WebSocket
- 视图层(View)只从单一来源获取数据
需要使用Redux的项目:
- 用户的使用方式复杂
- 不同身份的用户有不同的使用方式(比如普通用户和管理员)
- 多个用户之间可以协作
与服务器大量交互
,或者使用了WebSocket
View要从多个来源获取数据
从组件层面考虑,什么样子的需要Redux:
- 某个组件的状态,需要共享
- 某个
状态需要在任何地方都可以拿到
- 一个
组件需要改变全局状态
- 一个组件需要改变另一个组件的状态
4.2、redux的设计思想:
- Web 应用是一个状态机,
视图
与状态
是一一对应
的。 - 所有的
状态
,保存在一个对象里面(唯一数据源)
。
注意:flux、redux都不是必须和react搭配使用的,因为flux和redux是完整的架构,在学习react的时候,只是将react的组件作为redux中的视图层去使用了。
4.3、redux的使用的三大原则:
-
Single Source of Truth
(唯一的数据源)
-
State is read-only
(状态是只读的)
-
Changes are made with pure function
(数据的改变必须通过纯函数完成)
4.4、原生模拟实现redux
reducer.js
文件
纯函数:
1:相同的入参,得到相同的输出
2:不能修改入参
const reducer = ( state , action ) => {
action = action ||{ type : ''}
switch(action.type){
case 'increment':
return {
...state,
count : state.count + 1
}
case " decrement":
return {
...state,
count : state.count - 1
}
default:
return state
}
}
export {
reducer
}
store.js
文件
import { reducer } from './reducer'
let state = {
count: 0
}
const createStore = () => {
// getState 获取状态
const getState = () => state
// 观察者模式
const listeners = []
// subscribe 订阅
const subscribe = listener => listeners.push(listener)
const dispatch = action => {
// console.log(reducer(state, action))
state = reducer(state, action)
// publish 发布
listeners.forEach(listener => listener())
}
return {
dispatch,
getState,
subscribe
}
}
const store = createStore()
const render = () => {
document.querySelector('#count').innerHTML = store.getState().count
}
store.subscribe(render)
export default store
App.js
某任意组件
import React, { Component } from 'react'
import store from './Store'
class App extends Component {
componentDidMount(){
store.dispatch()
}
render() {
return (
<div >
<button onClick={store.dispatch.bind(this,{type : 'increment'})}>+</button>
<span id='count'></span>
<button onClick={store.dispatch.bind(this,{type : 'decrement'})}>-</button>
</div>
)
}
}
export default App
4.4.1、整体思路
(1)在store文件里面定义createStore()方法,createStore方法包含三种方法:
getState
:通过reducer改变state状态,并获取最新state状态subscribe
:发布订阅模式,通过subscribe
来订阅,即用一个空数组来push
方法dispatch
:调用action,里面包含两个操作,一个是调用reducer(state,action)
纯函数来获得最新的state;一个是publish发布
,把刚才通过空数组subscribe订阅的方法数组,forEach遍历运行,即完成所谓的发布。
所以每dispatch一次
,就会改变一次状态
;另外会publish发布
一次刚才subscribe订阅
的东西。所以createStore最后return出去的是dispatch,getState,subscribe。
在createStore外面可以定义render函数
和首次的订阅subscribe
,并且在store文件最开始的时候引入reducer。
(2)在reducer(state,action)纯函数里面纯函数的特点:1、相同的入参,得到相同的输出 2、函数的输出结果不能对入参做解构修改
,通过action.type来区分,进而执行不同的操作,返回一个新的state。
(3)在某个组件里面引入store文件,通过调用store.dispatch
,并传入不同的type类型,进行不同操作。
4.5、细说redux
安装redux
yarn add redux
4.5.1、redux里面有什么
在安装好redux之后,可以随便创建一个js文件,在里面导入require
刚刚安装的redux,并打印,可以看到redux里面都包括什么东西
可以看出,redux里面包含:
_DO_NOT_USE__ActionTypes对象
applyMiddleware
bindActionCreators
combineReducers
compose
createStore
这些方法(下文细讲)
4.5.2、store里面有什么
我们从redux里面解构出createStore,将写好的reducer作为参数传入其中,打印查看
reducer.js
文件
const defaultState = {count: 0}
const reducer = (state = defaultState, action) => {
switch(action.type) {
case 'increment':
return {
...state,
count: state.count + 1
}
case 'decrement':
return {
...state,
count: state.count - 1
}
default:
return state
}
}
module.exports = reducer
可以看出,如之前我们自己用原生模拟的redux一样,这次的store里面包含:
dispatch
subscribe
getState
replaceReducer
observable
等方法
store.dispatch()
还是刚才的reducer文件,这次我们store.dispatch()派发一个type类型,并且打印store.getState(),看看里面有什么
调用几次就改变几次
store.dispatch()
和store.subscribe()
subscribe
相当于订阅
,而每一次dispatch
相当于发布
,这不同于vue
中的 object.defineproperty()
4.6、react+redux实现TodoList
安装react-redux
:提供Provider
和connect
(注意区分redux
和react-redux
的区别)
yarn add react-redux
react-redux里面有:
mapStateToProps
:映射state到当前组件的props上面mapDispatchToProps
:映射某个方法到当前组件的props上面,该方法里面通过dispatch派发事件。
mapStateToProps
和mapDispatchToProps
都是通过return
来返回一个对象
,在对象里面拿到数据和函数
(附上一张redux的流程图)
4.6.1、基础版(无Action Creater和Middleware版)
外部入口index.js文件
从react-redux
里面解构出Provider
,然后引用store
,将store注入到Provider
里面,这样就可以在使用的时候,通过connect( mapStateToProps ,mapDispatchToProps)
来获取到写在props上
面的state
和各种方法
了
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './13-redux/04-todolist/TodoList'
import store from './13-redux/04-todolist/store/index'
ReactDOM.render(
<Provider store={store}>
<App ></App>
</Provider>,
document.querySelector('#root')
)
TodoList文件:
import React, { Component } from 'react'
import Form from './Form'
import List from './List'
//引入两个单独的组件
class App extends Component {
render() {
return (
<div>
<Form></Form>
<List></List>
</div>
)
}
}
export default App
List文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
//映射state到当前组件的props上面
const mapStateToProps = ( state) => {
return{
list : state.list
}
}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
deleteData: (index) => {
dispatch({
type : 'DELETE_DATA',
index
})
}
}
}
@connect(mapStateToProps,mapDispatchToProps)
class List extends Component {
componentDidMount(){
console.log('this' ,this)
}
handleClick = (index) => {
return()=>{
this.props.deleteData(index)
}
}
render() {
return (
<ul>
{
this.props.list.map((value ,index)=>{
return(
<li key={index}>
{value}
<button onClick={this.handleClick(index)}>删除</button>
</li>
)
})
}
</ul>
)
}
}
export default List
Form文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
//映射state到当前组件的props上面
const mapStateToProps = () => {
return{}
}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
putData: (task) => {
dispatch({
type : 'PUT_DATA',
task
})
}
}
}
@connect(null,mapDispatchToProps)
class Form extends Component {
state = {
task : ''
}
handleChange = (e) =>{
this.setState({
task : e.target.value
})
}
handleKeyUp = (e) => {
if(e.keyCode === 13){
// console.log(this)
this.props.putData(this.state.task)
this.setState({
task : ''
})
}
}
handleClick = () => {
this.props.putData(this.state.task)
this.setState({
task : ''
})
}
render() {
return (
<div>
<input
type="text"
name="" id=""
value={this.state.task}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
/>
<button onClick={this.handleClick}>确定</button>
</div>
)
}
}
export default Form
store下面的index
这里的主要目的是引入reducer
,对外抛出store
//从react-redux里面解构出createStore
import { createStore } from 'redux'
import reducer from './reducer'
//将reducer引入createStore里面
const store = createStore(reducer)
export default store
store下面的reducer
reducer
是一个纯函数,所以defaultState是个对象,reducer里面不管哪一个type类型,返回出来的都应该是对象类型,这样才能保证传入相同,传出也相同
const defaultState = {
arr:[
'苹果','香蕉','葡萄'
],
list : [
'任务一' , '任务二'
]
}
const reducer = ( state = defaultState,action) => {
switch(action.type){
case 'LOAD_DATA':
return state;
case 'PUT_DATA':
return {
//...state是为了防止defaultState里面除了list数组之外还有别的属性
...state,
list : [
...state.list,
action.task
]
}
case 'DELETE_DATA':
let list1 = state.list.filter((v,i)=>{
return i!==action.index
})
return {
//...state是为了防止defaultState里面除了list数组之外还有别的属性
...state,
list : list1
}
default :
return state
}
}
export default reducer
效果图
实现原理
- 开始时,List文件里面打印this,发现
deleteData
和list
已经挂载在props上面,这个功劳属于从react-redux
里面解构的Provider
和connect
- 在Form文件里的输入框中输入值,点击‘确定’,触发挂载在
props
的putData
函数,派发dispatch一个type
为PUT_DATA
类型的函数,task参数
即为输入的内容
,然后store
里面的reducer
通过识别action.type
类型,将action.task
存入list当中,这样就完成了添加 - 在List文件里面识别挂载在
props
的list
数据,进行渲染;点击‘删除’,触发挂载在props
上面的deleteData
函数,派发dispatch一个type
为DELETE_DATA
类型的函数,index
为渲染list的某一项key值,然后store
里面的reducer
通过识别action.type
类型,将list数组中index不等于action.index
筛选出来,并重新赋值于list,这样就完成了删除
4.6.2、基础版(有Action Creater和无Middleware版)
从字面意思来讲,Action Creators就是集成所有Action动作的文件,帮助我们抽离
出来各种dispatch派发函数
。
外部入口index.js文件
同4.6.1的部分
TodoList文件:
同4.6.1的部分
List文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { deleteAction } from './store/actionCreator'
//映射state到当前组件的props上面
const mapStateToProps = ( state) => {
return{
list : state.list
}
}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
deleteData: (index) => {
// 引入actionCreator之后
dispatch(deleteAction(index))
}
}
}
@connect(mapStateToProps,mapDispatchToProps)
class List extends Component {
componentDidMount(){
// console.log('this' ,this)
}
handleClick = (index) => {
return()=>{
this.props.deleteData(index)
}
}
render() {
return (
<ul>
{
this.props.list.map((value ,index)=>{
return(
<li key={index}>
{value}
<button onClick={this.handleClick(index)}>删除</button>
</li>
)
})
}
</ul>
)
}
}
export default List
Form文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { putdataAction } from './store/actionCreator'
//映射state到当前组件的props上面
const mapStateToProps = () => {
return{}
}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
putData: (task) => {
//引入actionCreator之后
dispatch(putdataAction(task))
}
}
}
@connect(null,mapDispatchToProps)
class Form extends Component {
state = {
task : ''
}
handleChange = (e) =>{
this.setState({
task : e.target.value
})
}
handleKeyUp = (e) => {
if(e.keyCode === 13){
// console.log(this)
this.props.putData(this.state.task)
this.setState({
task : ''
})
}
}
handleClick = () => {
this.props.putData(this.state.task)
this.setState({
task : ''
})
}
render() {
return (
<div>
<input
type="text"
name="" id=""
value={this.state.task}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
/>
<button onClick={this.handleClick}>确定</button>
</div>
)
}
}
export default Form
store下面的index
同4.6.1的部分
store下面的reducer
同4.6.1的部分
store下面的actionCreator
将aciton集成
在一个文件,统一派发dispatch
,便于管理。
const deleteAction = (index) => {
return {
type : 'DELETE_DATA',
index
}
}
const putdataAction = (task) => {
return {
type : 'PUT_DATA',
task
}
}
export {
deleteAction,
putdataAction
}
效果图
4.6.3、升级版(有Middleware版)
4.6.3.1、 什么是Middleware
在4.6.1中,我们实现了简单版的react+redux,但是实际开发工作中,我们往往会就有一些异步操作
,比如ajax请求
,这时上面的基础写法就满足不了我们的业务需求,所以我们需要引入一些可以满足异步操作的工具,即中间件Middleware(能够满足异步操作的要求)
。
下图是一张react+redux的流程图:
同时红点
标出的地方就是我们进行异步操作
的位置。原因是:
store
相当于一个聚合容器
,不做细节的代码操作reducer
相当于纯函数
,不能进行有副作用
的操作React Components
组件里面可以做异步操作,进行同步派发,但是这样就表明redux是依赖于React,这跟redux是独立于React的原则相违背
Action Creator
就是简单派发一个对象,不适合做
所以我们只能在Action Creaters后
进行异步操作
。
4.6.3.2 、不使用Middleware
假设没有引用中间件,然后我们在actionCreator里面将扁平(plain)对象改为外面的多套一层函数
,这就意味着在组件中dispatch派发的不是一个扁平的对象
,而是一个异步的函数
,里面包着扁平的对象
,如图所示:
在List文件
里面添加测试代码:
其他文件保持不变,然后启动运行
发现控制台报出错误,提示我们如果直接在Action Creator后
进行异步操作
,不能成功
,因为Actions
最后必须是扁平对象
,所以我们必须使用中间件Middleware来帮助我们。
4.6.3.3、 使用Middleware
常用的中间件有redux-thunk
,redux-saga
等,这里我们引入redux-thunk
中间件
安装redux-thunk
yarn add redux-thunk
store下面的index
从redux
中解构出applyMiddleware
,引入thunk
,传入createStore
import { createStore ,applyMiddleware} from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
//中间件一旦挂上,组件中dispatch 就必然会被中间件拦下来
const middleware = applyMiddleware(thunk)
const store = createStore( reducer , middleware)
export default store
其他文件保持不变,再次运行程序,页面可以正常展示
所以我们可以看出,是中间件Middleware
帮助我们去运行异步操作
。
4.6.3.4、 使用Middleware进行异步ajax请求
场景:假定页面最开始就要展示从ajax请求回来的数据(我们利用fetch模拟),同时还要可以手动添加和删除。
外部入口index.js文件
从react-redux
里面解构出Provider
,然后引用store
,将store注入到Provider
里面,这样就可以在使用的时候,通过connect( mapStateToProps ,mapDispatchToProps)
来获取到写在props上
面的state
和各种方法
了
mport React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import App from './13-redux/05-todolist2/App'
import store from './13-redux/05-todolist2/store/index'
ReactDOM.render(
<Provider store={store}>
<App ></App>
</Provider>,
document.querySelector('#root')
)
TodoList文件:
import React, { Component } from 'react'
import Form from './Form'
import List from './List'
//引入两个单独的组件
class App extends Component {
render() {
return (
<div>
<Form></Form>
<List></List>
</div>
)
}
}
export default App
List文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
import {
deleteAction ,
setdataAction
} from './store/actionCreator'
//映射state到当前组件的props上面
const mapStateToProps = ( state) => {
return{
list : state.list
}
}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
deleteData: (index) => {
//没有引入actionCreator之前
// dispatch({
// type : 'DELETE_DATA',
// index
// })
// 引入actionCreator之后
dispatch(deleteAction(index))
},
setData : ()=>{
dispatch(setdataAction())
},
}
}
@connect(mapStateToProps,mapDispatchToProps)
class List extends Component {
componentDidMount(){
//场景规定刚开始就展示列表数据
this.props.setData()
}
handleClick = (index) => {
return()=>{
this.props.deleteData(index)
}
}
render() {
return (
<ul>
{
this.props.list?.map((value ,index)=>{
return(
<li key={index}>
{value.positionName}
<button onClick={this.handleClick(index)}>删除</button>
</li>
)
})
}
</ul>
)
}
}
export default List
Form文件
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { putdataAction } from './store/actionCreator'
//映射state到当前组件的props上面
// const mapStateToProps = () => {}
//映射dispatch到当前组件的props上面
const mapDispatchToProps = (dispatch) => {
return {
putData: (task) => {
// 没有引入actionCreator之前
// dispatch({
// type : 'PUT_DATA',
// task
// })
//引入actionCreator之后
dispatch(putdataAction(task))
}
}
}
@connect(null,mapDispatchToProps)
class Form extends Component {
state = {
task : ''
}
handleChange = (e) =>{
this.setState({
task : e.target.value
})
}
handleKeyUp = (e) => {
if(e.keyCode === 13){
this.props.putData(this.state.task)
this.setState({
task : ''
})
}
}
render() {
return (
<div>
<input
type="text"
name="" id=""
value={this.state.task}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
/>
</div>
)
}
}
export default Form
store下面的index文件
import { createStore ,applyMiddleware} from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'
//中间件一旦挂上,组件中dispatch 就必然会会被中间件拦下来
const middleware = applyMiddleware(thunk)
const store = createStore(reducer , middleware)
export default store
store下面的actionCreator文件
List
组件中调用的setdataAction
方法是进行异步操作
,派发非扁平数据
,这里是中间件Middleware
帮助我们运行return后的箭头函数
,并帮我们传入dispatch参数
,进行完异步操作之后,再
用拿到的数据去派发dispatch扁平的action
,从而继续调用reducer进行后续操作···
const deleteAction = (index) => {
return {
type : 'DELETE_DATA',
index
}
}
const putdataAction = (task) => {
return {
type : 'PUT_DATA',
task
}
}
// 派发非扁平数据,中间件Middleware帮助我们run里面那层
const setdataAction = () => {
return dispatch =>{
fetch('./data.json')
.then( response => response.json())
.then(result => {
dispatch(loaddataAction(result.result))
})
}
}
// 派发扁平的数据类型
const loaddataAction = (data) =>{
return {
type : 'SET_DATA',
data
}
}
export {
deleteAction,
putdataAction,
setdataAction
}
store下面的reducer文件
我们在reducer里面假如许多console.log,来帮助我们更直观来看一下调用过程
const defaultState = {}
const reducer = ( state = defaultState,action) => {
console.log('---action---',action)
switch(action.type){
case 'SET_DATA':
console.log('----SET_DATA----',state)
console.log('----SET_DATA----',action)
return {
...state,
list : action.data
}
case 'PUT_DATA':
console.log('---PUT_DATA---' , state)
console.log('---PUT_DATA---', action)
return {
...state,
list : [
...state.list,
{
positionName : action.task,
}
]
}
case 'DELETE_DATA':
console.log('---DELETE_DATA---' , state)
console.log('---DELETE_DATA---', action)
let list1 = state.list.filter((v,i)=>{
return i!==action.index
})
return {
//...state是为了防止defaultState里面除了list数组之外还有别的属性
...state,
list : list1
}
default :
return state
}
}
export default reducer
效果图
可以看出:
开始页面未进行任何操作时:
- 控制台中打印了
---action--- {type: "@@redux/INITi.1.2.k.w.c"}
,这个是redux本身
自己帮助我们一开始先执行
的(下篇博客讲); List文件
里面在componentDidMount里面先派发dispatch
一个异步任务setdataAction
;- 随后在
actionCretor文件
里面在中间件Middleware作用下
拿到fetch异步请求的数据
,然后再派发一个扁平数据类型
; reducer文件
识别action.type后,对action.data进行操作,并返回一个对象
List文件
拿到props中的list数据,进行最初的页面渲染- 控制台中打印了state为
----SET_DATA---- {}
,action为----SET_DATA---- {type: "SET_DATA", data: Array(15)}
随后再输入添加某些任务,删除任意任务,逻辑也是相对如此,reducer
根据不同的action.type类型
,进行不同的操作。