Что такое шаблон MVVM? Как вы понимаете принцип MVVM?Понимание его не только для собеседований, но и для лучшего понимания шаблонов проектирования фреймворков VUE, Backbone.js, angular, Ember и avalon.Возможно, что следующий популярный фреймворк будет вашим шедевром. ~~В конце этой статьи я также реализую собственную простую библиотеку MVVM, которая реализует основные функции библиотеки mvvm~
Во-первых, концепция MVVM
Определение Mvvm MVVM — это сокращение от Model-View-ViewModel. — это шаблон проектирования архитектуры программного обеспечения, разработанный архитекторами Microsoft WPF и Silverlight Кеном Купером и Тедом Питерсом, подход к программированию, управляемый событиями, который упрощает пользовательские интерфейсы. Опубликовано в его блоге в 2005 году Джоном Госсманом (также архитектором WPF и Silverlight). А именно Модель-Вид-ВьюМодель.
Во-вторых, история развития MVVM
var dom = document.getElementById('name');
dom.innerHTML = 'Homer';
dom.style.color = 'red';
$('#name').text('Homer').css('color', 'red');
MVVM был впервые предложен Microsoft.Он основан на идее MVC настольных приложений.На странице интерфейса Модель представлена чистым объектом JavaScript, а представление отвечает за отображение.Два добились максимального разделение.
Как пишется ViewModel? Общая ViewModel должна быть написана на JavaScript, чтобы можно было повторно использовать всю модель MVVM.
3. Формальное понимание MVVM
-
Шаблон MVVM
Появление MVVM способствует разделению фронтенд-разработки с графическим интерфейсом и внутренней бизнес-логики, что значительно повышает эффективность фронтенд-разработки.Ядром MVVM является слой ViewModel., это похоже на станцию передачи (преобразователь значений), отвечающую за преобразование объектов данных в модели, чтобы упростить управление данными и их использование, этот уровень выполняет двустороннюю привязку данных со слоем представления вверх и взаимодействует с моделью. слой вниз Запрос на взаимодействие данных, который играет роль в увязке вверх и вниз. Как показано ниже:
- Компоненты МВВМ
# Просмотр слоя
Представление — это уровень представления, представляющий собой пользовательский интерфейс. Внешний интерфейс в основном построен на HTML и CSS.Для более удобного отображения данных слоя ViewModel или Model были созданы различные языки шаблонов внешнего и внутреннего интерфейса, такие как FreeMarker, Marko, Pug , Jinja2 и т. д. Основные MVVM-фреймворки, такие как Avalon, Vue, Angular и т. д., также имеют собственные встроенные языки шаблонов для построения пользовательских интерфейсов.
# Слой модели
Модель относится к модели данных, которая обычно относится к различной обработке бизнес-логики и обработке данных, выполняемой серверной частью, в основном вокруг системы базы данных.
# Слой ViewModel
ViewModel — это уровень данных представления, который создается и поддерживается организацией разработчиков интерфейса. Ядро режима mvvm, это мост, соединяющий представление и модель. На этом уровне внешний разработчик преобразует данные модели, полученные от серверной части, и выполняет вторичную инкапсуляцию для создания модели данных представления, которая соответствует ожиданиям уровня представления. Следует отметить, что модель данных, инкапсулированная ViewModel, включает в себя состояние и поведение представления, в то время как модель данных слоя Model содержит только состояние, например, что отображается в этой части страницы, а что отображается в этой части. Они относятся к состоянию просмотра (Отображение), и что происходит, когда страница загружается, что происходит, когда вы нажимаете на эту часть, и что происходит, когда эта часть прокручивается, — все это поведение просмотра (взаимодействия) и состояния просмотра. и поведение инкапсулировано в ViewModel. Эта инкапсуляция позволяет ViewModel полностью описывать уровень представления. Благодаря реализации двусторонней привязки содержимое ViewModel будет отображаться в слое View в режиме реального времени, что очень удобно, поскольку разработчикам интерфейса больше не нужно неэффективно и хлопотно манипулировать DOM для обновления представления. Фреймворк MVVM уже сделал самое грязное и утомительное. После того, как эта часть будет выполнена, нашим разработчикам нужно будет только обработать и поддерживать ViewModel, а обновленное представление данных будет автоматически обновляться соответствующим образом, действительно реализуя разработку, управляемую данными. Видите, слой View отображает не данные слоя Model, а данные ViewModel. ViewModel отвечает за взаимодействие со слоем Model, что полностью разделяет слой View и слой Model. Эта развязка очень важна. Это важная часть реализации схемы разделения интерфейса и сервера.
------Я разделительная линия-----------------------------------
Мысль: конечно, если только Vue и React — это фреймворки, работающие на внешнем интерфейсе, почему приведенная выше модель разделяет внутренний бизнес? Разве это не противоречит? ? ?
Таким образом, можно также сказать следующее: MVVM делит V или View в предыдущем MVC (внутренняя разработка MVC: данные в M: база данных модели, V: интерфейсная страница просмотра, C: внутренний контроллер контроллера) на MVVM. режим :
Этот режим разделения: M: данные в js в модели, V: страница интерфейса, VM: режим просмотра модели представления;
Режим MVVM: пользователю не требуется вручную управлять домом, в основном для достижения двусторонней привязки данных.
———————————————————————————————————
С этого аспекта
- Преимущества и недостатки шаблона проектирования MVVM:
1,Конечно, это самая важная технология двустороннего связывания.Односторонняя привязка и двусторонняя привязка.
<p>Hello, <span id="name">LEE</span>!</p><p>You are <span id="age">18</span>.</p>
Измените содержимое узлов имени и возраста с помощью jQuery:
var name = '修改';
var age =100;
$('#name').text(name);
$('#age').text(age);
var person = {
name: 'LEEt',
age: 18
};
Чтобы изменить отображаемое имя из LEE и изменить отображаемый возраст с 18 до 100 лет, мы не манипулируем DOM, а напрямую модифицируем объект JavaScript:
person.name = '修改';
person.age = 100;
MVVMДизайн-мышление: ФокусModelменяется, пустьMVVMФреймворк для автоматического обновленияDOMсостояние, тем самым выводя отправителя из операцииDOMОсвободите себя от утомительных шагов!
2. Так как большинство функций контроллера перенесено в View для обработки, контроллер сильно уменьшился в размерах.
3. Функцию можно абстрагировать от части обработки данных View или ViewController для обработки модели. Таким образом, они специализируются на макете страницы и переходе между страницами, и их необходимо еще больше упростить.
4. Улучшить ремонтопригодность
5. Тестируемый. Интерфейсы всегда было трудно тестировать, и теперь тесты можно писать для ViewModels.
6. Низкая связанность и возможность повторного использования: представление можно изменять и модифицировать независимо от модели. Модель представления может быть привязана к различным «представлениям». Модель меняется постоянно. Вы можете поместить некоторую логику представления в ViewModel и позволить многим представлениям повторно использовать эту логику представления.
- Ошибки трудно отлаживать. Из-за двустороннего режима привязки, когда вы видите аномалию в интерфейсе, может быть ошибка в коде вашего Представления или может быть проблема с кодом Модели. Привязка данных позволяет быстро распространять ошибки в одном месте в другие места, что затрудняет обнаружение исходной проблемы. Кроме того, объявление привязки данных императивно написано в шаблоне представления, и нет возможности прервать отладку этого содержимого.
- Модель в большом модуле тоже будет очень большая.Хоть она и удобна в использовании, но легко обеспечить непротиворечивость данных.В то время, если ее держать долго, будет стоить больше памяти, если память не освобождается.
- Для крупномасштабных графических приложений существует множество состояний просмотра, а затраты на создание и обслуживание ViewModel будут относительно высокими.
-
Сфера применения МВВМ
Angular: разработан Google, известен, но сложен в освоении, подходит для ПК, структура кода будет более понятной;
Backbone.js: начать очень сложно, потому что слишком много API;
Ember: Большой и всеобъемлющий фреймворк, очень сложно написать Hello world.
Avalon: он легкий и имеет высокую степень поддержки старых браузеров с минимальной поддержкой IE6, поэтому он подходит для проектов, совместимых со старыми браузерами Liu;
Vue: в основном легкий, используется только как часть представления в MV *, преимущества легкие, простые в освоении и использовании, недостаток в том, что его сложнее использовать с другими фреймворками или библиотеками в больших проектах.
В-четвертых, библиотека js, реализующая MVVM.
- Обнаружение грязного значения (угловое):
- l Механизм обнаружения загрязнения не использует определение времени.
- l Момент грязного обнаружения — когда данные изменяются.
- l Angular инкапсулирует часто используемые события dom, события xhr и т. д. и запускает процесс дайджеста, входящий в него angular.
- l В процессе дайджеста он будет проходить из корневой области для проверки всех наблюдателей. (Для конкретного дизайна angular вы можете увидеть другие документы, здесь обсуждается только привязка данных), затем давайте посмотрим, как сделать грязное обнаружение: в основном через набор данных, чтобы найти все элементы, связанные с данными, а затем сравнить данные изменяется, и если она изменяется, выполняется операция инструкции.
3. Режим публикации-подписки (основной): связывание и мониторинг данных и представлений путем публикации и подписки на сообщения.
1. Реализовать Observer, перехватить данные и уведомить об изменении данных (основные моменты, которые нужно использовать: метод Object.defineProperty())
2. Реализовать компиляцию, разобрать инструкции, инициализировать представление, подписаться на изменения данных и привязать функцию обновления.
3. Реализовать Watcher в качестве промежуточной точки между двумя вышеуказанными.При получении изменений данных позволить Dep добавить текущего Watcher и вовремя уведомить представление об обновлении.
4. Реализовать некоторые другие функции VUE (вычисления, методы)
5. Реализовать MVVM, интегрировать вышеуказанные пункты в качестве функции входа
Ниже приведена часть кода:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>
实现MVVM的js库(模拟vue实现功能)</title>
<script src="./MVVM.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="person.name">
<p>
hello,{{person.name}}
</p>
<p>You are:{{person.age}}</p>
<!-- computed属性如果数据不变化 视图不更新 -->
<p>{{getNewName}}</p>
<button type="button" name="button" v-on:click="testToggle">
修改名字</button>
</div>
<script>
let vm = new Vue({
el: '#app',
data: {
person: {
name: 'lee',
age: 18
}
},
methods: {
testToggle(){
this.person.name = '修改后的名字:哈哈';
}
},
computed: {
getNewName(){
return this.person.name+' 是要成为海贼王的人'
}
},
})
</script>
</body>
</html>
js:
// 2019-4-4
// lee
// 草履虫的思考
// 简单模拟vue实现MVVM
/**
* 实现一个Vue的类
* 1、实现一个Observer,对数据进行劫持,通知数据的变化(将使用的要点为:Object.defineProperty()方法)
2、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数ComplieUtil解析指令的公共方法
3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update
4、实现一些VUE的其他功能(Computed、menthods)
*/
// 观察者模式(发布订阅)
class Dep {
constructor() {
this.subs = []; //存放所有watcher
}
// 订阅 添加watcher
addSub(watcher) {
this.subs.push(watcher);
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update());
}
}
// 观察者 vm.$watch(vm,'person.name',(newVal)=>{ })
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 默认存储一个老值
this.oldValue = this.get();
}
get() {
Dep.target = this;
// 取值 把这个观察者和数据关联起来
let val = ComplieUtil.getVal(this.vm,this.expr);
Dep.target = null;
return val;
}
// 更新操作 数据变化后 会调用观察者中的update方法
update() {
let newVal = ComplieUtil.getVal(this.vm,this.expr);
if (newVal !== this.oldValue) {
this.cb(newVal);
}
}
}
// 实现数据劫持作用
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
// 如果是对象才观察
if (data && typeof data === 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive(obj, key, value) {
this.observer(value);
// 给每个属性 都加上具有发布订阅的功能
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可重新定义
get() {
// 创建watcher时 会取到对应的内容,并且把watcher放到全局上
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => { // {person:{name:'lee'}
// 数据没有变不需要更新
if (newVal != value) {
// 需要递归
this.observer(newVal);
value = newVal;
dep.notify();
}
}
})
}
}
// 编译器
class Complier {
constructor(el, vm) {
// 判断el属性是不是一个元素 如果不是元素 那就获取他 (因为在vue的el中可能是el:'#app'
// 或者document.getElementById('app')
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 把当前节点中的元素 获取到 放到内存中
let fragment = this.nodeFragMent(this.el);
// 把节点中的内容进行替换
// 编译模板 用数据编译
this.complie(fragment);
// 把内容在塞到页面中
this.el.appendChild(fragment);
}
isElementNode(node) { //是不是元素节点
return node.nodeType === 1;
}
// 把节点移动到内存中
nodeFragMent(node) {
let frag = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
// appendChild 具有移动性
frag.appendChild(firstChild);
}
return frag;
}
// 是不是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 编译元素
complieElement(node) {
let attr = node.attributes;
[...attr].forEach(item => {
// item 有key = value ,type="text" v-model="person.name"
let {
name,
value: expr
} = item;
if (this.isDirective(name)) {
// v-mode v-html v-bind...
let [, directive] = name.split('-');
let [directiveName,eventName] = directive.split(':');
console.log(node, expr, this.vm, eventName);
// ComplieUtil[directive](node, expr, this.vm);
ComplieUtil[directiveName](node, expr, this.vm, eventName);
}
})
}
// 编译文本
// 判断当前文本节点中内容是否包括{{}}
complieText(node) {
let content = node.textContent;
var reg = /\{\{(.+?)\}\}/;
if (reg.test(content)) {
ComplieUtil['text'](node, content,this.vm); //{{}}
}
}
// 用来编译内存中的dom节点
complie(node) {
let childNode = node.childNodes;
// childNode 是类数组 转换为数组
[...childNode].forEach(item => {
// 元素 查找v-开头
if (this.isElementNode(item)) {
this.complieElement(item);
//如果是元素的话 需要把自己传进去
// 在去遍历子节点
this.complie(item);
// 文本 查找{{}}内容
} else {
this.complieText(item);
}
})
}
}
// 编译工具
ComplieUtil = {
// 解析v-model指令
// node是节点 expr是表达式 vm是实例 person.name vm.$data 解析v-model
model(node, expr, vm) {
// 给输入框赋予value属性 node.value = xxx
let fn = this.updater['modelUpdater'];
let val = this.getVal(vm, expr);
// 给输入框加一个观察者 如果稍后数据更i性能了会触发此方法,数据会更新
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
// 输入事件
node.addEventListener('input',(e)=>{
let val = e.target.value; //获取用户输入的内容
this.setVal(vm, expr, val);
});
fn(node, val);
},
html() {
},
// 返回了一个全的字符串
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
},
text(node, expr, vm) { //expr {{a}} {{b}} {{person.name}}
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//给表达式{{}}都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr));
});
return this.getVal(vm, args[1]);
});
let fn = this.updater['textUpdater'];
fn(node, content);
},
on(node, expr, vm,eventName){ //v-on:click
console.log(node, expr, vm, eventName);
node.addEventListener(eventName,(e)=>{
vm[expr].call(vm,e );
});
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
htmlUpdater() {},
// 处理文本节点
textUpdater(node, value) {
node.textContent = value;
}
},
//根据表达式取到的对应的数据 vm.$data expr是如 'person.name'
getVal(vm, expr) {
return expr.split('.').reduce((data, cur) => {
return data[cur];
}, vm.$data);
},
setVal(vm, expr,value){
expr.split('.').reduce((data, cur,index,arr) => {
if(index == arr.length-1){ //索引是最后一项
return data[cur] = value;
}
return data[cur];
}, vm.$data);
}
}
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
// 根元素存在在编译模板
if (this.$el) {
// 把数据 全部转化成用Object.defineProperty来定义
new Observer(this.$data);
// 实现methods中的方法
for (let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]; //进行了转化操作
}
});
}
// 实现computed中的方法
for (let key in computed) { //有依赖关系
Object.defineProperty(this.$data, key, {
get() {
return computed[key].call(this); //进行了转化操作
}
});
}
// 把数据获取操作 都代理到vm.$data
this.proxy(this.$data);
new Complier(this.$el, this);
}
}
// 代理 去掉$data
proxy(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key]; //进行了转化操作
}
});
}
}
}