深入浅出认识理解React、Flux和Redux

开始一个React应用

  1. npm i create-react-app -g 全局安装脚手架
  2. create-react-app project_name 新建项目
  3. cd project_name 进入项目目录
  4. npm start 运行项目
  5. 自动开启页面 localhost://3000

第一个 React 应用诞生!

JSX

jsx简介

所谓 JSX 其实是JS的语法扩展,从代码中也可以看到,这种语法让我们可以在JS中编写像 HTML 一样的代码。但是我们需要明白 JSXHTML 到底有什么不同:

  • 首先,JSX 中使用的“元素”不局限于 HTML 中的元素,可以是任何一个React组件(React判断一个元素是 HTML 元素还是React组件的原则就是看第一个字母是否大写
  • 其次,在 JSX 中可以通过 onClick 这样的方式给一个元素添加一个事件处理函数(注意是 onClick 不是 onclick

jsx的进步

看到上面第二条是不是感觉到一个困惑,长期以来不是一直不提倡在 HTML 中使用事件直接绑定元素(onclick)嘛,那为什么在React JSX 中我们却要使用 onClick 这样的方式来绑定事件处理函数呢?

首先说一下 HTML 中直接使用 onclick 绑定事件处理函数不专业的原因:

  • onclick 添加的事件处理函数是全局环境下执行的,污染了全局环境很容易产生bug
  • 给很多DOM元素添加 onclick 事件可能会影响网页在重绘时候的性能
  • 对于使用 onclick 的DOM元素,如果要动态从DOM树中删除的话,需要把对应的事件处理注销,否则可能造成内存泄漏产生bug

而这些问题,JSX 中都不存在:

  • onClick 挂载的每个函数都可以控制在组件范围内,不会污染全局空间
  • 使用的事件委托方式处理点击事件性能当然比为每个 onClick 都挂载一个事件处理函数要高
  • React控制了组件的生命周期,在组件注销时自然可以清除相关的所有事件处理函数,内存泄漏也不再是一个问题

React的工作方式

要了解一个新产品的特点和必要,最好的方法就是拿这个新产品和旧产品作比较,这里拿 jQuery 来作比较。

服务器渲染

web1.0 时,所有和状态数据相关的操作,都已经由服务器完成了,前端开发只需要根据state(状态数据)来决定view(页面)。这时候的前端开发思维是一个从state到view的“单向流”(当state变化时只要简单粗暴的刷新页面即可,服务器会把最新数据的页面渲染完返回到浏览器)。

这种方式的显著缺陷是:

  • 反复刷新对浏览器渲染性能消耗太大
  • 很多细腻的纯前端交互没法交给后端完成(比如菜单的收缩展开,tab的选中等)

于是,出现了ajax技术,web2.0时代到来,出现了大量的交互细腻内容丰富的应用,同时 jQuery 库也得到了很大的应用。

jQuery理念

jQuery 的解决方案是:根据 CSS 规则找到对应 id 值的元素,挂上一个匿名事件处理函数,在事件处理函数中选中需要被修改的DOM元素,读取其中的文本值加以修改,然后修改这个DOM元素。

这是一种最容易理解的开发模式(找到它,然后修改它),但是当项目越来越庞大,这种模式会造成代码结构复杂,难以维护,特别是当各种交互操作耦合起来以后,这种局部修改就会消耗大量的脑细胞,很容易变得顾此失彼,相信每个 jQuery 使用者都会是这种体会。

还是改变state,让view自动更新这种“单向流”更符合程序员的开发思维,所有各种新型框架应运而生。

React理念

React 开发应用组件并没有像 jQuery 那样“找到它,然后做一些事”。而是像一个函数(render),用户看到的界面(UI)就是这个函数的执行结果,只接受数据(data)作为参数,就像这样:

U I = r e n d e r ( s t a t e ) UI=render(state) UI=render(state)

根据确定的交互状态(state)一股脑的决定页面的呈现(view),这种“单向流”的开发状态对程序员来说是思维清晰、比较轻松的。

组件的生命周期

React定义组件的生命周期可能会经历以下三个过程:

  • 装载过程(Mounting),当组件实例被创建并插入 DOM 中时。
  • 更新过程(Updating),当组件的 propsstate 发生变化时会触发更新。
  • 卸载过程(Unmounting),当组件从 DOM 中移除时。
  • 错误处理,当渲染过程,生命周期,或子组件的构造函数中抛出错误时。

三种不同的过程会依次调用组件的一些成员函数,这些函数称为生命周期函数,定制一个React组件,实际上就是定制这些生命周期函数,生命周期图谱 如下,详细的关于每个生命周期的介绍可以查看相应生命周期API
在这里插入图片描述
在这里插入图片描述

装载过程(Mounting)

  • constructor()
  • static getDerivedStateFromProps()
  • render()
  • componentDidMount()

更新过程(Updating)

  • static getDerivedStateFromProps()
  • shouldComponentUpdate()
  • render()
  • getSnapshotBeforeUpdate()
  • componentDidUpdate()

卸载过程(Unmounting)

  • componentWillUnmount()

错误处理

  • static getDerivedStateFromError()
  • componentDidCatch()

React组件数据

React组件数据分为两种:propsstate。设计组件的时候关于数据的一个原则就是,props 是组件的对外接口,state 是组件的内部状态,对外用 props,内部用 state

props

props 是从外部传递给组件的数据,因为每个组件都是独立存在的模块,组件之外的一切都是外部世界,外部世界就是通过 props 来和组件对话。

赋值

具体使用呢,就像组件的 HTML 属性一样写在组件标签上的就是它的 props

<SampleButton onClick={
    
    onButtonClick} msg='Click me' />

读取

一个组件需要定义自己的构造函数一定要在构造函数的第一行通过 super 调用父类(也就是 React.Component)的构造函数,否则类实例的所有成员函数都无法通过 this.props 访问到父组件传递过来的 props 值,props的读取也是很简单:

class SampleButton extents Component {
    
    
  constructor(props){
    
    
    super(props)
    console.log(props.msg) // Click me
  }
}

state

state 代表组件的内部状态,也就是组件内部使用的变量放在 state 中,且 state 必须是对象,即使这个组件只有一个属性。需要注意的是,修改 state 中的值的时候需要使用 setState 方法,而不能直接重新赋值state 的值:

this.setState({
    
    count: this.state.count + 1})
// 不能这样赋值
// this.state.count = this.state.count + 1

state 和 props 的局限

组件的 state 存在的意义就是被修改,每一次通过 this.setState 函数修改 state 就改变了组件的状态然后通过渲染过程把这种变化体现出来,但是组件绝不应该去修改 props 的值,这样可能让程序陷入一团混乱中,完全违背React设计的初衷。

注意:避免将 props 的值复制给 state!如此做毫无必要,同时还产生了 bug。

在下面的案例中,每个 Counter 组件都有自己的状态记录当前计数,而父组件 ControlPanel 也有一个状态来存储所有 Counter 计数之和,也就是数据发生了重复。数据如果出现了重复,带来的问题就是如何保证重复的数据始终一致,如果数据存多份而且不一致,那就很难决定到底使用哪个数据作为正确结果了。
在这里插入图片描述

还有一个问题就是如果在一个应用中包含三级或者三级以上的组件结构,顶层的祖父组件想要传递一个数据给最低层的子组件,如果用 props 的方式,就只能通过父组件中转,即使这个父组件根本不需要这个值,那也要搬运这个值,暂且不说是不是违反了低耦合的设计要求了,想想都恶心不是么… …

所以,全局状态就是唯一可靠的数据源,这就是 FluxReduxStore 的概念。

Flux

The Flux project is in maintenance mode and there are many more sophisticated alternatives available (e.g. Redux, MobX) and we would recommend using them instead.
官方建议使用 ReduxMobX等其他方式,不建议使用 Flux了,所以这里仅仅当做一个理解即可。

Flux 和 React 没有直接的关系,二者是完全独立的概念。Flux 是一种前端代码的组织思想,比如说 Redux 库就可以认为是一种 Flux 思想的实现。

MVC

对于服务器端开发,Model 指的是和处理业务数据相关的代码,例如实现数据库的增删改查等;View 指的是和页面组装相关的代码,例如各种后端模板引擎等;Controller 指的是和用户交互行为的代码,指的就是各种 http 请求的 handler,并且和后端的 router 紧密相关,根据不同 url 和 http 请求参数将数据和模板绑定在一起,最终形成页面呈现给用户。

对于前端而言,Model 相当于后台数据;View 对应页面的内容,html、css等;Controller 主要是用户和网页之间的交互事件的handler,例如 click、enter事件等。

flux模式

当项目中的“单向流”被破坏,修改 Model 的 Controller 代码像一把黄豆一样散落在各个 View 组件中时,此时就需要一个特定的方式将这些散落的黄豆(行为)单独聚拢在一起。

参考 http 请求,我们将要定义的 action,需要一个 typeName 用来表示对 Model 操作的意图(类似于http 请求的 url 路径),还可能需要其他字段,用来描述怎样具体操作 Model(类似于 http 请求的参数)。也就是说,当用户在 View 上的交互行为(例如点击事件)应当引起 Model 发生变化时,我们不直接修改 Model,而是简单的 dispatch 一个 action 来表达我们修改的意图,这些 action 被集中起来转移到数据端来管理。

所以,从代码层面来看,Flux 相当于一个event dispatcher,目的就是要将以往 MVC 中分散在各个不同 View 组件内的 Controller 代码片段提取出来放到更恰当的地方集中管理,形成一个更加容易驾驭的“单向流”模式。Flux 可以说是对前端 MVC 思想的补充和优化吧。

Flux流程

Counter 组件和 Summary 组件需要共用一套数据,所以这里采用 Flux 的方式来统一管理这个共用的数据,下面是从 Model 到 View 的数据流方式:

在这里插入图片描述

那从 View 到 Model 的改动呢?比如 Counter 中对数据进行增减操作时,如何将数据的改变再映射到各个组件中去?

  • 首先需要全局注册一个 event dispatcher,也就是 AppDispatcher.js 文件
  • 每个模块(CounterSummary)都有一个自己的 store 用来注册这个模块中的动作(注册到 AppDispatcher 上)
  • 一个全局的 Actions.js 文件用来存储所有的动作(也就是 Controller),并通过 AppDispatcher 来派发这些动作

总结就是:不同模块中的动作都通过在对应模块的 xxxStore.js 文件中将动作注册到 AppDispatcher 上,同时所有动作都集中管理在 Actions 中并通过 AppDispatcher 来派发注册在上面的具体动作的 handler。

实例说明

统一处理器

// AppDispatcher.js
import {
    
     Dispatcher } from 'flux'
export default new Dispatcher()
// Actions.js
import AppDispatcher from './AppDispatcher'

export const increment = counterCaption => {
    
    
  AppDispatcher.dispatch({
    
    
    type: 'INCREMENT', // 相当于http请求的url路径,表示对model操作的意图
    counterCaption // 相当于http请求的参数,描述怎样具体操作model
  })
}

export const decrement = counterCaption => {
    
    
  AppDispatcher.dispatch({
    
    
    type: 'DECREMENT',
    counterCaption
  })
}

Counter组件流程

// Counter.js
import {
    
     Component } from 'react'
import CounterStore from './store/CounterStore'
import * as Actions from './store/Actions'

class Counter extends Component {
    
    
  constructor(props) {
    
    
    super(props)
    this.state = {
    
    
      count: CounterStore.getCounterValues()[props.caption]
    }
  }
  onClickIncrementButton = () => {
    
    
    Actions.increment(this.props.caption)
  }
  onClickDecrementButton = () => {
    
    
    Actions.decrement(this.props.caption)
  }
  componentDidMount() {
    
    
    CounterStore.on('changed', () => {
    
    
      const new_count = CounterStore.getCounterValues()[this.props.caption]
      this.setState({
    
    count: new_count})
    })
  }
  render() {
    
    
    return (
      <div>
        <button onClick={
    
    this.onClickIncrementButton}>+</button>
        <button onClick={
    
    this.onClickDecrementButton}>-</button>
        <span>{
    
    this.props.caption} count: {
    
    this.state.count}</span>
      </div>
    )
  }
}

export default Counter
// CounterStore.js
import {
    
     EventEmitter } from 'events'
import AppDispatcher from './AppDispatcher'

let counterValues = {
    
    
  'First': 0,
  'Second': 10,
  'Third': 30
}

const CounterStore = Object.assign({
    
    }, EventEmitter.prototype, {
    
    
  getCounterValues: () => counterValues
})

CounterStore.dispatchToken = AppDispatcher.register(action => {
    
    
  if(action.type === 'INCREMENT') {
    
    
    counterValues[action.counterCaption]++
    CounterStore.emit('changed')
  } else if(action.type === 'DECREMENT') {
    
    
    counterValues[action.counterCaption]--
    CounterStore.emit('changed')
  }
})

export default CounterStore
  • Counter 组件中点击按钮动作,通过 ActionsAppDispatcher 上派发
  • CounterStore 中检测到派发过来的动作,对数据进行修改后通过 CounterStore.emit('changed') 广播出去
  • Counter 中通过对 CounterStore.on('changed', cb) 对刚广播的事件进行触发,此时同一数据源的数据已经进行了修改,直接取出赋值给当前组件即可

Summary组件流程

// Summary.js
import {
    
     Component } from 'react'
import SummaryStore from './store/SummaryStore'

class Summary extends Component {
    
    
  constructor() {
    
    
    super()
    this.state = {
    
    
      sum: SummaryStore.getSum()
    }
  }
  componentDidMount() {
    
    
    SummaryStore.on('changed', () => {
    
    
      this.setState({
    
    sum: SummaryStore.getSum()})
    })
  }
  render() {
    
    
    return (
      <div>
        <span>Total Count: {
    
    this.state.sum}</span>
      </div>
    )
  }
}

export default Summary
// SummaryStore.js
import {
    
     EventEmitter } from 'events'
import CounterStore from './CounterStore'
import AppDispatcher from './AppDispatcher'

const SummaryStore = Object.assign({
    
    }, EventEmitter.prototype, {
    
    
  getSum: () => {
    
    
    let _sum = 0
    let _counter_values = CounterStore.getCounterValues()
    for(let key in _counter_values) {
    
    
      _sum += _counter_values[key]
    }
    return _sum
  }
})

SummaryStore.dispatchToken = AppDispatcher.register(action => {
    
    
  if(action.type === 'INCREMENT' || action.type === 'DECREMENT') {
    
    
    AppDispatcher.waitFor([CounterStore.dispatchToken])
    SummaryStore.emit('changed')
  }
})

export default SummaryStore

流程和上面的 Counter 组件流程类似,唯一不同的是 waitFor 函数的应用,它表示需要等 CounterStore 中的派发事件处理完之后再处理这里后面的内容。

到此可以看出,在 Flux 的理念里,如果要改变界面必须改变 Store 中的状态数据,要改变状态数据就必须派发一个 actiondispatcher。在这种规则之下驱动界面改变始于一个动作的派发,别无他法: 我们定义的 action 确实可以参考 http 请求,其中 action.type用来表示对 Model 操作的意图(类似于http 请求的 url 路径),其他字段,用来描述怎样具体操作 Model(类似于 http 请求的参数)

Flux 不足

  • 如果两个 Store 之间有逻辑关系就必须用上 waitFor 函数
  • Store 混杂了很多的逻辑和状态数据

所以,Redux 出现了…

Redux

基本原则

唯一数据源

应用的状态数据应该只存储在唯一的一个 Store

在 Flux 中,应用可以拥有多个 Store,根据功能把应用状态数据进行划分存储给若干个 Store 中,这样容易造成数据冗余,虽然利用 waitFor 可以保证多个 Store 之间的更新顺序,但是产生了不同 Store 之间的依赖关系,说好的不依赖呢?相互独立呢?所以,Redux 整个应用只保持一个 Store,那如何涉及这个 Store 结构就是 Redux 的核心问题了。

保持状态数据只读

不能直接修改状态数据,要修改必须通过派发的方式,而修改状态数据的方法不是去修改状态数据的值,而是创建一个新的状态对象返回给 Redux,由 Redux 自己去完成新状态数据的组装

状态数据改变只能通过纯函数完成

这里所说的纯函数就是 Reducer

r e d u c e r ( p r e v i o u s S t a t e , a c t i o n ) = > n e w S t a t e reducer(previousState, action) => newState reducer(previousState,action)=>newState

第一个参数 previousState 是当前状态,第二个参数 action 是接收到的 action 对象(想象一下 http 请求),reducer 根据这两个参数产生一个新的对象返回,只负责计算状态数据,不负责存储状态数据。

实例说明

统一处理器

// store/index.js
import {
    
     createStore } from 'redux'
import Reducer from './Reducer'

const initValues = {
    
    
  'First': 0,
  'Second': 10,
  'Third': 30
}
const store = createStore(Reducer, initValues)
export default store
// Reducer.js
const Reducer = (state, action) => {
    
    
  let _caption = action.counterCaption
  // 不会修改 state 本身的值,因为 reducer 是一个纯函数不产生副作用,而是返回一个新值
  switch (action.type) {
    
    
    case 'INCREMENT':
      return {
    
    ...state, [_caption]: state[_caption] + 1}
    case 'DECREMENT':
      return {
    
    ...state, [_caption]: state[_caption] - 1}
    default:
      return state
  }
}

export default Reducer
// Actions.js
export const increment = counterCaption => {
    
    
  return {
    
    
    type: 'INCREMENT',
    counterCaption
  }
}

export const decrement = counterCaption => {
    
    
  return {
    
    
    type: 'DECREMENT',
    counterCaption
  }
}

Counter组件流程

import {
    
     Component } from 'react'
import Store from './store'
import * as Actions from './store/Actions'

class Counter extends Component {
    
    
  constructor(props) {
    
    
    super(props)
    this.state = {
    
    
      count: Store.getState()[props.caption]
    }
  }
  onClickIncrementButton = () => {
    
    
    Store.dispatch(Actions.increment(this.props.caption))
  }
  onClickDecrementButton = () => {
    
    
    Store.dispatch(Actions.decrement(this.props.caption))
  }
  componentDidMount() {
    
    
    Store.subscribe(() => {
    
    
      this.setState({
    
    count: Store.getState()[this.props.caption]})
    })
  }
  render() {
    
    
    return (
      <div>
        <button onClick={
    
    this.onClickIncrementButton}>+</button>
        <button onClick={
    
    this.onClickDecrementButton}>-</button>
        <span>{
    
    this.props.caption} count: {
    
    this.state.count}</span>
      </div>
    )
  }
}

export default Counter
  • Store.getState() 可以获取到当前的 state 的值
  • Store.dispatch(action) 时, Reducer 会接收到 prevState 和这里的 action进行数据处理生成 newState
  • Store.subscribe() 可以监听到 state 发生变化(即得到了 newState),好进行下一步操作

同理 Summary 组件流程

Summary 组件流程

import {
    
     Component } from 'react'
import Store from './store'

class Summary extends Component {
    
    
  constructor() {
    
    
    super()
    this.state = {
    
    
      sum: this.getSum()
    }
  }
  getSum() {
    
    
    const _state = Store.getState()
    let _sum = 0
    for(let key in _state) {
    
    
      if(_state.hasOwnProperty(key)) {
    
    
        _sum+=_state[key]
      }
    }
    return _sum
  }
  componentDidMount() {
    
    
    Store.subscribe(() => {
    
    
      this.setState({
    
    sum: this.getSum()})
    })
  }
  render() {
    
    
    return (
      <div>
        <span>Total Count: {
    
    this.state.sum}</span>
      </div>
    )
  }
}

export default Summary

@reduxjs/toolkit 改写

Redux 推荐使用 @reduxjs/toolkit 这个库,它其实原理跟 Redux 是一样的,只是更加方便我们的书写方式。看如何改写上面的实例

// store/index.js
import {
    
     configureStore } from '@reduxjs/toolkit'
import CounterSlice from './CounterSlice'


const store = configureStore({
    
    
  reducer: {
    
    
    counter: CounterSlice.reducer
  }
})

export default store
// CounterSlice.js
import {
    
     createSlice } from '@reduxjs/toolkit'

const CounterSlice = createSlice({
    
    
  name: 'counter',
  initialState: {
    
    
    'First': 0,
    'Second': 10,
    'Third': 30
  },
  reducers: {
    
    
    incremented: (state, {
     
     payload}) => {
    
    
      state[payload] += 1
    },
    decremented: (state, {
     
     payload}) => {
    
    
      state[payload] -= 1
    }
  }
})

export default CounterSlice
// Counter.js
...
constructor(props) {
    
    
  super(props)
  this.state = {
    
    
    count: Store.getState().counter[props.caption]
  }
}
onClickIncrementButton = () => {
    
    
  Store.dispatch(CounterSlice.actions.incremented(this.props.caption))
}
onClickDecrementButton = () => {
    
    
  Store.dispatch(CounterSlice.actions.decremented(this.props.caption))
}
componentDidMount() {
    
    
  Store.subscribe(() => {
    
    
    this.setState({
    
    count: Store.getState().counter[this.props.caption]})
  })
}
...
// Summary.js
...
constructor() {
    
    
  super()
  this.state = {
    
    
    sum: this.getSum()
  }
}
getSum() {
    
    
  const _state = Store.getState().counter
  let _sum = 0
  for(let key in _state) {
    
    
    if(_state.hasOwnProperty(key)) {
    
    
      _sum+=_state[key]
    }
  }
  return _sum
}
componentDidMount() {
    
    
  Store.subscribe(() => {
    
    
    this.setState({
    
    sum: this.getSum()})
  })
}
...
  • configureStore 会使用默认设置自动设置好 store
  • createSlice允许将 reducerstateaction 集中成模块形式书写,这样可以将关联的数据动作统一在一个文件中进行
  • createSlicename 是唯一标识
  • reducers 方式通过使用 immer 包会把修改属性值得方式按照不可变方式修改 state,不需要手动做副本
  • Store.dispatch(CounterSlice.actions.incremented(this.props.caption)) 可以用来触发 action,是因为 reducers 方法中的每一个 case 都会生成一个 action

猜你喜欢

转载自blog.csdn.net/weixin_43443341/article/details/127360101