聊起React,就不得不提redux,虽然并不是每个项目中必须使用redux,但是作为一个新生代农民工(国家认证)
,必须对相关概念了解,这样使用的时候才能立马上手,最近的项目中我们没怎么使用redux,为了防止遗忘,就琢磨写下一些学习心得,做到温故知新。
先看张图
MVC
的全名是Model View Controller
,是模型(model)-视图(view)-控制器(controller)
的缩写,是一种软件设计典范。
V
即View视图
,是指用户看到并与之交互的界面
。
M
即Model模型是管理数据
,是很多业务逻辑都在模型中完成
。在MVC的三个部件中,模型拥有最多的处理任务。
C
即Controller控制器
,是指控制器接受用户的输入并调用模型和视图去完成用户的需求
。控制器本身不输出任何东西和做任何处理
。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
看似MVC框架的数据流很理想,请求先到Controller, 由Controller调用Model中的数据交给View进行渲染,但是在实际的项目中,又是允许Model和View直接通信的。然后就出现了这样的结果,如下图:
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
只是一个全局的插件。
Flux的流程:
组件获取到store中保存的数据挂载在自己的状态上
用户产生了操作,调用actions的方法
actions接收到了用户的操作,进行一系列的逻辑代码、异步操作
然后actions会创建出对应的action,action带有标识性的属性
actions调用dispatcher的dispatch方法将action传递给dispatcher
dispatcher接收到action并根据标识信息判断之后,调用store的更改数据的方法
store的方法被调用后,更改状态,并触发自己的某一个事件
store更改状态后事件被触发,该事件的处理程序会通知view去获取最新的数据
React 只是 DOM 的一个抽象层
,并不是 Web 应用的完整解决方案。有两个方面,它没涉及。
2013年 Facebook 提出了 Flux 架构的思想,引发了很多的实现。
2015年,Redux 出现,将 Flux 与函数式编程结合一起,很短时间内就成为了最热门的前端架构。
如果你不知道是否需要 Redux,那就是不需要它
只有遇到 React 实在解决不了的问题,你才需要 Redux
简单说,如果你的UI层非常简单,没有很多互动,Redux 就是不必要的,用了反而增加复杂性。
不需要使用Redux的项目:
需要使用Redux的项目:
与服务器大量交互
,或者使用了WebSocket
View要从多个来源获取数据
从组件层面考虑,什么样子的需要Redux:
状态需要在任何地方都可以拿到
组件需要改变全局状态
视图
与状态
是一一对应
的。状态
,保存在一个对象里面(唯一数据源)
。注意:flux、redux都不是必须和react搭配使用的,因为flux和redux是完整的架构,在学习react的时候,只是将react的组件作为redux中的视图层去使用了。
Single Source of Truth(唯一的数据源)
State is read-only(状态是只读的)
Changes are made with pure function(数据的改变必须通过纯函数完成)
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
(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类型,进行不同操作。
安装redux
yarn add redux
在安装好redux之后,可以随便创建一个js文件,在里面导入require
刚刚安装的redux,并打印,可以看到redux里面都包括什么东西
可以看出,redux里面包含:
_DO_NOT_USE__ActionTypes对象
applyMiddleware
bindActionCreators
combineReducers
compose
createStore
我们从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()
安装react-redux
:提供Provider
和connect
(注意区分redux
和react-redux
的区别)
yarn add react-redux
react-redux里面有:
mapStateToProps
:映射state到当前组件的props上面mapDispatchToProps
:映射某个方法到当前组件的props上面,该方法里面通过dispatch派发事件。mapStateToProps
和mapDispatchToProps
都是通过return
来返回一个对象
,在对象里面拿到数据和函数
外部入口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
deleteData
和list
已经挂载在props上面,这个功劳属于从react-redux
里面解构的Provider
和connect
props
的putData
函数,派发dispatch一个type
为PUT_DATA
类型的函数,task参数
即为输入的内容
,然后store
里面的reducer
通过识别action.type
类型,将action.task
存入list当中,这样就完成了添加props
的list
数据,进行渲染;点击‘删除’,触发挂载在props
上面的deleteData
函数,派发dispatch一个type
为DELETE_DATA
类型的函数,index
为渲染list的某一项key值,然后store
里面的reducer
通过识别action.type
类型,将list数组中index不等于action.index
筛选出来,并重新赋值于list,这样就完成了删除从字面意思来讲,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.1中,我们实现了简单版的react+redux,但是实际开发工作中,我们往往会就有一些异步操作
,比如ajax请求
,这时上面的基础写法就满足不了我们的业务需求,所以我们需要引入一些可以满足异步操作的工具,即中间件Middleware(能够满足异步操作的要求)
。
下图是一张react+redux的流程图:
同时红点
标出的地方就是我们进行异步操作
的位置。原因是:
store
相当于一个聚合容器
,不做细节的代码操作reducer
相当于纯函数
,不能进行有副作用
的操作React Components
组件里面可以做异步操作,进行同步派发,但是这样就表明redux是依赖于React,这跟redux是独立于React的原则相违背
Action Creator
就是简单派发一个对象,不适合做所以我们只能在Action Creaters后
进行异步操作
。
假设没有引用中间件,然后我们在actionCreator里面将扁平(plain)对象改为外面的多套一层函数
,这就意味着在组件中dispatch派发的不是一个扁平的对象
,而是一个异步的函数
,里面包着扁平的对象
,如图所示:
在List文件
里面添加测试代码:
其他文件保持不变,然后启动运行
发现控制台报出错误,提示我们如果直接在Action Creator后
进行异步操作
,不能成功
,因为Actions
最后必须是扁平对象
,所以我们必须使用中间件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
帮助我们去运行异步操作
。
场景:假定页面最开始就要展示从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数据,进行最初的页面渲染----SET_DATA---- {}
,action为----SET_DATA---- {type: "SET_DATA", data: Array(15)}
随后再输入添加某些任务,删除任意任务,逻辑也是相对如此,reducer
根据不同的action.type类型
,进行不同的操作。