Простая реализация интерфейсных MVC и MVVM

JavaScript

MVC

MVC — это шаблон проектирования, который делит приложение на 3 части: данные (модель), уровень представления (представление) и уровень взаимодействия с пользователем. Взгляните на рисунок ниже, чтобы лучше понять взаимосвязь между тремя.

Другими словами, возникновение события – это процесс

  1. Взаимодействие пользователя и приложения
  2. Обработчик событий контроллера срабатывает
  3. Контроллер запрашивает данные у модели и передает их представлению.
  4. Представления представляют данные пользователю

Модель: используется для хранения всех объектов данных приложения. Модель не должна знать детали представления и контроллера, она просто должна содержать данные и логику, непосредственно связанные с этими данными. Любой код обработки событий, шаблоны представлений и логика, не связанные с моделью, должны быть изолированы от модели. Представление: слой представления предоставляется пользователю, и пользователь взаимодействует с ним. В приложениях JavaScript представления в основном состоят из шаблонов html, css и JavaScript. Представления не должны содержать никакой логики, кроме простых условных операторов в шаблонах. На самом деле, как и в случае с моделью, представление также должно быть отделено от остального приложения. Контроллер: Контроллер является связующим звеном между моделью и представлением. Контроллер получает события и входные данные из представления, обрабатывает их и соответствующим образом обновляет представление. Когда страница загружается, контроллер добавляет в представление прослушиватели событий, таких как отправка форм и нажатие кнопок. Затем, когда пользователь взаимодействует с приложением, вступают в действие триггеры событий в контроллере. Например, ранняя основа платформы JavaScript использовала шаблон MVC.

Приведенный выше пример кажется слишком пустым, давайте поговорим на примере из жизни, чтобы объяснить: 1. Пользователь отправляет новое сообщение в чат 2. Срабатывает обработчик событий контроллера 3. Контроллер создает новую модель чата 4. Затем контроллер обновляет вид 5. Пользователь видит новое сообщение чата в окне чата Говоря о жизненном примере, мы имеем более глубокое понимание MVC в плане кода.

Model

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

var Model = {
	create: function() {
		this.records = {}
		var object = Object.create(this)
		object.prototype = Object.create(this.prototype)
		return object
	}
}

create используется для создания объекта, прототипом которого является Модель, а затем некоторые функции, включая манипулирование данными, включая поиск, хранение

var Model = {
	/*---代码片段--*/
	find: function () {
		return this.records[this.id]
	},
	save: function () {
		this.records[this.id] = this 
	}
}

Теперь мы можем использовать эту модель:

user = Model.create()
user.id = 1
user.save()
asset = Model.create()
asset.id = 2
asset.save()
Model.find(1)
=> {id:1}

Вы видите, что мы уже нашли этот объект. Модель является частью данных, и мы закончили.

Control

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

var ToggleView = {
	init: function (view) {
		this.view = $(view)
		this.view.mouseover(this.toggleClass, true)
		this.view.mouseout(this.toggleClass, false)
	},
	this.toggleClass: function () {
		this.view.toggleClass('over', e.data)
	}
}

Таким образом, мы можем добиться простого управления представлением: наведите указатель мыши на элемент, чтобы добавить дополнительный класс, и удалите его, чтобы удалить дополнительный класс. Затем добавьте несколько простых стилей, таких как

    ex:
    	.over {color: red}
    	p{color: black}

这样控制器就和视图建立起了连接。在MVC中有一个特性就是一个控制器控制一个视图,随着项目体积的增大,就需要一个状态机用于管理这些控制器。先来创建一个状态机
var StateMachine = function() {}
SateMachine.add = function (controller) {
	this.bind('change', function (e, current) {
		if (controller == current) {
			controller.activate()
		} else {
			controller.deactivate()
		}
	})

	controller.active = function () {
		this.trigger('change', controller)
	}
}
// 创建两个控制器
var con1 = {
	activate: funtion() {
		$('#con1').addClass('active')
	},
	deactivate: function () {
		$('#con1').removeClass('active')
	}
}

var con2 = {
	activate: funtion() {
		$('#con2').addClass('active')
	},
	deactivate: function () {
		$('#con2').removeClass('active')
	}
}

// 创建状态机,添加状态
var sm = new StateMachine
sm.add(con1)
sm.add(con2)

// 激活第一个状态
con1.active()

Это обеспечивает простое управление контроллером, и, наконец, мы добавляем несколько стилей css.

#con1, #con2 { display: none }
#con2.active, #con2.active { display: block }

При активации con1 меняется стиль, то есть меняется вид. Контроллер здесь, давайте взглянем на часть View в MVC, то есть представление

View

Представление — это интерфейс приложения, который предоставляет пользователю визуальное представление и взаимодействует с пользователем. В JavaScript представление представляет собой фрагмент HTML без логики, которым управляет контроллер приложения, а представление обрабатывает обратные вызовы событий и встроенные данные. Проще говоря, это написание HTML-кода на javaScript, а затем вставка HTML-фрагментов в HTML-страницы.Вот два метода:

Динамический рендеринг представлений

Используйте document.createElement для создания элементов DOM, установки их содержимого и добавления на страницу, например. var views = document.getElementById('представления') views.innerHTML = '' // элемент пустой вар ваппер = document.createElement('p') wrapper.innerText = 'добавить в представления' views.appendChild (обертка) На этом создание элементов с помощью createElement завершается, а затем они добавляются на HTML-страницу.

шаблон

Если у вас есть опыт бэкенд-разработки, вы должны быть знакомы с шаблонами. Например, ejs обычно используется в nodejs. Ниже приведен небольшой пример ejs. Вы можете видеть, что ejs напрямую отображает javascript в HTML.

str = '<h1><%= title %></h1>'
ejs.render(str, {
    title: 'ejs'
});

Тогда результат этого рендеринга

ejs

Конечно, на самом деле ejs более мощный, и мы даже можем добавлять к нему функции.Ощущается ли язык шаблонов очень похожим на то, как написаны Vue и React?Я тоже так думаю. Тогда очевидна роль представления, которое заключается в соединении HTML и javaScript. Остается проблема в том, что на схеме MVC мы видим взаимосвязь между представлением и моделью, при изменении модели представление также будет обновляться. Затем необходимо связать представление и модель, что означает, что при изменении записи вашему контроллеру не нужно обрабатывать обновление представления, поскольку эти обновления выполняются автоматически в фоновом режиме. Чтобы связать объект javaScript с представлением, нам нужно настроить функцию обратного вызова, которая отправляет уведомление для обновления представления при изменении свойств объекта. Ниже приведена функция обратного вызова, которая вызывается при изменении значения.Конечно, теперь мы можем использовать более простой набор и получить возможность отслеживать данные, что будет обсуждаться позже в MVVM.
var addChange = function (ob) {
	ob.change = function (callback) {
		if (callback) {
			if (!this._change) this._change = {}
			this._change.push(callback)
		} else {
			if (!this._change) return 
			for (var i = this._change.length - 1; i >= 0; i--) {
				this._change[i].apply(this)
			}
		}
	}
}

Рассмотрим практический пример

var addChange = function (ob) {
	ob.change = function (callback) {
		if (callback) {
			if (!this._change) this._change = {}
			this._change.push(callback)
		} else {
			if (!this._change) return 
			for (var i = this._change.length - 1; i >= 0; i--) {
				this._change[i].apply(this)
			}
		}
	}
}

var object = {}
object.name = 'Foo'

addChange(object)
object.change(function () {
	console.log('Changed!', this)
	// 更新视图的代码
})
obejct.change()
object.name = 'Bar'
object.change()

Это реализует выполнение и запуск события изменения. Я считаю, что у всех есть глубокое понимание MVC, поэтому давайте изучим шаблон MVVM.

MVVM

В настоящее время основные веб-фреймворки в основном используют модель MVVM, почему они отказались от модели MVC и обратились к модели MVVM? В предыдущем MVC мы упомянули, что контроллеру соответствует представление, а контроллер управляется конечным автоматом, здесь есть проблема: если проект достаточно большой, то объем кода конечного автомата сильно раздувается и сложно поддерживать. Другая проблема связана с производительностью.В MVC мы много манипулируем DOM, а большое количество манипуляций с DOM снизит производительность рендеринга страницы, замедлит скорость загрузки и повлияет на взаимодействие с пользователем. Наконец, когда модель часто меняется, разработчик активно обновляет представление, поэтому обслуживание данных становится затруднительным. Мир создают ленивые люди, поэтому для снижения нагрузки и экономии времени очень важен архитектурный паттерн, более подходящий для фронтенд-разработки. В это время появилось применение шаблона MVVM во внешнем интерфейсе. MVVM делает разделение пользовательского интерфейса и логики более четким. Ниже приведена схематическая диаграмма MVVM, как видно, она состоит из трех частей: Model, ViewModel и View.

Поговорим об их ролях отдельно

View

Представление используется в качестве шаблона представления для определения структуры и макета. Он не обрабатывает сами данные, он просто отображает данные в ViewModel. Кроме того, чтобы ассоциироваться с ViewModel, все, что нужно сделать, — это объявить привязку данных, объявить инструкцию и объявить привязку события. Это полностью отражено в популярной сегодня среде разработки MVVM. На диаграмме примера мы видим, что существует двусторонняя привязка между ViewModel и View, что означает, что изменения в ViewModel могут быть отражены в View, а изменения в View также могут изменить значение данных ViewModel. Итак, как реализовать двухстороннюю привязку, например, есть этот элемент ввода:

<input type='text' yg-model='message'>

По мере изменения значения, введенного пользователем во входных данных, сообщение в ViewModel также будет меняться, тем самым реализуя одностороннюю привязку данных из представления к ViewModel. Вот несколько идей:

  1. Просканируйте, чтобы увидеть, какие узлы имеют атрибуты yg-xxx
  2. Автоматически добавлять события onchange к этим узлам
  3. Обновите данные в ViewModel, например ViewModel.message = xx.innerText

Тогда привязка ViewModel к View может быть следующим примером:

<p yg-text='message'></p>

Значение, отображаемое в p после рендеринга, является значением переменной сообщения в ViewModel. Вот несколько идей:

  1. Сначала зарегистрируйте ViewModel
  2. Просканируйте все дерево DOM, чтобы увидеть, какие узлы имеют атрибут yg-xxx.
  3. Запишите неявную связь между этими узлами DOM с односторонней привязкой и ViewModels.
  4. Используйте innerText, innerHTML = ViewModel.message для назначения

ViewModel

ViewModel играет роль соединения представления и модели и используется для обработки логики в представлении. Во фреймворке MVC модель представления взаимодействует с моделью, вызывая методы в модели, однако в MVVM нет прямой связи между представлением и моделью, в MVVM ViewModel получает данные из модели, а затем применяет их. к представлению. По сравнению со многими контроллерами MVC очевидно, что в этом режиме проще управлять данными, и он не будет таким запутанным. Другое дело — обрабатывать события в представлении, например, когда пользователь нажимает кнопку, это действие запускает поведение ViewModel и выполняет соответствующую операцию. Действия могут включать изменение модели и повторную визуализацию представления.

Model

Слой модели соответствует модели предметной области слоя данных, в основном синхронизирует модель предметной области. Синхронизируйте клиентские и серверные бизнес-модели с помощью таких API, как Ajax/fetch. В отношениях между слоями он в основном используется для абстрагирования модели представления в ViewModel.

Простая реализация MVVM

Осознайте эффект:

<div id="mvvm">
	<input type="text" v-model="message">
	<p>{{message}}</p>
	<button v-click='changeMessage'></button>
</div>
<script type="">
	const vm = new MVVM({
		el: '#mvvm',
		methods: {
			changeMessage: function () {
				this.message = 'message has change'
			}
		},
		data: {
			message: 'this is old message'
		}
	})
</script>

Для простоты вот несколько методов Vue

Observer

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

hero = {
	name: 'A'
}

В это время, когда мы обращаемся к hero.name, будет напечатана некоторая информация:

hero.name 
// I'm A

Когда мы вносим изменения в hero.name, также печатается некоторая информация:

hero.name = 'B'
// the name has change

Таким образом, реализуем ли мы наблюдение за данными? Наблюдение за данными в Angular использует грязную проверку, то есть когда пользователь выполняет операцию, которая может изменить ViewModel, он сравнивает старую ViewModel, а затем вносит изменение. В Vue принят захват данных, то есть, когда данные получены или установлены, будет запущен Object.defineProperty(). Здесь мы берем метод наблюдения за данными Vue, который проще. Ниже приведен конкретный код

function observer (obj) {
	let keys = Object.keys(obj)
	if (typeof obj === 'object' && !Array.isArray(obj)) {
		keys.forEach(key => {
			defineReactive(obj, key, obj[key])
		})	
	}
}

function defineReactive (obj, key, val) {
	observer(val)
	Object.defineProperty(obj, key, {
		enumerable: true,
    	configurable: true,
		get: function () {
			console.log('I am A')
			return val
		},
		set: function (newval) {
			console.log('the name has change')
			observer(val)
			val = newval
		}
	}) 
}

Приведение героя в метод наблюдения дает тот же результат, что и ожидалось. Таким образом реализуется обнаружение данных, а затем уведомляется подписчик. Как уведомлять подписчиков, нам нужно реализовать подписчика сообщений, поддерживать массив для сбора подписчиков, изменения данных запускают notify(), а затем подписчики запускают метод update(), улучшенный код выглядит так:

function defineReactive (obj) {
	dep = new Dep()
	Object.defineProperty(obj, key, {
		enumerable: true,
    	configurable: true,
		get: function () {
			console.log('I am A')
			Dep.target || dep.depend()
			return val
		},
		set: function (newval) {
			console.log('the name has change')
			dep.notify()
			observer(val)
			val = newval
		}
	}) 
}

var Dep = function Dep () {
	this.subs = []
}
Dep.prototype.notify = function(){
	var subs = this.subs.slice()
	for (var i = 0, l = subs.length; i < l; i++) {
		subs[i].update()
	}
}
Dep.prototype.addSub = function(sub){
	this.subs.push(sub)
}
Dep.prototype.depend = function(){
	if (Dep.target) {
		Dep.target.addDep(this)
	}
}

Это похоже на исходный код Vue, который завершает добавление подписчиков к подписчику и уведомление подписчиков. Здесь, когда я раньше смотрел исходный код Vue, меня давно волновал вопрос, откуда в методе get взялся Dep. Скажу тут, что он глобальная переменная, и добавление целевой переменной используется для добавления подписчиков к подписчику. Подписчиком здесь является Wacther, и Watcher может подключиться к представлению, чтобы обновить представление. Ниже приведена часть кода наблюдателя.

Watcher.prototype.get = function(key){
	Dep.target = this
	this.value = obj[key] // 触发get从而向订阅器中添加订阅者
	Dep.target = null // 重置
};

Compile

Говоря о концепции MVVM, одним из шагов в процессе View -> ViewModel является поиск элемента с yg-xx в дереве DOM. Этот раздел посвящен разбору шаблонов и подключению View и ViewModel. Обход дерева DOM очень требователен к производительности, поэтому узел el будет преобразован во фрагмент фрагмента документа для операций синтаксического анализа и компиляции. После завершения операции добавьте фрагмент в исходный реальный узел DOM. Ниже его код

function Compile (el) {
	this.el = document.querySelector(el)
	this.fragment = this.init()
	this.compileElement()
}

Compile.prototype.init = function(){
	var fragment = document.createDocumentFragment(), chid 
	while (child.el.firstChild) {
		fragment.appendChild(child)
	}
	return fragment
};

Compile.prototype.compileElement = function(){
	fragment = this.fragment 
	me = this 
	var childNodes = el.childNodes 
	[].slice.call(childNodes).forEach(function (node) {
		var text = node.textContent 
		var reg = /\{\{(.*)\}\}/ // 获取{{}}中的值
		if (reg.test(text)) {
			me.compileText(node, RegExp.$1)
		}

		if (node.childNodes && node.childNodes.length) {
			me.compileElement(node)
		}
	})
}
Compile.prototype.compileText = function (node, vm, exp) {
	updateFn && updateFn(node, vm[exp])
	new Watcher(vm, exp, function (value, oldValue) {
		// 一旦属性值有变化,就会收到通知执行此更新函数,更新视图
		updateFn() && updateFn(node, val)
	})
}
// 更新视图
function updateFn (node, value) {
	node.textContent = value 
}

Таким образом, компиляция фрагмента проходит успешно, а изменение значения в ViewModel может привести к изменению слоя View. Далее идет реализация Watcher, метод get уже обсуждался, давайте посмотрим на другие методы.

Watcher

Watcher — это мост между Observer и Compile. Вы можете видеть, что в Observer вы добавили себя в подписчики. Когда происходит dep.notice(), вызывается sub.update(), поэтому необходим метод update().После изменения значения можно запустить обратный вызов в Compile для обновления представления. Ниже приведена конкретная реализация Watcher.

var Watcher = function Watcher (vm, exp, cb) {
	this.vm = vm 
	this.cb = cb 
	this.exp = exp 
	// 触发getter,向订阅器中添加自己
	this.value = this.get()
}

Watcher.prototype = {
	update: function () {
		this.run()
	},
	addDep: function (dep) {
		dep.addSub(this)
	},
	run: function () {
		var value = this.get()
		var oldVal = this.value 
		if (value !== oldValue) {
			this.value = value 
			this.cb.call(this.vm, value, oldValue) // 执行Compile中的回调
		}
	},
	get: function () {
		Dep.target = this 
		value = this.vm[exp] // 触发getter
		Dep.target = null 
		return value 
	}
}

В приведенном выше коде наблюдатель играет роль соединения наблюдателя и компилятора.При изменении значения наблюдатель уведомляется, а затем наблюдатель вызывает метод обновления.Из-за того, что наблюдатель определен в компиляторе, при изменении значения , Watcher() для обновления представления. Самая важная часть сделана. Можно добавить конструктор MVVM. Рекомендовать статьюВнедрите MVVM самостоятельно, который более подробно описан здесь.

Суммировать

Хорошо, эта статья окончена. Я надеюсь, что благодаря сравнению читатели смогут получить более четкое представление о текущем интерфейсном фреймворке. Спасибо вам всем