Изучение реализации режима MVVM в VUE

Node.js внешний интерфейс Vue.js MVVM

Оригинальная статья Tencent DeepOcean:делать про.IO/v UE-MV VM-Hot…

timg

1. Паттерн MVVM

Идея дизайна MVVM: обращайте внимание на изменения Модели (данных), пусть инфраструктура MVVM автоматически обновляет состояние DOM, более распространенными реализациями являются: angular (обнаружение грязных значений) vue (перехват данных -> режим публикации и подписки) Мы фокусируемся на понимании Реализация vue (перехват данных -> режим публикации-подписки) освобождает нас от утомительных операций по манипулированию DOM

2, основной методObject.definePropertyпонимание

Давайте сначала кратко рассмотрим этот метод. Это также ключевой метод, используемый для реализации нашего захвата данных (мониторинга данных). Мы знаем, что платформа vue несовместима с более ранними версиями IE6 ~ 8, главным образом потому, что она использует этот объект в ES5.defineProperty, и на данный момент у этого метода нет хорошего решения для перехода на более раннюю версию.
var a = {};
Object.defineProperty(a, 'b', {    value: 123,         // 设置的属性值    writable: false,    // 是否只读    enumerable: false,  // 是否可枚举    configurable: false //
});
console.log(a.b); //123

Метод очень прост в использовании, он принимает три параметра, и все ониНеобходимыйиз

  • Первый параметр: целевой объект
  • Второй параметр: имя свойства или метода, который необходимо определить.
  • Третий параметр: характеристики целевого атрибута
Первые два параметра легко понять, в основном посмотрите на третий параметр, который является объектом, чтобы увидеть, какие атрибуты определены.
  • значение: значение свойства.
  • перезаписываемый: если false, значение свойства не может быть переопределено, только для чтения.
  • enumerable: перечисляется, по умолчанию false, не может быть перечислено (обычно установлено значение true)
  • Конфигурируемый: Total Switch, после того, как он окажется ложным, вы больше не сможете настроить другие (Value, Writable, Enumerable)
  • получать():функция, метод для выполнения при получении значения свойства(Не может сосуществовать с атрибутами, доступными для записи и значениями)
  • задавать():функция, метод для выполнения, когда значение свойства установлено(Не может сосуществовать с атрибутами, доступными для записи и значениями)
// 常用定义
var obj = {};
Object.defineProperty(obj, 'school', {    enumerable: true,    get: function() {        
       // 获取属性值时会调用get方法    },    set: function(newVal) {        
       // 设置属性值时会调用set方法        return newVal    } });

С помощью метода Object.defineProperty мы можем реализовать мониторинг реализации определенного эталонного типа данных.Когда объект, отслеживаемый методом, когда значение, определенное в нем, получено и установлено, параметры в Object.defineProperty будут активированы соответственно , Три метода получения и установки.

3. Перехват данных

После понимания метода Object.defineProperty мы теперь будем использовать его для реализации нашей функции захвата данных.Obaerveметод, см. следующий код

function MyVue(options = {}) {    
   // 将所有的属性挂载到$options身上    this.$options = options;    
   // 获取到data数据(Model)    var data = this._data = this.$options.data;    
   // 劫持数据    observe(data) }

// 给需要观察的对象都添加 Object.defineProperty 的监听
function Observe(data) {    
   for (let key in data) {        
       let val = data[key];        
       // 递归 =》来实现深层的数据监听        observe(val)        
       Object.defineProperty(data, key, {            enumerable: true,            get() {                
               return val            },            set(newval) {                if (val === newval) { //设置的值是否和以前是一样的,如果是就什么都不做                    return                }                val = newval // 这里要把新设置的值也在添加一次数据劫持来实现深度响应,                observe(newval);            }        })    } }
function observe(data) {
   // 这里做一下数据类型的判断,只有引用数据类型才去做数据劫持    if (typeof data != 'object') return    return new Observe(data) }

1) Вышеприведенный код делает эти вещи, сначала определяет начальный конструктор заменыMyVueМы используем его для получения данных, которые мы передали, и диапазона узлов DOM, который мы определили, а затем передаем данные в заранее определенный метод захвата данных.observe

2)ObserveРеализована общая логика мониторинга данных.Вот детальный момент.Вместо того,чтобы напрямую использовать конструктор Observe для захвата наших данных,мы написали небольшой методObserve для нового Observe,и сделали в нем эталонный тип данных.суждение. Цель этого состоит в том, чтобы облегчить рекурсию для реализации глубокого мониторинга структур данных, потому что наши структуры данных должны быть сложными и разнообразными, такими как следующий код

// 这里数据结构嵌套很多,要实现深层的数据监听采用了递归的实现方式
data: { a: {b:2} , c:{q:12,k:{name:'binhemori'}} , f:'mvvvm',o:[12,5,9,8]}

3) Здесь также следует отметить, что мы еще раз установили новое значение в методе установки и снова выполнили метод наблюдения, чтобы добиться глубокого отклика,Поскольку значение ссылочного типа данных может быть присвоено при присвоении, мы знаем, что vue имеет особенность, заключающуюся в том, что он не может добавлять несуществующие атрибуты и не может существовать без методов get и set.Если назначенное новое значение атрибута является ссылочным типом данных, он примет метод перехвата данных, который мы ранее выполнили.Объект адрес заменяется, а новый объект не имеет методов get и set без перехвата данных, поэтому нам нужно повторно выполнить перехват данных для него при установке нового значения, чтобы гарантировать, что разработчик установит значение несмотря ни на что. угнали, когда


Сказав так много, давайте используем его, чтобы увидеть, был ли реализован захват данных (мониторинг данных).

<div id="app">     
   <div>         <div>这里的数据1======<span style="color: red;">{{a.b}}</span></div>         <div>这里是数据2======<span style="color: green;">{{c}}</span></div>     </div>     <input type="text" v-model="a.b" value=""> </div> <!-- 引入自己定义的mvvm模块 --> <script src="./mvvm.js"></script>

<script type="text/javascript">     var myvue = new MyVue({         el: '#app',         data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] }     })
</script>

3

Видно, что уже есть методы получения и установки для данных в данных, которые мы определили, На этом этапе мы можем отслеживать изменения данных в данных.

4. Агент данных

Прокси данных, мы все знаем, что мы использовали vue, в реальном использовании мы можем напрямую получать данные через экземпляр + атрибут (vm.a), и наш код выше должен получать данные, подобные этому myvue._data.a Чтобы получить данные таким образом, есть дополнительная ссылка _data посередине, которую не очень удобно использовать.Давайте позволим нашему экземпляру это проксировать (_data) данные, чтобы операции, такие как myvue.a, можно было напрямую получить.

function MyVue(options = {}) {    
   // 将所有的属性挂载到$options身上    this.$options = options;    
   // 获取到data数据(Model)    var data = this._data = this.$options.data;    observe(data);    
   
   // this 就代理数据 this._data    for (const key in data) {        
   Object.defineProperty(this, key, {            enumerable: true,            get() {                
               // this.a 这里取值的时候 实际上是去_data中的值                return this._data[key]            },            set(newVal) {                // 设置值的时候其实也是去改this._data.a的值                this._data[key] = newVal            }        })    } }

Приведенный выше код реализует наш прокси-сервер данных, то есть при создании экземпляра один раз проходите данные в data и по очереди добавляете их в this.Не забудьте добавить Object.defineProperty во время процесса добавления. это данные, они нам нужны Добавьте монитор. Как показано на рисунке ниже, мы реализовали прокси для данных

2

5. Скомпилируйте шаблон (Compile)

Мы завершили захват данных и реализовали этот прокси для данных, поэтому следующее, что нужно сделать, это как скомпилировать данные в наш DOM-узел, то есть позволить слою представления (view) отображать наши данные.

// 将数据和节点挂载在一起
function Compile(el, vm) {    
   // el表示替换的范围    vm.$el = document.querySelector(el);    
   // 这里注意我们没有去直接操作DOM,而是把这个步骤移到内存中来操作,这里的操作是不会引发DOM节点的回流    let fragment = document.createDocumentFragment(); // 文档碎片    let child;  
   
   while (child = vm.$el.firstChild) {
       // 将app的内容移入内存中        fragment.appendChild(child);    }
           replace(fragment)    
   function replace(fragment) {        
       Array.from(fragment.childNodes).forEach(function (node) { //循环每一层            let text = node.textContent;            
           let reg = /\{\{(.*)\}\}/g;
                       
           // 这里做了判断只有文本节点才去匹配,而且还要带{{***}}的字符串            if (node.nodeType === 3 && reg.test(text)) {  
               // 把匹配到的内容拆分成数组              
               let arr = RegExp.$1.split('.');                let val = vm;                
               
               // 这里对我们匹配到的定义数组,会依次去遍历它,来实现对实例的深度赋值                arr.forEach(function (k) { // this.a.b  this.c                    val = val[k]                })      
                         
               // 用字符串的replace方法替换掉我们获取到的数据val                node.textContent = text.replace(/\{\{(.*)\}\}/, val)            }        
                  
           // 这里做了判断,如果有子节点的话 使用递归            if (node.childNodes) {                replace(node)            }        })    }    
   // 最后把编译完的DOM加入到app元素中    vm.$el.appendChild(fragment) }

Приведенный выше код реализует нашу компиляцию данныхCompileКак показано на рисунке ниже, мы можем видеть, что мы сохранили все дочерние узлы под el во фрагменте документа и временно сохранили (в памяти), потому что нам приходится часто манипулировать DOM и искать DOM, поэтому переместите его в средняя операция памяти

4
  • 1) сначала с помощью цикла while, сначала все дочерние узлы добавляются к фрагментам документаfragment.appendChild(child);
  • 2) Потом проходимreplaceметод для обхода всех дочерних узлов в документе, получения всего содержимого в их текстовых узлах (node.nodeType = 3) с синтаксисом {{}}, разбиения совпавших значений на массивы, а затем их обхода по очереди Найти и получить в данных, если у пройденного узла есть дочерние узлы, продолжайте использовать метод замены, пока он не вернется к неопределенным
  • 3) После получения данных используйте метод replace для замены всего содержимого {{}} в тексте, а затем поместите его обратно в элемент el vm.$el.appendChild(fragment),

6. Свяжите представление (представление) с данными (моделью)

После успешной привязки наших данных к узлу DOM, чтобы реализовать ассоциацию между нашим уровнем представления (представлением) и уровнем данных (моделью), теперь он фактически не связан, поскольку представление не может быть запущено путем изменения значения данных. давайте поговорим о наиболее часто используемых шаблонах проектирования в JS перед реализацией этого шагамодель публикации-подпискиТакже очень важным шагом для Vue является реализация двусторонней привязки данных.

Шаблон публикации-подписки (также известный как шаблон наблюдателя)

Давайте просто реализуем его вручную (то есть отношение массива)

// 发布订阅
function Dep() {    
   this.subs = [] }
// 订阅
Dep.prototype.addSub = function (sub) {    
   this.subs.push(sub) }
// 通知
Dep.prototype.notify = function (sub) {    
   this.subs.forEach(item => item.update()) }
   
// watcher是一个类,通过这个类创建的函数都会有update的方法
function Watcher(fn) {    
   this.fn = fn; } Watcher.prototype.update = function () {    
   this.fn() }

Здесь используется метод Dep для реализации подписки и уведомления.В этом классе есть два метода: addSub (добавить) и notify (уведомить).Мы добавляем дела, которые нужно сделать (функции) в массив через addSub, и уведомляем, когда время пришло, все методы в уведомлении выполняются

Вы узнаете, почему для создания функции нужно определить наблюдатель метода, а не бросать метод прямо в addSub, разве это не лишнее? На самом деле у этого есть своя цель, одно из преимуществ в том, что функции, которые мы создаем через watcher, будут иметь метод выполнения обновления, который мы можем легко вызвать. И еще одно использование, о котором я расскажу ниже, давайте сначала применим его.

function replace(fragment) {     
   Array.from(fragment.childNodes).forEach(function (node) {         let text = node.textContent;        
        let reg = /\{\{(.*)\}\}/g;  
             
        if (node.nodeType === 3 && reg.test(text)) {            
            let arr = RegExp.$1.split('.');            
            let val = vm;             arr.forEach(function (k) {                 val = val[k]             })            
            // 在这里运用了Watcher函数来新增要操作的事情             new Watcher(vm, RegExp.$1, function (newVal) {                 node.textContent = text.replace(/\{\{(.*)\}\}/, newVal)             })

            node.textContent = text.replace(/{{(.*)}}/, val)         }        
           
        if (node.childNodes) {             replace(node)         }     }) }

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

// vm做数据代理的地方
function MyVue(options = {}) {    
   this.$options = options;    
   var data = this._data = this.$options.data;    observe(data);    
   for (const key in data) {        
       Object.defineProperty(this, key, {            enumerable: true,            get() {                
               return this._data[key]            },            set(newVal) {                this._data[key] = newVal            }        })    } }
// 数据劫持函数
function Observe(data) {          let dep = new Dep();    
   for (let key in data) {        
       let val = data[key];        observe(val)        
       Object.defineProperty(data, key, {            enumerable: true,            get() {                
               /* 获取值的时候 Dep.target
                  对于着 watcher的实例,把他创建的实例加到订阅队列中
               */                Dep.target && dep.addSub(Dep.target);                return val            },            set(newval) {                if (val === newval) {                    
                   return                }                val = newval;                observe(newval);                
               // 设置值的时候让所有的watcher.update方法执行即可触发所有数据更新                dep.notify()            }        })    } }

function Watcher(vm, exp, fn) {    
   this.fn = fn;    
   // 这里我们新增了一些内容,用来可以获取对于的数据    this.vm = vm;    
   this.exp = exp;    Dep.target = this    let val = vm;    
   let arr = exp.split('.');    
   /* 执行这一步的时候操作的是vm.a,
   而这一步操作其实就是操作的vm._data.a的操作,
   会触发this代理的数据和_data上面的数据
   */    arr.forEach(function (k) {        val = val[k]    })    Dep.target = null; }
// 这里是设置值操作
Watcher.prototype.update = function () {    
   let val = this.vm;    
   let arr = this.exp.split('.');    arr.forEach(function (k) {        val = val[k]    })    
   this.fn(val) //这里面要传一个新值
}

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

1) Прежде всего, давайте посмотрим, что некоторые частные свойства были добавлены в конструктор Watcher для представления:

  • Dep.target = this(Текущий экземпляр наблюдателя временно хранится в конструкторе Dep.target)
  • this.vm = vm(vm = экземпляр myvue)
  • this.exp = exp(exp = соответствующий объект поиска "a.b" является значением типа string)
После того, как мы сохраним эти атрибуты, следующим шагом будет получение данных в строке, соответствующей exp.vm.a.b, но exp в это время представляет собой строку, вы не можете напрямую получить значение vm[a.b] Это неправильный синтаксис, поэтому вам нужно зациклиться, чтобы получить значение для
arr.forEach(function (k) {        
       // arr = [a,b]        val = val[k] })
  • Когда первый цикл vm[a] = {b:12}, объект a получен, а затем присваивание обратно состоит в том, чтобы изменить текущий val на объект a
  • Во втором цикле val стал объектом a, k стал b, а val стал: a[b] = 12
После двух обходов мы получаем значение объекта над данными vm proxy,То есть он вызовет метод получения данных над данными прокси.(vm.ab) и эта операция возвращает this._data[k], что вызовет вызов метода get для данных vm._data.ab, то есть перейти к get в функции Observe, в это время Dep.target хранит Это экземпляр текущего метода наблюдателя (в этом экземпляре уже есть информация о данных, которыми нужно манипулировать), и методу передается последнее полученное значение.
get() {    
   // 走到这里的时候 Dep.target 已经存储了 watcher的当前实例实例,把他创建的实例加到订阅队列中    Dep.target && dep.addSub(Dep.target);    return val },
   
// 把要做的更新视图层的操作方法用Watcher定义好,里面已经定义好了要操作的对象
new Watcher(vm, RegExp.$1, function (newVal) {    node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) })

Watcher.prototype.update = function () {    
   let val = this.vm;    
   let arr = this.exp.split('.');    arr.forEach(function (k) {        val = val[k]    })    this.fn(val) // 把对于的新值传递到方法里面
}

Здесь, поскольку есть дополнительный уровень прокси данных, такой как vm.a, логика немного сбивает с толку. Запомните это предложение, чтобы понятьПри работе с вышеуказанным значением данных прокси vm.a это фактически данные в управляемом vm._data.Таким образом, он вызовет методы get и set в двух местах.Так много всего нужно сказать, давайте посмотрим, вызовет ли изменение данных изменение уровня представления.

text2

Здесь изменение данных запускает операцию обновления слоя представления.

7. Реализация входной двусторонней привязки данных

Последний шаг - реализовать операцию изменения структуры данных, вызванную изменением слоя представления. Выше мы объяснили основной код представления и ассоциации данных. Остальная часть изменения представления запускает изменение данных, которое будет реализовано. .

<div id="app">    
   <div>        <div>这里的数据1======<span style="color: red;">{{a.b}}</span></div>        <div>这里是数据2======<span style="color: green;">{{c}}</span></div>    </div>    <input type="text" v-model="a.b" value="">
</div>

<!-- 引入自己定义的mvvm模块 -->
<script src="./mvvm.js"></script>
<script type="text/javascript">    var myvue = new MyVue({        el: '#app',        data: { a: { b: '你好' }, c: 12, o: [12, 5, 9, 8] }    })
</script>
// 获取所有元素节点
if (node.nodeType === 1) {    
   let nodeAttr = node.attributes    
   Array.from(nodeAttr).forEach(function (attr) {        
       let name = attr.name; // v-model="a.b"        let exp = attr.value; // a.b

       if (name.indexOf('v-') >= 0) {            
           let val = vm;            
           let arr = exp.split('.');            arr.forEach(function (n) {                val = val[n]            })            
           // 这个还好处理,取到对应的值设置给input.value就好            node.value = val;        }        
       // 这里也要定义一个Watcher,因为数据变更的时候也要变更带有v-model属性名的值        new Watcher(vm, exp, function (newVal) {            node.value = newVal        })        
       // 这里是视图变化的时候,变更数据结构上面的值        node.addEventListener('input', function (e) {            
           let newVal = e.target.value            
           if (name.indexOf('v-') >= 0) {                
               let val = vm;                
               let arr = exp.split('.');                arr.forEach(function (k,index) {                        if (typeof val[k] === 'object') {                        val = val[k]                    } else{                        
                   if (index === arr.length-1) {                            val[k] = newVal                        }                    }                })            }        })    }) }

Логика приведенного выше кода для изменения данных для запуска изменений слоя представления может быть такой же, как и в предыдущем разделе, в основном:node.addEventListener('input')Проблема настройки данных здесь на самом деле является той же, что и логика ассоциирования просмотра и модели в разделе 6. Следует отметить, что здесь добавляется решение типа справочного типа, в противном случае его цикл достигнет конца. Основное Значение типа данных (то есть, основной тип данных) 1) оценивается, что полученный тип данных не является типом данных объекта, и операция замены не выполняется (val = val [k]) 2) Определите, был ли последний уровень индексом === ARR.Length-1, если это так, напрямую назначить значение на входе в текущие данные

arr.forEach(function (k,index) {     
    if (typeof val[k] === 'object') {        
       // 如果有嵌套的话就继续查找        val = val[k]    } else{        
       if (index === arr.length-1) {            
       // 查找到最后一个后直接赋值            val[k] = newVal        }    } })

Вышеизложенный простой принцип реализации всей двусторонней привязки данных mvvm.Есть некоторые места, где содержание не объяснено хорошо или есть лучшие мнения.Добро пожаловать, чтобы оставить сообщение :)

Добро пожаловать в публичный аккаунт Tencent DeepOcean в WeChat, и мы будем еженедельно публиковать оригинальные высококачественные технические статьи, связанные с интерфейсом, искусственным интеллектом, SEO/ASO и другими областями:

Видя, как тяжело редактору двигаться, обратите внимание на один :)