Bootstrap

React学习 day-07(redux)

day-07

Redux 基础

今日目标

✔ 掌握 Redux 核心概念和基本使用。

✔ 掌握 react-redux 的使用。

✔ 掌握 TODOLIST 的实现思路。

了解 Redux

目标

能够说出为什么需要使用 Redux。

内容

Redux 中文官网中文文档

  • 概念

Redux 是一个全局状态管理的 JS 库。

  • 背景

    a,React 的定位只是一个用来构建用户界面的库,并不是 Web 应用的完整解决方案。因此 React 在涉及到复杂组件之间的通信时会比较棘手,而对于大型项目来说,这方面恰恰是最关键的,因此只用 React 写大型项目会比较吃力。

    b,2014 年 Facebook 提出了 Flux 架构的概念,引发了很多的实现。

    c,2015 年,Redux 出现,将 Flux 与函数式编程(Reducer)结合一起,很短时间内就成为了最热门的前端状态管理库,类似的还有 Mobx、Saga 等状态管理工具。

为什么要用 Redux

在这里插入图片描述

  • 不使用 Redux(图左边)

    a,只能使用父子组件通讯、状态提升等 React 自带机制,理远房亲戚(非父子)关系的组件通讯时乏力。

    b,组件之间的数据流混乱,出现 Bug 时难定位。

  • 使用 Redux(图右边)

    a,集中式存储和管理应用的状态,处理组件通讯问题时,无视组件之间的层级关系。

    b,简化复杂应用中的数据传递问题,数据流清晰,易于定位 Bug。

小结

Redux 解决了什么问题?

React跨组件的通信问题。

Redux 安装

目标

能够在 React 项目中准备 Redux 开发环境。

步骤

  1. 创建 React 项目。
npx create-react-app redux-basic
  1. 启动项目
yarn start
  1. 安装 Redux 包
yarn add redux

Redux 核心概念

目标

能够了解 Redux 三个核心概念的职责。

内容

阮一峰

  • 为了让代码各部分职责清晰、明确,Redux 提出三个核心概念,需要我们写代码的时候遵守。

    a,action(动作):描述要做的事情(要干啥)。

    b,reducer(函数):更新状态(怎么干)。

    c,store(仓库):整合 action 和 reducer(谁来指挥)。

  • 通过例子来理解三个核心概念。

    a,action:相当于公司里面要做的事情,比如打扫卫生这个事等。

    b,reducer:相当于公司的员工,负责执行。

    c,store:相当于公司的老板。

    d,流程:老板(store)分配(dispatch)要做的事情(action)给员工(reducer),员工干完活把结果交给老板。

    e,在视图当中,通过 store dispatch 一个 action,reducer 会自动收到通知来更新 state,state 一旦变化,说有使用 state 的视图自然就变了。

在这里插入图片描述

Redux 核心概念 action

目标

定义一个最基本的 action。

内容

  • action 用来描述要做的事情,项目中的每一个功能都是一个 action,比如:

    a,计数器案例:计数器加 1、减 1。

    b,todomvc 案例:添加任务、删除任务等。

    c,项目:登录,退出等。

  • 特点:

    a,只描述做什么。

    b,本质上就是一个 JS 对象,必须带有 type 属性,用于区分动作的类型。

    c,可以通过 payload 携带额外的数据。

{ type: 'increment' }

// payload: 参数
{ type: 'decrement', payload: 2 }

{ type: 'addTodo', payload: '吃饭' }
{ type: 'addTodo', payload: '睡觉' }
{ type: 'removeTodo', payload: 3 }
store/actions.js
export const incremen = {
    type: 'INCREMENT',
    payload: 5,
}

export const decrement = {
    type: 'DECREMENT',
    payload: 5,
}

Redux 核心概念 action creator

目标

学会使用函数(actionCreator)去创建一个 action。

内容

  1. 问题:直接使用对象来创建 action 不灵活,参数写死了。
  2. 解决:可以使用函数来创建 action,通过传参把不一样的数据传递过去就好了,我们把这个创建 action 的函数叫做 actionCreator。
  3. 返回值:返回一个 action 对象。
  4. 好处:代码更加简洁,容易复用。

核心代码

store/actions.js
export const increment = (payload) => ({
    type: 'INCREMENT',
    payload,
})

export const decrement = (payload) => ({
    type: 'DECREMENT',
    payload,
})

小结

actionCreator 的作用和返回值是什么?

答:actionCreator作用是创建action,可以传参;返回值是action对象。

Redux 核心概念 reducer

目标

能够掌握 reducer 的基本写法。

内容

reducer:本质上是一个函数,作用是根据 action 来更新状态,有如下特点。

  • 函数签名为:(prevState, action) => newState
  • 接收上一次的状态和 action,根据 action 的类型执行对应的操作,最终返回新的状态。
  • 原则:不要在 reducer 函数内部直接修改 state。

store/reducers.js

export default function counter(state = 10, action) {
    // 处理各种各样的 action
    switch (action.type) {
        case 'INCREMENT':
            return state + action.payload
        case 'DECREMENT':
            return state - action.payload
        default:
            // 记得要有默认返回的处理
            return state
    }
}

Redux 核心概念 纯函数

目标

了解纯函数的特点。

内容

  • 纯函数是函数式编程中的概念,对于纯函数来说有一个很重要的特点:相同的输入总是得到相同的输出

  • 纯函数常具有以下特点。

    a,不得改写参数,不能使用全局变量。

    b,不能调用 Date.now() 或者 Math.random() 等不纯的方法,因为每次会得到不一样的结果。

    c,不包含副作用的处理,副作用:AJAX 请求、操作本地数据、或者操作函数外部的变量等。

  • 好处:代码简洁、方便测试、方便性能优化。

  • 为什么说纯函数呢?因为 reducer 要求自身就必须是一个纯函数。

const add = (a, b) => a + b
add(1, 2)
const add = (a, b) => a + b + Math.random()
add(1, 2)
add(1, 2)
const arr = [1, 2, 3, 4, 5]
arr.slice(1, 2)
arr.slice(1, 2)
arr.slice(1, 2)
const arr = [1, 2, 3, 4, 5]
arr.splice(1, 2)
arr.splice(1, 2)

Redux 核心概念 store

目标

掌握 store 的创建和基本使用。

内容

  • store:仓库,是 Redux 的核心,负责整合 action 和 reducer。

  • 特点

    a,一个应用只有一个 store。

    b,创建:const store = createStore(reducer)

    c,获取数据:store.getState()

    d,更新数据:store.dispatch(action)

  • 其他 API

    a,订阅(监听)状态变化:const unSubscribe = store.subscribe(() => {}),注意要订阅,后续的更新才能被观测到。

    b,取消订阅状态变化:unSubscribe()

store/index.js,注意演示上面 API 需要先在 index.js 中引入此文件。

// store: 整个数据的仓库,负责关联 reducer 和 action,通过 store 对象可以给 reducer 分配 action
import { createStore } from 'redux'
import reducer from './reducers'
const store = createStore(reducer)
export default store

小结

通过哪个 API 来创建 Store?创建 Store 需要什么参数?

答:通过createStore来创建Store,创建Store需要reducer。

点击计数 📝

目标

掌握在视图中使用和更新数据。

读取数据

在这里插入图片描述

App.js

import React from 'react'
import store from './store'

export default function App() {
    return (
        <div>
            <h3>count: {store.getState()}</h3>
        </div>
    )
}

更改数据

import React from 'react'
import store from './store'
import { increment, decrement } from './store/actions'

export default function App() {
    return (
        <div>
            <p>count: {store.getState()}</p>
            <div>
                <button onClick={() => store.dispatch(increment(1))}>+1</button>
                <button onClick={() => store.dispatch(increment(5))}>+5</button>
                <button onClick={() => store.dispatch(decrement(1))}>-1</button>
                <button onClick={() => store.dispatch(decrement(5))}>-5</button>
            </div>
        </div>
    )
}

解决问题

import ReactDOM from 'react-dom'
import App from './App'
import store from './store'

ReactDOM.render(<App />, document.querySelector('#root'))

store.subscribe(() => {
    ReactDOM.render(<App />, document.querySelector('#root'))
})

小结

store 中数据的变化,通过哪个方法可以被观测到?如何让视图更新?

答:store.subscribe()方法可以观测到store中数据的变化,重新渲染数据。

Redux 执行过程

目标

了解 Redux 的执行过程。

获取默认值的执行过程

  • 只要创建 store,Redux 内部就会调用一次 reducer,打印试一下 console.log(action.type)

  • 类似:reducer(undefined, {type: "@@redux/INITv.a.4.t.t.p"})

  • 这一次调用 reducer 的目的:获取状态的默认值

  • 因为传入的状态值是 undefined ,并且是一个随机的 action type,所以!

    a,状态值因为 undefined,所以,我们设置的默认值就会生效,比如,此处的:10。

    b,因为是一个随机的 action type,所以,reducer 中 switch 一定无法命中,那就一定会走 default,也就是直接返回了状态的默认值,也就是:10。

  • Redux 内部拿到这个数据(比如此处的 10)以后,就用这个数据作为了 store 中的最新状态值。

  • 因此,将来当我们调用 store.getState() 方法来获取 Redux 状态值的时候,拿到的就是 10 了。

import { createStore } from 'redux'
import reducer from './reducer'
// 只要创建 store 传递了 reducer,Redux 内部就会自动的 dispatch 一次 action
// 目的:就是为了 store 能够有初始值,store.dispatch({ type: '@@xx699' })
const store = createStore(reducer)

// 所以可以拿到初始值
store.getState()

export default store

点击按钮后的执行过程

  1. 点击按钮,派发动作 store.dispatch(action)
  2. 只要触发了 action,Redux 内部就会自动调用 reducer,根据上一次的状态和 action 计算出新的状态并返回。
  3. reducer 执行完毕后,将最新的状态交给 store,store 用最新的状态替换旧状态,状态更新完毕。

小结

点击按钮视图更新的流程是什么?

答:1.点击按钮,派发动作store.dispatch(action)

​ 2.只要触发了action,Redux内部就会自动调用reducer,根据上一次的状态和action计算出新的状态并返回。

​ 3.reducer执行完毕后,将最新的状态交给store,store用最新的状态替换

react-redux

基本介绍

  • 问题:为什么要使用 react-redux 这个库?
  • 回答:React 和 Redux 是两个独立的库,两者之间职责独立,因此,为了更好的实现在 React 中使用 Redux 进行状态管理,就需要一种机制,将这两个独立的库关联在一起,这就是 react-redux 出现的原因。
  • react-redux 是 Redux 官方提供的 React 绑定库。

基本使用

  1. 安装
yarn add react-redux
  1. 配置
index.js
import ReactDOM from 'react-dom'
import App from './App.js'
import store from './store/store.js'
// 1.导入包
import { Provider } from 'react-redux'

// 2.通过 Provider 包裹根组件并提供 store 供其他组件内部使用
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.querySelector('#root')
)

// 用了 react-redux 下面手动触发更新的方式就没用了
/* store.subscribe(() => {
    ReactDOM.render(<App />, document.querySelector('#root'))
}) */

✍ 一旦使用了 react-redux,获取和更新数据的方式就变化了,要按照这个库的要求来。

useSelector

  • react-redux 这个库提供了 useSelector,作用:从 store 中获取状态。
  • selector 函数应该是个纯函数。
App.js
import React from 'react'
import { useSelector } from 'react-redux'
const App = () => {
    //3.获取store中的数据,接收函数,函数参数代表所有的状态,返回值表示需要返回的状态
    const count = useSelector((state) => state)
    return (
        <div>
            <h3>{count}</h3>
        </div>
    )
}
export default App

useDispatch

作用:得到 dispatch 来触发 action(触发 action 会执行 reducer,reducer 负责数据的修改,react-redux 内部会监听数据的变化自动进行视图更新)。

App.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/actions'
const App = () => {
    const count = useSelector((state) => state)
    //4.使用useDispatch hook生成dispatch
    const dispatch = useDispatch()
    return (
        <div>
            <h3>{count}</h3>
            <div>
                <button onClick={() => dispatch(increment(1))}>+1</button>
                <button onClick={() => dispatch(increment(5))}>+5</button>
                <button onClick={() => dispatch(decrement(1))}>-1</button>
                <button onClick={() => dispatch(decrement(5))}>-5</button>
            </div>
        </div>
    )
}
export default App

如果 Test 组件想用,看看有多方便吧,无需传值,直接拽过来!

import React from 'react'
import { useSelector } from 'react-redux'

export default function Test() {
    const money = useSelector((state) => state)
    return <div>{money}</div>
}

Reducer 的分离与合并

概述

  • 随着项目功能变得越来越复杂,需要 Redux 管理的状态也会越来越多,此时,有两种方式来处理状态的更新。

    a,使用一个 reducer,处理项目中所有状态。

    b,使用多个 reducer,按照项目功能划分,每一个 reducer 处理该功能的状态。

  • 推荐:使用第二种方案,每个 reducer 处理单一功能的状态,职责更明确。

  • 问题:此时项目中会有多个 reducer,但是 store 只能接收一个 reducer,因此需要将多个 reducer 合并为一个 reducer,才能传递给 store 使用。

  • 解决:使用 Redux 中的 combineReducers({ counter: counterReducer, user: userReducer }) 函数。

  • 注意:组件中再想只使用 counter 的状态,需要 const count = useSelector((state) => state.counter)

  • 每个 reducer 应该只关注自己的数据,例如:

    a,登录功能:loginReducer 处理的只应该是跟登录相关的状态。

    b,个人资料:profileReducer 处理的只应该是跟个人资料相关的状态。

    c,文章列表、文章详情、文章评论等。

步骤

  1. reducers.js 中新建 userReducer。
  2. 通过 combineReducers 合并 counter 和 user 并导出。
  3. 修改 App.jsTest.js 获取数据的方式。
  4. 新建 User.js 测试 userReducer 的使用。

代码

reducers.js
import { combineReducers } from 'redux'

function counter(state = 10, action) {
    switch (action.type) {
        case 'INCREMENT':
            return state + action.payload
        case 'DECREMENT':
            return state - action.payload
        default:
            return state
    }
}

function user(state = { name: 'ifer', age: 18 }, action) {
    switch (action.type) {
        case 'UPDATENAME':
            return {
                ...state,
                name: action.payload,
            }
        default:
            return state
    }
}

export default combineReducers({
    counter,
    user,
})
App.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment, decrement } from './store/actions'
import Test from './Test'
import User from './User'
const App = () => {
    const count = useSelector((state) => state.counter)
    const dispatch = useDispatch()
    return (
        <div>
            <h3>{count}</h3>
            <div>
                <button onClick={() => dispatch(increment(1))}>+1</button>
                <button onClick={() => dispatch(increment(5))}>+5</button>
                <button onClick={() => dispatch(decrement(1))}>-1</button>
                <button onClick={() => dispatch(decrement(5))}>-5</button>
            </div>
            <Test />
            <User />
        </div>
    )
}
export default App
Test.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { increment } from './store/actions'

export default function Test() {
    const count = useSelector((state) => state.counter)
    const dispatch = useDispatch()
    return (
        <div>
            <p>Test {count}</p>
            <div>
                <button onClick={() => dispatch(increment(5))}>click</button>
            </div>
        </div>
    )
}
User.js
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { updateName } from './store/actions'

export default function User() {
    const user = useSelector((state) => state.user)
    const dispatch = useDispatch()
    const handleClick = () => {
        dispatch(updateName('xxx'))
    }
    return (
        <div>
            <h3>{user.name}</h3>
            <button onClick={handleClick}>update name</button>
        </div>
    )
}
actions.js
export const updateName = (payload) => ({
    type: 'UPDATENAME',
    payload,
})

ActionTypes

概述

  • 是什么:action 对象中的 type 属性。
  • Redux 项目中,同一个 type 会在不同文件中多次被用到,比如 actions.js、reducers.js 等。
  • 目标:集中处理 action type,保持一致性,容易维护!

操作

  1. 在 store 目录中创建 actionTypes.jsconstants.js 文件。
  2. 使用常量创建 ActionType 并导出。
  3. 命名推荐:模块_动作,比如:
    • 计数器:COUNTER_INCREMENT 表示计数器模块中的 INCREMENT 动作。
    • TodoList:TODOLIST_ADD 表示 TodoList 案例中 ADD 动作。
    • 登录:LOGIN_GETCODE 表示登录模块中获取验证码的动作,LOGIN_SUBMIT 表示登录模块中的提交功能。
    • 个人信息:PROFILE_GETINFO 表示个人资料模块中的获取信息动作;PROFILE_UPDATEINFO 等。
  4. 哪里需要用到就按需导入。

代码

actionTypes.js
export const COUNTER_INCREMENT = 'COUNTER_INCREMENT'
export const COUNTER_DECREMENT = 'COUNTER_DECREMENT'
export const USER_UPDATENAME = 'USER_UPDATENAME'
actions.js
import { COUNTER_INCREMENT, COUNTER_DECREMENT, USER_UPDATENAME } from './actionTypes'
export const increment = (payload) => ({
    type: COUNTER_INCREMENT,
    payload,
})

export const decrement = (payload) => ({
    type: COUNTER_DECREMENT,
    payload,
})

export const updateName = (payload) => ({
    type: USER_UPDATENAME,
    payload,
})
reducers.js
import { combineReducers } from 'redux'
import { COUNTER_INCREMENT, COUNTER_DECREMENT, USER_UPDATENAME } from './actionTypes'

function counter(state = 10, action) {
    switch (action.type) {
        case COUNTER_INCREMENT:
            return state + action.payload
        case COUNTER_DECREMENT:
            return state - action.payload
        default:
            return state
    }
}

function user(state = { name: 'ifer', age: 18 }, action) {
    switch (action.type) {
        case USER_UPDATENAME:
            return {
                ...state,
                name: action.payload,
            }
        default:
            return state
    }
}

export default combineReducers({
    counter,
    user,
})

在项目中配置和使用Redux完整流程

1.先准备reducer

export default function counter(state = 10, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + action.payload
    case 'DECREMENT':
      return state - action.payload
    default:
      return state
  }
}

2.通过combineReducers合并reducer得到rootReducer

import { combineReducers } from 'redux'
import counter from './counter'
const rootReducer = combineReducers({ counter })
export default rootReducer

3.根据rootReducer创建store

export default createStore(rootReducer)

4.在入口文件配置react-redux并传递store

import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.querySelector('#root')
)

5.获取状态

import { useSelector } from 'react-redux'
const App = () => {
  const counter = useSelector((state) => state.counter)
  return (
    <div>
      <h3>{counter}</h3>
    </div>
  )
}
export default App

6.修改状态

import { useDispatch } from 'react-redux'
import { increment } from './store/actions'
const App = () => {
  const dispatch = useDispatch()
  return (
    <div>
      <button onClick={() => dispatch(increment(1))}>+1</button>
    </div>
  )
}
export default App

中间件概述

目标

能够理解为什么需要 Redux 中间件。

内容

默认情况下,Redux 自身只能处理同步数据流,但是在实际项目开发中,状态的更新、获取,通常是使用异步操作来实现。

  • 问题:如何在 Redux 中进行异步操作呢?
  • 回答:通过 Redux 中间件机制来实现。
  • 中间件,可以理解为处理一个功能的中间环节,对于 Redux 中间件来说就是在数据到达 reducer 之前进行一系列的处理操作。

触发时机

  • Redux 中间件执行时机:在 dispatching action 和 到达 reducer 之间

    a,没有中间件:dispatch(action) => reducer

    b,使用中间件:dispatch(action) => 执行中间件代码 => reducer

  • 原理:封装了 Redux 的 dispatch 方法。

    a,没有中间件:store.dispatch() 使用的是 Redux 库自己提供的 dispatch 方法,用来发起状态更新。

    b,使用中间件:store.dispatch() 使用的是中间件封装处理后的 dispatch,但是最终还是会调用 Redux 库自己提供的 dispatch 方法。

没有中间件

在这里插入图片描述

有中间件

在这里插入图片描述

logger 中间件

  1. 安装:yarn add redux-logger
  2. 导入 redux-logger。
  3. 从 redux 中导入 applyMiddleware 函数。
  4. 将 applyMiddleware() 调用作为 createStore 函数的第二个参数。
  5. 调用 applyMiddleware 函数时,将 logger 作为参数传入。
  6. 后续调用 store.dispatch() 时,控制台就会有日志信息输出。
store/index.js
import { createStore, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import rootReducer from './reducers'
export default createStore(rootReducer, applyMiddleware(logger))

redux-thunk 使用

redux-thunk 中间件可以处理函数形式的 action,而在函数形式的 action 中就可以执行异步操作代码,完成异步操作。

  1. 安装:yarn add redux-thunk
  2. 导入 redux-thunk。
  3. 将 thunk 添加到中间件列表中。
  4. 修改 action creator,返回一个函数。
  5. 在函数形式的 action 中执行异步操作,在异步操作成功后,分发 action 更新状态。
store/index.js
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import rootReducer from './reducers'
export default createStore(rootReducer, applyMiddleware(thunk, logger))
store/actions/todo.js
export const clearTodo = () => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch({
                type: CLEAR_TODO,
            })
        }, 1000)
    }
}

redux-devtools-extension

目标

开发 React 项目时,能够通过 Chrome 开发者工具调试跟踪 Redux 状态。

步骤

  1. 保证浏览器安装了 Redux 的开发者工具。
  2. 通过包管理器在项目中安装 yarn add redux-devtools-extension
  3. 在 store/index.js 中进行配置。
  4. 启动 react 项目,打开 chrome 开发者工具,测试

文档 redux-devtools-exension

import { createStore, applyMiddleware } from 'redux'
import reducer from './reducers'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
export default createStore(reducer, composeWithDevTools(applyMiddleware(thunk)))
;