高效的Mobx模式(Part 3 高阶应用实例)

前两部分侧重于MobX的基本构建块。 有了这些块,咱们如今能够经过MobX的角度开始解决一些真实场景。 这篇文章将是一系列应用咱们迄今为止所见概念的例子。javascript

固然,这不是一个详尽的清单,但应该让你体会到应用MobX角度所须要的心理转变。 全部示例都是在没有@decorator (装饰器)语法的状况下建立的。 这容许您在Chrome控制台,Node REPL或支持临时文件的WebStorm等IDE中进行尝试。java

改变思惟方式

当您学习某些库或框架背后的理论并尝试将其应用于您本身的问题时,您最初可能会画一个空白。 它发生在像我这样的普通人身上,甚至是最好的人。 写做界称之为“Writer’s block”,而在艺术家的世界里,它就是“Painter’s block”react

咱们须要的是从简单到复杂的例子来塑造咱们的思惟方式。 只有看到应用程序,咱们才能开始想象解决咱们本身的问题的方法。segmentfault

对于MobX,它首先要了解您有一个reactive object-graph这一事实。 树的某些部分可能依赖于其余部分。 当树变异时,链接的部分将做出反应并更新以反映变化。数组

思惟方式的转变是将手头的系统设想为一组反应性变更 + 一组相应的结果。

效果能够是因为反应性变化而产生输出的任何事物。 让咱们探索各类现实世界的例子,看看咱们如何用MobX建模和表达它们。
promise


Example 1: 发送重要操做的分析

问题:咱们在应用程序中有一些必须记录到服务器的一次性操做。 咱们但愿跟踪执行这些操做的时间并发送分析。缓存

一、是对创建状态模型。 咱们的行为是有限的,咱们只关心它执行一次。 咱们可使用动做方法的名称创建对应的布尔值类型状态, 这是咱们可观察到的状态。服务器

const actionMap = observable({
    login: false,
    logout: false,
    forgotPassword: false,
    changePassword: false,
    loginFailed: false
});

二、接下来,咱们必须对这些行动状态发生的变化做出反应。 由于它们只在生命周期中发生过一次,因此咱们不会使用长期运行的效果,如autorun()reaction()。 咱们也不但愿这些效果在执行后存在。 好吧,这给咱们留下了一个选择:....
....
....react-router

when

Object.keys(actionMap)
    .forEach(key => {
        when(
            () => actionMap[key],
            () => reportAnalyticsForAction(key)
        );
    });

function reportAnalyticsForAction(actionName) {
    console.log('Reporting: ', actionName);

    /* ... JSON API Request ... */
}

在上面的代码中,咱们只是循环遍历actionMap中的键并为每一个键设置when()反作用。 当tracker-function(第一个参数)返回true时,反作用将运行。 运行效果函数(第二个参数)后,when()将自动处理。 所以,没有从应用程序发送多个报告的问题!并发

三、咱们还须要一个MobX动做来改变可观察状态。 请记住:永远不要直接修改您的observable。 始终经过action来作到这一点。
对上面的例子来讲,以下:

const markActionComplete = action((name) => {
    actionMap[name] = true;
});

markActionComplete('login');
markActionComplete('logout');

markActionComplete('login');

// [LOG] Reporting:  login
// [LOG] Reporting:  logout

请注意,即便我将登陆操做标记触发两次,也没有发送日志报告。 完美,这正是咱们须要的结果。
它有两个缘由:

  1. login标记已经为true,所以值没有变化
  2. 此外,when()反作用已被触发执行,所以再也不发生追踪。


Example 2: 做为工做流程的一部分启动操做

问题:咱们有一个由几个状态组成的工做流程。 每一个状态都映射到某些任务,这些任务在工做流到达该状态时执行。

一、从上面的描述中能够看出,惟一可观察的值是工做流的状态。 须要为每一个状态运行的任务能够存储为简单映射。 有了这个,咱们能够模拟咱们的工做流程:

class Workflow {
    constructor(taskMap) {
        this.taskMap = taskMap;
        this.state = observable({
            previous: null,
            next: null
        });

        this.transitionTo = action((name) => {
            this.state.previous = this.state.next;
            this.state.next = name;
        });

        this.monitorWorkflow();
    }

    monitorWorkflow() {
        /* ... */
    }
}

// Usage
const workflow = new Workflow({
    start() {
        console.log('Running START');
    },

    process(){
        console.log('Running PROCESS');
    },

    approve() {
        console.log('Running APPROVE');
    },

    finalize(workflow) {
        console.log('Running FINALIZE');

        setTimeout(()=>{
            workflow.transitionTo('end');
        }, 500);
    },

    end() {
        console.log('Running END');
    }
});

请注意,咱们正在存储一个名为state的实例变量,该变量跟踪工做流的当前和先前状态。 咱们还传递state->task的映射,存储为taskMap

2如今有趣的部分是关于监控工做流程。 在这种状况下,咱们没有像前一个例子那样的一次性操做。 工做流一般是长时间运行的,可能在应用程序的生命周期内。 这须要autorunreaction()

只有在转换到状态时才会执行状态任务。 所以咱们须要等待对this.state.next进行更改才能运行任何反作用(任务)。 等待更改表示使用reaction()由于它仅在跟踪的可观察值更改值时才会运行。 因此咱们的监控代码以下所示:

class Workflow {
    /* ... */
    monitorWorkflow() {
        reaction(
            () => this.state.next,
            (nextState) => {
                const task = this.taskMap[nextState];
                if (task) {
                    task(this);
                }
            }
        )
    }
}

reaction()第一个参数是跟踪函数,在这种状况下只返回this.state.next。 当跟踪功能的返回值改变时,它将触发效果功能。 效果函数查看当前状态,从this.taskMap查找任务并简单地调用它。

请注意,咱们还将工做流的实例传递给任务。 这可用于将工做流转换为其余状态。

workflow.transitionTo('start');

workflow.transitionTo('finalize');

// [LOG] Running START
// [LOG] Running FINALIZE
/* ... after 500ms ... */
// [LOG] Running END

有趣的是,这种存储一个简单的observable的技术,好比this.state.next和使用reaction()来触发反作用,也能够用于:

  1. 经过react-router进行路由
  2. 在演示应用程序中导航
  3. 基于模式在不一样视图之间切换


Example 3: 输入更改时执行表单验证

问题:这是一个经典的Web表单用例,您须要验证一堆输入。 若是有效,容许提交表单。

一、让咱们用一个简单的表单数据类对其进行建模,其字段必须通过验证。

class FormData {
    constructor() {
        extendObservable(this, {
            firstName: '',
            lastName: '',
            email: '',
            acceptTerms: false,

            errors: {},

            get valid() { // this becomes a computed() property
                return (this.errors === null);
            }
        });

        this.setupValidation(); // We will look at this below
    }
}

extendObservable()API是咱们之前从未见过的。 经过在咱们的类实例(this)上应用它,咱们获得一个ES5至关于建立一个@observable类属性。

class FormData {
    @observable firstName = '';
    /* ... */
}

二、接下来,咱们须要监视这些字段什么时候发生变化并运行一些验证逻辑。 若是验证经过,咱们能够将实体标记为有效并容许提交。 使用计算属性跟踪有效性自己:有效。

因为验证逻辑须要在FormData的生命周期内运行,所以咱们将使用autorun()。 咱们也可使用reaction()但咱们想当即运行验证而不是等待第一次更改。

class FormData {
    setupValidation() {
        autorun(() => {
            // Dereferencing observables for tracking
            const {firstName, lastName, email, acceptTerms} = this;
            const props = {
                firstName,
                lastName,
                email,
                acceptTerms
            };

            this.runValidation(props, {/* ... */})
                .then(result => {
                    this.errors = result;
                })
        });
    }

    runValidation(propertyMap, rules) {
        return new Promise((resolve) => {
            const {firstName, lastName, email, acceptTerms} = propertyMap;

            const isValid = (firstName !== '' && lastName !== '' && email !== '' && acceptTerms === true);
            resolve(isValid ? null : {/* ... map of errors ... */});
        });
    }

}

在上面的代码中,autorun()将在跟踪的observables发生更改时自动触发。 请注意,要使MobX正确跟踪您的observable,您必须使用解除引用。

runValidation()是一个异步调用,这就是咱们返回一个promise的缘由。 在上面的示例中,它并不重要,但在现实世界中,您可能会调用服务器进行一些特殊验证。 当结果返回时,咱们将设置错误observable,这将反过来更新有效的计算属性。

若是你有一个耗时较大的验证逻辑,你甚至可使用autorunAsync(),它有一个参数能够延迟执行去抖动。

二、好吧,让咱们的代码付诸行动。 咱们将设置一个简单的控制台记录器(经过autorun())并跟踪有效的计算属性。

const instance = new FormData();

// Simple console logger
autorun(() => {
    // input的每一次输入,结果都会触发error变动,autorun随即执行
    const validation = instance.errors;

    console.log(`Valid = ${instance.valid}`);
    if (instance.valid) {
        console.log('--- Form Submitted ---');
    }

});

// Let's change the fields
instance.firstName = 'Pavan';
instance.lastName = 'Podila';
instance.email = 'pavan@pixelingene.com';
instance.acceptTerms = true;

//     输出日志以下
//     Valid = false
//    Valid = false
//    Valid = false
//    Valid = false
//    Valid = false
//    Valid = true
//    --- Form Submitted ---

因为autonrun()当即运行,您将在开头看到两个额外的日志,一个用于instance.errors,一个用于instance.valid,第1-2行。 其他四行(3-6)用于现场的每次更改。

每一个字段更改都会触发runValidation(),每次都会在内部返回一个新的错误对象。 这会致使instance.errors的引用发生更改,而后触发咱们的autorun()以记录有效标志。 最后,当咱们设置了全部字段时,instance.errors变为null(再次更改引用)并记录最终的“Valid = true”。

四、简而言之,咱们经过使表单字段可观察来进行表单验证。 咱们还添加了额外的errors属性和有效的计算属性来跟踪有效性。 autorun()经过将全部内容捆绑在一块儿来节省时间。


Example 4: 跟踪全部已注册的组件是否已加载

问题: 咱们有一组已注册的组件,咱们但愿在全部组件都加载后跟踪。 每一个组件都将公开一个返回 promise的load()方法。 若是promise解析,咱们将组件标记为已加载。 若是它拒绝,咱们将其标记为失败。 当全部这些都完成加载时,咱们将报告整个集是否已加载或失败。

一、咱们先来看看咱们正在处理的组件。 咱们正在建立一组随机报告其负载状态的组件。 另请注意,有些是异步的。

const components = [
    {
        name: 'first',
        load() {
            return new Promise((resolve, reject) => {
                Math.random() > 0.5 ? resolve(true) : reject(false);
            });
        }
    },
    {
        name: 'second',
        load() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    Math.random() > 0.5 ? resolve(true) : reject(false);
                }, 1000);
            });
        }
    },
    {
        name: 'third',
        load() {
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    Math.random() > 0.25 ? resolve(true) : reject(false);
                }, 500);
            });
        }
    },
];

二、下一步是为Tracker设计可观察状态。 组件的load()不会按特定顺序完成。 因此咱们须要一个可观察的数组来存储每一个组件的加载状态。 咱们还将跟踪每一个组件的报告状态。

当全部组件都已报告时,咱们能够通知组件集的最终加载状态。 如下代码设置了可观察量。

class Tracker {
    constructor(components) {
        this.components = components;

        extendObservable(this, {

            // Create an observable array of state objects,
            // one per component
            states: components.map(({name}) => {
                return {
                    name,
                    reported: false,
                    loaded: undefined
                };
            }),

            // computed property that derives if all components have reported
            get reported() {
                return this.states.reduce((flag, state) => {
                    return flag && state.reported;
                }, true);
            },

            // computed property that derives the final loaded state 
            // of all components
            get loaded() {
                return this.states.reduce((flag, state) => {
                    return flag && !!state.loaded;
                }, true);
            },

            // An action method to mark reported + loaded
            mark: action((name, loaded) => {
                const state = this.states.find(state => state.name === name);

                state.reported = true;
                state.loaded = loaded;
            })

        });

    }
}

咱们回到使用extendObservable()来设置咱们的可观察状态。 reportedload的计算属性跟踪组件完成其加载的时间。 mark()是咱们改变可观察状态的动做方法。

顺便说一句,建议在须要从您的observables派生值的任何地方使用computed。 将其视为产生价值的可观察物。 计算值也会被缓存,从而提升性能。 另外一方面,autorunreaction不会产生价值。 相反,它们提供了建立反作用的命令层。

三、为了启动跟踪,咱们将在Tracker上建立一个track()方法。 这将触发每一个组件的load()并等待返回的Promise解析/拒绝。 基于此,它将标记组件的负载状态。

when()全部组件都已reported时,跟踪器能够报告最终加载的状态。 咱们在这里使用,由于咱们正在等待条件变为真(this.reported)。 报告的反作用只须要发生一次,很是适合when()

如下代码负责以上事项:

class Tracker {
    /* ... */ 
    track(done) {
        when(
            () => this.reported,
            () => {
                done(this.loaded);
            }
        );

        this.components.forEach(({name, load}) => {
            load()
                .then(() => {
                    this.mark(name, true);
                })
                .catch(() => {
                    this.mark(name, false);
                });
        });
    }

    setupLogger() {
        autorun(() => {
            const loaded = this.states.map(({name, loaded}) => {
                return `${name}: ${loaded}`;
            });

            console.log(loaded.join(', '));
        });
    }
}

setupLogger()实际上不是解决方案的一部分,但用于记录报告。 这是了解咱们的解决方案是否有效的好方法。

四、如今咱们来测试一下:

const t = new Tracker(components);
t.setupLogger();
t.track((loaded) => {
    console.log('All Components Loaded = ', loaded);
});

// first: undefined, second: undefined, third: undefined
// first: true, second: undefined, third: undefined
// first: true, second: undefined, third: true
// All Components Loaded =  false
// first: true, second: false, third: true

记录的输出显示其按预期工做。 在组件报告时,咱们记录每一个组件的当前加载状态。 当全部人报告时,this.reported变为true,咱们看到“All Components Loaded”消息。

但愿上面的一些例子让你体会到在MobX中的思考。

  1. 设计可观察状态
  2. 设置变异动做方法以更改可观察状态
  3. 放入跟踪功能(when,autorun,reaction)以响应可观察状态的变化

上述公式应该适用于须要在发生变化后跟踪某些内容的复杂场景,这可能致使重复1-3步骤。

相关文章
相关标签/搜索