走进Dva的世界

866 阅读4分钟

入职不久,由于公司的项目使用的是umi框架进行React开发,所以接触到了Dva,也开发了一段时间,之前使用的是Mobx,最近刚好抽出时间来,全面学习下它的前世今生。

Dva 是什么

Dva 首先是一个基于 reduxredux-saga 的数据流方案,然后为了简化开发体验,Dva 还额外内置了 react-routerfetch,所以也可以理解为一个轻量级的应用框架。我个人理解为dva的核心其实是 saga 的封装,将 actionreducer等等全部引入到model中。

Dva的几个特性

学习属性前,拿来了官方的一张图 来了解下它的数据流向。

PPrerEAKbIoDZYr.png

从上图可以看出,当组件的数据发生改变时会通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 Stateconnect方法连接着 组件和 state,基本和 redux的玩法一样,connect方法。下面具体介绍它的几个特性。

  • State

    它表示状态数据,需要注意的是:操作的时候每次都要当作不可变数据来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。

  • Dispatch

    它是改变state的唯一的途径,说的更严谨些,它也需要 action 的配合,它是一个配置对象,需要接受 type 表示我们要执行什么操作,如果需要传参的话,通过设置 payload 属性可以实现。需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。书写形式如下:

    dispatch({
      type: 'counter/add',
      payload: {}, // 需要传递的信息
    });
    
  • Reducers

    它是包含着若干个改变 state 方法的配置对象,可以通过 dispatch 触发,也可以通过 effects里面的 put 方法触发,入参和 dispatch 相同。需要注意的是 每次操作都是返回一个全新的数据,也就是纯函数。这样做的目的 可以便于我们的数据跟踪和响应。

  • Effect

    它是用来处理异步操作的,当然也可以进行同步操作,被称为副作用,这样就造成函数不纯,dva为了解决这个问题,使用了 generator,把异步转为同步的方式,内部使用 yield 关键字,标识每一步的操作(不管是异步或同步)。

  • Subscription

    这个在我们项目里面没有用到,所以借用了官方的代码,它在项目初始化的时候就会注册执行里面的方法,适合做监听用。

     subscriptions: {
         setup({ dispatch, history }) {
           return history.listen(({ pathname }) => {
             if (pathname === '/') {
               dispatch({
                 type: 'query',
               })
             }
           })
         }
       }
    

动手实践

俗话说的好,光说不练假把式,下面就实现一个比较简单的 TodoList 功能,把上面的理论知识串起来。

  1. 安装 dva-cli,执行 npm install dva-cli -g 命令安装脚手架,安装完成后,执行dva -v 如果出现 dva-cli version [版本号] 代表安装成功。
  2. 创建项目,执行 dva new dva-learn 命令来创建名为 dva-learn的项目,然后我们 cd 进入项目里面,使用 code ./ 用 vscode 编辑器打开,下面我们分析下它初始化为我们生成的目录结构。
 .
 ├── mock           # 模拟数据存放的位置
 ├── package.json   # 项目配置文件
 ├── public         # 公共目录,不会被构建工具处理
 │   └── index.html # 入口文件
 ├── src
 │   ├── assets     # 静态资源
 │   │   └── yay.jpg
 │   ├── components # 组件
 │   │   └── Example.js
 │   ├── index.css # 公共的css样式
 │   ├── index.js # 入口文件
 │   ├── models   # dva状态管理
 │   │   └── example.js
 │   ├── router.js # 路由配置
 │   ├── routes # 路由页面
 │   │   ├── IndexPage.css
 │   │   └── IndexPage.js
 │   ├── services # 接口 提供API 
 │   │   └── example.js
 │   └── utils # 存放工具
 │       └── request.js
  1. src/index.jsapp.model(require('./models/example').default);注释打开,表示引入该model文件。

  2. 编写 src/models/example.js 代码

      import { getList } from "../services/example";
      export default {
        namespace: "example",
    
        state: {
          todoList: [],
        },
    
        subscriptions: {
          setup({ dispatch, history }) {},
        },
    
        effects: {
          *getTodoList({ payload }, { call, put }) {
            const res = yield call(getList);
            yield put({ type: "save", payload: res });
          },
          *deleteItem({ payload }, { put, select }) {
            const todoList = yield select((state) => state.example.todoList);
            yield put({
              type: "save",
              payload: todoList.filter((v) => v.id != payload),
            });
          },
          *addItem({ payload }, { put, select }) {
            const todoList = yield select((state) => state.example.todoList);
            todoList.push({ id: todoList.length, name: payload });
            console.log(todoList );
            yield put({
              type: "save",
              payload: [...todoList],
            });
          },
        },
    
        reducers: {
          save(state, action) {
            return { ...state, todoList: action.payload };
          },
        },
      };
    
  3. 修改 src/services/example.js,代码:

    export function getList() {
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve([
              { id: 0, name: "学习走路" },
              { id: 1, name: "学习吃饭" },
              { id: 2, name: "学习说话" },
            ]);
          }, 1000);
        });
    };
    
    
  4. 接来下,我们在 src/routes/IndexPage.js 使用 我们 models里面的方法和状态值。

      import React, { useEffect, useState } from "react";
      import { connect } from "dva";
     
      function IndexPage(props) {
        const { todoList, dispatch } = props;
        const [inputText, setInputText] = useState("");
    
        useEffect(() => {
          dispatch({ type: "example/getTodoList" });
        }, []);
    
        return (
          <>
            <h1>todoList</h1>
            <input
              type="text"
              value={inputText}
              onChange={(e) => {
                setInputText(e.target.value);
              }}
            />
            <button
              onClick={() => {
                setInputText("");
                dispatch({ type: "example/addItem", payload: inputText });
              }}
            >
              添加
            </button>
            <ul>
              {todoList.map(({ name, id }) => (
                <li key={id}>
                  {name}
                  <button
                    onClick={() => {
                      dispatch({ type: "example/deleteItem", payload: id });
                    }}
                  >
                    完成
                  </button>
                </li>
              ))}
            </ul>
          </>
        );
      }
    
      export default connect(({ example }) => ({ ...example }))(IndexPage);
    
  5. 查看页面效果:

    QQ20220313-112716.gif

问题探讨

在使用的时候,也会遇到一些问题,所以在此记录下。

  1. effects 方法里面使用 yield put 调用另一个方法去发请求,不会等这个请求结束才会执行下面的方法。请看下图:

image.png 期望输出顺序为:

  • --开始--
  • result1
  • result2
  • --结束-- 实际输出顺序为:result1和result2的顺序有时会换
  • --开始--
  • --结束--
  • result1
  • result2

由此我们得出结论:

put是一个非阻塞的方法,put的使用效果和在外部使用dispatch是一样的;

解决方案: 将 put方法调用换成put.resolve,它类似于 put,但它是阻塞型的。