Какие преимущества вы получите от этого материала?
- Понимание принципов двухсторонней привязки данных в Vue и ключевых модулей кода.
- Утолщение любопытства и понимание того, как достигается двухсторонняя привязка.
Для удобства объяснения принципов и реализации, все относящиеся к этому материалу коды были взяты из источника кода Vue и адаптированы. Они являются более простыми и не учитывают такие аспекты, как обработка массивов и циклические зависимости данных. Однако это не должно затруднять чтение и понимание материала. После прочтения статьи вам будет легче анализировать исходный код Vue. Все связанные с этим материалом коды доступны на GitHub https://github.com/DMQ/mvvm.
<div id="mvvm-app">
<input type="text" v-model="word">
<p>{{word}}</p>
<button v-on:click="sayHi">Изменить модель</button>
</div>
```<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Привет, мир!'
},
methods: {
sayHi: function () {
this.word = 'Здравствуйте, все!';
}
}
});
</script>
```Результат:

### Некоторые способы реализации двухсторонней привязки данных
Существуют различные популярные MVC/VM-фреймворки, которые поддерживают одностороннюю привязку данных. По моему мнению, двухсторонняя привязка данных заключается в добавлении события изменения (change/input) к элементам, которые можно редактировать (например, input, textarea), чтобы динамически обновлять модель и представление. Это не является чем-то сложным.
Основные способы реализации привязки данных:
> Распределенная система публикаций и подписок (backbone.js)
> Отслеживание грязных значений (angular.js)
> Кража данных (vue.js)
**Распределенная система публикаций и подписок:** Обычно данные и представления связаны через методы sub и pub. Обычная практика обновления данных выглядит так: `vm.set('property', value)`. Есть хорошая статья на эту тему, которую вы можете найти [здесь](http://www.html-js.com/article/Study-of-twoway-data-binding-JavaScript-talk-about-JavaScript-every-day). Этот способ теперь действительно слишком примитивен, поэтому мы предпочитаем использовать метод `vm.property = value`, чтобы обновлять данные и автоматически обновлять представление. Вот два подхода:**Нечистый проверочный механизм:** Angular.js использует непрозрачный проверочный механизм для сравнения данных на наличие изменений и принятие решения о необходимости обновления представления. Самым простым способом является использование `setInterval()` для периодического опроса изменений данных. Конечно, Google не будет так примитивен; Angular вступает в режим непрозрачной проверки только при активации определённых событий, примерно следующим образом:- События DOM, такие как ввод пользователя, клик по кнопке и т.д. (`ng-click`)
- События ответа XHR (`$http`)
- Изменение браузера Location (`$location`)
- События таймера (`$timeout`, `$interval`)
- Вызов `$digest()` или `$apply()`
**Данные захвата:** Vue.js использует метод захвата данных с использованием публикатор-подписчик модели, где через `Object.defineProperty()` захватываются `setter` и `getter` каждого свойства, и когда данные меняются, сообщение отправляется подписчику, что вызывает соответствующий обратный вызов.
### Обзор идеи
Уже известно, что Vue использует метод захвата данных для реализации двустороннего привязывания данных, самым важным и основным методом которого является использование `Object.defineProperty()` для осуществления захвата свойств, позволяющего отслеживать изменения данных. Безусловно, этот метод является одним из самых важных и базовых в данной статье. Если вы не знакомы с `defineProperty`, то можете прочитать [это](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty).Собрав всё вместе, можно сказать, что для реализации двустороннего привязывания MVVM необходимо выполнить следующие пункты:
1. Реализовать наблюдатель данных Observer, который может слушать все свойства объекта данных, а также получать новые значения при изменении и уведомлять подписчиков.
2. Реализовать компилятор команд Compile, который сканирует и анализирует команды каждого узла элемента, заменяет шаблоны данных и связывает соответствующие функции обновления.
3. Реализовать наблюдателя Watcher, который служит мостом между Observer и Compile, позволяет подписаться на события и получать уведомления о каждом изменении свойства, выполняя соответствующие обратные вызовы команд.
4. Входная функция MVVM, которая объединяет вышеупомянутые три составляющих.Вышеупомянутый процесс представлен на следующей диаграмме:
![img2][img2]
### 1. Реализация Observer
Хорошо, идеи уже собраны, и логика и функционал модулей достаточно ясны, давайте начнем.
Мы знаем, что можем использовать `Object.defineProperty()` для отслеживания изменения свойств.
Тогда будем рекурсивно проходить по объекту данных, включая свойства внутренних объектов, и добавляем им `setter` и `getter`.
Таким образом, при присваивании значения какому-либо полю объекта будет вызван `setter`, и мы сможем отследить изменения данных. Вот пример кода:
```javascript
var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // Хаха, значение изменилось: kindeng --> dmq
функция observe(data) {
если (!(data)) || typeof(data) !== 'object' {
возврат;
}
// Вытягиваем все свойства для перебора
Object.keys(data).forEach(функция(key) {
defineReactive(data, key, data[key]);
});
};
функция defineReactive(data, key, val) {
observe(val); // Отслеживание подчинённых свойств
Object.defineProperty(data, key, {
enumerable: true, // Доступен для перечисления
configurable: false, // Недопустимо повторное определение
get: функция() {
возврат val;
},
set: функция(newVal) {
console.log('哈哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}
Перевод текста внутри функций:
функция observe(data) {
if (!(data)) || typeof(data) !== 'object' {
return;
}
// Вытягиваем все свойства для перебора
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
функция defineReactive(data, key, val) {
observe(val); // Отслеживание подчинённых свойств
Object.defineProperty(data, key, {
enumerable: true, // Доступен для перечисления
configurable: false, // Недопустимо повторное определение
get: function() {
return val;
},
set: function(newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}
Теперь мы можем отслеживать изменения каждого данных; следующий шаг — это как сообщить подписчику о том, что данные изменились. Поэтому нам нужно реализовать систему подписки на события; очень просто, поддерживая массив для хранения всех подписчиков, при изменении данных вызывается метод notify
, который затем вызывает метод update
у каждого подписчика. Улучшенный код выглядит так:
```
// ... сокращено
функция defineReactive(data, key, val) {
var dep = новое Dep();
observe(val); // Отслеживание подчинённых свойств
Object.defineProperty(data, key, {
// ... сокращено
set: функция(newVal) {
если (val === newVal) возврат;
console.log('哈哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
dep.notify(); // Уведомление всех подписчиков
}
});
}
функция Dep() {
это. subs = [];
}
Dep. прототип = {
addSub: функция(sub) {
это. subs.push(sub);
},
notify: функция() {
это. subs.forEach(функция(sub) {
sub.update();
});
}
};
```
Тогда вопрос возникает: кто является подписчиком? Как добавить подписчика в систему подписки?
Действительно, вышеупомянутый подход указывает, что подписчиками должны быть объекты типа Watcher, а также `var dep = новый Dep();` определяется внутри функции `defineReactive`, поэтому чтобы добавить подписчика через `dep`, требуется выполнить операцию внутри замыкания. Таким образом, можно добавить подписчика внутри геттера:
``````javascript
// Observer.js
// ... сокращено
Object.defineProperty(data, key, {
get: function() {
// Для добавления watcher в замыкании используется глобальная переменная target, которая временно хранит watcher, после добавления удаляется
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... сокращено
});
```
```markdown
// Watcher.js
Watcher.prototype = {
get: function(ключ) {
Dep.target = this;
this.value = данные[ключ]; // Здесь будет вызван геттер свойства, что приведёт к добавлению подписчика
Dep.target = null;
}
}
```
Здесь уже реализован Observer, который способен слушать изменения данных и уведомлять подписчиков о таких изменениях, [полный код](https://github.com/DMQ/mvvm/blob/master/js/observer.js). Далее следует реализация компилятора Compile.### 2. Реализация Compile
Основной задачей Compile является парсинг шаблонных директив, замена переменных в шаблоне на данные, а также инициализация отображения страницы и связывание каждого инструктажа с функциями обновления, добавление подписчиков данных. При изменении данных, получение уведомления приводит к обновлению представления, как показано на следующем рисунке:

При многократном обращении к узлам DOM для повышения производительности и эффективности, корневой узел `el` преобразуется в фрагмент документа `fragment`, где происходит парсинг и компиляция, после чего `fragment` снова добавляется обратно в реальный DOM.
```javascript
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { this.compileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(), child;
// Копируем нативные узлы в fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};
```
Метод `compileElement` проходит через все узлы и их потомков, выполняет сканирование и парсинг, вызывает соответствующие функции рендеринга данных и связывает их с функциями обновления, подробнее см. код и комментарии:```javascript
Compile.prototype = {
// ... Пропущено
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var текст = node.textContent;
var рег = /\{\{(.*)\}\}/; // Выражение текста
// По элементному узлу компилируем
if (me.isElementNode(node)) {
me.compile(node);
} else if (me.isTextNode(node) && рег.test(текст)) {
me.compileText(node, RegExp.$1);
}
// Проходимся по всем потомкам
if (node.childNodes && node.childNodes.length) {
me.compileElement(node);
}
});
},
```
```javascript
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// Условие: директивы должны называться с префиксом v-xxx
// Например, в <span v-text="content"></span>, директива — это v-text
var attrName = attr.name; // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2); // text
if (me.isEventDirective(dir)) {
// событийные директивы, такие как v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else {
// обычные директивы
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};
``````markdown
// Коллекция обработчиков директив
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ... пропущено
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// Первичная инициализация представления
updaterFn && updaterFn(node, vm[exp]);
// Создание экземпляра наблюдателя, который добавляет этот наблюдатель watcher в подписчики свойства
new Watcher(vm, exp, function(value, oldValue) {
// При изменении значения свойства, получаем уведомление и выполняем эту функцию обновления, чтобы обновить представление
updaterFn && updaterFn(node, value, oldValue);
});
}
};
// Обновители представления
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}
// ... пропущено
};
```
Здесь рекурсивный проход гарантирует, что каждый узел и его потомки будут анализированы и скомпилированы, включая текстовые узлы, объявленные через `{{}}`. Директивы объявляются через атрибуты узлов с особым префиксом, таким как `v-text` в `<span v-text="content">`.
Обработка данных и привязка функций обновления происходит в методе `compileUtil.bind()`, где создается новый экземпляр `Watcher`, который добавляет обратный вызов для получения уведомлений при изменениях значений.
```Таким образом, простой компилятор готов. [Полный код доступен здесь](https://github.com/DMQ/mvvm/blob/master/js/compile.js). Теперь можно рассмотреть реализацию `Watcher` как подписчика.
### 3. Реализация Watcher
Watcher подписчика выступает в роли моста между Observer и Compile, выполняя следующие задачи:1. При создании экземпляра добавляет себя в свойство подписчика (dep).
2. Обязательно имеет метод `update()`.
3. При получении уведомлений от `dep.notice()` вызывает метод `update()`, что приводит к выполнению заранее связанной функции обратного вызова в Compile.
Если это кажется запутанным, можно вернуться к [предыдущей схеме](#_2).
```markdown
```javascript
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
// Для того чтобы активировать getter свойства и добавить себя в dep
this.value = this.get();
}
Watcher.prototype = {
update: function() {
this.run(); // Уведомление о изменении значения свойства
},
run: function() {
var value = this.get(); // Получение нового значения
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal); // Вызов заранее связанной функции обратного вызова, обновление представления
}
},
get: function() {
Dep.target = this; // Установка текущего подписчика как этого экземпляра
var value = this.vm[this.exp]; // Активация getter, добавление себя в dep
Dep.target = null; // Окончание процесса добавления, установка Dep.target как null
return value;
}
};
// Второй раз приведены Observer и Dep для лучшего понимания
Object.defineProperty(data, key, {
get: function() {
// Поскольку требуется добавить watcher внутри замкнутого контекста, то можно определить глобальное свойство target в Dep, которое временно хранит watcher, после чего удаляем его
Dep.target && dep.addDep(Dep.target);
return val;
}
// ... Пропущено
});
```Dep.prototype = {
notify: function() {
this.subs.forEach(function(sub) {
sub.update(); // Вызов метода update у подписчика, уведомление о изменениях
});
}
};
```
При создании экземпляра `Watcher`, вызывается метод `get()`. Этот метод использует `Dep.target = watcherInstance` для отметки текущего подписчика как данного экземпляра `watcher`, активируя тем самым getter определенного свойства, который добавляет данный экземпляр `watcher` в свойство подписчика `dep`. Таким образом, при изменении значений свойств, экземпляр `watcher` будет получать уведомления о этих изменениях.
Хорошо, реализация `Watcher` завершена, полный код доступен [здесь](https://github.com/DMQ/mvvm/blob/master/js/watcher.js).
Основные модули данных Vue, связанные с привязкой данных, также были реализованы. Подробнее [тут](https://github.com/vuejs/vue), исходный код находится в директории `src`.
Итак, давайте поговорим о логике и реализации входного файла MVVM, что будет немного проще.
### 4. Реализация MVVM
MVVM выступает как точка входа для данных, объединяющая Observer, Compile и Watcher. Observer отслеживает изменения в модели данных, Compile анализирует и компилирует шаблоны с помощью директив, а Watcher создаёт мост между Observer и Compile, обеспечивая двустороннюю привязку данных — изменения данных приводят к обновлению представления, а взаимодействие пользователя с представлением приводит к изменению модели данных.Пример простого конструктора MVVM:
```javascript
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data;
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
```
Однако здесь есть проблема: данные, которые слушаются, это `options.data`. При каждом обновлении представления требуется использовать такой подход:
```javascript
var vm = new MVVM({data: {name: 'kindeng'}});
vm._data.name = 'dmq';
```
Это явно не соответствует нашему первоначальному ожиданию. Мы хотели бы иметь возможность вызова следующего типа:
```javascript
var vm = new MVVM({data: {name: 'kindeng'}});
vm.name = 'dmq';
```
Поэтому нам нужно добавить метод для создания прокси-объектов в экземпляр MVVM, чтобы доступ к свойствам `vm` был эквивалентен доступу к свойствам `vm._data`.
Переработанный код выглядит так:
```javascript
function MVVM(options) {
this.$options = options;
var data = this._data = this.$options.data, me = this;
// Создание прокси-объектов для свойств, чтобы vm.xxx было эквивалентно vm._data.xxx
Object.keys(data).forEach(function(key) {
me._proxy(key);
});
observe(data, this);
this.$compile = new Compile(options.el || document.body, this)
}
MVVM.prototype = {
_proxy: function(key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}
};
```Здесь мы используем метод `Object.defineProperty`, чтобы захватить права доступа к свойствам объекта `vm`. Таким образом, чтение и запись свойств `vm` становятся эквивалентными операциям чтения и записи свойств `vm._data`. Это позволяет достичь эффекта "подмены" значений. На этом все модули и функции были завершены, как было обещано в начале статьи. Был реализован простой модуль MVVM, основная идея и принципы которого заимствованы из упрощённой версии Vue [источника](https://github.com/vuejs/vue), нажмите [здесь](https://github.com/DMQ/mvvm), чтобы просмотреть все связанные с данной статьёй коды.
Из-за практически ориентированного содержания статьи объём кода довольно велик, поэтому рекомендуется тем, кто хочет более подробно изучить материал, снова обратиться к исходному коду этой статьи для чтения, что поможет легче понять и усвоить информацию.
### Обзор
В данной статье рассматриваются принципы и реализация двустороннего привязывания данных через несколько модулей: "Различные подходы к реализации двустороннего привязывания", "Реализация Observer", "Реализация Compile", "Реализация Watcher" и "Реализация MVVM". В работе также представлено пошаговое объяснение некоторых ключевых моментов и деталей, а также показана часть важных фрагментов кода для демонстрации процесса создания системы двустороннего привязывания MVVM.В тексте могут содержаться недочеты и ошибки, поэтому любые замечания и предложения по улучшению работы будут очень полезны!
Спасибо за чтение!
[img1]: ./img/1.gif
[img2]: ./img/2.png
[img3]: ./img/3.png
### Ссылки:
- https://github.com/qieguo2016/Vueuv
- https://github.com/DMQ/mvvm
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )