当前位置:   article > 正文

React基础学习-Day03

React基础学习-Day03

React基础学习-Day03

1.Redux 概述和概念

你将学到什么
  • Redux 是什么,为什么需要使用它
  • Redux 的关键术语和概念
  • Redux 如何处理数据流

Redux 是什么?

首先理解 “Redux” 是什么。它有什么作用?它帮助我解决什么问题?我为什么要使用它?

Redux 是一个使用叫做 “action” 的事件来管理和更新应用状态的模式和工具库 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

为什么要使用 Redux?

Redux 帮你管理“全局”状态 - 应用程序中的很多组件都需要的状态。

Redux 提供的模式和工具使你更容易理解应用程序中的状态何时、何地、为什么、state 如何被更新,以及当这些更改发生时你的应用程序逻辑将如何表现. Redux 指导你编写可预测和可测试的代码,这有助于你确信你的应用程序将按预期工作。

我什么时候应该使用 Redux?

Redux 可帮助你处理共享状态的管理,但与任何工具一样,它也需要权衡利弊。使用 Redux 有更多的概念需要学习,还有更多的代码需要编写,需要添加了一些额外代码,并要求你遵循某些限制。这是短期和长期生产力之间的权衡。

在以下情况下使用 Redux:

  • 应用中有很多 state 在多个组件中需要使用
  • 应用 state 会随着时间的推移而频繁更新
  • 更新 state 的逻辑很复杂
  • 中型和大型代码量的应用,很多人协同开发

并非所有应用程序都需要 Redux。

Redux 库和工具

Redux 是一个小型的独立 JS 库。 但是,它通常与其他几个包一起使用:

React-Redux

Redux 可以集成到任何的 UI 框架中,其中最常见的是 React 。React-Redux 是我们的官方包,它可以让 React 组件访问 state 片段和 dispatch actions 更新 store,从而同 Redux 集成起来。

Redux Toolkit

Redux Toolkit 是我们推荐的编写 Redux 逻辑的方法。 它包含我们认为对于构建 Redux 应用程序必不可少的包和函数。 Redux Toolkit 构建是我们建议的最佳实践中,简化了大多数 Redux 任务,预防了常见错误,并使编写 Redux 应用程序变得更加容易。

Redux DevTools 扩展

Redux DevTools 扩展 可以显示 Redux 存储中状态随时间变化的历史记录。这允许你有效地调试应用程序,包括使用强大的技术,如“时间旅行调试”。

Redux 术语和概念

在我们深入研究一些实际代码之前,让我们先谈谈使用 Redux 需要了解的一些术语和概念。

State 管理

让我们从一个小的 React 计数器组件开始。 它跟踪组件状态中的数字,并在单击按钮时增加数字:

function Counter() {
  // State: counter 值
  const [counter, setCounter] = useState(0)

  // Action: 当事件发生后,触发状态更新的代码
  const increment = () => {
    setCounter(prevCounter => prevCounter + 1)
  }

  // View: 视图定义
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  )
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

这是一个包含以下部分的自包含应用程序:

  • state:驱动应用的真实数据源头
  • view:基于当前状态的视图声明性描述
  • actions:根据用户输入在应用程序中发生的事件,并触发状态更新

接下来简要介绍 “单向数据流(one-way data flow)”:

  • 用 state 来描述应用程序在特定时间点的状况
  • 基于 state 来渲染出 View
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新,生成新的 state
  • 基于新的 state 重新渲染 View

单向数据流

然而,当我们有多个组件需要共享和使用相同 state时,可能会变得很复杂,尤其是当这些组件位于应用程序的不同部分时。有时这可以通过 “提升 state” 到父组件来解决,但这并不总是有效。

解决这个问题的一种方法是从组件中提取共享 state,并将其放入组件树之外的一个集中位置。这样,我们的组件树就变成了一个大“view”,任何组件都可以访问 state 或触发 action,无论它们在树中的哪个位置!

通过定义和分离 state 管理中涉及的概念并强制执行维护 view 和 state 之间独立性的规则,代码变得更结构化和易于维护。

这就是 Redux 背后的基本思想:应用中使用集中式的全局状态来管理,并明确更新状态的模式,以便让代码具有可预测性。

不可变性 Immutability

“Mutable” 意为 “可改变的”,而 “immutable” 意为永不可改变。

JavaScript 的对象(object)和数组(array)默认都是 mutable 的。如果我创建一个对象,我可以更改其字段的内容。如果我创建一个数组,我也可以更改内容:

const obj = { a: 1, b: 2 }
// 对外仍然还是那个对象,但它的内容已经变了
obj.b = 3

const arr = ['a', 'b']
// 同样的,数组的内容改变了
arr.push('c')
arr[1] = 'd'
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这就是 改变 对象或数组的例子。内存中还是原来对象或数组的引用,但里面的内容变化了。

如果想要不可变的方式来更新,代码必需先*复制*原来的 object/array,然后更新它的复制体

JavaScript array/object 的展开运算符(spread operator)可以实现这个目的:

const obj = {
  a: {
    // 为了安全的更新 obj.a.c,需要先复制一份
    c: 3
  },
  b: 2
}

const obj2 = {
  // obj 的备份
  ...obj,
  // 覆盖 a
  a: {
    // obj.a 的备份
    ...obj.a,
    // 覆盖 c
    c: 42
  }
}

const arr = ['a', 'b']
// 创建 arr 的备份,并把 c 拼接到最后。
const arr2 = arr.concat('c')

// 或者,可以对原来的数组创建复制体
const arr3 = arr.slice()
// 修改复制体
arr3.push('c')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

Redux 期望所有状态更新都是使用不可变的方式

Action

action 是一个具有 type 字段的普通 JavaScript 对象。你可以将 action 视为描述应用程序中发生了什么的事件.

type 字段是一个字符串,给这个 action 一个描述性的名字,比如"todos/todoAdded"。我们通常把那个类型的字符串写成“域/事件名称”,其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情。

action 对象可以有其他字段,其中包含有关发生的事情的附加信息。按照惯例,我们将该信息放在名为 payload 的字段中。

一个典型的 action 对象可能如下所示:

const addTodoAction = {
  type: 'todos/todoAdded',
  payload: 'Buy milk'
}
  • 1
  • 2
  • 3
  • 4
Action Creator

action creator 是一个创建并返回一个 action 对象的函数。它的作用是让你不必每次都手动编写 action 对象:

const addTodo = text => {
  return {
    type: 'todos/todoAdded',
    payload: text
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
Reducer

reducer 是一个函数,接收当前的 state 和一个 action 对象,必要时决定如何更新状态,并返回新状态。函数签名是:(state, action) => newState你可以将 reducer 视为一个事件监听器,它根据接收到的 action(事件)类型处理事件。

说明

“Reducer” 函数的名字来源是因为它和 Array.reduce() 函数使用的回调函数很类似。

Reducer 必需符合以下规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

reducer 函数内部的逻辑通常遵循以下步骤:

  • 检查 reducer 是否关心这个 action
    • 如果是,则复制 state,使用新值更新 state 副本,然后返回新 state
  • 否则,返回原来的 state 不变

下面是 reducer 的小例子,展示了每个 reducer 应该遵循的步骤:

const initialState = { value: 0 }

function counterReducer(state = initialState, action) {
  // 检查 reducer 是否关心这个 action
  if (action.type === 'counter/increment') {
    // 如果是,复制 `state`
    return {
      ...state,
      // 使用新值更新 state 副本
      value: state.value + 1
    }
  }
  // 返回原来的 state 不变
  return state
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

Reducer 可以在内部使用任何类型的逻辑来决定新状态应该是什么,如 if/elseswitch、循环等等。

Store

当前 Redux 应用的 state 存在于一个名为 store 的对象中。

store 是通过传入一个 reducer 来创建的,并且有一个名为 getState 的方法,它返回当前状态值:

import { configureStore } from '@reduxjs/toolkit'

const store = configureStore({ reducer: counterReducer })

console.log(store.getState())
// {value: 0}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
Dispatch

Redux store 有一个方法叫 dispatch更新 state 的唯一方法是调用 store.dispatch() 并传入一个 action 对象。 store 将执行所有 reducer 函数并计算出更新后的 state,调用 getState() 可以获取新 state。

store.dispatch({ type: 'counter/increment' })

console.log(store.getState())
// {value: 1}
  • 1
  • 2
  • 3
  • 4

dispatch 一个 action 可以形象的理解为 “触发一个事件”。发生了一些事情,我们希望 store 知道这件事。 Reducer 就像事件监听器一样,当它们收到关注的 action 后,它就会更新 state 作为响应。

我们通常调用 action creator 来调用 action:

const increment = () => {
  return {
    type: 'counter/increment'
  }
}

store.dispatch(increment())

console.log(store.getState())
// {value: 2}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
Selector

Selector 函数可以从 store 状态树中提取指定的片段。随着应用变得越来越大,会遇到应用程序的不同部分需要读取相同的数据,selector 可以避免重复这样的读取逻辑:

const selectCounterValue = state => state.value

const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
  • 1
  • 2
  • 3
  • 4
  • 5

Redux 数据流

早些时候,我们谈到了“单向数据流”,它描述了更新应用程序的以下步骤序列:

  • State 描述了应用程序在特定时间点的状况
  • 基于 state 来渲染视图
  • 当发生某些事情时(例如用户单击按钮),state 会根据发生的事情进行更新
  • 基于新的 state 重新渲染视图

具体来说,对于 Redux,我们可以将这些步骤分解为更详细的内容:

  • 初始启动:
    • 使用最顶层的 root reducer 函数创建 Redux store
    • store 调用一次 root reducer,并将返回值保存为它的初始 state
    • 当视图 首次渲染时,视图组件访问 Redux store 的当前 state,并使用该数据来决定要呈现的内容。同时监听 store 的更新,以便他们可以知道 state 是否已更改。
  • 更新环节:
    • 应用程序中发生了某些事情,例如用户单击按钮
    • dispatch 一个 action 到 Redux store,例如 dispatch({type: 'counter/increment'})
    • store 用之前的 state 和当前的 action 再次运行 reducer 函数,并将返回值保存为新的 state
    • store 通知所有订阅过的视图,通知它们 store 发生更新
    • 每个订阅过 store 数据的视图 组件都会检查它们需要的 state 部分是否被更新。
    • 发现数据被更新的每个组件都强制使用新数据重新渲染,紧接着更新网页

动画的方式来表达数据流更新:

数据流更新动画

Redux 确实有许多新的术语和概念需要记住。提醒一下,这是我们刚刚介绍的内容:

总结
  • Redux 是一个管理全局应用状态的库
    • Redux 通常与 React-Redux 库一起使用,把 Redux 和 React 集成在一起
    • Redux Toolkit 是编写 Redux 逻辑的推荐方式
  • Redux 使用 “单向数据流”
    • State 描述了应用程序在某个时间点的状态,视图基于该 state 渲染
    • 当应用程序中发生某些事情时:
      • 视图 dispatch 一个 action
      • store 调用 reducer,随后根据发生的事情来更新 state
      • store 将 state 发生了变化的情况通知 UI
    • 视图基于新 state 重新渲染
  • Redux 有这几种类型的代码
    • Action 是有 type 字段的纯对象,描述发生了什么
    • Reducer 是纯函数,基于先前的 state 和 action 来计算新的 state
    • 每当 dispatch 一个 action 后,store 就会调用 root reducer

2.计数器示例应用程序

应用目录

你已经知道了应用的功能,现在看一下代码实现。

以下是构成此应用程序的关键文件:

  • /src
    	index.js: app 入口
    	App.js: 顶级 React 组件
    /app
    	store.js: 创建 Redux store 实例
    /features
    	/counter
    		Counter.js: 展示 counter 特性的 React 组件
    		counterSlice.js: counter 特性相关的 redux 逻辑
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

让我们先看看 Redux store 是如何创建的。

创建 Redux Store

打开 app/store.js,代码是这样:

app/store.js

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'

export default configureStore({
  reducer: {
    counter: counterReducer
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Redux store 是使用 Redux Toolkit 中的 configureStore 函数创建的。configureStore 要求我们传入一个 reducer 参数。

我们的应用程序可能由许多不同的特性组成,每个特性都可能有自己的 reducer 函数。当我们调用configureStore 时,我们可以传入一个对象中的所有不同的 reducer。 对象中的键名 key 将定义最终状态树中的键名 key。

我们有一个名为 features/counter/counterSlice.js 的文件,它为计数器逻辑导出了一个 reducer 函数。 我们可以在此处导入 counterReducer 函数,并在创建 store 时包含它。

当我们传入一个像 {counter: counterReducer} 这样的对象时,它表示我们希望在 Redux 状态对象中有一个 state.counter 部分,并且我们希望每当 dispatch action 时 counterReducer 函数负责决定是否以及如何更新 state.counter 部分。

Redux 允许使用不同类型的插件(“middleware”和“enhancers”)自定义 store 设置。configureStore 默认会自动在 store setup 中添加几个中间件以提供良好的开发者体验,并且还设置 store 以便 Redux DevTools Extension 可以检查其内容。

Redux Slice

“slice” 是应用中单个功能的 Redux reducer 逻辑和 action 的集合, 通常一起定义在一个文件中。该名称来自于将根 Redux 状态对象拆分为多个状态 “slice”。

比如,在一个博客应用中,store 的配置大致长这样:

import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

例子中,state.usersstate.posts,和 state.comments 均是 Redux state 的一个 独立的 “slice”。由于 usersReducer 负责更新 state.users slice,我们将其称为 “slice reducer” 函数。

创建 Slice Reducer 和 Action

既然我们知道counterReducer 函数来自 features/counter/counterSlice.js,那么让我们一块一块地看看那个文件里有什么。

features/counter/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。
      // 并不是真正的改变 state 因为它使用了 immer 库
      // 当 immer 检测到 "draft state" 改变时,会基于这些改变去创建一个新的
      // 不可变的 state
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

早些时候,我们看到单击视图中的不同按钮会 dispatch 三种不同类型的 Redux action:

  • {type: "counter/increment"}
  • {type: "counter/decrement"}
  • {type: "counter/incrementByAmount"}

我们知道 action 是带有 type 字段的普通对象,type 字段总是一个字符串,并且我们通常有 action creator 函数来创建和返回 action 对象。那么在哪里定义 action 对象、类型字符串和 action creator 呢?

我们可以每次都手写。但是,那会很乏味。此外,Redux 中真正重要的是 reducer 函数,以及其中计算新状态的逻辑。

Redux Toolkit 有一个名为 createSlice 的函数,它负责生成 action 类型字符串、action creator 函数和 action 对象的工作。你所要做的就是为这个 slice 定义一个名称,编写一个包含 reducer 函数的对象,它会自动生成相应的 action 代码。name 选项的字符串用作每个 action 类型的第一部分,每个 reducer 函数的键名用作第二部分。因此,"counter" 名称 + "increment" reducer 函数生成了一个 action 类型 {type: "counter/increment"}。(毕竟,如果计算机可以为我们做,为什么要手写!)

除了 name 字段,createSlice 还需要我们为 reducer 传入初始状态值,以便在第一次调用时就有一个 state。在这种情况下,我们提供了一个对象,它有一个从 0 开始的 value 字段。

我们可以看到这里有三个 reducer 函数,它们对应于通过单击不同按钮 dispatch 的三种不同的 action 类型。

createSlice 会自动生成与我们编写的 reducer 函数同名的 action creator。我们可以通过调用其中一个来检查它并查看它返回的内容:

console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
  • 1
  • 2

它还生成知道如何响应所有这些 action 类型的 slice reducer 函数:

const newState = counterSlice.reducer(
  { value: 10 },
  counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Reducer 的规则

上一节讲过,Reducer 必需符合以下规则:

  • 仅使用 stateaction 参数计算新的状态值
  • 禁止直接修改 state。必须通过复制现有的 state 并对复制的值进行更改的方式来做 不可变更新(immutable updates)
  • 禁止任何异步逻辑、依赖随机值或导致其他“副作用”的代码

但为什么这些规则很重要?有几个不同的原因:

  • Redux 的目标之一是使你的代码可预测。当函数的输出仅根据输入参数计算时,更容易理解该代码的工作原理并对其进行测试。
  • 另一方面,如果一个函数依赖于自身之外的变量,或者行为随机,你永远不知道运行它时会发生什么。
  • 如果一个函数 mutate 了其他对象,比如它的参数,这可能会意外地改变应用程序的工作方式。这可能是错误的常见来源,例如“我更新了我的状态,但现在我的视图没有在应该更新的时候更新!”
  • Redux DevTools 的一些功能取决于你的 reducer 是否正确遵循这些规则

“不可变更新(Immutable Updates)” 这个规则尤其重要,值得进一步讨论。

Reducer 与 Immutable 更新

前面讲过 “mutation”(更新已有对象/数组的值)与 “immutability”(认为值是不可以改变的)

在 Redux 中,*永远* 不允许在 reducer 中更改 state 的原始对象!

// ❌ 非法 - 默认情况下,这将更改 state!
state.value = 123
  • 1
  • 2

:::

不能在 Redux 中更改 state 有几个原因:

  • 它会导致 bug,例如视图未正确更新以显示最新值
  • 更难理解状态更新的原因和方式
  • 编写测试变得更加困难
  • 它打破了正确使用“时间旅行调试”的能力
  • 它违背了 Redux 的预期精神和使用模式

所以如果我们不能更改原件,我们如何返回更新的状态呢?

TIP

Reducer 中必需要先创建原始值的*副本*,然后可以改变副本。

// ✅ 这样操作是安全的,因为创建了副本
return {
  ...state,
  value: 123
}
  • 1
  • 2
  • 3
  • 4
  • 5

我们已经看到我们可以手动编写 immutable 更新,通过使用 JavaScript 的数组/对象扩展运算符和其他返回原始值副本的函数。但是,如果你认为“以这种方式手动编写 immutable 更新看起来很难记住和正确执行”……是的,你是对的!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/857388
推荐阅读
相关标签