入职不久,由于公司的项目使用的是umi框架进行React开发,所以接触到了Dva,也开发了一段时间,之前使用的是Mobx,最近刚好抽出时间来,全面学习下它的前世今生。
Dva 是什么
Dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,Dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。我个人理解为dva的核心其实是 saga 的封装,将 action,reducer等等全部引入到model中。
Dva的几个特性
学习属性前,拿来了官方的一张图 来了解下它的数据流向。
从上图可以看出,当组件的数据发生改变时会通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,connect方法连接着 组件和 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 功能,把上面的理论知识串起来。
- 安装 dva-cli,执行
npm install dva-cli -g命令安装脚手架,安装完成后,执行dva -v如果出现dva-cli version [版本号]代表安装成功。 - 创建项目,执行
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
-
在
src/index.js将app.model(require('./models/example').default);注释打开,表示引入该model文件。 -
编写
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 }; }, }, }; -
修改
src/services/example.js,代码:export function getList() { return new Promise((resolve) => { setTimeout(() => { resolve([ { id: 0, name: "学习走路" }, { id: 1, name: "学习吃饭" }, { id: 2, name: "学习说话" }, ]); }, 1000); }); }; -
接来下,我们在
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); -
查看页面效果:
问题探讨
在使用的时候,也会遇到一些问题,所以在此记录下。
- 在
effects方法里面使用yield put调用另一个方法去发请求,不会等这个请求结束才会执行下面的方法。请看下图:
期望输出顺序为:
- --开始--
- result1
- result2
- --结束-- 实际输出顺序为:result1和result2的顺序有时会换
- --开始--
- --结束--
- result1
- result2
由此我们得出结论:
put是一个非阻塞的方法,put的使用效果和在外部使用dispatch是一样的;
解决方案:
将 put方法调用换成put.resolve,它类似于 put,但它是阻塞型的。