张大侠

Create a fully Reactive UI Framework with JavaScript Proxies and Virtual DOM

张大侠 · 2016-12-14推荐 · 260阅读 CET/4 120 CET/6 6 原文链接

Recently I got interested in the Reactive Programming world, especially in the MobX ecosystem. I really like the idea behind this framework: to be reactive in a transparent way. So I asked myself…

What does it take to create a fully Reactive (and transparent) UI frameworkin JavaScript?

Don’t worry, you will not see another JavaScript framework on the npm registry (at least for now). But I think that this is a good architectural exercise. We’re going to split the problem in two parts. the first one is a UI library that helps us render our state to the DOM, the second one it’s a reactive state management library. Yes, we are going to create a shitty version of React+MobX stack. :)


UI Framework

At a high level of abstraction a UI framework should be just a pure function of the state of our application. To express this concept in a mathematical way…

If we want to have an highly performant rendering without all the fuss of a complete library like React, we can just use a pure implementation of a virtual-dom algorithm, like the one that you can find on this GitHubrepository by @MatthewEsch. To render a simple list from an array we can write:

**import** { create, h } **from 'virtual-dom'**;

**const** _render_ = (state) => {
    **const** children = state.**list**.map(t => h(**'li'**,{},[t]));
    **return** h(**'ul'**, {}, children);
};

**const** INITIAL_STATE = {
    **list**:[**'first'**,**'second'**]
};

**let** tree = _render_(INITIAL_STATE);
**let** rootNode = create(tree);

document.**body**.appendChild(rootNode);

As you can see this virtual-dom implementation uses the HyperScript format to define HTML elements. When the list changes we need to patch our DOM in this way:

**const** _updateDom_ = (state) => {
    **const** newTree = _render_(state);
    **const** patches = diff(tree, newTree);

    tree = newTree;
    rootNode = patch(rootNode, patches);
};

Ok so we have our pure functions to create and update our DOM tree. In other words, the UI part of our framework it’s completed. Let’s talk about the state management part.


Reactive State Management Library

The state management library needs to be Reactive. But what is the meaning of _“Be Reactive”? _In my opinion, the simplest way to define a Reactive application is…

Obviously I’m oversimplifying here, but in the end Reactive Programming it’s all about Observables. My purpose here it’s to create a reactive state management library that it’s also transparent for the user of the framework itself. Just like in a MobX application I want to re-render my application just changing the model. So to add a new element to the list I just want to write something like this:

state.list = […state.list,’Another Element’];

The fastest way that I know in JavaScript to achieve this objective is to use the EcmaScript 2015 Proxies. The MDN definition of ‘Proxy’ states that:

The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).

Ok, before we start digging in the code, consider that Object Proxies are not supported by all the browsers. You can use this plugin for Babel or a simple polyfill created by the Google Chrome team.

Oh you IE…

To create a Proxy we can just use the Proxy constructor. In the next example we create a simple ‘Loggable’ object factory that prints on the console every property lookup or assignment that we do on the target object.

**export default** (target) => {
    **const** loggingHandler = {
        get: **function** (target,name) {
            **const** value = target[name];
            **console**.log(**`getting** ${name}**:** ${value}**`**);
            **return** value;
        },
        set: **function** (target,name,value) {
            **console**.log(**`setting** ${name}**:** ${value}**`**);
            target[name] = value;
            **return true**;
        }
    };

    **return new** Proxy(target,loggingHandler);
};

Then we can create a ‘loggable’ object in a very simple way:

**const** INITIAL_STATE = {
    **startValue**:0
};

**const** state = _loggable_(INITIAL_STATE);

**const** value = state.**startValue**; //prints 'getting startValue: 0'

state.**startValue** = 1; //prints 'setting startValue: 1'

We can use the same technique to create a generic Observable, we just need a target object and a listener callback.

**export default** ({target,listener}) => {
    **let** observable;

    **const** _set_ = (target,name,value) => {
        target[name] = value;
        listener(observable);
        **return true**;
    };

    **const** _get_ = (target,name) => {
        **return** Object.freeze(target[name]);
    };

    **const** handler = {
        _set_,
        _get_ };

    observable = **new** Proxy(target,handler);

    **return** observable;
};

Now that we have all the pieces of our framework we just need to put that in place.

**import** { patch, create, diff } **from 'virtual-dom'**;
**import** { _render_ } **from './view'**;
**import** _loggable_ **from './loggable'**;
**import** _observable_ **from './observable'**;

**const** _updateDom_ = (state) => {
    **const** newTree = _render_(state);
    **const** patches = diff(tree, newTree);

    tree = newTree;
    rootNode = patch(rootNode, patches);
};

**const** INITIAL_STATE = {
    _//The state of your application
_};

**const** state = _observable_({
    **target**:_loggable_(INITIAL_STATE),
    listener:_updateDom
_});

**let** tree = _render_(state);
**let** rootNode = create(tree);

document.**body**.appendChild(rootNode);

And that’s it! We just render our initial DOM with the render function, and then we update it automatically (or I should say transparently?) just changing the ‘state’ variable. Here you can find a live demo of a todo list built with this approach. The source code it’s available on my GitHub account.


Conclusions

Obviously this is not a real framework, but I think that sometimes it’s useful to “reinvent the wheel” on your own. Because in some contexts you should consider not to use a framework but to start from scratch, in order to keep your technical debt at a minimal level. This is also the reason because I love the JavaScript ecosystem. It’s true that we have a new ‘shiny’ framework every week, but it’s not a fatigue, it’s a great opportunity to learn new ways to write and organize our code.

Post Scriptum

If you’re interested in the Reactive Programming and MobX Development you can check my talk “Stay (React)ive with MobX” at ReactJS Day in Verona. I talked about MobX and the meaning of “Be Reactive”.

相关文章