mobxjs / mobx

Simple, scalable state management.
http://mobx.js.org
MIT License
27.55k stars 1.77k forks source link

How to optimizing rendering native dom? #2444

Closed Aqours closed 4 years ago

Aqours commented 4 years ago
import { autorun, IObservableArray, observable } from 'mobx';

interface IStageItem {
    message: string;
    contextMessage?: string;
    status?: string;
}

class Segement {
    @observable stages = <IObservableArray<IStageItem>>[];

    setView() {
        autorun(() => {
            const fragment = document.createDocumentFragment();

            for (const stage of this.stages) {
                const buff: string[] = [];

                buff.push(`Message: ${stage.message}`);
                if (stage.contextMessage) {
                    buff.push(`ContextMessage: ${stage.contextMessage}`);
                }
                if (stage.status) {
                    buff.push(`Status: ${stage.status}`);
                }

                fragment.append(`<section>${buff.join('<br>')}</section>`);

                /**
                 * The First dom node will be always created even if I don't change this.stages[0] data.
                 */
                if (stage.message === 'First') {
                    console.count('First');
                }
            }
            /**
             * It will have some redundant dom node if I use document.body.appendChild(fragment);
             */
            document.body.replaceWith(fragment);
        });

        return this;
    }
}

const s = new Segement().setView();

/**
 * Set stage.message (Change frequently)
 */
s.stages.push({ message: 'First' });
s.stages.push({ message: 'Second' });
setTimeout(() => {
    s.stages.push({ message: 'Third' });
});
setTimeout(() => {
    // Remove the first element
    s.stages.shift();
}, 100);

/**
 * Change stage.status (Change frequently)
 */
setTimeout(() => {
    s.stages[1].status = 'done';
}, 10);
setTimeout(() => {
    s.stages[2].status = 'warn';
    s.stages[2].contextMessage = 'Catch an error';
}, 20);

Background

I create an observable array Segment.stages which contains some objects. mobx.autorun will rendering Segment.stages and append dom node to Document without any frameworks.

Question

The first element which I don't change anymore, will always re-rendering when Segment.stages has been changed. I think it will affect dom rendering performace when Segment.stages include many items or this code had been used in many places.

In this case, how to optimizing rendering native dom?

Possible Solutions

  1. Virtual DOM (Same as React)
  2. Use mobx.observe or mobxUtils.deepObserve to distinguish array ADD, UPDATE, REMOVE actions Shortcoming
    • mobx.observe can not observe deep changes
    • mobxUtils.deepObserve can not fire immediately Expected
    • Can observe deep changes, fire immediately, and receive shallow changed item/object not trivial details
  3. Anymore?
mweststrate commented 4 years ago

Yeah to do this more efficient you could split it up in smaller autoruns / computeds (it is possible to create computeds or autoruns inside autoruns (but don't forget you need to cleanup for the latter)), or you could compare the items you have with what you have in the DOM, or deepUtils indeed. It's all possible I'd expect, and I recommend to just play and study how different solutions like https://github.com/ryansolid/mobx-jsx and https://github.com/adobe/lit-mobx. Beyond that this question is basically asking how to build a framework, which is a bit out of scope of an issue tracker :)

Aqours commented 4 years ago

Yep, it's out of scope. Close it.

it is possible to create computeds ...

A little confused. I can get ADD and UPDATE actions via computed value, but REMOVE action will lost. So what are you means?

interface IStageItem {
    message: string;
    contextMessage?: string;
    status?: string;
    needUpdate?: boolean; // Change it manually
}

class Segement {
    @observable stages = <IObservableArray<IStageItem>>[];

    @computed
    get needUpdateStages() {
        return this.stages.filter(item => (item.needUpdate == null || item.needUpdate));
    }

    setView() {
        autorun(() => {
            /**
             * Get ADD/UPDATE stages via @computed value, and then re-rendering them.
             */
            for (const item of this.needUpdateStages) {
                // Rendering Code
            }
        });

        return this;
    }
}

I try mobx.observe and it works fine except the code looks redundant when update values.

interface IStageItem {
    message: string;
    contextMessage?: string;
    status?: string;
}

class Segement {
    @observable stages = <IObservableArray<IStageItem>>[];

    @action.bound
    protected observeListener<T>(change: IArrayChange<T> | IArraySplice<T>): void {
        if (isObservableArray(change.object)) {
            switch (change.type) {
                case MobXEventType.Update:
                    // UPDATE
                    // this.onListUpdate(change.newValue, change.oldValue, change.object, change.index);
                    break;
                case MobXEventType.Splice:
                    if (change.removedCount) {
                        // REMOVE
                        // this.onListRemove(change.removed, change.removedCount, change.object, change.index);
                    }
                    if (change.addedCount) {
                        // ADD
                        // this.onListAdd(change.added, change.addedCount, change.object, change.index);
                    }
                    break;
            }
        }
    }

    setView() {
        observe(this.stages, this.observeListener, true);
        return this;
    }
}

const s = new Segement().setView();

s.stages.push({ message: 'First', status: 'fail' });

/**
 * Update .stages[0].status value
 *
 * It works fine but looks redundant when update values. (Working with `Intercept` maybe a good idea)
 */
s.stages[0] = Object.assign({}, s.stages[0], { status: 'done' });