脸脸的小家

脸脸的个人小家

欢迎来到我的个人站~ | PM, deveplopment, Linux


Redux入门

目录

入门

ReduxJavaScript应用的状态容器,提供可预测的状态管理。

可以让你开发出行为稳定可预测的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供超爽的开发体验,比如有一个Redux Devtools可以查看实时预览。

Redux 除了和React一起用外,还支持其他 UI 框架。它体小精悍(只有 2kb,包括依赖),却有很强大的插件扩展生态。

安装

使用 React 脚手架安装 Redux

官方推荐的创建React Redux新应用的方式是使用 官方 Redux+JS 模版,它基于 Create React App,它利用了 Redux ToolkitReduxReact组件的集成.

npx create-react-app my-app --template redux

NPM 安装 Redux

Redux 核心库同样有NPM 软件包,安装方式如下:

# NPM
npm install redux

# Yarn
yarn add redux

安装 Redux Toolkit

Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。它包含了Redux核心,并包含我们认为对于构建 Redux 应用必不可少的软件包和功能。Redux Toolkit 简化了大多数 Redux 任务,防止了常见错误,并使编写Redux 应用程序更加容易。

RTK 包含了有助于简化许多常见场景的工具,包括 配置 Store创建 reducer 并编写 immutable 更新逻辑, 甚至还包含 一次性创建整个 State 的 “slice”

无论新手还是老手,都可以用RTK开发出更好的代码

安装方法

# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

三大核心

Redux有 3 大核心概念:

  • Action
  • Reducer
  • Store

其中ActionStore都非常好理解,我们可以直接按照其字面意思,将他们理解为动作储存

Action

action表示应用中的各类动作或操作,不同的操作会改变应用相应的state状态,说白了就是一个带type属性的对象。

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

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

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

const addTodoAction = {
  type: "todos/todoAdded",
  payload: "Buy milk",
};

Action Creator

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

const addTodo = (text) => {
  return {
    type: "todos/todoAdded",
    payload: text,
  };
};

Store

store 则是我们储存state的地方。

createStore

我们通过redux当中的createStore方法来创建一个store,它提供 3 个主要的方法。

// 以下代码示例来自redux官方教程
const createStore = (reducer) => {
  let state; //实际存放数据的地方,叫做对象树
  let listeners = []; //监听器
  // 用来返回当前的state
  const getState = () => state;
  // 根据action调用reducer返回新的state并触发listener
  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((listener) => listener());
  };
  /* 这里的subscribe有两个功能
   * 调用 subscribe(listener) 会使用listeners.push(listener)注册一个listener
   * 而调用 subscribe 的返回函数则会注销掉listener
   */
  const subscribe = (listener) => {
    listeners.push(listener);
    return () => {
      listeners = listeners.filter((l) => l !== listener);
    };
  };

  return { getState, dispatch, subscribe };
};

Dispatch

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

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

console.log(store.getState());
// {value: 1}

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

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

const increment = () => {
  return {
    type: "counter/increment",
  };
};

store.dispatch(increment());

console.log(store.getState());
// {value: 2}

Selector

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

const selectCounterValue = (state) => state.value;

const currentValue = selectCounterValue(store.getState());
console.log(currentValue);
// 2

Reducer

中文可以叫做缩减器或者折叠器,reduce操作被称为Fold折叠,之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 的回调函数属于相同的类型。

我们来看一下array使用reduce方法的具体例子:

// 以下代码示例来自 MDN JavaScript 文档

/* 这里的callback是和reducer非常相似的函数
 * arr.reduce(callback, [initialValue])
 */

var sum = [0, 1, 2, 3].reduce(function (acc, val) {
  return acc + val;
}, 0);
// sum = 6

/* 注意这当中的回调函数 (prev, curr) => prev + curr
 * 与我们redux当中的reducer模型 (previousState, action) => newState 看起来是不是非常相似呢
 */
[0, 1, 2, 3, 4].reduce((prev, curr) => prev + curr);

我们再来看一个简单的具体的reducer的例子:

// 以下代码示例来自redux官方教程

// reducer接受state和action并返回新的state
const todos = (state = [], action) => {
  // 根据不同的action.type对state进行不同的操作,一般都是用switch语句来实现
  switch (action.type) {
    case "ADD_TODO":
      return [
        // 这里是ES7里的对象展开运算符语法 将下面3个值赋值给state对象
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false,
        },
      ];
    // 不知道是什么action类型的话则返回默认state
    default:
      return state;
  }
};

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

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;
}

Redux 数据流

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

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

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

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

基础示例

应用的所有状态以一个对象的形式存储在唯一的 store 里面,我们可以把这个存储状态的对象叫状态树,而更改对象树的唯一方式就是 emit 一个 action(这个 action 是一个描述发生了什么的对象),为了把这个 action 转换为对象树里面的某个值,就需要提供一个 reducer, 所以简单来说,一个 redux 程序包括一个 store、一个 reducer 和多个 action

一个简单的例子

import { createStore } from 'redux'

//这是一个reducer,更具不同的类型变更状态
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}

// 创建一个新的store对象
// 它拥有以下API { subscribe, dispatch, getState }.
let store = createStore(counterReducer)

// 你可以通过subscribe()更新UI库的响应状态
// 通常你是用视图绑定器,而不是直接使用subscribe()方法
// 他们帮助其他使用这个类型的订阅者

store.subscribe(() => console.log(store.getState()))

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'counter/incremented' })
// {value: 1}
store.dispatch({ type: 'counter/incremented' })
// {value: 2}
store.dispatch({ type: 'counter/decremented' })
// {value: 1}

在典型的Redux应用程序中,只有一个store以及一个根reducer函数。随着应用程序的增长,您可以将根 reducer 拆分为较小的 reducer,分别在状态树的不同部分上进行操作。这就像在 React 应用程序中只有一个根组件一样,但是它是由许多小组件组成的。

可能对于我们简单的应用程序来说,这种设计方式过于复杂,不过可以扩展到大型或者超大型的应用程序

Redux Toolkit 示例

Redux Toolkit 简化了编写 Redux 逻辑和设置 store 的过程。 使用 Redux Toolkit,相同的逻辑如下所示:

import { createSlice, configureStore } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0,
  },
  reducers: {
    incremented: (state) => {
      // Redux Toolkit 允许我们在Reducer中编写"变化"逻辑
      // 不过实际上是使用 RTK 的Immer 库,检测"草稿状态"的更改,变更state为新的状态
      state.value += 1;
    },
    decremented: (state) => {
      state.value -= 1;
    },
  },
});

export const { incremented, decremented } = counterSlice.actions;

const store = configureStore({
  reducer: counterSlice.reducer,
});

// 通过订阅获取状态
store.subscribe(() => console.log(store.getState()));

// 仍然将动作对象传递给`dispatch`
store.dispatch(incremented());
// {value: 1}
store.dispatch(incremented());
// {value: 2}
store.dispatch(decremented());
// {value: 1}

Redux Toolkit使我们可以编写更精短的代码,更易于阅读,同时仍然遵循同样的 Redux 规范和数据流。

教程

使用的时候建议浏览器中安装了 React 和 Redux DevTools 扩展:

Redux 库和工具

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

React-Redux

Redux可以与任何 UI 框架集成,并且最常与 React 一起使用。 React-Redux 是我们的官方包,它允许您的 React 组件通过读取状态片段和调度操作来更新存储来与 Redux 存储进行交互。

Redux Toolkit

Redux 的工具库,简化写法

Redux DevTools 扩展

可以显示 Redux 存储中状态随时间变化的历史记录。这允许您有效地调试应用程序,如time-travel debugging

概念

State 管理

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

function Counter() {
  // State: 一个计数器应用,默认状态为0
  const [counter, setCounter] = useState(0);

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

  // View: UI 定义
  return (
    <div>
      Value: {counter} <button onClick={increment}>Increment</button>
    </div>
  );
}

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

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

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

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

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

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

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

这就是 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";

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

如果想要不可变的方式来更新,代码必需先复制原来的 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");