夜猫子的知识栈 夜猫子的知识栈
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

夜猫子

前端练习生
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《Web Api》
    • 《ES6教程》
    • 《Vue》
    • 《React》
    • 《TypeScript》
    • 《Git》
    • 《Uniapp》
    • 小程序笔记
    • 《Electron》
    • JS设计模式总结
  • 《前端架构》

    • 《微前端》
    • 《权限控制》
    • monorepo
  • 全栈项目

    • 任务管理日历
    • 无代码平台
    • 图书管理系统
  • HTML
  • CSS
  • Nodejs
  • Midway
  • Nest
  • MySql
  • 其他
  • 技术文档
  • GitHub技巧
  • 博客搭建
  • Ajax
  • Vite
  • Vitest
  • Nuxt
  • UI库文章
  • Docker
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 核心概念

  • 高级指引

  • Hook

  • 案例演示

  • Router

  • Redux

    • 入门Redux
    • Redux基础
      • Reduce 术语
        • Action
        • Action Creator
        • Reducer
        • Store
        • Dispatch
        • Selector
      • 创建 Redux Store
        • 基于 proxy 实现的更新不可变逻辑
        • 用 Thunk 编写异步逻辑
        • 使用createAsyncThunk 生成 Thunk
      • 在应用程序中加入Redux
      • 组件中使用 Redux
    • 数据范式化和性能优化
    • Redux Toolkit详细示例
  • Jotai

  • Mobx

  • 其他

  • 《React》笔记
  • Redux
夜猫子
2022-11-23
目录

Redux基础

# Reduce 术语

# Action

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

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

const addTodoAction = {
  type: 'todos/todoAdded', // 其中第一部分是这个 action 所属的特征或类别,第二部分是发生的具体事情
  payload: 'Buy milk' // 可以添加有关发生的事情的附加信息
}
1
2
3
4

# Action Creator (opens new window)

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 必需符合以下规则:

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

下面是 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

# 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 可以形象的理解为 "触发一个事件"。

我们通常调用 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

笔记

类似于 vuex 的计算属性

# 创建 Redux Store

一个可能的配置可能是这样的:

app/store.js

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

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

其中 counterReducer 模块可能是这样的:

features/counter/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

// Redux Toolkit 的 createSlice 的函数,负责生成 action 类型字符串、action creator 函数和 action 对象的工作。
// 比如 name + reducers 名称 (counter + increment),生成了一个 action 类型 {type: "counter/increment"}。
export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit 允许我们在 reducers 写 "可变" 逻辑。
      // 并不是真正的改变 state 因为它使用了 immer 库
      // 当 immer 检测到 "draft state" 改变时,会基于这些改变去创建一个新的(immer使用了proxy来监听变化)
      // 不可变的 state
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
    // prepare 函数让我们可以实现自定义 action
    // 组件中可以直接使用 dispatch(postAdded(title, content)) 来调度这个 reducer action
    postAdded: {
      reducer(state, action) {
        state.push(action.payload)
      },
      prepare(title, content) {
        return {
          payload: {
            id: nanoid(),
            title,
            content
          }
        }
      }
    }
  }
})

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

// 异步Thunk
export const incrementAsync = (amount) => (dispatch) => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount))
  }, 1000)
}

// Selector 属性包装
export const selectCount = (state) => state.counter.value

// counterSlice.reducer就是一个通过内置 createReducer 生成的,签名类型为 `(state, action) => newState` 的 reducer 函数
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# 基于 proxy 实现的更新不可变逻辑

常见的手动编写不可变的更新逻辑可能是这样的:

// 通过创建原始值的副本的方式来更新不可变数据
function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

所以,上面的代码可以变成这样:

function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}
1
2
3

警告!

你只能在 Redux Toolkit 的 createSlice 和 createReducer 中编写 “mutation” 逻辑,因为它们在内部使用 Immer!如果你在没有 Immer 的 reducer 中编写 mutation 逻辑,它将改变状态并导致错误!

# 用 Thunk 编写异步逻辑

目前我们的一系列流程都是同步的:dispatch action,store 调用 reducer 来计算新状态,然后 dispatch 函数完成并结束。

但实际上也有一些 API 请求数据之类的异步逻辑可能需要我们去处理。

thunk 是一种特定类型的 Redux 函数,可以包含异步逻辑。Thunk 是使用两个函数编写的:

  • 一个内部 thunk 函数,它以 dispatch 和 getState 作为参数
  • 外部创建者函数,它创建并返回 thunk 函数

features/counter/counterSlice.js

// 下面这个函数就是一个 thunk ,它使我们可以执行异步逻辑
// 你可以 dispatched 异步 action `dispatch(incrementAsync(10))` 就像一个常规的 action
// 调用 thunk 时接受 `dispatch` 函数作为第一个参数
// 当异步代码执行完毕时,可以 dispatched actions
export const incrementAsync = amount => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount))
  }, 1000)
}
1
2
3
4
5
6
7
8
9

并像使用普通 Redux action creator 一样使用它们:

store.dispatch(incrementAsync(5))
1

AJAX 调用以从服务器获取数据时,你可以将该调用放入 thunk 中。

// 外部的 thunk creator 函数
const fetchUserById = userId => {
  // 内部的 thunk 函数
  return async (dispatch, getState) => {
    try {
      // thunk 内发起异步数据请求
      const user = await userAPI.fetchById(userId)
      // 当数据响应完成后 dispatch 一个 action
      dispatch(userLoaded(user))
      // 输出更改后的值
      const data = getState()
      console.log(data)
    } catch (err) {
      // 如果过程出错,在这里处理
    }
  }
}

store.dispatch(fetchUserById(42))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 使用createAsyncThunk 生成 Thunk

Redux Toolkit 的 createAsyncThunk API 生成 thunk,并能够自动 dispatch 那些 "start/success/failure" action。

createAsyncThunk 接收 2 个参数:

  • 将用作生成的 action 类型的前缀的字符串
  • 一个 “payload creator” 回调函数,它应该返回一个包含一些数据的 Promise,或者一个被拒绝的带有错误的 Promise

features/posts/postsSlice

import { createSlice, nanoid, createAsyncThunk } from '@reduxjs/toolkit'
import { client } from '../../api/client'

// 这里我们用了额外的字段来标识数据状态
// 如 status:'idle' | 'loading' | 'succeeded' | 'failed',
const initialState = {
  posts: [],
  status: 'idle',
  error: null
}

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // omit existing reducers here
  },
    // 在这个例子中,我们需要监听我们 fetchPosts thunk dispatch 的 "pending" 和 "fulfilled" action 类型。这些 action creator 附加到我们实际的 fetchPost 函数中,我们可以将它们传递给 extraReducers 来监听这些 action
  extraReducers(builder) {
    builder
      .addCase(fetchPosts.pending, (state, action) => {
        state.status = 'loading'
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.status = 'succeeded'
        // Add any fetched posts to the array
        state.posts = state.posts.concat(action.payload)
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.error.message
      })
  }
})

// 传入 'posts/fetchPosts' 作为 action 类型的前缀
// 我们希望我们 dispatch 的 Redux action 有一个 payload,也就是 posts 数组。所以,我们提取 response.data,并从回调中返回它。
export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => {
  const response = await client.get('/fakeApi/posts')
  return response.data
})

// 当调用 dispatch(fetchPosts()) 的时候,fetchPosts 这个 thunk 会首先 dispatch 一个 action 类型为'posts/fetchPosts/pending'
// 我们可以在我们的 reducer 中监听这个 action 并将请求状态标记为 “loading 正在加载”。
// 一旦 Promise 成功,fetchPosts thunk 会接受我们从回调中返回的 response.data 数组,并 dispatch 一个 action,action 的 payload 为 posts 数组,action 的 类型为 'posts/fetchPosts/fulfilled'。
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

createAsyncThunk,只能传递一个参数,无论我们传入的是什么,它都将成为 payload creation 回调的第一个参数。

payload creator 的第二个参数是一个' thunkAPI '对象,包含几个有用的函数和信息:

  • dispatch 和 getState:dispatch 和 getState 方法由 Redux store 提供。你可以在 thunk 中使用这些来发起 action,或者从最新的 Redux store 中获取 state (例如在发起 另一个 action 后获取更新后的值)。
  • extra:当创建 store 时,用于传递给 thunk 中间件的“额外参数”。这通常时某种 API 的包装器,比如一组知道如何对应用程序的服务器进行 API 调用并返回数据的函数,这样你的 thunk 就不必直接包含所有的 URL 和查询逻辑。
  • requestId:该 thunk 调用的唯一随机 ID ,用于跟踪单个请求的状态。
  • signal:一个AbortController.signal 函数,可用于取消正在进行的请求。
  • rejectWithValue:一个用于当 thunk 收到一个错误时帮助自定义 rejected action 内容的工具。

(如果你要手写 thunk 而不是使用 createAsyncThunk,则 thunk 函数将获取 (dispatch, getState) 作为单独的参数,而不是将他们放在一个对象中。)

# 在应用程序中加入Redux

在这里使用了一个名为 <Provider> 的组件在幕后传递 Redux store,以便他们可以访问 useSelector 等相关 hook。

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import store from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 组件中使用 Redux

features/counter/Counter.js

import React, { useState, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount
} from './counterSlice'
import styles from './Counter.module.css'

export function Counter() {
  // 因为我们无法直接访问store,可以使用 useSelector 这个 hooks ,让我们的组件从 Redux 的 store 状态树中提取它需要的任何数据。
  // 比如 selectCount 就是一个导出的计算属性
  const count = useSelector(selectCount)
  // 当然也可以直接写逻辑
  const countPlusTwo = useSelector(state => state.counter.value + 2)
  const postStatus = useSelector(state => state.posts.status)
  
  const dispatch = useDispatch()
  const [incrementAmount, setIncrementAmount] = useState('2')
  
  //  dispatch thunk(异步 action,调用 api 初始化数据)
  useEffect(() => {
    if (postStatus === 'idle') {
      dispatch(fetchPosts())
    }
  }, [postStatus, dispatch])

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <button
          className={styles.button}
          aria-label="Increment value by 20"
          onClick={() => dispatch(incrementByAmount(20))}
        >
          +20
        </button>
      </div>
      {/* 这里省略了额外的 render 代码 */}
    </div>
  )
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
编辑 (opens new window)
上次更新: 2024/12/19 17:25:54
入门Redux
数据范式化和性能优化

← 入门Redux 数据范式化和性能优化→

最近更新
01
IoC 解决了什么痛点问题?
03-10
02
如何调试 Nest 项目
03-10
03
Provider注入对象
03-10
更多文章>
Copyright © 2019-2025 Study | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式