1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/xiaohuan171-grow-vue

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Внести вклад в разработку кода
Синхронизировать код
Отмена
Подсказка: Поскольку Git не поддерживает пустые директории, создание директории приведёт к созданию пустого файла .keep.
Loading...
readme.md

Анализ принципов реализации Vue — как достигается двухсторонняя привязка данных MVVM

Какие преимущества вы получите от этого материала?

  1. Понимание принципов двухсторонней привязки данных в Vue и ключевых модулей кода.
  2. Утолщение любопытства и понимание того, как достигается двухсторонняя привязка.

Для удобства объяснения принципов и реализации, все относящиеся к этому материалу коды были взяты из источника кода Vue и адаптированы. Они являются более простыми и не учитывают такие аспекты, как обработка массивов и циклические зависимости данных. Однако это не должно затруднять чтение и понимание материала. После прочтения статьи вам будет легче анализировать исходный код Vue. Все связанные с этим материалом коды доступны на GitHub https://github.com/DMQ/mvvm.

Думаю, что большинство людей знакомо с концепцией двухсторонней привязки данных MVVM. Без лишних слов, давайте рассмотрим пример, который мы реализуем в этом материале. Это тот же синтаксис, что и в Vue. Если вы ещё не знакомы с двухсторонней привязкой данных, нажмите сюда.
<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>
```Результат:
![](img1)

### Некоторые способы реализации двухсторонней привязки данных
Существуют различные популярные 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 является парсинг шаблонных директив, замена переменных в шаблоне на данные, а также инициализация отображения страницы и связывание каждого инструктажа с функциями обновления, добавление подписчиков данных. При изменении данных, получение уведомления приводит к обновлению представления, как показано на следующем рисунке:
![](img3)

При многократном обращении к узлам 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 )

Вы можете оставить комментарий после Вход в систему

Введение

Мини-фреймворк Vue, реализующий большинство директив Vue. Этот проект является расширением фреймворка DMQ/MVVM, поэтому он сохраняет ссылку на ридми проекта. Развернуть Свернуть
MIT
Отмена

Обновления

Пока нет обновлений

Участники

все

Недавние действия

Загрузить больше
Больше нет результатов для загрузки
1
https://api.gitlife.ru/oschina-mirror/xiaohuan171-grow-vue.git
git@api.gitlife.ru:oschina-mirror/xiaohuan171-grow-vue.git
oschina-mirror
xiaohuan171-grow-vue
xiaohuan171-grow-vue
master