mobx-簡單可擴充套件的狀態管理庫

渣渣的生存之道發表於2018-09-01

mobx

mobx,作用類似於redux,相比於reduxmobx的學習成本更低,開發難度低,開發程式碼少,渲染效能好(狀態和元件是一對一的,比如你又三個轉檯對應三個元件,如果元件狀態發生改變之後,只會處理受影響的元件,不受影響的不做處理)

核心思想

狀態變化引起的副作用應該被自動觸發

mobx-簡單可擴充套件的狀態管理庫

  • action是唯一可以修改state的東西,並可能有其他副作用
  • State是可觀察和最低響度定義的,不應包含冗餘活推倒資料,能算出來的就寫在Computed values裡
  • Computed values是可以使用pure function 從state中推倒出的值。mobx會自動更新它並在它不實用是將其優化掉
  1. 應用邏輯只需要修改狀態資料即可,mobx回自動渲染UI,無需人工干預
  2. 資料變化只會渲染對應的元件
  3. MobX提供機制來儲存和更新應用狀態供 React 使用 eact 通過提供機制把應用狀態轉換為可渲染元件樹並對其進行渲染

概念的東西可能有點抽象,我們先試試

mkdir mobx-text

npm init -y

npm i webpack webpack-cli babel-core babel-loader babel-preset-env babel-preset-react babel-preset-stage-0 babel-plugin-transform-decorators-legacy mobx mobx-react react -D

安裝完成後,開啟專案

建立mobx-test/webpack.config.js,

mobx-test/webpack.config.js

const path = require('path');
module.exports = {
    entry: './src/index.js' ,//檔案入口
    output:{
        path: path.resolve('dist'),
        filename: 'bundle.js'
    },
    //模組編譯
    module:{
        rules:[
            {
                test:/\.jsx?/,
                use:{
                    loader:'babel-loder',
                    options: {
                        presets: ["env", "react", "stage-0"],
                        plugins: ["transform-decorators-legacy"]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
}
複製程式碼

建立tsconfig.json

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "allowJs": true
    }
}
複製程式碼

修改package.json

 "scripts": {
    "start": "webpack  --mode development -w"
  },
複製程式碼

然後嘗試npm run start會出來mobx/dist/bundle.js, 我們在`mobx/dist

Decorator 修飾器

Decorator是一個修飾類的函式

用來修改類的行為,修飾器本質就是編譯時執行的函式,如果想新增例項屬性,可以通過目標類的prototype物件操作

 @testable
class Person {
}
function testable(target) {
    target.testable = true;
}
// testable(Person) 蕾絲遇將類本身傳到函式裡面修改
console.log(Person.testable);
複製程式碼

修飾屬性

//target類 key類的屬性 discriptor描述器
function readonly(target, key, discriptor){
    discriptor.writable = false;   
}
class Circle {
    @readonly PI = 3.14; //這是例項的屬性
}
let c1 = new Circle();
console.log('Circle.prototype.PI=',Circle.prototype.PI);
c1.PI = 3.15
console.log(c1.PI);

let obj = {};
obj.name = 'zfpx';
Object.defineProperty(obj, 'age', {
    value: 9,//實際的值
    enumerable: false,//是否可列舉
    writable: false,//是否可修改
    configurable: false//是否可配置  delete obj.age;
});
console.log(obj.age);
obj.age = 10;
console.log(obj.age);

複製程式碼

修飾類的圓形(方法)

function logger(target, key, descriptor) {
    let oldVal = descriptor.value;//獲取老函式
    descriptor.value = function () {
        console.log(`${key}(${Array.from(arguments).join(',')})`);
        return oldVal.apply(this, arguments);
    }
}
class Calculator {
    @logger
    add(a, b) {
        return a + b;
    }
}
logger(Calculator.prototype, 'add', Object.getOwnPropertyDescriptor(Calculator.prototype, 'add'));

let c1 = new Calculator();
console.log(Calculator.prototype.add);
let ret = c1.add(1, 2);// add(1,2)
console.log(ret);
複製程式碼

Proxy

  • Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫
  • get方法用於攔截某個屬性的讀取操作,可以接受三個引數,依次為目標物件、屬性名和 proxy 例項本身
  • set方法用來攔截某個屬性的賦值操作,可以接受四個引數,依次為目標物件、屬性名、屬性值和 Proxy 例項本身

代替objectdefinePropty

let p1 = new Proxy({ name: 'zfpx', age: 9},{
    get: function(target, key){
        console.log(`get ${key}`);
        return Reflect.get(target,key);
    },
    set: function(target, key,value){
        console.log(`set ${key} ${value}`);
        target[key] = value;
    }
});
console.log(p1.name,p1.age);

get name
index.js:8 get age
index.js:16 zfpx 9
複製程式碼

mobx

MobX為現有的資料結構(如物件,陣列和類例項)新增了可觀察的功能。

observable就是一種讓資料的變化可以被觀察的方法

先把資料轉化成可以被觀察的物件,那麼對這些資料的修改就可以備監視

let arr1 = observable([1, 2, 3]);
console.log(arr1);
//資料處理
arr1.pop();
arr1.push(4);
arr1.unshift(0);
console.log(arr1); //0124
複製程式碼

o1 輸出一個proxy物件,可觀察物件,原理就是proxy,對於基本資料型別需要裝箱,在類裡面的基本資料型別不需要裝箱

//新增觀察
let { observable, observe } = require('mobx');
let o1 = observable({ name: 'zdl' });
console.log(o1);

observe(o1, change => console.log(change));
o1.name = 'zdl2';

let num = observable.box(1);
observe(num, c => console.log(c));
console.log(num.get());
num.set(2);

let bool = observable.box(true);
console.log(bool.get());

let str = observable.box('hello');
console.log(str.get());
複製程式碼

使用對可觀察物件做出響應

computed

  • 計算值(computed values)是可以根據現有的狀態或其它計算值衍生出的值
  • 組合已有的可觀察資料,成為新的可觀察資料 既是反應又是可觀察資料
  • 可以作為函式使用也可以作為decorator使用 使用 .get() 來獲取計算的當前值 使用 .observe(callback) 來觀察值的改變。
  • computed值可以引用其它computed的值,但是不能迴圈引用
let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    @observable name = 'zfpx';
    @observable age = 10;
    @observable province = '廣東';
    @observable city = '深圳';
    @observable area = '010';
    @observable number = '1899999999';
    @computed get home() {
        return this.province + '-' + this.city;
    }

}
let p1 = new Person();
console.log(p1.home);

//要監聽變數變化可以用observe,但是計算得放到外面
let phone = computed(() => {
    return "number:" + p1.area + '-' + p1.number;
});
phone.observe(c => console.log(c));
p1.area = '202'
複製程式碼

autorun

如果使用修飾器模式,則不能再用observe方法了 當你想建立一個響應式函式,而該函式本身永遠不會有觀察者時,可以使用 mobx.autorun 當使用 autorun 時,所提供的函式總是立即被觸發一次,然後每次它的依賴關係改變時會再次被觸發 資料渲染後自動渲染

上述如果將@computed修飾phone,p1.phone.observe(change=>console.log(change))會出錯,因為p1.phone得到的是字串,所以不能監聽,此時我們就需要用到autorun,立即被觸發一次,然後每次它的依賴關係改變時會再次被觸發。

let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    @observable name = 'zfpx';
    @observable age = 10;
    @observable province = '廣東';
    @observable city = '深圳';

    @observable area = '010';
    @observable number = '1899999999';
    @computed get phone() {
        return this.area + '-' + this.number;
    }
}
let p1 = new Person();
// p1.phone.observe(change=>console.log(change));
console.log(p1.phone);
//自動執行,當系統啟動之後自動執行此函式
autorun(() => {
    console.log(p1.phone);
});
p1.area='020';
複製程式碼

when

when 觀察並執行給定的 predicate,直到返回true。 一旦返回 true,給定的 effect 就會被執行,然後 autorunner(自動執行程式) 會被清理。 該函式返回一個清理器以提前取消自動執行程式。

let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    @observable name = 'zfpx';
    @observable age = 10;
    @observable province = '廣東';
    @observable city = '深圳';

    @observable area = '010';
    @observable number = '1899999999';

    @computed get home() {
        return this.province + '-' + this.city;
    }

    @computed get phone() {
        return this.area + '-' + this.number;
    }
}
let p1 = new Person();
// when會等待條件滿足,一旦滿足就會執行回撥並銷燬監聽
when(() => p1.age >= 11, () => {
    console.log(p1.age);
});
setInterval(() => {
    p1.age++;
}, 1000);

// 會返回一個取消監聽的函式,如果 呼叫它就直接取消監聽
let disposer = when(() => p1.age >= 18, () => {
    console.log(p1.age);
});
disposer();
//這樣就直接取消監聽了,不會在console.log
setInterval(() => {
    p1.age++;
}, 1000);
複製程式碼

reaction

  • autorun的變種,autorun會自動觸發,reaction對於如何追蹤observable賦予了更細粒度的控制
  • 它接收兩個函式引數,第一個(資料 函式)是用來追蹤並返回資料作為第二個函式(效果 函式)的輸入
  • 不同於autorun的是當建立時效果 函式不會直接執行,只有在資料表示式首次返回一個新值後才會執行
  • 可以用在登入資訊儲存和寫快取邏輯
// 監聽陣列中變數的變化 ,變化之後才執行回撥函式
reaction(() => [p1.age, p1.name], arr => {
    console.log(arr);
});
p1.age = 11;
p1.name = 'zfpx8';
複製程式碼

action

即批量處理,全部改完在觸發

...
autorun(() => {
    console.log(p1.phone);
});
p1.area='020';
p1.number='020';

//會輸出兩次,我們想輸出一個,需要用action函式
let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    ...
    @action switchPhone(area, number) {
        this.area = area;
        this.number = number;
    }
}
let p1 = new Person();
autorun(() => {
    console.log(p1.phone);
});
//p1.area='020';
//p1.number='020';
p1.switchPhone('200', '300');

複製程式碼

除了action 還有action.bound

let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    ...
    @action.bound switchPhone(area, number) {
        this.area = area;
        this.number = number;
    }
}
let p1 = new Person();
autorun(() => {
    console.log(p1.phone);
});
//p1.area='020';
//p1.number='020';
//p1.switchPhone('200', '300');
let s = p1.switchPhone;
s('200', '300');
//s() 的this指標不一樣,此時我們就需要bounld操作
複製程式碼

注意點:

let num = 1;
let numObj = observable.box(num);
numObj.observe(x => console.log(x));
numObj.set(100);
num = 2;//由於num是普通型別,所以改變num對numObj沒有影響
複製程式碼

但是如果我們要隨意組合,那我們不可能寫無數個方法,解決方法如下

runInAction

將其作為臨時程式碼塊執行,runInAction不執行完,任何都無法發現,作為action執行

runInAction(() => {
    store.province='山東';
    store.city='濟南';
});
複製程式碼

我們用mobx 和 mobx-react 結合 模擬react-redux

安裝mobx-react && react-dom src/index.js

import React, { Component } from 'react';
import { observable, action } from 'mobx';
import { observer } from 'mobx-react';
import ReactDOM from 'react-dom';

class Store {
    @observable number = 0;
    @action.bound add(num) {
        this.number = this.number + num;
    }
}
@observer
class Counter extends Component {
    render() {
        let store = this.props.store;
        return (
            <div>
                <p>{store.number}</p>
                <button onClick={() => store.add(1)}>+</button>
            </div>
        )
    }
}
let store = new Store();
//store.add = store.add.bind(store);
ReactDOM.render(<Counter store={store} />, document.querySelector('#root'));
複製程式碼

import React, { Component } from 'react';
import { observable, action ,observe} from 'mobx';
import { observer } from 'mobx-react';
import ReactDOM from 'react-dom';

class Store {
    @observable todos = [];
    constructor() {
        observe(this.todos, event => {
            console.log(event);
        });
    }
}
let store = new Store();
store.todos.push({id: 1, name : 'zfpx'});
store.todos.push({id: 2, name : 'zfpx'});
console.log(store.todos.get(0).name);
store.todos.get(0).name = 'zfpx3'
console.log(store.todos.get(0).name);
複製程式碼

disposers

import React, { Component } from 'react';
import { observable, action ,observe} from 'mobx';
import { observer } from 'mobx-react';
import ReactDOM from 'react-dom';

class Store {
    @observable todos = []
    disposers = [];//裡面存放著所有的取消監聽的函式 
    //希望監聽store的變化,變化發生之後自動執行回撥函式
    constructor() {
        observe(this.todos, event => {
            console.log(event);
            //讓以前的所有取消監聽函式執行
            this.disposers.forEach(disposer => disposer());
            this.disposers = [];
            for (let todo of event.object) {
                let disposer = observe(todo, e => {
                    console.log(e);
                });
                this.disposers.push(disposer);
            }
        });
    }
}
let store = new Store();
store.todos.push({id: 1, name : 'zfpx'});
store.todos.push({id: 2, name : 'zfpx'});
store.todos.get(0).name = 'zfpx3'
複製程式碼

spy

監聽每一個變化,但是效能很差,一般挑食的時候才用

import React, { Component } from 'react';
import { observable, action ,observe} from 'mobx';
import { observer } from 'mobx-react';
import ReactDOM from 'react-dom';
spy(event => console.log(event));
class Store {
    @observable todos = []
    disposers = [];//裡面存放著所有的取消監聽的函式 
}
let store = new Store();
store.todos.push({id: 1, name : 'zfpx'});
store.todos.push({id: 2, name : 'zfpx'});
store.todos.get(0).name = 'zfpx3'
複製程式碼

結合react例項

src/index.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { observable, action, computed, trace } from 'mobx';
import { observer } from 'mobx-react';

//放資料
class Todo {
    id = Math.random();
    @observable text = '';
    @observable completed = false;
    constructor(text) {
        this.text = text;
    }
    //切換完成狀態 
    @action.bound toggle() {
        this.completed = !this.completed;
    }
}

class Store {
    @observable todos = [];
    @observable filter = 'all';
    @action.bound changeFilter(filter) {
        this.filter = filter; 
    } 
    @action.bound addTodo(text) {
        this.todos.push(new Todo(text));
    }
    @action.bound removeTodo(todo) {
        this.todos.remove(todo);
    }
    @computed get filteredTodos() {
        return this.todos.filter(todo => {
            switch (this.filter) {
                case 'completed':
                    return todo.completed;
                case 'uncompleted':
                    return !todo.completed;
                default:
                    return true;
            }
        });
    }
    @computed get reminder() {
        return this.todos.reduce((count, todo) => {
            count = count + (todo.completed ? 0 : 1);
            return count;
        }, 0);
    }
}
let store = new Store();
@observer
class TodoItem extends Component {
    trace();
    render() {
        return (
            <React.Fragment>
                <input type="checkbox" 
                    onChange={this.props.todo.toggle}
                    checked={this.props.todo.completed} />
                <span style={{ textDecoration: this.props.todo.completed ? 'line-through' : '' }}>{this.props.todo.text}</span>
            </React.Fragment>
        )
    }
}

@observer
class Todos extends Component {
    state = {
        text: ""
    }
    handleSubmit = (event) => {
        event.preventDefault();
        let text = this.state.text;
        this.props.store.addTodo(text);
        this.setState({ text: '' });
    }  
    handleChange = (event) => {
        this.setState({
            text: event.target.value
        });
    }
    render() {
        trace()
        let store = this.props.store;
        return (
            <div>
                <form onSubmit={this.handleSubmit}>
                    <input type="text"
                        onChange={this.handleChange}
                        value={this.state.text} />
                </form>
                <ul>
                    {
                        store.filteredTodos.map(todo => (
                            <li key={todo.id}>
                                <TodoItem todo={todo} />
                                <span onClick={() => store.removeTodo(todo)}>X</span>
                            </li>                         
                        ))
                    }
                </ul>
                <div>
                    <p>你還有{store.reminder}件待辦事項</p>
                    <p>
                        <button
                            onClick={() => store.changeFilter('all')}
                            style={{ color: store.filter == 'all' ? 'red' : 'black' }}
                        >全部</button>
                        <button
                            onClick={() => store.changeFilter('uncompleted')}
                            style={{ color: store.filter == 'uncompleted' ? 'red' : 'black' }}
                        >未完成</button>
                        <button
                            onClick={() => store.changeFilter('completed')}
                            style={{ color: store.filter == 'completed' ? 'red' : 'black' }}
                        >已完成</button>
                    </p>
                </div>
            </div>
        )
    }
}
ReactDOM.render(<Todos store={store} />, document.querySelector('#root'));
複製程式碼

我們可以看到使用mobx優雅,簡潔,但這還不夠,我們用其中的trace測試效能,結果並不太好,因此我們對述程式碼做出優化,

程式碼優化

優化原則

  • 把檢視拆解的更細緻
  • 使用專門的檢視渲染列表資料
  • 儘可能晚的解構使用資料

src/index

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { observable, action, computed, trace , observe, toJS} from 'mobx';
import { observer } from 'mobx-react';

//放資料
class Todo {
    id = Math.random();
    @observable text = '';
    @observable completed = false;
    constructor(text, completed = false) {
        this.text = text;
    }
    //切換完成狀態 
    @action.bound toggle() {
        this.completed = !this.completed;
    }
}

class Store {
    @observable todos = [];
    
    @observable filter = 'all';
    @action.bound changeFilter(filter) {
        this.filter = filter; 
    } 
    @action.bound addTodo(text) {
        this.todos.push(new Todo(text));
    }
    @action.bound removeTodo(todo) {
        this.todos.remove(todo);
    }
    @computed get filteredTodos() {
        return this.todos.filter(todo => {
            switch (this.filter) {
                case 'completed':
                    return todo.completed;
                case 'uncompleted':
                    return !todo.completed;
                default:
                    return true;
            }
        });
    }
    @computed get reminder() {
        return this.todos.reduce((count, todo) => {
            count = count + (todo.completed ? 0 : 1);
            return count;
        }, 0);
    }
}
let store = new Store();
@observer
class TodoItem extends Component {
    render() {
        return (
            <React.Fragment>
                <input type="checkbox" 
                    onChange={this.props.todo.toggle}
                    checked={this.props.todo.completed} />
                <span style={{ textDecoration: this.props.todo.completed ? 'line-through' : '' }}>{this.props.todo.text}</span>
            </React.Fragment>
        )
    }
}
@observer
class TodoHeader extends Component {
    state = {
        text: ""
    }
    handleChange = (event) => {
        this.setState({
            text: event.target.value
        });
    }
    handleSubmit = (event) => {
        event.preventDefault();
        let text = this.state.text;
        this.props.store.addTodo(text);
        this.setState({ text: '' });
    }  
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                    <input type="text"
                        onChange={this.handleChange}
                        value={this.state.text} />
            </form>
        )
    }
}
@observer
class TodoItems extends Component {
    render() {
        return (
            <ul>
                {
                    this.props.store.filteredTodos.map(todo => (
                        <li key={todo.id}>
                            <TodoItem todo={todo} />
                            <span onClick={() => store.removeTodo(todo)}>X</span>
                        </li>                         
                    ))
                }
            </ul>
        )
    }
}
@observer
class TodoFooter extends Component {
    render() {
        let store = this.props.store;
        return (
            <div>
                <p>你還有{store.reminder}件待辦事項</p>
                <p>
                    <button
                        onClick={() => store.changeFilter('all')}
                        style={{ color: store.filter == 'all' ? 'red' : 'black' }}
                    >全部</button>
                    <button
                        onClick={() => store.changeFilter('uncompleted')}
                        style={{ color: store.filter == 'uncompleted' ? 'red' : 'black' }}
                    >未完成</button>
                    <button
                        onClick={() => store.changeFilter('completed')}
                        style={{ color: store.filter == 'completed' ? 'red' : 'black' }}
                    >已完成</button>
                </p>
            </div>
        )
    }
}

@observer
class Todos extends Component {
    render() {
        let store = this.props.store;
        return (
            <div>
                <TodoHeader store={store} />
                <TodoItems store={store} />
                <TodoFooter store={store} />
            </div>
        )
    }
}
ReactDOM.render(<Todos store={store} />, document.querySelector('#root'));
複製程式碼

資料持久

每次重新整理保持原來狀態

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { observable, action, computed, trace , observe, toJS} from 'mobx';
import { observer } from 'mobx-react';

//放資料
class Todo {
    id = Math.random();
    @observable text = '';
    @observable completed = false;
    constructor(text, completed = false) {
        this.text = text;
    }
    //切換完成狀態 
    @action.bound toggle() {
        this.completed = !this.completed;
    }
}

class Store {...
    @action.bound load(todos) {
        todos.forEach(todo => {
            this.todos.push(new Todo(todo.text, todo.completed));
        });
    }
    cancelObserves = [];
    constructor() {
        observe(this.todos, (event) => {
            console.log('event', event);
            this.save();
            this.cancelObserves.forEach(d => d());
            this.cancelObserves = [];
            for (let todo of event.object) {
                this.cancelObserves.push(observe(todo, this.save));
            }
        });
    }
    @action.bound save() {
        let todos = toJS(this.todos);
        localStorage.setItem('todos', JSON.stringify(todos));
    }
    @action.bound addTodo(text) {
        window.setTimeout(() => {
            this.todos.push(new Todo(text));
        }, 1000)
    }
   ...
}
...
@observer
class Todos extends Component {
    componentDidMount() {
        let todosStr = localStorage.getItem('todos');
        let todos = todosStr ? JSON.parse(todosStr) : [];
        this.props.store.load(todos);
    }
    render() {...}
}
ReactDOM.render(<Todos store={store} />, document.querySelector('#root'));
複製程式碼

相關文章