当前位置: 首页 > 文档资料 > Mithril 中文文档 >

1.3.1 自动重绘

优质
小牛编辑
131浏览
2023-12-01

Mithril is designed around the principle that data always flows from the model to the view. This makes it easy to reason about the state of the UI and to test it. In order to implement this principle, the rendering engine must run a redraw algorithm globally to ensure no parts of the UI are out of sync with the data. While at first glance, it may seem expensive to run a global redraw every time data changes, Mithril makes it possible to do this efficiently thanks to its fast diffing algorithm, which only updates the DOM where it needs to be updated. Because the DOM is by far the largest bottleneck in rendering engines, Mithril's approach of running a diff against a virtual representation of the DOM and only batching changes to the real DOM as needed is surprisingly performant.

In addition, Mithril attempts to intelligently redraw only when it is appropriate in an application lifecycle. Most frameworks redraw aggressively and err on the side of redrawing too many times because, as it turns out, determining the best time to do a redraw is quite complicated if we want to be as efficient as possible.

Mithril employs a variety of mechanisms to decide the best time and the best strategy to redraw. By default, Mithril is configured to auto-redraw from scratch after component controllers are initialized, and it is configured to diff after event handlers are triggered. In addition, it's possible for non-Mithril asynchronous callbacks to trigger auto-redrawing by calling m.startComputation and m.endComputation in appropriate places (see below). Any code that is between a m.startComputation and its respective m.endComputation call is said to live in the context of its respective pair of function calls.

It's possible to defer a redraw by calling m.request or by manually nesting m.startComputation and m.endComputation contexts. The way the redrawing engine defers redrawing is by keeping an internal counter that is incremented by m.startComputation and decremented by m.endComputation. Once that counter reaches zero, Mithril redraws. By strategically placing calls to this pair of functions, it is possible to stack asynchronous data services in any number of ways within a context without the need to pass state variables around the entire application. The end result is that you can call m.request and other integrated data services seamlessly, and Mithril will wait for all of the asynchronous operations to complete before attempting to redraw.

In addition to being aware of data availability when deciding to redraw, Mithril is also aware of browser availability: if several redraws are triggered in a short amount of time, Mithril batches them so that at most only one redraw happens within a single animation frame (around 16ms). Since computer screens are not able to display changes faster than a frame, this optimization saves CPU cycles and helps UIs stay responsive even in the face of spammy data changes.

Mithril also provides several hooks to control its redrawing behavior with a deep level of granularity: m.startComputation and m.endComputation create redrawable contexts. m.redraw forces a redraw to happen in the next available frame (or optionally, it can redraw immediately for synchronous processing). The config's retain flag can be used to change how specific elements are redrawn when routes change. m.redraw.strategy can change the way Mithril runs the next scheduled redraw. Finally, the low-level m.render can also be used if a developer chooses to opt out of rest of the framework altogether.


Integrating with The Auto-Redrawing System

If you need to do custom asynchronous calls without using Mithril's API, and find that your views are not redrawing automatically, you should consider using m.startComputation / m.endComputation so that Mithril can intelligently auto-redraw once your custom code finishes running.

In order to integrate asynchronous code to Mithril's autoredrawing system, you should call m.startComputation BEFORE making an asynchronous call, and m.endComputation at the end of the asynchronous callback.

//this service waits 1 second, logs "hello" and then notifies the view that
//it may start redrawing (if no other asynchronous operations are pending)
var doStuff = function() {
    m.startComputation(); //call `startComputation` before the asynchronous `setTimeout`

    setTimeout(function() {
        console.log("hello");

        m.endComputation(); //call `endComputation` at the end of the callback
    }, 1000);
};

To integrate synchronous code, call m.startComputation at the beginning of the method, and m.endComputation at the end.

window.onfocus = function() {
    m.startComputation(); //call before everything else in the event handler

    doStuff();

    m.endComputation(); //call after everything else in the event handler
}

For each m.startComputation call a library makes, it MUST also make one and ONLY one corresponding m.endComputation call.

If you want to a recurring callback (such as setInterval or a web socket event handler) to trigger redraws, you should call m.startComputation at the beginning of the function, not outside of it.

setInterval(function() {
    m.startComputation(); //call before everything else in the event handler

    doStuff();

    m.endComputation(); //call after everything else in the event handler
})

Integrating multiple execution threads

When integrating with third party libraries, you might find that you need to call asynchronous methods from outside of Mithril's API.

In order to integrate non-trivial asynchronous code with Mithril's auto-redrawing system, you need to ensure all execution threads call m.startComputation / m.endComputation.

An execution thread is basically any amount of code that runs before other asynchronous threads start to run.

Integrating multiple execution threads can be done in two different ways: in a layered fashion or in comprehensive fashion.

Layered integration

Layered integration is recommended for modular code where many different APIs may be put together at the application level.

Below is an example where various methods implemented with a third party library can be integrated in layered fashion: any of the methods can be used in isolation or in combination.

Notice how doBoth repeatedly calls m.startComputation since that method calls both doSomething and doAnother. This is perfectly valid: there are three asynchronous computations pending after the jQuery.when method is called, and therefore, three pairs of m.startComputation / m.endComputation in play.

var doSomething = function(callback) {
    m.startComputation(); //call `startComputation` before the asynchronous AJAX request

    return jQuery.ajax("/something").done(function() {
        if (callback) callback();

        m.endComputation(); //call `endComputation` at the end of the callback
    });
};
var doAnother = function(callback) {
    m.startComputation(); //call `startComputation` before the asynchronous AJAX request

    return jQuery.ajax("/another").done(function() {
        if (callback) callback();
        m.endComputation(); //call `endComputation` at the end of the callback
    });
};
var doBoth = function(callback) {
    m.startComputation(); //call `startComputation` before the asynchronous synchronization method

    jQuery.when(doSomething(), doAnother()).then(function() {
        if (callback) callback();

        m.endComputation(); //call `endComputation` at the end of the callback
    })
};

Comprehensive integration

Comprehensive integration is recommended if integrating a monolithic series of asynchronous operations. In contrast to layered integration, it minimizes the number of m.startComputation / m.endComputation calls to avoid clutter.

The example below shows a convoluted series of AJAX requests implemented with a third party library.

var doSomething = function(callback) {
    m.startComputation(); //call `startComputation` before everything else

    jQuery.ajax("/something").done(function() {
        doStuff();
        jQuery.ajax("/another").done(function() {
            doMoreStuff();
            jQuery.ajax("/more").done(function() {
                if (callback) callback();

                m.endComputation(); //call `endComputation` at the end of everything
            });
        });
    });
};