# Redux 核心概念

Redux (opens new window)

action reducer store

# MVC

它是一个 UI 的解决方案,用于降低 UI,以及 UI 关联的数据的复杂度。

传统的服务器端的 MVC

环境:

  1. 服务端需要响应一个完整的 HTML
  2. 该 HTML 中包含页面需要的数据
  3. 浏览器仅承担渲染页面的作用

以上的这种方式叫做服务端渲染,即服务器端将完整的页面组装好之后,一起发送给客户端。

服务器端需要处理 UI 中要用到的数据,并且要将数据嵌入到页面中,最终生成一个完整的 HTML 页面响应。

为了降低处理这个过程的复杂度,出现了 MVC 模式。

Controller: 处理请求,组装这次请求需要的数据 Model:需要用于 UI 渲染的数据模型 View:视图,用于将模型组装到界面中

前端 MVC 模式的困难

React 解决了 数据 -> 视图 的问题

  1. 前端的 controller 要比服务器复杂很多,因为前端中的 controller 处理的是用户的操作,而用户的操作场景是复杂的。
  2. 对于那些组件化的框架(比如 vue、react),它们使用的是单向数据流。若需要共享数据,则必须将数据提升到顶层组件,然后数据再一层一层传递,极其繁琐。 虽然可以使用上下文来提供共享数据,但对数据的操作难以监控,容易导致调试错误的困难,以及数据还原的困难。并且,若开发一个大中型项目,共享的数据很多,会导致上下文中的数据变得非常复杂。

比如,上下文中有如下格式的数据:

value = {
  users: [{}, {}, {}],
  addUser: function (u) {},
  deleteUser: function (u) {},
  updateUser: function (u) {},
};
1
2
3
4
5
6

# 前端需要一个独立的数据解决方案

Flux

Facebook 提出的数据解决方案,它的最大历史意义,在于它引入了 action 的概念

action 是一个普通的对象,用于描述要干什么。action 是触发数据变化的唯一原因

store 表示数据仓库,用于存储共享数据。还可以根据不同的 action 更改仓库中的数据

示例:

var loginAction = {
  type: "login",
  payload: {
    loginId: "admin",
    loginPwd: "123123",
  },
};

var deleteAction = {
  type: "delete",
  payload: 1, // 用户id为1
};
1
2
3
4
5
6
7
8
9
10
11
12

Redux

在 Flux 基础上,引入了 reducer 的概念

reducer:处理器,用于根据 action 来处理数据,处理后的数据会被仓库重新保存。

# Action

  1. action 是一个 plain-object(平面对象)
    1. 它的proto指向 Object.prototype
  2. 通常,使用 payload 属性表示附加数据(没有强制要求)
  3. action 中必须有 type 属性,该属性用于描述操作的类型
    1. 但是,没有对 type 的类型做出要求
  4. 在大型项目,由于操作类型非常多,为了避免硬编码(hard code),会将 action 的类型存放到一个或一些单独的文件中(样板代码)。
  5. 为了方面传递 action,通常会使用 action 创建函数(action creator)来创建 action
    1. action 创建函数应为无副作用的纯函数
      1. 不能以任何形式改动参数
      2. 不可以有异步
      3. 不可以对外部环境中的数据造成影响
  6. 为了方便利用 action 创建函数来分发(触发)action,redux 提供了一个函数bindActionCreators,该函数用于增强 action 创建函数的功能,使它不仅可以创建 action,并且创建后会自动完成分发。

# Reducer

Reducer 是用于改变数据的函数

  1. 一个数据仓库,有且仅有一个 reducer,并且通常情况下,一个工程只有一个仓库,因此,一个系统,只有一个 reducer
  2. 为了方便管理,通常会将 reducer 放到单独的文件中。
  3. reducer 被调用的时机
    1. 通过 store.dispatch,分发了一个 action,此时,会调用 reducer
    2. 当创建一个 store 的时候,会调用一次 reducer
      1. 可以利用这一点,用 reducer 初始化状态
      2. 创建仓库时,不传递任何默认状态
      3. 将 reducer 的参数 state 设置一个默认值
  4. reducer 内部通常使用 switch 来判断 type 值
  5. reducer 必须是一个没有副作用的纯函数
    1. 为什么需要纯函数
      1. 纯函数有利于测试和调式
      2. 有利于还原数据
      3. 有利于将来和 react 结合时的优化
    2. 具体要求
      1. 不能改变参数,因此若要让状态变化,必须得到一个新的状态
      2. 不能有异步
      3. 不能对外部环境造成影响
  6. 由于在大中型项目中,操作比较复杂,数据结构也比较复杂,因此,需要对 reducer 进行细分。
    1. redux 提供了方法,可以帮助我们更加方便的合并 reducer
    2. combineReducers: 合并 reducer,得到一个新的 reducer,该新的 reducer 管理一个对象,该对象中的每一个属性交给对应的 reducer 管理。

# Store

Store:用于保存数据

通过 createStore 方法创建的对象。

该对象的成员:

  • dispatch:分发一个 action
  • getState:得到仓库中当前的状态
  • replaceReducer:替换掉当前的 reducer
  • subscribe:注册一个监听器,监听器是一个无参函数,该分发一个 action 之后,会运行注册的监听器。该函数会返回一个函数,用于取消监听

# Redux 中间件(Middleware)

中间件:类似于插件,可以在不影响原本功能、并且不改动原本代码的基础上,对其功能进行增强。在 Redux 中,中间件主要用于增强 dispatch 函数。

实现 Redux 中间件的基本原理,是更改仓库中的 dispatch 函数。

Redux 中间件书写:

  • 中间件本身是一个函数,该函数接收一个 store 参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅包含 getState,dispatch。该函数运行的时间,是在仓库创建之后运行。

    • 由于创建仓库后需要自动运行设置的中间件函数,因此,需要在创建仓库时,告诉仓库有哪些中间件
    • 需要调用 applyMiddleware 函数,将函数的返回结果作为 createStore 的第二或第三个参数。
  • 中间件函数必须返回一个 dispatch 创建函数

  • applyMiddleware 函数,用于记录有哪些中间件,它会返回一个函数

    • 该函数用于记录创建仓库的方法,然后又返回一个函数

# redux-logger

一个专门为 Redux 设计的日志打印中间件,它将每一个 dispatch 的动作和当前 state 的变化以日志的形式展示在浏览器的控制台中,帮助开发者可视化地理解和追踪应用状态流。
redux-logger (opens new window)

import { createStore, applyMiddleware } from "redux";

// 默认选项的日志记录器
import logger from "redux-logger";
const store = createStore(reducer, applyMiddleware(logger));
1
2
3
4
5

# 利用中间件进行副作用处理

# redux-thunk

thunk 允许 action 是一个带有副作用的函数,当 action 是一个函数被分发时,thunk 会阻止 action 继续向后移交。

thunk 会向函数中传递三个参数:

  • dispatch:来自于 store.dispatch
  • getState:来自于 store.getState
  • extra:来自于用户设置的额外参数
import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import logger from "redux-logger";
import thunk from "redux-thunk";

export default createStore(reducer, applyMiddleware(thunk, logger));
1
2
3
4
5
6
import store from "./index";
import { fetchUsers } from "./action/usersAction";

store.dispatch(fetchUsers());
1
2
3
4
// action/usersAction.js
export function fetchUsers() {
  //由于thunk的存在,允许action是一个带有副作用的函数
  return async function (dispatch) {
    dispatch(createSetLoadingAction(true)); //正在加载
    const users = await getAllStudents();
    const action = createSetUsersAction(users);
    dispatch(action);
    dispatch(createSetLoadingAction(false));
  };
}
export const createSetUsersAction = (users) => ({
  type: SETUSERS,
  payload: users, //用户数组
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# redux-promise

# redux-promise

如果 action 是一个 promise,则会等待 promise 完成,将完成的结果作为 action 触发,如果 action 不是一个 promise,则判断其 payload 是否是一个 promise,如果是,等待 promise 完成,然后将得到的结果作为 payload 的值触发。

import { createStore, applyMiddleware } from "redux";
import reducer from "./reducer";
import logger from "redux-logger";
import promise from "../redux-promise";
const store = createStore(reducer, applyMiddleware(promise, logger));

export default store;
1
2
3
4
5
6
7
/**
 * 由于使用了redux-promise中间件,因此,允许action是一个promise,在promise中,如果要触发action,则使用resolve
 */
export function fetchStudents() {
  return new Promise((resolve) => {
    setTimeout(() => {
      const action = setStudentsAndTotal(
        [
          { id: 1, name: "aaa" },
          { id: 2, name: "bbb" },
        ],
        2,
      );
      resolve(action);
    }, 3000);
  });
}

export async function fetchStudents(condition) {
  const resp = await searchStudents(condition);
  return setStudentsAndTotal(resp.datas, resp.cont);
}

export async function fetchStudents(condition) {
  return {
    type: actionTypes.setStudentsAndTotal,
    payload: searchStudents(condition).then((resp) => ({
      datas: resp.datas,
      total: resp.cont,
    })),
  };
}
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

# 迭代器和可迭代协议

解决副作用的 redux 中间件
redux-thunk:需要改动 action,可接收 action 是一个函数
redux-promise:需要改动 action,可接收 action 是一个 promise 对象,或 action 的 payload 是一个 promise 对象
以上两个中间件,会导致 action 或 action 创建函数不再纯净。
redux-saga 将解决这样的问题,它不仅可以保持 action、action 创建函数、reducer 的纯净,而且可以用模块化的方式解决副作用,并且功能非常强大。
redux-saga 是建立在 ES6 的生成器基础上的,要熟练的使用 saga,必须理解生成器。
要理解生成器,必须先理解迭代器和可迭代协议。

# 迭代

类似于遍历

遍历:有多个数据组成的集合数据结构(map、set、array 等其他类数组),需要从该结构中依次取出数据进行某种处理。

迭代:按照某种逻辑,依次取出下一个数据进行处理。

# 迭代器 iterator

JS 语言规定,如果一个对象具有 next 方法,并且 next 方法满足一定的约束,则该对象是一个迭代器(iterator)。

next 方法的约束:该方法必须返回一个对象,该对象至少具有两个属性:

  • value:any 类型,下一个数据的值,如果 done 属性为 true,通常,会将 value 设置为 undefined
  • done:bool 类型,是否已经迭代完成

通过迭代器的 next 方法,可以依次取出数据,并可以根据返回的 done 属性,判定是否迭代结束。

# 迭代器创建函数 iterator creator

它是指一个函数,调用该函数后,返回一个迭代器,则该函数称之为迭代器创建函数,可以简称位迭代器函数。

DETAILS
function createIterator(total) {
  i = 1;
  return {
    next() {
      var obj = {
        //当前这一次迭代到的数据
        value: i > total ? undefined : Math.random(),
        done: i > total,
      };
      i++;
      return obj;
    },
  };
}

var iterator = createIterator(5);
var next = iterator.next();
while (!next.done) {
  //若当前迭代的数据不是迭代器的结束
  //如果当前还有数据
  console.log(next.value);
  next = iterator.next();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 可迭代协议

ES6 中出现了 for-of 循环,该循环就是用于迭代某个对象的,因此,for-of 循环要求对象必须是可迭代的(对象必须满足可迭代协议)

可迭代协议是用于约束一个对象的,如果一个对象满足下面的规范,则该对象满足可迭代协议,也称之为该对象是可以被迭代的。

可迭代协议的约束如下:

  1. 对象必须有一个知名符号属性(Symbol.iterator
  2. 该属性必须是一个无参的迭代器创建函数
DETAILS
//obj满足可迭代协议
//obj可被迭代
var obj = {
  [Symbol.iterator]: function () {
    var total = 3;
    i = 1;
    return {
      next() {
        var obj = {
          //当前这一次迭代到的数据
          value: i > total ? undefined : Math.random(),
          done: i > total,
        };
        i++;
        return obj;
      },
    };
  },
};

//模拟for-of循环
var iterator = obj[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
  //有数据
  const item = result.value;
  console.log(item); //执行循环体
  result = iterator.next();
}

for (const item of obj) {
  console.log(item);
}
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
# for-of 循环的原理

调用对象的[Symbol.iterator]方法,得到一个迭代器。不断调用 next 方法,只有返回的 done 为 false,则将返回的 value 传递给变量,然后进入循环体执行一次。

# 生成器 generator

# generator

生成器:由构造函数 Generator 创建的对象,该对象既是一个迭代器,同时,又是一个可迭代对象(满足可迭代协议的对象)

//伪代码

var generator = new Generator();
generator.next(); //它具有next方法
var iterator = generator[Symbol.iterator]; //它也是一个可迭代对象
for (const item of generator) {
  //由于它是一个可迭代对象,因此也可以使用for of循环
}
1
2
3
4
5
6
7
8

注意:Generator 构造函数,不提供给开发者使用,仅作为 JS 引擎内部使用

# generator function

生成器函数(生成器创建函数):该函数用于创建一个生成器。

ES6 新增了一个特殊的函数,叫做生成器函数,只要在函数名与 function 关键字之间加上一个*号,则该函数会自动返回一个生成器

生成器函数的特点:

  • 调用生成器函数,会返回一个生成器,而不是执行函数体(因为,生成器函数的函数体执行,受到生成器控制)
  • 每当调用了生成器的 next 方法,生成器的函数体会从上一次 yield 的位置(或开始位置)运行到下一个 yield
    • yield 关键字只能在生成器内部使用,不可以在普通函数内部使用
    • 它表示暂停,并返回一个当前迭代的数据
    • 如果没有下一个 yield,到了函数结束,则生成器的 next 方法得到的结果中的 done 为 true
  • yield 关键字后面的表达式返回的数据,会作为当前迭代的数据
  • 生成器函数的返回值,会作为迭代结束时的 value
    • 但是,如果在结束过后,仍然反复调用 next,则 value 为 undefined
  • 生成器调用 next 的时候,可以传递参数,该参数会作为生成器函数体上一次 yield 表达式的值。
    • 生成器第一次调用 next 函数时,传递参数没有任何意义
  • 生成器带有一个 throw 方法,该方法与 next 的效果相同,唯一的区别在于:
    • next 方法传递的参数会被返回成一个正常值
    • throw 方法传递的参数是一个错误对象,会导致生成器函数内部发生一个错误。
  • 生成器带有一个 return 方法,该方法会直接结束生成器函数
  • 若需要在生成器内部调用其他生成器,注意:如果直接调用,得到的是一个生成器,如果加入*号调用,则进入其生成器内部执行。如果是yield* 函数()调用生成器函数,则该函数的返回结果,为该表达式的结果
DETAILS
function* g2() {
  console.log("g2-开始");
  let result = yield "g1";
  console.log("g2-运行1");
  result = yield "g2";
  return 123;
}
//下面的函数是一个生成器函数,用于创建生成器
function* createGenerator() {
  console.log("生成器函数的函数体 - 开始");
  let result = yield 1; //将1作为第一次的迭代的值
  result = yield* g2(); //result为g2函数的返回值
  console.log("生成器函数的函数体 - 运行1", result);
  result = yield 2; //将2作为第二次迭代的值
  console.log("生成器函数的函数体 - 运行2", result);
  result = yield 3;
  console.log("生成器函数的函数体 - 运行3", result);
  return "结束";
}

var generator = createGenerator(); //调用后,一定得到一个生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# redux-saga

# redux-saga

中文文档地址:https://redux-saga-in-chinese.js.org/

  • 纯净
  • 强大
  • 灵活

在 saga 任务中,如果 yield 了一个普通数据,saga 不作任何处理,仅仅将数据传递给 yield 表达式(把得到的数据放到 next 的参数中),因此,在 saga 中,yield 一个普通数据没什么意义。

saga 需要你在 yield 后面放上一些合适的 saga 指令(saga effects),如果放的是指令,saga 中间件会根据不同的指令进行特殊处理,以控制整个任务的流程。

每个指令本质上就是一个函数,该函数调用后,会返回一个指令对象,saga 会接收到该指令对象,进行各种处理

一旦 saga 任务完成(生成器函数运行完成),则 saga 中间件一定结束

  • take 指令:【阻塞】监听某个 action,如果 action 发生了,则会进行下一步处理,take 指令仅监听一次。yield 得到的是完整的 action 对象
  • all 指令:【阻塞】该函数传入一个数组,数组中放入生成器,saga 会等待所有的生成器全部完成后才会进一步处理
  • takeEvery 指令:不断的监听某个 action,当某个 action 到达之后,运行一个函数。takeEvery 永远不会结束当前的生成器

# redux-actions

该库用于简化 action-types、action-creator 以及 reducer 官网文档:https://redux-actions.js.org/

# createAction

该函数用于帮助你创建一个 action 创建函数(action creator)

# createActions

该函数用于帮助你创建多个 action 创建函数

# handleAction(s)

# handleAction

简化针对单个 action 类型的 reducer 处理,当它匹配到对应的 action 类型后,会执行对应的函数

# handleActions

简化针对多个 action 类型的 reducre 处理

# combineActions

配合 createActions 和 handleActions 两个函数,用于处理多个 action-type 对应同一个 reducer 处理函数。