Build A View-Framework-Free Data Layer Based on MobX — Integration With Vue (1)

kuitos
ITNEXT
Published in
6 min readJun 6, 2018

--

Notices: mobx-vue had been moved to mobxjs organization!

A couple of weeks ago I wrote an article describing the usage of mobx with angularjs and the purpose of it, this time I will introduce how to combine MobX with Vue.

Installation

npm i mobx-vue -S

Usage

mobx-vue is very simple to use. All you need is to use Connect to decorate your mobx-defined store for vue component:

<template>
<section>
<p v-text="amount"></p>
<p v-for="user in users" :key="user.name">{{user.name}}</p>
</section>
</template>
<script lang="ts">
import { Connect } from "mobx-vue";
import Vue from "vue";
import Component from "vue-class-component";
class ViewModel {
@observable users = [];
@computed get amount() { return this.users.length }
@action fetchUsers() {}
}
@Connect(new ViewModel())
@Component()
export default class App extends Vue {
mounted() {
this.fetchUsers();
}
}
</script>

Why MobX/mobx-vue

We know that both MobX and Vue are based on data hijacking&dependency collection to implement a responsive mechanism. The mobx official also mentioned inspired by vue several times. So why do we combine two things that are almost the same?

Yes, it ‘s weird.

In 2016, when I was building a company-level component library, I began to think about a problem: how can we migrate component libraries to other frameworks/libraries in the future with as little effort as possible when the code base is based on a special framework? It can’t be completely rewritten based on new technology, that waste of life. Aside for the basic controls, the interaction/behavior logic is basically determinable, and at most a few adjustments in the UI. It is also very irresponsible for the company to simply push down the underlying library to try new technologies. So we have to accept being kidnapped by the framework and stuck in a technology stack and get stuck?It is obviously unacceptable for a field where the half-life of the front-end framework is especially short, and the result is either someone who departure or can’t get someone to fill the hole together… Simply put, we can’t enjoy the benefits of new technology.

From the perspective of MVVM architecture, the more heavy the application is, the more the complexity is concentrated on the two layers of M (Model) and VM (ViewModel), especially the Model layer. Theoretically, it should be independent of the upper-level view to run/test/publish independently. The different view frameworks only use dynamic template engines with different binding syntax, which I’ve covered in previous articles.. If we make the view layer very thin, the cost of our migration will naturally fall into an acceptable category, and it is even possible to automatically generate view layer code for different frameworks at compile-time through tools.

To make the Model and even the ViewModel reusable independently, what we need is a generic and framework-free state management mechanism that helps us describe the dependency graphs between the data models. During this period, I tried ES6 accessor, redux, rxjs and other solution, but they were not satisfactory. The accessor is too low-level and asynchronously unfriendly, the redux development experience is too poor, rxjs is too heavy, and so on. It wasn’t until I met MobX: MobX was simple enough, unopinioned, oop-oriented, framework free, and other features exactly matched my needs.

Over the past year, I have tried to build a VM/M layer based on MobX on react, angularjs, and angular. There are two on-line projects and one personal project. The practical results have basically met my expectations. In architecture, we only need to use the relevant connector to switch between different framework based on the same data layer. Now only Vue has not been verified with this set of ideas.

Before mobx-vue, the community already had some excellent connector implementations, such as move vue-mobx, but they were basically based on vue’s plugin mechanism and inspired by vue-rx, except for the tedious of using, the biggest problem is the implementation is based on Vue.util.defineReactive. That is to say, it is based on Vue’s own responsive mechanism. This not only waste the MobX reactive ability in a certain extent, and will be migrated to other view framework with a predictable behavior(that to say, you can’t be sure who Vue or MobX is react to the state changes).

Ideally, mobx should manage data dependencies, vue only have to react to mobx and makes re-rendering. Vue is treated as a dynamic template rendering engine, just like react.

How mobx-vue works

Since our goal is to turn vue into a pure template rendering engine (vdom) and use the mobx reactive mechanism instead of vue’s, as long as we hijack the component mounting and updating methods of vue component, then collect the dependencies when component mounting, update the component when dependencies updated.

The following would called how mobx-vue works rather than how Vue works:

We know that Vue is usually initialized like this:

new Vue({ el: '#app', render: h => h(App)});

then we found Vue constructor:

function Vue (options) {   ......   this._init(options) }

go to _init function and we found $mount function:

if (vm.$options.el) {   vm.$mount(vm.$options.el) }

With web runtime as an example, the $mount function was:

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
...
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
vm._watcher = new Watcher(vm, updateComponent, noop)

As you can see, the updateComponent method will be the key entry for component update, follow up the Watcher constructor, and see how Vue calls this method:

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
...
}
this.value = this.lazy
? undefined
: this.get()
...........
get () {
...
try {
value = this.getter.call(vm, vm)
} catch (e) {
...
}

Now we can found that initiator of component loading/updating is: value = this.getter.call(vm, vm), and we can get the method reference via: vm._watcher.getter, it means updateComponent := vm._watcher.getter.

So we just need to embed the data managed by MobX into the component context directly for the component before `$mount`, let MobX collect the corresponding dependencies when `$mount` called, and call `updateComponent` when MobX detects changes occurs. This not only allows MobX’s reactive mechanism to hack into the Vue system in a simple way, but also ensures that the component’s native behavior is not affected (lifecycle hooks, etc.).

The fundamental is to use MobX’s reactive mechanism to take over Vue’s Watcher and downgrade Vue to a pure vdom rendering engine.

The core implementation is very simple:

const { $mount } = Component.prototype;Component.prototype.$mount = function (this: any, ...args: any[]) {
let mounted = false;
const reactiveRender = () => {
reaction.track(() => {
if (!mounted) {
$mount.apply(this, args);
mounted = true;
} else {
this._watcher.getter.call(this, this);
}
});
return this;
};
const reaction = new Reaction(`${name}.render()`, reactiveRender);
dispose = reaction.getDisposer();
return reactiveRender();
};

Here is the complete code: https://github.com/mobxjs/mobx-vue/blob/master/src/connect.ts

Conclusion

Evan You had said that: mobx + react is a more verbose Vue. Which is essentially the case, the ability of the mobx&react combination is exactly what Vue is born with. And what mobx-vue does is just the opposite: downgrade Vue to react and then upgrade to Vue with MobX. This is really weird. However, what I want to say is that our original intention is not means that Vue’s reactive mechanism is not implemented well enough and wanna replace it with MobX, but wanna through mobx, a relatively neutral state management platform, to provide a relatively universal data-layer programming paradigm for different view layer technology, to try to smooth the syntax difference between different framework and technology stacks, in order to provide developers with more decision-making power and possibility of view technology, without being caught in a framework.

--

--