本博客pdf链接: https://pan.baidu.com/s/1QFyYm_hJhZE4EWgq6yvpAg 提取码: 5mgx
React进阶之路系列学习笔记:React进阶之路系列学习笔记
github源码:React_learning
1、observable P204-211
使用方式: observable(value)
@observable classProperty=value 【建议使用】
2、computed P211-212
使用方式:computed(() => expression)
@computed get classProperty(){return expression;}【建议使用】
3、reaction P212-214
使用方式: observer((props, context) => ReactElement)【建议使用】
observer(class MyComponent extends React.Component{…})
@observer class MyComponent extends React.Component{…}【建议使用】
4、action P215-216
使用方式: action(fn)
@action classMethod 【建议使用】
Store的职责:将组件使用的业务逻辑和状态封装到单独的模块中,从而组件专注于UI渲染。
state的分类:
1.与领域直接相关的领域状态数据
2.反映应用行为的应用状态数据
3.代表UI状态的UI状态数据
后两种state一般不会设计太多逻辑,仅仅是关于应用、UI的一些松散状态的读取和简单修改,封装这两种state的store实现很直观。领域store的数据结构较为复杂,且往往涉及较多的逻辑处理。
//普通对象
var todo={id: 1, title: "Todo1", finished: false}
//class
class Todo{
id;
title;
finished;
}
class描述的优势:
稍复杂的领域store建议用class描述。
一个领域的state和state的管理都由领域store负责。
具体来说,领域store职责:
本项目中store设计如下:
领域store: PostStore、CommentsStore
应用状态store:AppStore、AuthStore
UI store: UIStore
领域state: PostModel、CommentModel
说明:MobX中的props来源自inject和Provider的结合使用,从Provider提供的state中选取所需数据,作为props传递给目标组件;而state则通过observable来声明
下面对stores中的PostStore的整个流程代码详解,该部分可作为后期使用Readct+MobX开发的一个组织结构的参考。
先来看一个大致的思维导图:
stores模块的index.js
/**
* 此JS文件将stores中的所有store整合到一个stores并创建一个实例对象(只能有唯一一个实例对象)
* import *Store from "./*Store"; 导入新建对象所在的文件
* import *Api from "./api/Api"; 导入api模块下的api文件
*/
import AppStore from "./AppStore";
import AuthStore from "./AuthStore";
import PostsStore from "./PostsStore";
import CommentsStore from "./CommentsStore";
import UIStore from "./UIStore";
import authApi from "../api/authApi";
import postApi from "../api/postApi";
import commentApi from "../api/commentApi";
/// 创建实例对象,将*Api和*Store作为参数传入构造器使对应的store能使用它的属性和方法
const appStore = new AppStore();
const authStore = new AuthStore(authApi, appStore);
const postsStore = new PostsStore(postApi, appStore, authStore);
const commentsStore = new CommentsStore(commentApi, appStore, authStore);
const uiStore = new UIStore();
///整合到一个stores对象中方便根目录下的Provider的引入
const stores = {
appStore,
authStore,
postsStore,
commentsStore,
uiStore
};
/// 默认导出接口
export default stores;
PostModel.js
import { observable, action } from "mobx";
/**
* PostModel类中的可观测属性与数据库表中的字段对应,认为是一个中间类
*/
class PostModel {
// store与id为不可观测属性, MobX中不可观测属性是指对MobX数据流不会产生影响
store;
id;
@observable title;
@observable content;
@observable vote;
@observable author;
@observable createdAt;
@observable updatedAt;
/// 构造器
constructor(store, id, title, content, vote, author, createdAt, updatedAt) {
this.store = store;
this.id = id;
this.title = title;
this.content = content;
this.vote = vote;
this.author = author;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// 根据JSON对象更新帖子
@action updateFromJS(json) {
this.title = json.title;
this.content = json.content;
this.vote = json.vote;
this.author = json.author;
this.createdAt = json.createdAt;
this.updatedAt = json.updatedAt;
}
// 静态方法,创建新的PostModel实例
static fromJS(store, object) {
return new PostModel(
store,
object.id,
object.title,
object.content,
object.vote,
object.author,
object.createdAt,
object.updatedAt
);
}
}
export default PostModel;
api模块中的postApi.js
import { get, post, put } from "../utils/request";
import url from "../utils/url"; ///导入网络请求对应的utils模块的url文件
/// 导出方法的默认接口,其他模块直接调用getPostList()、getPostById()等四个方法
export default {
/// 箭头函数实现接口方法与url的一个映射
getPostList: () => get(url.getPostList()),
getPostById: id => get(url.getPostById(id)),
createPost: data => post(url.createPost(), data),
updatePost: (id, data) => put(url.updatePost(id), data)
};
PostStore.js
import { observable, action, toJS } from "mobx";
import PostModel from "../models/PostModel";//导入需要的PostModel类
class PostsStore {
api;
appStore;
authStore;
@observable posts = []; // 数组的元素是PostModel的实例,存储帖子
///此构造器在index.js实例化时使用;后面可以使用api、appStore、authStore的属性和方法
constructor(api, appStore, authStore) {
this.api = api;
this.appStore = appStore;
this.authStore = authStore;
}
// 根据帖子id,获取当前store中的帖子
getPost(id) {
return this.posts.find(item => item.id === id);///获取当前store中帖子id等于id即item.id===id的帖子
}
// 从服务器获取帖子列表
@action fetchPostList() {
this.appStore.increaseRequest();///requestQuantity+1,当前进行的请求数量+1
/**
* .then()是异步处理,先执行this.api.getPostList()获取帖子列表,然后执行then()
*/
return this.api.getPostList().then(
action(data => {
this.appStore.decreaseRequest();///requestQuantity-1,当前进行的请求数量-1
if (!data.error) {///如果获取的data没有出错
this.posts.clear();///清空帖子列表
/**
* 先执行箭头函数部分,将post作为参数传入PostModel.fromJS()
* 再调用PostModel.fromJS(this, post)创建PostModel实例,
* 然后执行this.posts.push()添加到帖子列表中
* data.forEach()是对data的所有帖子进行一个遍历
*/
data.forEach(post => this.posts.push(PostModel.fromJS(this, post)));
/**
* 创建一个Promise对象并返回a resolved promise,data没出错时调用
*/
return Promise.resolve();
} else {//数据出错
this.appStore.setError(data.error);//数据出错,调用appStore设置错误信息
/**
* 创建一个Promise对象,返回a rejected promise,出错时调用这个方法
*/
return Promise.reject();//这里是请求数据出错的一个表示
}
})
);
}
// 从服务器获取帖子详情
@action fetchPostDetail(id) {
this.appStore.increaseRequest();
return this.api.getPostById(id).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error && data.length === 1) {///数据没有出错且数据长度为1(因为id是唯一的)
const post = this.getPost(id);
// 如果store中当前post已存在,更新post;否则,添加post到store
if (post) {
post.updateFromJS(data[0]);
} else {
this.posts.push(PostModel.fromJS(this, data[0]));
}
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
// 新建帖子
@action createPost(post) {
//新建帖子,使用扩展运算符...添加到post
const content = { ...post, author: this.authStore.userId, vote: 0 };
this.appStore.increaseRequest();
return this.api.createPost(content).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
this.posts.unshift(PostModel.fromJS(this, data));
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
// 更新帖子
@action updatePost(id, post) {
this.appStore.increaseRequest();
return this.api.updatePost(id, post).then(
action(data => {
this.appStore.decreaseRequest();
if (!data.error) {
const oldPost = this.getPost(id);
if (oldPost) {
/* 更新帖子的API,返回数据中的author只包含authorId,
因此需要从原来的post对象中获取完整的author数据。
toJS是MobX提供的函数,用于把可观测对象转换成普通的JS对象。 */
/**
* toJS()是MobX提供的,这里将可观测对象转换为toJS()对象是将可观测对象转化为json格式便于
* 调用updateFromJS(data)作为参数传入
* 在MobX2.2前的命名为toJSON()
*/
data.author = toJS(oldPost.author);
oldPost.updateFromJS(data);
}
return Promise.resolve();
} else {
this.appStore.setError(data.error);
return Promise.reject();
}
})
);
}
}
export default PostsStore;
视图层的重构工作主要是observer/@observer将组件转换成一个个reaction,同时使用mobx-react提供的inject/@inject注入组件所需的store。
首先,在组件树的最外层使用mobx-react提供的Provider组件注入合并后的store。
import React from "react";
import ReactDOM from "react-dom";
import { useStrict } from 'mobx';///导入useStrict
import { Provider } from "mobx-react";//Provider组件注入合并后的store
import App from "./components/App";
import stores from "./stores";
///在严格模式下,运行MobX
useStrict(true);
ReactDOM.render(
,
document.getElementById("root")
);
组件App:
import React, { Component } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { inject, observer } from "mobx-react";//使用@inject和@observer
import asyncComponent from "../../utils/AsyncComponent";
import ModalDialog from "../../components/ModalDialog";
import Loading from "../../components/Loading";
import connectRoute from "../../utils/connectRoute";
/**
* 使用高阶组件实现异步加载
*/
const AsyncHome = connectRoute(asyncComponent(() => import("../Home")));
const AsyncLogin = connectRoute(asyncComponent(() => import("../Login")));
@inject("appStore")
@observer// 将App组件转化为一个reaction,自动响应state的变化
class App extends Component {
//在开发环境下,添加MobX调试工具
renderDevTool() {
if (process.env.NODE_ENV !== "production") {
const DevTools = require("mobx-react-devtools").default;
return ;
}
}
render() {
const { error, isLoading, removeError } = this.props.appStore;
// 错误弹窗
const errorDialog = error && (
{error.message || error}
);
return (
{/*exact属性限定室友访问根路径时,第一个Route才会破匹配成功*/}
{/*调用错误弹窗*/}
{errorDialog}
{/*调用加载组件*/}
{isLoading && }
{/* 添加MobX调试组件 */}
{this.renderDevTool()}
);
}
}
export default App;
mobx-react-devtools是一个用于调试MobX+React项目的工具,它可以追踪组件的渲染以及组件依赖的可观测数据。
安装:npm install mobx-react-devtools --save-dev
将mobx-react-devtools提供的调试组件添加到App组件。 代码及解析见11.3 视图层部分。
运行程序,界面右上角会多出三个小图标,这是mobx-react-devtools的功能键。
点击第一个图标,当组件发生渲染行为时,对应的组件会被高亮显示,高亮组件右上角显示的3个数字分别代表截止当前组件渲染的次数、组件render方法执行的时间、组件从render方法开始到渲染到浏览器界面使用的时间。
点击第二个图标后,再用鼠标选择任意一个组件,可以查看该组件会对哪些数据的变化做出响应。下图显示的是一个PostItem组件实例所依赖的数据。
点击第三个图标,控制台会输出发生的action、响应的reaction等调试日志信息。
Observer/@observer包装的组件会跟踪render方法中使用的所有可观测对象,所以组件越小,组件追踪的独享越少,引起组件重新渲染的可能性也越小。
列表数据的渲染是比较耗费性能的,尤其是在列表数据量大的情况下。
MobX通过追踪对象属性的访问来追踪值的变化,所以在层级越低的组件中解引用对象属性,由这个属性的变化导致的重新渲染的组价的数量越少。(只有解引用对象属性的组件及其子组件会重新渲染)
Redux是单一数据源,整个应用共享一个store对象,而MobX可以使用多个Store。当应用越来越复杂时,在维护store在组件间的数据共享上Mobx相对Redux更复杂。
Redux使用普通JS对象存储state,并且state是不可变的,每次状态变更必须重新创建新的state。MobX中的state是可观测对象,并且state是可以直接改变的
Redux是基于函数式的编程思想,MobX是基于面向对象的编程思想,对于传统OOP开发者而言,MobX更友好。Redux有严格的规范约束,MobX更灵活,开发者可以更随意编写代码。对于大型项目而言,有严格规范更易于后期维护和扩展。
因为Redux有严格的规范,所以往往需要写更多的代码来执行这些规范。MobX相对Redux的代码量更少。
因为MobX的OOP的编程思想以及没有太多规范约束,学习曲线平缓,容易上手;而Readux相对更难一些。
基于以上比较,一般建议开发相对简单的应用时,选择使用MobX,它易于学习、上手快、代码量少;当团队规模较大或应用复杂度较高时,选择使用Redux。它严格的规范有利于保障项目代码的可维护性和可扩展性。但是,技术选择没有绝对,最终要根据实际业务场景选择。
本章结合项目实例,从项目结构的组织方式、store的设计等方面详细介绍了如何在真实项目中使用MobX,还介绍了MobX应用中常用的调试工具和常用的性能优化方法,最后针对Redux和MobX进行了比较,并且对技术选择给予一定建议。