参考视频链接:https://www.bilibili.com/video/BV1tL4y1h7ND?p=14&spm_id_from=pageDriver
一、前期准备
在初次使用React 的装饰器时,第一次在项目中使用 @ 会报错,原因是react默认是不支持装饰器的,所以才会报错,所以是需要做一些配置来支持装饰器。
【报错显示:Parsing error: This experimental syntax requires enabling one of the following parser plugin(s): “decorators-legacy”, “decorators”.】
- 创建项目
npm install -g create-react-app
create-react-app mobx-study
-
安装插件 —— 改变 create-react-app 中 webpack 配置
yarn add -D react-app-rewired customize-cra
yarn add -D @babel/core @babel/plugin-proposal-decorators @babel/preset-env
-
修改package.json文件中 scripts 脚本
// package.json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}
- 在项目根目录下创建
config-overrides.js
并写入以下内容
const path = require('path')
const { override, addDecoratorsLegacy } = require('customize-cra')
function resolve(dir) {
return path.join(__dirname, dir)
}
const customize = () => (config, env) => {
config.resolve.alias['@'] = resolve('src')
if (env === 'production') {
config.externals = {
'react': 'React',
'react-dom': 'ReactDOM'
}
}
return config
};
module.exports = override(addDecoratorsLegacy(), customize())
- 在项目根目录下创建
.babelrc
并写入以下内容
{
"presets": [
"@babel/preset-env"
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
基本完成以上步骤就可以正常使用装饰器了,再也不会报 @ 的错误了。同时Support for the experimental syntax ‘decorators-legacy’ isn’t currently enabled这个错误也将消失。
二、计数器 小demo
- 安装依赖
yarn add mobx@5 mobx-react@6
- 小demo
import React from 'react';
import ReactDOM from 'react-dom';
// import App from './App';
import { observable, action} from 'mobx'
import { observer } from 'mobx-react'
// 1. 初始化 mobx 容器仓库
class Store {
@observable count = 0
@action.bound increment() {
this.count++
}
}
// 2. 在组件中使用 mobx容器状态
@observer
class App extends React.Component {
render () {
const {store} = this.props
return (
<div>
<h1>App Component</h1>
<p>{store.count}</p>
<button onClick={store.increment}>加1</button>
</div>
)
}
}
ReactDOM.render(<App store={new Store()}/>,document.getElementById('root'));
三、es6装饰器
// 装饰器是一个对类进行处理的函数
// 基本用法
function fn1(target) {
target.foo = 'bar'
}
// 返回函数,为装饰器传参
function fn2(value) {
return function (target) {
target.count = value
}
}
// 添加实例属性
function fn3(target) {
target.prototype.foo = 'baz'
}
// 装饰类的成员
function readOnly(target, name, descriptor) {
// console.log(target); //目标类的.prototype
// console.log(name); // 被修饰的类成员的名称
// console.log(descriptor); // 被修饰的类成员的描述对象,包括对象是否可枚举(enumerable)、可写(writable)等等
descriptor.writable = false
}
function unEnumerable(target, name, descriptor) {
descriptor.enumerable = false
}
@fn1
@fn2(10)
@fn3
class MyClass {
@readOnly message = 'hello';
@unEnumerable sayHello() {
console.log('hello decorator');
}
}
const cl = new MyClass()
// console.log(cl.foo);
// console.log(cl.count);
// console.log(cl.foo);
console.log(cl.message);
// cl.message = "hello world" // 【报错】该属性只读,不可以修改
for(let key in cl) {
console.log(key + ":" + cl[key]);
}
cl.sayHello()
// decorator-mixins.js
function mixins(...list) {
return function (target) {
// Object.assign(target, source)方法用于对象的合并,
// 将源对象(source)的所有可枚举属性,复制到目标对象(target) 【浅复制】
Object.assign(target.prototype, ...list)
}
}
const obj = {
name:'lfy',
sayName() {
console.log('My name is lfy');
}
}
@mixins(obj)
class MyClass {}
let person = new MyClass();
console.log(person.name);
person.sayName()
四、mobx知识补充
1. observable, autorun
const store = new Store()
// 检测@observable count的值的改变,
// 默认执行一次,然后每次它的依赖关系改变时会再次被触发
autorun(()=>{
console.log('autorun', store.count);
})
2. computed计算属性
class Store {
@observable count = 0
@observable price = 10
@action.bound increment() {
this.count++
}
// 会进行缓存,多次重复使用不会再次触发计算,当计算依赖的值改变时才再次进行计算
@computed get totalPrice () {
// console.log('计算totalPrice');
return this.price * this.count
}
}
3. action
知识点:
- @action.bound this的绑定
- 尽量使用装饰了@action的方法来修改容器中的数据
- 配置mobx的configure
- runInAction
- 异步action的三种方法
// 强制必须使用装饰了@action的方法来修改容器中的数据
configure({
enforceActions: 'observed'
})
class Store {
@observable count = 0
@observable price = 10
@action.bound change () {
console.log(this);
this.count = 20
this.price = 50
this.price = 100
}
}
const store = new Store()
// 检测@observable count\price的值的改变,
// 总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发
autorun(()=>{
console.log('autorun', store.count, store.price);
})
// 每一次修改都会触发autorun,过于频繁
// store.count = 20
// store.price = 50
// store.price = 100
// 尽量使用装饰了@action的方法来修改容器中的数据,这样子autorun只会执行一次
store.change()
// @action.bound 使用bound后绑定this指向类Store 防止this丢失
const changeMethod = store.change
changeMethod() // 如果没有使用.bound绑定this,this输出为undefined
// 使用runInAction也可以修改容器中的数据
runInAction(()=>{
store.count = 999
})
// 异步action的三种方法
class Store{
@observable count = 0
@action.bound asyncChange() {
setTimeout(() => {
// this.count = 100 // 报错
// 方法1 定义action函数【若业务逻辑比较复杂,推荐使用】
// this.changeCount()
// 方法2 直接调用action函数【不推荐】
// action('changeFoo', ()=>{
// this.count = 100
// })()
// 方法3 runInAction【简单场景 推荐使用】
runInAction(()=>{
this.count = 100
})
},500)
}
@action.bound changeCount() {
this.count = 100
}
}
4. 数据监测
监测使用 @observable 装饰的属性
方法一:computed
**执行时机/次数:**会进行缓存,多次重复使用不会再次触发计算,当计算依赖的值改变时才再次进行计算
经验法则:如果你想响应式的产生一个可以被其它 observer 使用的值,请使用 @computed
class Store {
@observable count = 100
@observable price = 10
@computed get totalPrice () {
// console.log('计算totalPrice');
return this.price * this.count
}
}
方法二:autorun
执行时机/次数:总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发
经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,而是想要达到一个效果,请使用autorun。
const store = new Store()
autorun(()=>{
console.log('autorun', store.count);
})
方法三:when
执行时机/次数:该方法只执行一次;若初始化时 条件成立也是会执行的
// 第一个参数为when函数执行的条件函数,第一个参数返回true时,执行第二个函数参数
when(
() => store.count >= 100,
() => {
console.log('when', store.count);
}
)
方法四:reaction
**执行时机/次数:**初始化时不会执行,当监测的数据改变时才执行
reaction(
() => {
// 执行一些业务逻辑操作,返回数据给下一个函数使用
return store.count/100
},
(data, reaction) => {
console.log('reaction', data); // data是第一个函数参数的返回值
reaction.dispose() // 手动停止当前reaction的监听
}
)
五、购物车案例
代码:https://gitee.com/lfy__fly/learn-mobx.git
运行截图:
1. 模拟后台数据
⭕ src / api / shop.js
const _products = [
{'id':1, 'title':'ipad 9','price':500.01,'inventory':2},
{'id':2, 'title':'ipad 8','price':300.01,'inventory':12},
{'id':3, 'title':'ipad 10','price':1500.01,'inventory':62},
]
export const getAllProducts = callback => {
setTimeout(function () {
callback(_products)
}, 100)
}
export const buyProducts = (callback, errorCallback) => {
setTimeout(()=>{
Math.random() > 0.5 ? callback() : errorCallback()
}, 100)
}
2. mobx 管理数据
⭕ src / stores / index.js
主文件,进行整合,并通过构造器让productsStore和cartStore联系起来,可以互相访问
import productsStore from "./products";
import cartStore from "./cart";
export default class rootStore {
constructor() {
this.productsStore = new productsStore(this)
this.cartStore = new cartStore(this)
}
}
⭕ src / stores / products.js
import { observable, action } from "mobx";
import * as shop from '../api/shop'
export default class productsStore {
@observable all = [];
constructor (rootStore) {
this.rootStore = rootStore
}
// 异步action
@action.bound getAllProducts () {
shop.getAllProducts(products => {
this.setAll(products)
})
}
@action.bound setAll(products) {
this.all = products
}
@action.bound decrementInventory(product) {
const prod = this.all.find(prodItem => prodItem.id === product.id)
prod.inventory -= 1
}
}
⭕ src / stores / cart.js
import { action, computed, isObservable, observable } from "mobx";
import * as shop from '../api/shop'
export default class cartStore {
@observable items = [];
@observable checkoutStatus = null;
@computed get totalPrice() {
return this.items.reduce((total, item) => {
return total + item.price * item.quantity
},0)
}
@action.bound addToCart (product) {
// 判断购物车数据中是否已经有该商品
// 如果有,则让购物车中的商品数量+1
// 如果没有,则添加新的商品到购物车
const prod = this.items.find(cartItem => cartItem.id === product.id)
if(prod) {
prod.quantity++
} else {
this.items.push({
id: product.id,
title: product.title,
price: product.price,
quantity: 1
})
}
// 添加完购物车商品以后,需要让商品列表中的库存数据-1
this.rootStore.productsStore.decrementInventory(product)
}
@action.bound checkout() {
// 备份购物车数据
const savedProducts = [...this.items]
// 清空结算状态
this.setCheckoutStatus(null)
// 清空购物车
this.setItems([])
// 发起结算请求
// 成功:将结算状态设置为 成功
// 失败:将结算状态设置为 失败 ,还原购物车数据
shop.buyProducts(
() => {
this.setCheckoutStatus('成功')
},
() => {
this.setCheckoutStatus('失败')
this.setItems(savedProducts)
}
)
}
@action.bound setCheckoutStatus(status) {
this.checkoutStatus = status
}
@action.bound setItems(items) {
this.items = items
}
constructor (rootStore) {
this.rootStore = rootStore
}
}
3. 入口文件
⭕ src / index.js
Provider 为APP组件及其子组件提供mobx容器数据
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import rootStore from './stores';
import { Provider} from 'mobx-react'
ReactDOM.render(
<Provider {...new rootStore()}>
<App/>
</Provider>,
document.getElementById('root')
);
4. 组件
⭕ src / App.js
import React, { Component } from 'react'
import Product from './components/Products'
import Cart from './components/Cart'
export default class App extends Component {
render() {
return (
<div>
<Product/>
<hr/>
<Cart/>
</div>
)
}
}
⭕ src / components / Product.js
由于装饰器的顺序依赖问题,确保@observable需要最先依赖再注入@inject(官网详见:4.5)
@inject(‘XXX’) 用谁就先声明一下
import React, { Component } from 'react'
import { observer,inject } from 'mobx-react'
@inject('productsStore', 'cartStore')
@observer
export default class Product extends Component {
render() {
console.log('111');
const {productsStore, cartStore} = this.props
return (
<div>
<h1>我是商品列表</h1>
<ul>
{
productsStore.all.map( goods => (
<li key={goods.id}>
{goods.title + " ----- ¥" + goods.price + "-----库存: " + goods.inventory }
<br/>
<button
disabled={!goods.inventory}
onClick={ () => cartStore.addToCart(goods) }>
{!goods.inventory ? 'Sold Out' : 'Add to cart'}
</button>
</li>
))
}
</ul>
</div>
)
}
componentDidMount () {
console.log('222');
this.props.productsStore.getAllProducts();
}
}
⭕ src / components / Cart.js
import React, { Component } from 'react'
import { observer,inject } from 'mobx-react'
@inject('cartStore')
@observer
export default class Cart extends Component {
render() {
const {cartStore} = this.props
return (
<div>
<h1>我是购物车</h1>
<ul>
{
cartStore.items.map( goods => (
<li key={goods.id}>
{goods.title + " ----- ¥" + goods.price + "-----购买数量: " + goods.quantity }
</li>
))
}
</ul>
<p>Total: ¥{cartStore.totalPrice.toFixed(2)}</p>
<p>
<button disabled={!cartStore.items.length} onClick={() => cartStore.checkout()}> Checkout</button>
</p>
{cartStore.checkoutStatus && <p>Checkout {cartStore.checkoutStatus}</p>}
</div>
)
}
}