React-mobx解析

小圖子發表於2019-02-28

1. mobx

mobx是一個簡單可擴充套件的狀態管理庫

2. mobx vs redux

mobx學習成本更低,效能更好的的狀態解決方案

  • 開發難度低
  • 開發程式碼量少
  • 渲染效能好

3. 核心思想

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

  • 應用邏輯只需要修改狀態資料即可,mobx回自動渲染UI,無需人工干預
  • 資料變化只會渲染對應的元件
  • MobX提供機制來儲存和更新應用狀態供 React 使用
  • eact 通過提供機制把應用狀態轉換為可渲染元件樹並對其進行渲染
    Alt
    ;

4. 環境準備

4.1 安裝依賴模組

pm 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 -D
複製程式碼

4.2 webpack.config.js

const path=require('path');
module.exports = {
    mode: 'development',
    entry: path.resolve(__dirname,'src/index.js'),
    output: {
        path: path.resolve(__dirname,'dist'),
        filename:'main.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['env','react','stage-0'],
                        plugins:['transform-decorators-legacy']
                    }
                }
            }
        ]
    },
    devtool:'inline-source-map'
}
複製程式碼

4.3 package.json

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

5. Decorator

5.1 類的修飾

  • 修飾器(Decorator)函式,用來修改類的行為
  • 修飾器是一個對類進行處理的函式。修飾器函式的第一個引數,就是所要修飾的目標類
  • 修飾器本質就是編譯時執行的函式
  • 如果想新增例項屬性,可以通過目標類的prototype物件操作
@testable
class Person{

}
//修改了Person的類的行為,增加了靜態屬性isTestable
function testable(target) {
    target.isTestable=true;
}
console.log(Person.isTestable)

@decorator
class A{}
---------------
class A{}
A = decorator(A);
複製程式碼

5.2 修飾屬性

class Circle{
    @readonly PI=3.14;
}

//descriptor {value:func,enumerable:false,configurable:true,writable:true}
function readonly(target,name,descriptor) {
    console.log(descriptor);
    descriptor.writable=false;
}
let c1=new Circle();
c1.PI=3.15;
console.log(c1.PI)
複製程式碼

5.3 修飾方法

  • 修飾器不僅可以修飾類,還可以修飾類的屬性
class Calculator{
    //@logger
    add(a,b) {
        return a+b; 
    }
}

function logger(target,name,descriptor) {
    let oldValue=descriptor.value;
    descriptor.value=function () {
        console.log(`${name}(${Array.prototype.join.call(arguments,',')})`);
        return oldValue.apply(this,arguments);
    }
}
let oldDescriptor=Object.getOwnPropertyDescriptor(Calculator.prototype,'add');
logger(Calculator.prototype,'add',oldDescriptor);
Object.defineProperty(Calculator.prototype,'add',oldDescriptor);

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

6. Proxy

  • Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫
  • get方法用於攔截某個屬性的讀取操作,可以接受三個引數,依次為目標物件、屬性名和 proxy 例項本身
  • set方法用來攔截某個屬性的賦值操作,可以接受四個引數,依次為目標物件、屬性名、屬性值和 Proxy 例項本身
var proxy = new Proxy(target, handler);
複製程式碼
let p1=new Proxy({name:'zfpx'},{
    get: function (target,key,receiver) {
        console.log(`getting ${key}`);
        console.log(receiver);
        return Reflect.get(target,key,receiver);
    },
    set: function (target,key,value,receiver) {
        console.log(`setting ${key}`);
        return Reflect.set(target,key,value,receiver);
    }
});
console.log(p1.name);
複製程式碼

7. mobx

7.1 observable

  • MobX為現有的資料結構(如物件,陣列和類例項)新增了可觀察的功能。
  • observable就是一種讓資料的變化可以被觀察的方法
  • 先把資料轉化成可以被觀察的物件,那麼對這些資料的修改就可以備監視

7.1.1 引用型別 (observable)

型別 描述
物件
陣列
const {observable,isArrayLike}=require('mobx');
function observable2(target) {
    return new Proxy(target,{});
}
const p1=observable2({name:'zfpx'});
console.log(p1.name);
複製程式碼
const {observable}=require('mobx');
function observable2(target) {
    return new Proxy(target,{

    });
}
const p1=observable([1,2,3]);
p1.push(4);
p1.pop();
console.log(p1);
console.log(Array.isArray(p1));
複製程式碼

7.1.2 基本型別(observable.box)

型別 描述
String 字串
Boolean 布林值
Number 數字
Symbol 獨一無二的值
const {observable}=require('mobx');
let num=observable.box(10);
let str=observable.box('hello');
let bool=observable.box(true);
console.log(num.get(),str.get(),bool.get());
num.set(100);
str.set('world');
bool.set(false);
console.log(num.get(),str.get(),bool.get());
複製程式碼

7.1.3 decorator

import {observable} from 'mobx';
class Store {
    @observable name='zfpx';
    @observable age=9;
    @observable isMarried=false;

    @observable hobbies=[];
    @observable home={name:'北京'};
    @observable skills=new Map();
}
複製程式碼

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

8.1 computed

  • 計算值(computed values)是可以根據現有的狀態或其它計算值衍生出的值
  • 組合已有的可觀察資料,成為新的可觀察資料
  • 既是反應又是可觀察資料
  • 可以作為函式使用也可以作為decorator使用
  • 使用 .get() 來獲取計算的當前值
  • 使用 .observe(callback) 來觀察值的改變。
  • computed值可以引用其它computed的值,但是不能迴圈引用
let {observable,computed} = require('mobx');
class Store {
    @observable name='zfpx';
    @observable age=9;
    @observable area='010';
    @observable number="18910092296"

    @observable province="廣東";
    @observable city="東莞";
    @computed get home() {
        return this.province+this.city;
    }
}

let store=new Store();
let cell = computed(function () {
    return store.area+'-'+store.number;
});
cell.observe(change=>console.log(change));
console.log(cell.get());
store.area='020';
store.number='15718856132';
console.log(cell.get());
console.log(store.home);
store.province='山東';
store.city='濟南';
console.log(store.home);
複製程式碼

8.2 autorun

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

store.province='山東';
store.city='濟南';
複製程式碼

8.3 when

  • when 觀察並執行給定的 predicate,直到返回true。
  • 一旦返回 true,給定的 effect 就會被執行,然後 autorunner(自動執行程式) 會被清理。
  • 該函式返回一個清理器以提前取消自動執行程式。
when(predicate: () => boolean, effect?: () => void, options?)
複製程式碼
let dispose = when(() => store.age>=18,()=>{
    console.log('你已經成年了!')
});
dispose();
store.age=10;
store.age=20;
store.age=30;
複製程式碼

8.4 reaction

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

9. action

  • 前面的方式每次修改都會觸發autorun和reaction執行
  • 使用者一次操作需要修改多個變數,但是檢視更新只需要一次
  • 任何應用都有動作,動作是任何用來修改狀態的東西
  • 動作會分批處理變化並只在(最外層的)動作完成後通知計算值和反應
  • 這將確保在動作完成之前,在動作期間生成的中間值或未完成的值對應用的其餘部分是不可見的

9.1 action

let {observable,computed,autorun,when,reaction,action} = require('mobx');
class Store {
    @observable province="廣東";
    @observable city="東莞";
    @action moveHome(province,city) {
         this.province=province;
         this.city=city;
    }
}
let store=new Store();
reaction(() => [store.province,store.city],arr => console.log(arr.join(',')));
store.moveHome('山東','濟南');
複製程式碼

9.2 action.bound

let {observable,computed,autorun,when,reaction,action} = require('mobx');
class Store {
    @observable province="廣東";
    @observable city="東莞";
    @action.bound moveHome(province,city) {
         this.province=province;
         this.city=city;
    }
}
let store=new Store();
reaction(() => [store.province,store.city],arr => console.log(arr.join(',')));
let moveHome=store.moveHome;
moveHome('山東','濟南');
複製程式碼

9.3 runInAction

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

10. mobx應用

  • mobx-react 核心是將render方法包裝為autorun
  • 誰用到了可觀察屬性,就需要被observer修飾,按需渲染
cnpm i react react-dom mobx-react -S 
複製程式碼

10.1 計數器

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

class Store {
    @observable number=0;
    @action.bound add() {
        this.number++;
    }
}
let store=new Store();

@observer
class Counter extends Component{
    render() {
        return (
            <div>
                <p>{store.number}</p>
                <button onClick={store.add}>+</button>
            </div>
        )
    }
}
ReactDOM.render(<Counter/>,document.querySelector('#root'));
複製程式碼
import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import {observable,action} from 'mobx';
import PropTypes from 'prop-types';
import {observer} from 'mobx-react';

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

10.2 TODO

import React,{Component,Fragment} from 'react';
import ReactDOM from 'react-dom';
import {observable,action, computed} from 'mobx';
import PropTypes from 'prop-types';
import {observer,PropTypes as ObservablePropTypes} 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=[];
    @computed get left() {
        return this.todos.filter(todo=>!todo.completed).length;
    }
    @computed get filterTodos() {
        return this.todos.filter(todo => {
            switch (this.filter) {
                case 'completed':
                    return todo.completed;
                case 'uncompleted':
                    return !todo.completed;
                default:
                    return true;
            }
        });
    }
    @observable filter='all';
    @action.bound changeFilter(filter) {

        this.filter=filter;
        console.log(this.filter);
    }
    @action.bound addTodo(text) {
        this.todos.push(new Todo(text));
    }
    @action.bound removeTodo(todo) {
        this.todos.remove(todo);
    }
}
@observer
class TodoItem extends Component{
    static porpTypes={
        todo: PropTypes.shape({
            id: PropTypes.number.isRequired,
            text: PropTypes.string.isRequired,
            completed:PropTypes.bool.isRequired
        }).isRequired
    }
    render() {
        let {todo}=this.props;
        return (
            <Fragment>
                <input
                    type="checkbox"
                    onChange={todo.toggle}
                    checked={todo.completed} />
                <span className={todo.completed? 'completed':''}>{todo.text}</span>

            </Fragment>
        )
    }
}
@observer
class TodoList extends Component{
    static propsTypes={
        store: PropTypes.shape({
            addTodo:PropTypes.func,
            todos:ObservablePropTypes.observableArrayOf(ObservablePropTypes.observableObject)
        }).isRequired
    };
    state={text:''}
    handleSubmit=(event) => {
        event.preventDefault();
        this.props.store.addTodo(this.state.text);
        this.setState({text:''});
    }
    handleChange=(event) => {
        this.setState({text:event.target.value});
    }
    render() {
        let {filterTodos,left,removeTodo,filter,changeFilter}=this.props.store;
        return (
            <div className="todo-list">
                <form onSubmit={this.handleSubmit}>
                    <input placeholder="請輸入待辦事項" type="text" value={this.state.text} onChange={this.handleChange}/>
                </form>
                <ul>
                    {
                        filterTodos.map(todo => (
                            <li key={todo.id}>
                                <TodoItem todo={todo} />
                                <button onClick={()=>removeTodo(todo)}>X</button>
                            </li>
                        ))
                    }
                </ul>
                <p>
                    <span>你還有{left}件待辦事項!</span>
                    <button
                        onClick={()=>changeFilter('all')}
                        className={filter==='all'?'active':''}>全部</button>
                    <button onClick={() => changeFilter('uncompleted')}
                        className={filter==='uncompleted'?'active':''}>未完成</button>
                    <button
                        onClick={()=>changeFilter('completed')}
                        className={filter==='completed'?'active':''}>已完成</button>
                </p>
            </div>
        )
    }
}
let store=new Store();
ReactDOM.render(<TodoList store={store}/>,document.querySelector('#root'));
複製程式碼

11.優化

11.1 observe

  constructor() {
        observe(this.todos,change => {
            console.log(change);
            this.disposers.forEach(disposer => disposer());
            this.disposers=[];
            for (let todo of change.object) {
                this.disposers.push(observe(todo,change => {
                    this.save();
                    //console.log(change)
                }));
            }
            this.save();
        });
    }
複製程式碼

11.2 spy

spy(event => {
    //console.log(event);
})
複製程式碼

11.3 toJS

  constructor() {
        observe(this.todos,change => {
            console.log(change);
            this.disposers.forEach(disposer => disposer());
            this.disposers=[];
            for (let todo of change.object) {
                this.disposers.push(observe(todo,change => {
                    this.save();
                    //console.log(change)
                }));
            }
            this.save();
        });
    }
    save() {
        localStorage.setItem('todos',JSON.stringify(toJS(this.todos)));
    }
複製程式碼

11.4 trace

trace

12. 優化

  • 把檢視拆解的更細緻
  • 使用專門的檢視渲染列表資料
  • 儘可能晚的解構使用資料
import React,{Component,Fragment} from 'react';
import ReactDOM from 'react-dom';
import {trace,observable,action, computed, observe, spy,toJS} from 'mobx';
import PropTypes from 'prop-types';
import {observer,PropTypes as ObservablePropTypes} from 'mobx-react';
spy(event => {
  //console.log(event);
})
class Todo{
  id=Math.random();
  @observable text='';
  @observable completed=false;
  constructor(text) {
      this.text=text;

  }
  @action.bound toggle() {
      this.completed=!this.completed;
  }
}
class Store{
  disposers=[];
  constructor() {
      observe(this.todos,change => {
          console.log(change);

          this.disposers.forEach(disposer => disposer());
          this.disposers=[];
          for (let todo of change.object) {
              this.disposers.push(observe(todo,change => {
                  this.save();
                  //console.log(change)
              }));
          }
          this.save();
      });
  }
  save() {
      localStorage.setItem('todos',JSON.stringify(toJS(this.todos)));
  }
  @observable todos=[];
  @computed get left() {
      return this.todos.filter(todo=>!todo.completed).length;
  }
  @computed get filterTodos() {
      return this.todos.filter(todo => {
          switch (this.filter) {
              case 'completed':
                  return todo.completed;
              case 'uncompleted':
                  return !todo.completed;
              default:
                  return true;
          }
      });
  }
  @observable filter='all';
  @action.bound changeFilter(filter) {

      this.filter=filter;
      console.log(this.filter);
  }
  @action.bound addTodo(text) {
      this.todos.push(new Todo(text));
  }
  @action.bound removeTodo(todo) {
      this.todos.remove(todo);
  }
}
@observer
class TodoItem extends Component{
  static porpTypes={
      todo: PropTypes.shape({
          id: PropTypes.number.isRequired,
          text: PropTypes.string.isRequired,
          completed:PropTypes.bool.isRequired
      }).isRequired
  }
  render() {
      trace();
      let {todo}=this.props;
      return (
          <Fragment>
              <input
                  type="checkbox"
                  onChange={todo.toggle}
                  checked={todo.completed} />
              <span className={todo.completed? 'completed':''}>{todo.text}</span>

          </Fragment>
      )
  }
}
@observer
class TodoFooter extends Component{
  static propTypes={

  };
  render() {
      trace();
      let {left,filter} = this.props.store;
      return (
          <div>
                  <span>你還有{left}件待辦事項!</span>
                  <button
                      onClick={()=>changeFilter('all')}
                      className={filter==='all'?'active':''}>全部</button>
                  <button onClick={() => changeFilter('uncompleted')}
                      className={filter==='uncompleted'?'active':''}>未完成</button>
                  <button
                      onClick={()=>changeFilter('completed')}
                      className={filter==='completed'?'active':''}>已完成</button>
              </div>
      )
  }
}    
@observer
class TodoViews extends Component{
  render() {
      return (
              <ul>
                  {
                      this.props.store.filterTodos.map(todo => (
                          <li key={todo.id}>
                              <TodoItem todo={todo} />
                              <button onClick={()=>removeTodo(todo)}>X</button>
                          </li>
                      ))
                  }
              </ul>
      )
  }
}    
@observer
class TodoHeader extends Component{
  state={text:''}
  handleSubmit=(event) => {
      event.preventDefault();
      this.props.store.addTodo(this.state.text);
      this.setState({text:''});
  }
  handleChange=(event) => {
      this.setState({text:event.target.value});
  }
  render() {
      return (
          <form onSubmit={this.handleSubmit}>
                  <input placeholder="請輸入待辦事項" type="text" value={this.state.text} onChange={this.handleChange}/>
              </form>
      )
  }
}
@observer
class TodoList extends Component{
  static propsTypes={
      store: PropTypes.shape({
          addTodo:PropTypes.func,
          todos:ObservablePropTypes.observableArrayOf(ObservablePropTypes.observableObject)
      }).isRequired
  };

  render() {
      trace();
      return (
          <div className="todo-list">
              <TodoHeader store={this.props.store}/>
              <TodoViews store={this.props.store}/>
              <TodoFooter store={this.props.store}/>
          </div>
      )
  }
}
let store=new Store();
ReactDOM.render(<TodoList store={store}/>,document.querySelector('#root'));
複製程式碼