читать оригинал
Прошлое и настоящее 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
, что значительно повышает производительность рендеринга страниц, а также позволяет уделять больше сил развитию бизнес-логики.