mobx

Mobx is similar to ReduxMobx. Compared with ReduxMobx, the learning cost is lower, the development difficulty is lower, the development code is less, and the rendering performance is good (the state and components are one-to-one, for example, you have three turntables corresponding to three components, if the component state changes, only the affected components will be processed, and the unaffected components will not be processed).

core idea

Side effects caused by state changes should be triggered automatically

  • Action is the only thing that can change state, and may have other side effects
  • State is defined by observable and minimum loudness, and should not contain redundant live data, which can be calculated in Computed values
  • Computed values are values that can be pushed out of State using pure Function. Mobx automatically updates it and optimizes it when it’s not useful
  1. The application logic only needs to modify the state data, and Mobx automatically renders the UI without human intervention
  2. Data changes only render corresponding components
  3. MobX provides a mechanism to store and update application state. React uses eACT to provide a mechanism to convert application state into a tree of renderable components and render it

The conceptual stuff can be a little abstract, so let’s try it out

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

When the installation is complete, open the project

Create mobx – test/webpack. Config. Js,

mobx-test/webpack.config.js

const path = require('path');
module.exports = {
    entry: './src/index.js' ,// File entry
    output:{
        path: path.resolve('dist'),
        filename: 'bundle.js'
    },
    // Module compiles
    module: {rules:[
            {
                test:/\.jsx? /.use: {loader:'babel-loder'.options: {
                        presets: ["env"."react"."stage-0"].plugins: ["transform-decorators-legacy"]}},exclude: /node_modules/}}}]Copy the code

Create tsconfig. Json

{
    "compilerOptions": {
        "experimentalDecorators": true."allowJs": true}}Copy the code

Modify the package. The json

 "scripts": {
    "start": "webpack  --mode development -w"
  },
Copy the code

Then try NPM run start and it will come out mobx/dist/bundle.js, we are in ‘mobx/dist

The Decorator Decorator

A Decorator is a function that decorates a class

Modifiers are essentially functions that are executed at compile time. If you want to add instance properties, you can do so through the prototype object of the target class

 @testable
class Person {}function testable(target) {
    target.testable = true;
}
The testable testable testable testable testable testable testable testable method
console.log(Person.testable);
Copy the code

Modify properties

// Target Class Properties of the key class discriptor descriptors
function readonly(target, key, discriptor){
    discriptor.writable = false;   
}
class Circle {
    @readonly PI = 3.14; // This is an instance property
}
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.// The actual value
    enumerable: false.// Whether enumerable
    writable: false.// Whether to change
    configurable: false// Whether delete obj. Age can be configured;
});
console.log(obj.age);
obj.age = 10;
console.log(obj.age);

Copy the code

Modify the circle of a class (method)

function logger(target, key, descriptor) {
    let oldVal = descriptor.value;// Get the old function
    descriptor.value = function () {
        console.log(`${key}(The ${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);
Copy the code

Proxy

  • Proxy can be understood as a layer of “interception” before the target object. All external access to the object must pass this layer of interception. Therefore, Proxy provides a mechanism for filtering and rewriting external access
  • The GET method is used to intercept the reading of an attribute and can take three parameters: the target object, the attribute name, and the proxy instance itself
  • The set method is used to intercept the assignment of an attribute and can take four parameters: the target object, the attribute name, the attribute value, and the Proxy instance itself

Instead of 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
Copy the code

mobx

MobX adds observable functionality to existing data structures such as objects, arrays, and class instances.

An Observable is a way to observe changes in data

The data is first converted into objects that can be observed, so that changes to the data can be monitored

let arr1 = observable([1.2.3]);
console.log(arr1);
// Data processing
arr1.pop();
arr1.push(4);
arr1.unshift(0);
console.log(arr1); / / 0124
Copy the code

O1 outputs a proxy object, which is an observable object. The principle is proxy. The basic data types need to be boxed, but the basic data types in the class need not be boxed

// Add observations
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());
Copy the code

Use to respond to observables

computed

  • Computed values are values that can be derived from existing states or other computed values
  • Combining existing observable data into new observable data is both reaction and observable data
  • This can be used as a function or as a decorator using.get() to get the calculated current value and.observe(callback) to observe the change in value.
  • Computed values can refer to values of other computed values, but they cannot be referenced in a circular manner
let { observable, observe, computed, autorun, when, reaction, action,runInAction } = require('mobx');
class Person {
    @observable name = 'zfpx';
    @observable age = 10;
    @observable province = 'in guangdong';
    @observable city = 'shenzhen';
    @observable area = '010';
    @observable number = '1899999999';
    @computed get home() {
        return this.province + The '-' + this.city; }}let p1 = new Person();
console.log(p1.home);

// Observe variables with observe, but the calculation must be outside
let phone = computed((a)= > {
    return "number:" + p1.area + The '-' + p1.number;
});
phone.observe(c= > console.log(c));
p1.area = '202'
Copy the code

autorun

You can’t use the observe method anymore if you’re using the decorator mode. When you want to create a responsive function that never has an observer, you can use mobx.autorun. When using Autorun, the provided function is always fired immediately, Then every time its dependencies change it will be triggered again after the data is rendered automatically

P1.phone.observe (change=>console.log(change)), if you use @computed to modify phone, p1.phone.observe(change=>console.log(change)) will fail, because p1.phone gets a string, so you can’t monitor it. In this case, you need autorun. It fires once immediately, and then again every time its dependencies change.

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

    @observable area = '010';
    @observable number = '1899999999';
    @computed get phone() {
        return this.area + The '-' + this.number; }}let p1 = new Person();
// p1.phone.observe(change=>console.log(change));
console.log(p1.phone);
// Run automatically when the system starts
autorun((a)= > {
    console.log(p1.phone);
});
p1.area='020';
Copy the code

when

When observes and runs the given predicate until it returns true. Once true is returned, the given effect is executed and the Autorunner is cleaned up. This function returns a cleaner to cancel the autorun program ahead of time.

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

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

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

    @computed get phone() {
        return this.area + The '-' + this.number; }}let p1 = new Person();
// When waits for the condition to be satisfied, once it is, the callback is executed and the listener is destroyed
when((a)= > p1.age >= 11, () = > {console.log(p1.age);
});
setInterval((a)= > {
    p1.age++;
}, 1000);

// Returns a function that cancels listening
let disposer = when((a)= > p1.age >= 18, () = > {console.log(p1.age);
});
disposer();
// This is done directly, not in console.log
setInterval((a)= > {
    p1.age++;
}, 1000);
Copy the code

reaction

  • A variant of Autorun, autorun triggers automatically, reaction gives you more granular control over how an Observable is tracked, right
  • It takes two function arguments, the first (data function) is used to trace and return data as input to the second (effect function)
  • Unlike Autorun, the effect function does not run directly when created, but only when the data expression returns a new value for the first time
  • Can be used in login information storage and write cache logic
// Listen for changes in the array variables before executing the callback function
reaction((a)= > [p1.age, p1.name], arr => {
    console.log(arr);
});
p1.age = 11;
p1.name = 'zfpx8';
Copy the code

action

That is, batch processing, all changed in the trigger

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

// Will output two times, we want to output one, need action function
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((a)= > {
    console.log(p1.phone);
});
//p1.area='020';
//p1.number='020';
p1.switchPhone('200'.'300');

Copy the code

In addition to action, there is 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((a)= > {
    console.log(p1.phone);
});
//p1.area='020';
//p1.number='020';
//p1.switchPhone('200', '300');
let s = p1.switchPhone;
s('200'.'300');
// the this pointer to s() is different, in which case we need to bounld
Copy the code

Note:

let num = 1;
let numObj = observable.box(num);
numObj.observe(x= > console.log(x));
numObj.set(100);
num = 2;// Since num is a common type, changing num has no effect on numObj
Copy the code

But if we’re going to do random combinations, we can’t write an infinite number of ways, so here’s the solution

runInAction

It is executed as a temporary code block, and the runInAction does not complete execution, so nothing can be found, and runs as an action

runInAction((a)= > {
    store.province='shandong';
    store.city=Jinan ' ';
});
Copy the code

We used mobx and Mobx-React to simulate react-Redux

Install 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'));
Copy the code

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);
Copy the code

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 = [];// This contains all the functions that cancel listening
    // You want to listen for changes in the store and execute the callback automatically
    constructor() {
        observe(this.todos, event => {
            console.log(event);
            // Make all previous cancel listener functions execute
            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'
Copy the code

spy

Monitor every change, but the performance is poor, generally only used when picky food

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 = [];// This contains all the functions that cancel listening
}
let store = new Store();
store.todos.push({id: 1.name : 'zfpx'});
store.todos.push({id: 2.name : 'zfpx'});
store.todos.get(0).name = 'zfpx3'
Copy the code

Combine react instances

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';

/ / data
class Todo {
    id = Math.random();
    @observable text = ' ';
    @observable completed = false;
    constructor(text) {
        this.text = text;
    }
    // The switchover is complete
    @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 > you have {store. Reminder} a to-do list < / p > < p > < button  onClick={() => store.changeFilter('all')} style={{ color: store.filter == 'all' ? </button> <button onClick={() => store. ChangeFilter ('uncompleted')} style={{color: red; store.filter == 'uncompleted' ? </button> <button onClick={() => store. ChangeFilter ('completed')} style={{color: blue; store.filter == 'completed' ? 'red' : 'black'}} > completed < / button > < / p > < / div > < / div >)}} ReactDOM. Render (< Todos store = {store} / >. document.querySelector('#root'));Copy the code

We can see that using Mobx is elegant and simple, but that’s not enough. We tested performance with trace, and the results weren’t very good, so we optimized the code.

Code optimization

Optimization principle

  • Let’s disassemble the view in more detail
  • Render the list data using a dedicated view
  • Deconstruct the data as late as possible

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';

/ / data
class Todo {
    id = Math.random();
    @observable text = ' ';
    @observable completed = false;
    constructor(text, completed = false) {
        this.text = text;
    }
    // The switchover is complete
    @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; <p> <p> <button onClick={() => store.changefilter ('all')} style={{color: blue; store.filter == 'all' ? </button> <button onClick={() => store. ChangeFilter ('uncompleted')} style={{color: red; store.filter == 'uncompleted' ? </button> <button onClick={() => store. ChangeFilter ('completed')} style={{color: blue; store.filter == 'completed' ? 'red' : </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'));Copy the code

Data persistence

Each refresh keeps the original state

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

/ / data
class Todo {
    id = Math.random();
    @observable text = ' ';
    @observable completed = false;
    constructor(text, completed = false) {
        this.text = text;
    }
    // The switchover is complete
    @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((a)= > {
            this.todos.push(new Todo(text));
        }, 1000)}... }... @observerclass 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'));
Copy the code