Mobx快速应用

585 阅读1分钟

前言

本小册是《千锋大前端小册》系列之 Mobx 部分。通过本小册,可以系统学习 Mobx 基础知识,为将来 Mobx 在大前端项目的应用打下坚实的基础。

—— 作者:千锋教育·古艺散人

mobx搭建环境

mkdir my-app
cd my-app
npm init -y
npm i webpack webpack-cli webpack-dev-server -D
npm i html-webpack-plugin -D
npm i babel-loader @babel/core @babel/preset-env -D
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime -S
npm i mobx -S
mkdir src
mkdir dist
touch index.html
touch src/index.js
touch webpack.config.js

编写webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'development',
  entry: path.resolve(__dirname, 'src/index.js'),
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [
              //支持装饰器
              ["@babel/plugin-proposal-decorators", { "legacy": true }],
              ["@babel/plugin-proposal-class-properties", { "loose" : true }],
              ['@babel/plugin-transform-runtime']
            ]
          }
        }
      }
    ]
  },
  plugins: [new HtmlWebpackPlugin()],
  devtool: 'inline-source-map'
}

编写index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

mobx入门

observable可观察的状态

1. map

import {observable} from 'mobx'
// 声明
const map = observable.map({a: 1, b: 2});
// 设置
map.set('a', 11);
// 获取
console.log(map.get('a'));
console.log(map.get('b'));
// 删除
map.delete('a');
console.log(map.get('a'));
// 判断是否存在属性
console.log(map.has('a'));

2. object

import {observable} from 'mobx'
// 声明
const obj = observable({a: 1, b: 2});
// 修改
obj.a = 11;
// 访问
console.log(obj.a, obj.b);

3. array

import {observable} from 'mobx'
const arr = observable(['a', 'b', 'c', 'd']);
// 访问
console.log(arr[0], arr[10]);
// 操作
arr.pop();
arr.push('e');

4. 基础类型

import {observable} from 'mobx'/
const num = observable.box(10);
const str = observable.box('hello');
const bool = observable.box(true);
// 获得值
console.log(num.get(), str.get(), bool.get());
// 修改值
num.set(100);
str.set('hi');
bool.set(false);
console.log(num.get(), str.get(), bool.get());

observable装饰器

import {observable} from 'mobx'

// observable这个函数可以识别当成普通函数调用还是装饰器调用
// 如果是装饰器,会自动识别数据类型,使用不同的包装转换方案。
class Store{
  @observable arr = [];
  @observable obj = {a: 1};
  @observable map = new Map();
  @observable str = 'hello';
  @observable num = 123;
  @observable bool = false;
}

const store = new Store();

console.log(store);
console.log(store.obj.a);

注意:vscode编译器中,js文件使用装饰器会报红。解决方式:

在根目录编写jsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "experimentalDecorators": true
  },
  "include": ["src/**/*"]
}

对 observables 作出响应

0. 基础代码:

import {observable} from 'mobx'
class Store{
  @observable arr = [];
  @observable obj = {a: 1};
  @observable map = new Map();
  @observable str = 'hello';
  @observable num = 123;
  @observable bool = false;
}
const store = new Store();

1. computed

计算值是可以根据现有的状态或其它计算值衍生出的值, 跟vue中的computed非常相似。

const result = computed(()=>store.str + store.num);
console.log(result.get());
// 监听数据的变化
result.observe((change)=>{
  console.log('result:', change);
})
//两次对store属性的修改都会引起result的变化
store.str = 'world';
store.num = 220;

computed可作为装饰器, 将result的计算添加到类中:

class Store{
  @observable arr = [];
  @observable obj = {a: 1};
  @observable map = new Map();

  @observable str = 'hello';
  @observable num = 123;
  @observable bool = false;

  @computed get result(){
    return this.str + this.num;
  }  
}

2. autorun

当你想创建一个响应式函数,而该函数本身永远不会有观察者时,可以使用 mobx.autorun

所提供的函数总是立即被触发一次,然后每次它的依赖关系改变时会再次被触发。

经验法则:如果你有一个函数应该自动运行,但不会产生一个新的值,请使用autorun。 其余情况都应该使用 computed

//aotu会立即触发一次
autorun(()=>{
  console.log(store.str + store.num);
})

autorun(()=>{
  console.log(store.result);
})
//两次修改都会引起autorun执行
store.num = 220;
store.str = 'world';

3. when

when(predicate: () => boolean, effect?: () => void, options?)

when 观察并运行给定的 predicate,直到返回true。 一旦返回 true,给定的 effect 就会被执行,然后 autorunner(自动运行程序) 会被清理。 该函数返回一个清理器以提前取消自动运行程序。

对于以响应式方式来进行处理或者取消,此函数非常有用。

when(()=>store.bool, ()=>{
  console.log('when function run.....');
})
store.bool = true;

4. reaction

用法: reaction(() => data, (data, reaction) => { sideEffect }, options?)

autorun 的变种,对于如何追踪 observable 赋予了更细粒度的控制。 它接收两个函数参数,第一个(数据 函数)是用来追踪并返回数据作为第二个函数(效果 函数)的输入。 不同于 autorun 的是当创建时效果 函数不会直接运行,只有在数据表达式首次返回一个新值后才会运行。 在执行 效果 函数时访问的任何 observable 都不会被追踪。

// reaction
reaction(()=>[store.str, store.num], (arr)=>{
  console.log(arr.join('/'));
})
//只要[store.str, store.num]中任意一值发生变化,reaction第二个函数都会执行
store.num = 220;
store.str = 'world';

改变 observables状态

1. action

接上面案例,添加action到类中:

class Store{
  @observable arr = [];
  @observable obj = {a: 1};
  @observable map = new Map();

  @observable str = 'hello';
  @observable num = 123;
  @observable bool = false;

  @computed get result(){
    return this.str + this.num;
  }

  @action bar(){
    this.str = 'world';
    this.num = 40;
  }
}
const store = new Store();

//调用action,只会执行一次
store.bar();

2. action.bound

action.bound 可以用来自动地将动作绑定到目标对象。

class Store{
  @observable arr = [];
  @observable obj = {a: 1};
  @observable map = new Map();

  @observable str = 'hello';
  @observable num = 123;
  @observable bool = false;

  @computed get result(){
    return this.str + this.num;
  }

  @action bar(){
    this.str = 'world';
    this.num = 40;
  }
	
  //this 永远都是正确的
  @action.bound foo(){
    this.str = 'world';
    this.num = 40;
  }
}

const store = new Store();
setInterval(store.foo, 1000)

3. runInAction

action 只能影响正在运行的函数,而无法影响当前函数调用的异步操作。如果你使用async function来处理业务,那么我们可以使用 runInAction 这个API来解决这个问题。

@action async fzz() {
  await new Promise((resolve) => { 
    setTimeout(() => {
      resolve({
        num: 220,
        str: 'world'
      })
    }, 1000) 
  })
  runInAction(()=>{
    store.num = 220
    store.str = 'world'
  })	
}

在react中使用mobx

在react中使用mobx,需要借助mobx-react。

它的功能相当于在react中使用redux,需要借助react-redux。

首先来搭建环境:

create-react-app react-app
cd react-app
npm run eject
npm i @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties -D
npm i mobx mobx-react -S

修改package.json中babel的配置:

  "babel": {
    "presets": [
      "react-app"
    ],
    "plugins": [
      [
        "@babel/plugin-proposal-decorators",
        {
          "legacy": true
        }
      ],
      [
        "@babel/plugin-proposal-class-properties",
        {
          "loose": true
        }
      ]
    ]
  }

注意:vscode编译器中,js文件使用装饰器会报红。解决方式:

在根目录编写写jsconfig.json

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "experimentalDecorators": true
  },
  "include": ["src/**/*"]
}

React + Mobx 案例

修改index.js

应用 react-mobx-app 创建完项目基本环境,修改项目根目录下的 index.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

ReactDOM.render(
  <App/>,
  document.querySelector('#root')
)

创建 store

在项目根目录下创建store目录,在store目录下创建index.js,内容如下:

import { observable, action, computed } from 'mobx'

class AppStore {

  @observable title = 'mobx'
  @observable todos = []

  @computed get desc(){
    return `一共${this.todos.length}条`
  }

  @action addTodo(todo) {
    this.todos.push(todo)
  }
  @action deleteTodo() {
    this.todos.pop()
  }
  @action resetTodo() {
    this.todos = []
  }
}

const store = new AppStore()

export default store

修改App.js

修改项目根目录下的 App.js

import React, { Component } from 'react'
import Home from './pages/Home'
import { Provider } from 'mobx-react'
import store from './store/'
export default class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <Home></Home>
      </Provider>
    )
  }
}

创建 Home.js

在项目根目录下创建 pages 文件夹,在 pages文件夹下创建 Home.js 组件,内容如下:

import React, { Component } from 'react'
import {inject, observer} from 'mobx-react'

@inject('store') 
@observer
class Home extends Component {
  render() {
    const {store} = this.props
    return (
      <div>
        <div>{store.title}</div>
        <button onClick={this.handleTodos.bind(this, 'add')}>添加</button>
        <button onClick={this.handleTodos.bind(this, 'delete')}>删除</button>
        <button onClick={this.handleTodos.bind(this, 'reset')}>重置</button>
        <h6>{store.length}</h6>
        {
          store.todos.map((ele, index)=>(
            <div key={index}>
              {ele}
            </div>
          ))
        }
      </div>
    )
  }
  handleTodos(type){
    const {store} = this.props
    switch (type) {
      case 'add':
        store.addTodo('这是一条新内容')
        break
      case 'delete':
        store.deleteTodo()
        break
      case 'reset':
        store.resetTodo()
        break
      default:
        break
    }
  }
}
export default Home;