Смоделируйте Vue и напишите MVVM вручную

внешний интерфейс Шаблоны проектирования Vue.js MVVM


читать оригинал


Прошлое и настоящее MVVM

Шаблон проектирования MVVM является развитием таких шаблонов проектирования, как MVC (изначально производный от серверной части) и MVP.M — модель данных (модель), VM — модель представления (ViewModel), V — уровень представления (представление).

В шаблоне MVC, за исключением уровней модели и представления, вся остальная логика находится в контроллере, который отвечает за отображение страниц, реагирование на действия пользователя, сетевые запросы и взаимодействие с моделью. итерация продуктов, контроллер в логике обработки становится все более сложной и трудной в обслуживании. Чтобы лучше управлять кодом и удобнее расширять бизнес, необходимо «похудеть» Контроллер, а также необходимо более четко отделить разработку пользовательского интерфейса (UI) от бизнес-логики и поведения приложения. MVVM был создан для этого.

Многие реализации MVVM отделяют логику представления от других уровней посредством привязки данных, что можно кратко представить на следующем рисунке:



Существует множество интерфейсных фреймворков, использующих шаблон проектирования MVVM, среди которых типичным представителем является прогрессивный фреймворк Vue, которому отдает предпочтение большинство фронтенд-разработчиков при разработке и использовании.В этой статье мы просто смоделируем версия MVVM, основанная на реализации MVVM библиотекой Vue.


Анализ процесса MVVM

В дизайне Vue MVVM мы в основном нацеленыCompile(сборка шаблона),Observer(захват данных),Watcher(мониторинг данных) иDep(опубликовать и подписаться) несколько частей для достижения, основной логический поток может относиться к следующему рисунку:



Коды, подобные этому «строительству колеса», несомненно, реализованы с помощью объектно-ориентированного программирования и строго следуют принципу открытого и закрытого.Поскольку объектно-ориентированное программирование ES5 более громоздко, ES6 будет единообразно использоваться в следующем коде.classреализовать.


Реализация класса MVVM

В Vue только один названныйVueконструктор, при использованииnewОдинVueэкземпляр, а затем передан вoptionsпараметр, тип — объект, включая текущийVueобласть действия экземпляраel, данные, привязанные к шаблонуdataи т.п.

Когда мы имитируем этот шаблон MVVM, мы также создаем класс, имя которого называетсяMVVM, аналогично фреймворку Vue, при его использовании нужно пройтиnewСоздание инструкцииMVVMэкземпляр и передатьoptions.

// MVVM.js 文件
class MVVM {
    constructor(options) {
        // 先把 el 和 data 挂在 MVVM 实例上
        this.$el = options.el;
        this.$data = options.data;

        // 如果有要编译的模板就开始编译
        if (this.$el) {
            // 数据劫持,就是把对象所有的属性添加 get 和 set
            new Observer(this.$data);

            // 将数据代理到实例上
            this.proxyData(this.$data);

            // 用数据和元素进行编译
            new Compile(this.el, this);
        }
    }
    proxyData(data) { // 代理数据的方法
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key];
                }
                set(newVal) {
                    data[key] = newVal;
                }
            });
        });
    }
}

Из приведенного выше кода мы видим, что в нашемnewОдинMVVMкогда параметрoptionsпрошел в А.DomУзел корневого элемента и данныеdataи завис на текущемMVVMна экземпляре.

Когда есть корневой узел, пройтиObserverпара классовdataданные были украдены и переданыMVVMметод экземпляраproxyDataПучокdataДанные зависают в текущейMVVMВ данном случае данные также похищаются, потому что мы можем напрямую передавать данные при получении и изменении данных.thisилиthis.$data, основным методом реализации захвата данных в Vue являетсяObject.defineProperty, мы также используем этот способ, добавляяgetterа такжеsetterдля захвата данных.

последнее использованиеCompileКласс анализирует и компилирует шаблон и связанные данные и отображает их на корневом узле. Причина, по которой перехват данных и анализ шаблона реализованы в виде классов, заключается в том, что код легко поддерживать и расширять. Это не сложно. чтобы увидеть это,MVVMкласс фактически действует какCompileкласс иObserverМост для класса.


Компиляция шаблона Компиляция реализации класса

CompileПри создании экземпляра классу необходимо передать два параметра. Первый параметр — текущий.MVVMКорневой узел роли экземпляра, второй параметрMVVMнапример, зачем проходитьMVVMЭкземпляр для более легкого доступаMVVMсвойства экземпляра.

существуетCompileНа занятии мы попытаемся извлечь некоторую общую логику для максимального повторного использования, избежать избыточного кода и улучшить ремонтопригодность и масштабируемость.CompileМетоды экземпляра, извлеченные из класса, в основном делятся на две категории: вспомогательные методы и основные методы, которые помечаются в коде комментариями.

1. Разбирайте структуру DOM в корневом узле

// Compile.js 文件
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        // 如过传入的根元素存在,才开始编译
        if (this.el) {
            // 1、把这些真实的 Dom 移动到内存中,即 fragment(文档碎片)
            let fragment = this.node2fragment(this.el);
        }
    }

    /* 辅助方法 */
    // 判断是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }

    /* 核心方法 */
    // 将根节点转移至文档碎片
    node2fragment(el) {
        // 创建文档碎片
        let fragment = document.createDocumentFragment();
        // 第一个子节点
        let firstChild;

        // 循环取出根节点中的节点并放入文档碎片中
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}

В процессе компиляции приведенного выше шаблона предварительным условием является то, что должен быть узел корневого элемента, а входящий узел корневого элемента может быть истинным.Domэлемент, также может быть селектором, поэтому мы создаем вспомогательные методыisElementNodeчтобы помочь нам определить, является ли входящий элементDom, если это так, используйте его напрямую, если это селектор, получите этоDom, и, наконец, сохраните этот корневой узел вthis.elв свойствах.

В процессе парсинга шаблона для производительности мы должны вынести дочерние узлы в корневом узле и сохранить их во фрагменте документа (памяти).DomВ процессе сохранения дочерних узлов в узле во фрагмент документа они будут сохранены в исходномDomЭтот узел удаляется из контейнера, поэтому при обходе дочерних узлов корневого узла первый узел всегда вынимается и сохраняется во фрагменте документа до тех пор, пока узел не перестанет существовать.

2. Скомпилируйте структуру во фрагменте документа

Есть две основные части компиляции шаблона в Vue, которые также являются частью, которую браузер не может проанализировать: директивы в узле элемента и синтаксис Mustache (двойные фигурные скобки) в текстовом узле.

// Compile.js 文件
class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;

        // 如过传入的根元素存在,才开始编译
        if (this.el) {
            // 1、把这些真实的 Dom 移动到内存中,即 fragment(文档碎片)
            let fragment = this.node2fragment(this.el);

            // ********** 以下为新增代码 **********
            // 2、将模板中的指令中的变量和 {{}} 中的变量替换成真实的数据
            this.compile(fragment);

            // 3、把编译好的 fragment 再塞回页面中
            this.el.appendChild(fragment);
            // ********** 以上为新增代码 **********
        }
    }

    /* 辅助方法 */
    // 判断是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1;
    }

    // ********** 以下为新增代码 **********
    // 判断属性是否为指令
    isDirective(name) {
        return name.includes("v-");
    }
    // ********** 以上为新增代码 **********

    /* 核心方法 */
    // 将根节点转移至文档碎片
    node2fragment(el) {
        // 创建文档碎片
        let fragment = document.createDocumentFragment();
        // 第一个子节点
        let firstChild;

        // 循环取出根节点中的节点并放入文档碎片中
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }

    // ********** 以下为新增代码 **********
    // 解析文档碎片
    compile(fragment) {
        // 当前父节点节点的子节点,包含文本节点,类数组对象
        let childNodes = fragment.childNodes;

        // 转换成数组并循环判断每一个节点的类型
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) { // 是元素节点
                // 递归编译子节点
                this.compile(node);

                // 编译元素节点的方法
                this.compileElement(node);
            } else { // 是文本节点
                // 编译文本节点的方法
                this.compileText(node);
            }
        });
    }
    // 编译元素
    compileElement(node) {
        // 取出当前节点的属性,类数组
        let attrs = node.attributes;
        Array.form(attrs).forEach(attr => {
            // 获取属性名,判断属性是否为指令,即含 v-
            let attrName = attr.name;

            if (this.isDirective(attrName)) {
                // 如果是指令,取到该属性值得变量在 data 中对应得值,替换到节点中
                let exp = attr.value;

                // 取出方法名
                let [, type] = attrName.split("-");

                // 调用指令对应得方法
                CompileUtil[type](node, this.vm, exp);
            }
        });

    }
    // 编译文本
    compileText(node) {
        // 获取文本节点的内容
        let exp = node.contentText;

        // 创建匹配 {{}} 的正则表达式
        let reg = /\{\{([^}+])\}\}/g;

        // 如果存在 {{}} 则使用 text 指令的方法
        if (reg.test(exp)) {
            CompileUtil["text"](node, this.vm, exp);
        }
    }
    // ********** 以上为新增代码 **********
}

Основная логика нового контента в приведенном выше коде состоит в том, чтобы сделать две вещи:

  • передачаcompileпара методовfragmentДокументный мусор скомпилирован, то есть заменяет значение, соответствующее переменной во внутренней инструкции и синтаксисе усов;
  • будет компилироватьfragmentФрагменты документа помещаются обратно в корневой узел.

На первом шаге логика более громоздкая, прежде всегоcompileПолучить все дочерние узлы в методе, скомпилировать в цикле, если это узел элемента, вам нужно рекурсивноcompile, передать текущий узел элемента. В этом процессе были извлечены два метода,compileElementа такжеcompileTextИспользуется для обработки атрибутов узлов элементов и текстовых узлов.

compileElementОсновная логика заключается в обработке инструкции, удалении всех атрибутов узла элемента, чтобы определить, является ли это инструкцией, и вызове метода, соответствующего инструкции, если это инструкция.compileTextОсновная логика заключается в извлечении содержимого текста и использовании регулярных выражений для сопоставления содержимого, заключенного в «{{ }}» в синтаксисе Mustache, и вызове методаtextметод.

Содержимое текстового узла может иметь "{{ }} {{ }} {{ }}", а обычное сопоставление по умолчанию является жадным. Чтобы предотвратить совпадение первого "{" и последнего "}" , поэтому в регулярном выражении следует использовать нежадное сопоставление.

Вызывается при вызове метода инструкцииCompileUtilСоответствующие методы ниже, причина, по которой мы разделяем методы, соответствующие этим инструкциям, отдельно и храним их вCompileUtilЦель состоит в том, чтобы отделить следующий объект, а также потому, что другие классы позже используют.

3. Реализация метода инструкции в объекте CompileUtil

CompileUtilВсе методы инструкций и методы обновления, соответствующие инструкциям, хранятся в Vue.Поскольку в Vue много инструкций, мы реализуем здесь только наиболее типичные из них.v-modelМетод, соответствующий "{{ }}", с учетом последующих обновлений, мы равномерно устанавливаем значение вDomЛогика извлекает методы, соответствующие двум вышеуказанным ситуациям, и сохраняет их вCompileUtilизupdaterв объекте.

// CompileUtil.js 文件
CompileUtil = {};

// 更新节点数据的方法
CompileUti.updater = {
    // 文本更新
    textUpdater(node, value) {
        node.textContent = value;
    },
    // 输入框更新
    modelUpdater(node, value) {
        node.value = value;
    }
};

Вся идея в том, что в этой частиCompileшаблоны после компиляцииv-modelи "{{ }}", на самом деле оба используютdataзаменить данные вfragmentПеременная в соответствующем узле фрагмента документа. Поэтому часто будет получатьсяdataЗначение в , которое сбрасывается при обновлении узлаdataзначение в , поэтому мы извлекли три методаgetVal,getTextValа такжеsetValвисел наCompileUtilпод объект.

// CompileUtil.js 文件
// 获取 data 值的方法
CompileUtil.getVal = function (vm, exp) {
    // 将匹配的值用 . 分割开,如 vm.data.a.b
    exp = exp.split(".");

    // 归并取值
    return exp.reduce((prev, next) => {
        return prev[next];
    }, vm.$data);
};

// 获取文本 {{}} 中变量在 data 对应的值
CompileUtil.getTextVal = function (vm, exp) {
    // 使用正则匹配出 {{ }} 间的变量名,再调用 getVal 获取值
    return exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {
        return this.getVal(vm, args[1]);
    });
};

// 设置 data 值的方法
CompileUtil.setVal = function (vm, exp, newVal) {
    exp = exp.split(".");
    return exp.reduce((prev, next, currentIndex) => {
        // 如果当前归并的为数组的最后一项,则将新值设置到该属性
        if(currentIndex === exp.length - 1) {
            return prev[next] = newVal;
        }

        // 继续归并
        return prev[next];
    }, vm.$data);
}

получить и установитьdataЦенность двух методовgetValа такжеsetValИдея похожа, потому что приобретенные иерархии переменных могут бытьdata.a, или возможноdata.obj.a.b, поэтому все они используют идею слияния, заимствованияreduceметод, разница в том, чтоsetValВ процессе слияния метод должен определить, слился ли он с последним уровнем, и если да, то установить новое значение, иgetTextValтолько что вgetValПередает на аутсорсинг слой логики, который обрабатывает "{{ }}".

После того, как эти приготовления готовы, можно реализовать нашу основную логику, то естьCompileИспользуются переменные в проанализированном текстовом узле и директивы узла элемента в классе.dataЗначение заменяется, помните, что для предыдущегоv-modelи "{{ }}", поэтому дизайнmodelа такжеtextДва основных метода.

CompileUtil.modelРеализация метода:

// CompileUtil.js 文件
// 处理 v-model 指令的方法
CompileUtil.model = function (node, vm, exp) {
    // 获取赋值的方法
    let updateFn = this.updater["modelUpdater"];

    // 获取 data 中对应的变量的值
    let value = this.getVal(vm, exp);

    // 添加观察者,作用与 text 方法相同
    new Watcher(vm, exp, newValue => {
        updateFn && updateFn(node, newValue);
    });

    // v-model 双向数据绑定,对 input 添加事件监听
    node.addEventListener('input', e => {
        // 获取输入的新值
        let newValue = e.target.value;

        // 更新到节点
        this.setVal(vm, exp, newValue);
    });

    // 第一次设置值
    updateFn && updateFn(vm, value);
};

CompileUtil.textРеализация метода:

// CompileUtil.js 文件
// 处理文本节点 {{}} 的方法
CompileUtil.text = function (node, vm, exp) {
    // 获取赋值的方法
    let updateFn = this.updater["textUpdater"];

    // 获取 data 中对应的变量的值
    let value = this.getTextVal(vm, exp);

    // 通过正则替换,将取到数据中的值替换掉 {{ }}
    exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {
        // 解析时遇到了模板中需要替换为数据值的变量时,应该添加一个观察者
        // 当变量重新赋值时,调用更新值节点到 Dom 的方法
        new Watcher(vm, args[1], newValue => {
            // 如果数据发生变化,重新获取新值
            updateFn && updateFn(node, newValue);
        });
    });

    // 第一次设置值
    updateFn && updateFn(vm, value);
};

Вышеупомянутые два метода логически похожи, и оба получают свои собственныеupdaterМетод в , установите значение и установите его для последующихdataИзменение данных в представлении, обновление представления, созданногоWatcherэкземпляр , и повторно обновляет узел с новым значением внутри, в отличие от Vuev-modelДиректива реализует двустороннюю привязку данных в форме, если элемент формыvalueКогда значение изменяется, вам нужно обновить новое значение доdata, и ответьте на страницу.

Итак, наша реализация состоит в том, чтобы связать этоv-modelЭлемент формы слушаетinputсобытий, а в событии в режиме реального времениvalueзначение обновлено доdata, что касаетсяdataТребуется еще три класса, чтобы ответить на страницу после изменения вWatcher,Observerа такжеDepЧтобы достичь вместе, мы добьемся следующегоWatcherДобрый.


Наблюдатель Реализация класса Watcher

существуетCompileUtilметод объекта созданWatcherВ экземпляр передаются три параметра, а именноMVVMэкземпляр, имя переменной данных, привязанных к шаблонуexpс однимcallback,этоcallbackВнутренняя логика заключается в обновлении данных доDom, так что нашWatcherЧто делать внутри класса понятно, получить значение перед изменением, сохранить его и создатьupdateМетоды экземпляра, которые выполняют метод экземпляра при изменении значения.callbackобновить представление.

// Watcher.js 文件
class Watcher {
    constructor(vm, exp, callback) {
        this.vm = vm;
        this.exp = exp;
        this.callback = callback;

        // 更改前的值
        this.value = this.get();
    }
    get() {
        // 将当前的 watcher 添加到 Dep 类的静态属性上
        Dep.target = this;

        // 获取值触发数据劫持
        let value = CompileUtil.getVal(this.vm, this.exp);

        // 清空 Dep 上的 Watcher,防止重复添加
        Dep.target = null;
        return value;
    }
    update() {
        // 获取新值
        let newValue = CompileUtil.getVal(this.vm, this.exp);
        // 获取旧值
        let oldValue = this.value;

        // 如果新值和旧值不相等,就执行 callback 对 dom 进行更新
        if(newValue !== oldValue) {
            this.callback();
        }
    }
}

Глядя на приведенный выше код, должно быть два вопроса:

  • использоватьgetПочему метод зависает в текущем экземпляре, когда получает старое значение?DepВыше, почему он очищается после получения значения;
  • updateметод выполняется внутриcallbackфункция, ноupdateВыполнять в какое время.

Это последние два классаDepа такжеobserverЧтобы сделать, давайте сначала представимDep, а затем представитьObserverНаконец, соедините все отношения между ними.


Реализация класса публикации-подписки Dep

На самом деле, проще говоря, публикация и подписка предназначены для хранения и управления функциями, которые должны выполняться в массиве.Когда достигается определенное условие выполнения, массив зацикливается, и каждый член выполняется.

// Dep.js 文件
class Dep {
    constructor() {
        this.subs = [];
    }
    // 添加订阅
    addSub(watcher) {
        this.subs.push(watcher);
    }
    // 通知
    notify() {
        this.subs.forEach(watcher => watcher.update());
    }
}

существуетDepВ классе есть только одно свойство, свойство с именемsubsмассив для управления каждымwatcher,Прямо сейчасWatcherэкземпляр класса, в то время какaddSubиспользуется, чтобыwatcherдобавить вsubsВ массиве мы видимnotifyМетод решает вышеуказанную проблему,WatcherКатегорияupdateКак метод выполняется, он выполняется в цикле.

Затем мы интегрируем слепые зоны:

  • DepГде экземпляр создает объявление и гдеwatcherдобавить вsubsмножество;
  • Depизnotifyгде должен вызываться метод;
  • Watcherсодержание, использованиеgetПочему метод зависает в текущем экземпляре, когда получает старое значение?DepВыше, почему он очищается после получения значения.

Эти вопросы в последнем классеObserverЭто будет ясно при реализации.Давайте сосредоточимся на последней части основной логики.


Реализация класса Observer для захвата данных

не забудьте достичьMVVMЭкземпляр этого класса был создан при создании класса, и параметры, переданные в то время, былиMVVMпримерdataсвойства, вMVVMпередавать данные черезObject.definePropertyподключился к экземпляру и добавилgetterа такжеsetter,фактическиObserverОсновная цель урока – датьdataЭто делается для всех уровней данных внутри.

// Observer.js 文件
class Observer {
    constructor (data) {
        this.observe(data);
    }
    // 添加数据监听
    observe(data) {
        // 验证 data
        if(!data || typeof data !== 'object') {
            return;
        }

        // 要对这个 data 数据将原有的属性改成 set 和 get 的形式
        // 要将数据一一劫持,先获取到 data 的 key 和 value
        Object.keys(data).forEach(key => {
            // 劫持(实现数据响应式)
            this.defineReactive(data, key, data[key]);
            this.observe(data[key]); // 深度劫持
        });
    }
    // 数据响应式
    defineReactive (object, key, value) {
        let _this = this;
        // 每个变化的数据都会对应一个数组,这个数组是存放所有更新的操作
        let dep = new Dep();

        // 获取某个值被监听到
        Object.defineProperty(object, key, {
            enumerable: true,
            configurable: true,
            get () { // 当取值时调用的方法
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set (newValue) { // 当给 data 属性中设置的值适合,更改获取的属性的值
                if(newValue !== value) {
                    _this.observe(newValue); // 重新赋值如果是对象进行深度劫持
                    value = newValue;
                    dep.notify(); // 通知所有人数据更新了
                }
            }
        });
    }
}

в кодексеobserveЦель состоит в том, чтобы обойти объект и захватить данные внутри, т.е. добавитьgetterа такжеsetterМы берем логику угона вdefineReactiveметод, следует отметить, чтоobserveМетод выполняет проверку типа данных для текущих данных в начале выполнения, а затем повторно использует каждый атрибут объекта для захвата каждого из них.ObjectПодсвойства типов вызываются рекурсивноobserveГлубокий захват.

существуетdefineReactiveметод, созданныйDepэкземпляр, иdataИспользование данныхgetа такжеsetУгон, помните, что в процессе компиляции шаблона, когда вы столкнетесь с переменными, связанными в шаблоне, он будет проанализирован и созданwatcher,Будет вWatcherВнутренняя часть класса получает старое значение, то есть текущее значение, которое вызываетget,существуетgetвы можете положить это вwatcherдобавить вDepизsubsЕдиное управление в массиве, потому что оно получено в кодеdataВ нем много операций со значениями.get, мы должны гарантироватьwatcherне будет добавляться повторно, поэтому вWatcherкласс, сразу после получения старого значения и его сохраненияDep.targetназначить какnull, и запускgetвременная параDep.targetВыполняется операция короткого замыкания, и она вызывается только тогда, когда она существует.DepизaddSubдобавить.

а такжеdataСрабатывает при изменении значения inset,существуетsetПроведена оптимизация производительности в , то есть оценивается равно ли переназначенное значение старому значению, и если оно равно, то страница не будет перерисовываться. Возможны два случая неравенства. Если исходное изменилось value является базовым типом данных, он не имеет никакого эффекта.Ссылочный тип, нам нужно перехватить данные внутри этого ссылочного типа, поэтому рекурсивный вызовobserve, наконец звонитDepизnotifyспособ уведомления, выполненияnotifyбудет выполнятьsubsвсе удалосьwatcherизupdate, выполнит созданиеwatcherвходящийcallback, страница будет обновлена.

существуетMVVMкласс будетdataСвойства висят наMVVMЭкземпляр и угон и пройтиObserverпара классовdataСуществует еще один уровень взлома, потому что вся логика публикации-подписки находится вdataизgetа такжеsetвключен, пока спусковой крючокMVVMсерединаgetа такжеsetВнутренний автоматически вернется или установитdataсоответствующее значение, оно вызоветdataизgetа такжеset, будет выполнена логика публикации и подписки.

После длинного описания выше следует полностью описать отношения между несколькими классами, используемыми в этом шаблоне MVVM.Хотя это относительно абстрактно, вы все равно поймете отношения и логику, если хорошенько об этом подумаете.Давайте посмотрим на нашу собственную реализацию , Этот MVVM проверен.


Проверить МВВМ

Мы просто написали шаблон по содержанию нашей собственной реализации MVVM на манер Vue следующим образом:

<!-- index.html 文件 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>
    <div id="app">
        <!-- 双向数据绑定 靠的是表单 -->
        <input type="text" v-model="message">
        <div>{{message}}</div>
        <ul>
            <li>{{message}}</li>
        </ul>
        {{message}}
    </div>

    <!-- 引入依赖的 js 文件 -->
    <script src="./js/Watcher.js"></script>
    <script src="./js/Observer.js"></script>
    <script src="./js/Compile.js"></script>
    <script src="./js/CompileUtil.js"></script>
    <script src="./js/Dep.js"></script>
    <script src="./js/MVVM.js"></script>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                message: 'hello world!'
            }
        });
    </script>
</body>
</html>

Откройте консоль браузера Chrom и проверьте, выполнив на ней следующее:

  • войтиvm.message = "hello"Посмотрите, обновляется ли страница;
  • войтиvm.$data.message = "hello"Посмотрите, обновляется ли страница;
  • Измените значение в поле ввода текста, чтобы увидеть, обновляются ли другие элементы страницы.

Суммировать

Я считаю, что с помощью приведенных выше тестов мы должны понять значение шаблона MVVM для разработки интерфейса, реализовать двустороннюю привязку данных, обеспечить синхронизацию данных между уровнем представления и уровнем модели в режиме реального времени и позволить нам программировать на основе данных во время разработки, с наименьшим количеством времени.Dom, что значительно повышает производительность рендеринга страниц, а также позволяет уделять больше сил развитию бизнес-логики.