Flux

  • 官方文档

  • Flux 是一种架构思想,专门解决软件的结构问题。它跟MVC架构是同一类东西,但是更加简单和清晰。Flux存在多种实现(至少15种)

  • Facebook Flux是用来构建客户端Web应用的应用架构。它利用*单向数据流*的方式来组合React 中的视图组件。它更像一个模式而不是一个正式的框架,开发者不需要太多的新代码就可以快 速的上手Flux
    image

Redux

  • 官方文档

  • Redux最主要是用作应用状态的管理,类似于Vue中的Vuex,是一个集中式的状态管理插件,但是Redux并不是React团队出品的,仅仅只是在React项目中使用的比较多,且与React的名字非常的相似而已; Redux可以在React,Vue以及Angular等前端框架中使用 !。简言之,Redux用一个单独的常量状态树(state对象)保存这一整个应用的 状态,这个对象不能直接被改变。当一些数据变化了,一个新的对象就会被创建(使用actionsreducers),这 样就可以进行数据追踪,实现时光旅行。

redux介绍及设计和使用的三大原则

image

  1. 单一数据源

整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:

Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;

单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改;

  1. State是只读的

唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:

这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;

这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题;

  1. 使用纯函数来执行修改

通过reducer旧stateactions联系在一起,并且返回一个新的State:

随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;

但是所有的reducer都应该是纯函数,不能产生任何的副作用;

redux的工作流

image

上述动图可以看出,前端页面(UI)通过点击事件,触发Dispatch将一个带有最新的数据状态以Action对象包裹的形式发送给Store中的Reducer,Reducer接收到Action对象后再对Store中的State进行状态修改,随后State状态的修改引发页面的重新渲染(也就是页面中的数据重新变化!),这就是Redux的工作流程

Redux的基本使用

  1. 安装Redux
npm install redux --save
  1. 在项目的src目录下创建store文件夹并在该文件夹下创建index.js
    image

  2. 设置index.js

// 1. 引入redux
import {createStore} from 'redux'

// 4. 创建初始数据(保存初始值)
const initialState = {
isShow:true
}

/*
由于创建的state是不能直接放到创建的store中的, 需要通过reducer将数据添加到store中,
因此创建store时必须创建reducer;reducer函数的返回值, 会作为store之后存储的state,
只要调用dispatch就会重新执行reducer函数,reducer是一个纯函数,不可以直接修改state,
*/

// 3. 创建修改store的方法(reducer)
const reducer = (preState,action)=>{
console.log('数据更新',action,preState);
// 作语句判断(通过页面传过来的语句作数据修改,切记不能直接修改)
let newState = {...preState}//作数据替换,不能直接修改全局state中的状态
switch (action.type) {//监听action的字段
case 'showBottom': // 展示底部区域
newState.isShow = action.isShow
return newState
case 'closeBottom': // 关闭底部区域
newState.isShow = action.isShow
return newState
default:
return initialState // 默认开启
}
}

// 2. 创建全局store
const store = createStore(reducer)

// 5. 导出store
export default store
  1. 在其他的组件中,我们可以使用store.getState来获取全局store中的数据
//引入全局store
import store from '../../store/index.js'

//获取store中的state
console.log(store.getState())
  1. 在组件中我们可以通过dispatch发送action对象给reducer来修改全局store中的数据
// 通过action来修改state
store.dispatch({
type:'showBottom', // 修改字段,用于指定修改逻辑
isShow:true,// 可以携带参数
})

// 当然也可以将action设置成一个对象
const myAction = {type:'showBottom',isShow:true}
store.dispatch(myAction)// 派发action

/*
切记不可直接修改全局store中的数据,必须要通过dispatch来派发action来修改;
通常action中都会有type属性,用来指定修改字段,也可以携带其他的数据;
*/
  1. 在全局的reducer函数中设置处理逻辑,一般是推荐使用switch分支来指定对应的type字段执行对应的逻辑
/*
reducer接收两个参数:
参数一: store中当前保存的state
参数二: 本次需要更新的action

只要调用dispatch就会重新执行reducer函数
*/
const reducer = (preState,action)=>{
console.log('数据更新',action,preState);
// 作语句判断(通过页面传过来的语句作数据修改,切记不能直接修改)
let newState = {...preState}//作数据替换,不能直接修改全局state中的状态
switch (action.type) {//监听action的字段
case 'showBottom': // 展示底部区域
newState.isShow = action.isShow
return newState
case 'closeBottom': // 关闭底部区域
newState.isShow = action.isShow
return newState
default:
return preState // 默认开启
}
}
  1. 我们同样可以设置在派发action之前,在任意组件中设置监听函数去监听全局store中的数据变化
// 生命周期(页面挂载完毕)
componentDidMount(){
/*
通过store.subscribe()函数可以监听store中的数据变化,
store.subscribe()函数的参数接收一个函数, 该函数在store数据发生更新自动回调
*/

store.subscribe(()=>{
console.log('home组件监听中....');
// 修改组件中state的状态重新渲染组件
this.setState({
isShow:store.getState().isShow
})
})
}
  1. 我们同样可以将action封装成函数,使得action变成动态派发
// 创建修改isShow的action
const changeIsShowAction = (isShow) => ({
type: "change_isShow",
isShow
})

// 在派发action时, 我们就可以调用函数即可获取action
store.dispatch(changeIsShowAction(false))
/* ------------------ 推荐使用 -------------------- */

案例展示:

要求: 首页组件包含并展示info组件以及about组件,点击对应的文字跳转对应的组件,进入info组件则展示底部区域,离开info组件则不展示底部区域

  • index.js(全局store设置)
// 1. 引入redux
import {createStore} from 'redux'

// 4. 创建初始数据(保存初始值)
const initialState = {
isShow:true
}

/*
由于创建的state是不能直接放到创建的store中的, 需要通过reducer将数据添加到store中,
因此创建store时必须创建reducer;reducer函数的返回值, 会作为store之后存储的state,
只要调用dispatch就会重新执行reducer函数,reducer是一个纯函数,不可以直接修改state,
*/

// 3. 创建修改store的方法(reducer)
const reducer = (preState,action)=>{
console.log('数据更新',action,preState);
// 作语句判断(通过页面传过来的语句作数据修改,切记不能直接修改)
let newState = {...preState}//作数据替换,不能直接修改全局state中的状态
switch (action.type) {//监听action的字段
case 'showBottom': // 展示底部区域
newState.isShow = action.isShow
return newState
case 'closeBottom': // 关闭底部区域
newState.isShow = action.isShow
return newState
default:
return initialState // 默认开启
}
}

// 2. 创建全局store
const store = createStore(reducer)

// 5. 导出store
export default store
  • home组件(两个子组件的父组件)
import React, { Component } from 'react'
// 引入路由相关的组件
import { HashRouter as Router, Route, Redirect, Switch, NavLink } from 'react-router-dom'
import store from '../../store/index.js'//引入全局store
// 引入对应的组件
import About from './about.js'//关于页
import Info from './info.js'//个人信息页
import Footer from './footer.js'//底部区域
import NotFound from './404.js'//404页面

export default class home extends Component {
componentDidMount(){
/*
通过store.subscribe()函数可以监听store中的数据变化,
store.subscribe()函数的参数接收一个函数, 该函数在store数据发生更新自动回调
*/

store.subscribe(()=>{
console.log('home组件监听中....');
// 修改组件中state的状态重新渲染组件
this.setState({
isShow:store.getState().isShow
})
})
}
state = {
// 我们可以在其他文件中通过 store.getState 来获取当前的state;
isShow:store.getState().isShow // 获取全局store中的状态
}
render() {
console.log('是否展示底部区域',this.state.isShow);
return (
<div>
<h1>首页</h1>
<p>(去到信息页关闭底部,去到关于页开启底部)</p>
<div style={{ width: '100%', height: '400px', backgroundColor: 'yellowgreen' }}>
<Router>
<ul>
<li><NavLink to={'/home/info'}>info</NavLink></li>
<li><NavLink to={'/home/about'}>about</NavLink></li>
</ul>
{/* 使用Switch来解决每次页面刷新,重定向功能执行的bug(模糊匹配) */}
<Switch>
<Route path="/home/about" component={About} />
<Route path="/home/info" component={Info} />

<Redirect from="/" to="/home/info" exact />
{/* 匹配不到的页面则展示404 */}
<Route component={NotFound} />
</Switch>
</Router>
</div>
{/* 通过判断全局store中的isShow来执行是否展示Footer组件 */}
{this.state.isShow && <Footer />}
</div>
)
}
}
  • info组件
import React ,{useEffect}from 'react'
import store from '../../store/index.js'//引入全局store

export default function Info() {
// 模拟生命周期
useEffect(()=>{
console.log('进入info组件');
// 进入info组件修改全局store中的isShow为true(显示底部区域)

/*
修改store中的数据不能直接修改, 必须要通过dispatch来派发action;
通常action中都会有type属性,也可以携带其他的数据;
*/
// 通过action来修改state
store.dispatch({
type:'showBottom',
isShow:true
})
return ()=>{
console.log('退出info组件');
// 离开info组件修改全局store中的isShow为false(关闭底部区域)
store.dispatch({
type:'closeBottom',
isShow:false
})
}
},[])

return (
<div>个人信息页</div>
)
}

结果展示:

image

拓展知识

Redux目录的结构划分

如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护

例如上面代码中, 我们封装的动态创建action的函数, 这种动态生成action的函数在项目中可能会有很多个, 而且在其他多个文件中也可能会使用, 所以我们最好是有一个单独的文件夹存放这些动态获取action的函数

接下来,我会对代码进行拆分,将storereduceractionconstants拆分成一个个文件。

  1. 创建store/index.js文件: index文件中, 我们只需要创建store即可
const { createStore } = require("redux")
// 引入reducer
const reducer = require("./reducer")

// 创建的store, 内部会自动回调reducer, 拿到initialState
const store = createStore(reducer)

// 导出store
module.exports = store

  1. 创建store/reducer.js文件: 在真实项目中, reducer这个函数我们会越写越复杂, 造成我们index.js文件越来越大, 所以我们将reducer也抽离到一个单独的文件中
const { CHANGE_isShow } = require("./constants")

// 创建的要存储的初始化state
const initialState = {
isShow: true,
}

// 定义reducer, 将要存储的state作为返回值返回
// 第一次state是undefined, 因此给一个默认值将初始化数据添加到store中
function reducer(preState = initialState, action) {
switch(action.type) {
case CHANGE_NAME:
return { ...preState, isShow: action.isShow }
default:
return initialState // 默认开启
}
}

module.exports = reducer
  1. 创建store/constants.js文件: 将type的类型定义为常量(防止写错的情况), 这些常量最好也防止一个单独的文件中
// store/constants.js

const CHANGE_isShow = "change_isShow"

module.exports = {
CHANGE_isShow,
}
  1. 创建store/actionCreators.js文件: 将封装的动态创建action的函数放在该文件中, 在需要使用的地方导入即可
const { CHANGE_isShow} = require("./store/constants")

// 创建修改isShow的action
const changeisShowAction = (isShow) => ({
type: CHANGE_isShow,
isShow
})

module.exports = {
changeisShowAction
}

  1. 最终形成如下目录结构, 这也是官方推荐的目录结构, 一个store中包含这四个文件夹
    image