предисловие
Это запись моего обучения, потому что я готовлюсь к интервью в последнее время, и меня во многих случаях будут спрашивать: пожалуйста, кратко опишитеmvvm
?
Обычно я бы ответил так:mvvm
это разделение зрения и логики, этоmodel view view-model
Аббревиатура двусторонней привязки данных через виртуальный дом (могу ответить вскользь)
Итак, вот вопрос, вы знаетеmvvm
Как это достигается?
отвечать:mvvm
главным образом черезObject
изdefineProperty
свойство, переопределениеdata
изset
а такжеget
функцию для реализации. хорошо, ответ 60 баллов, так что вы знаете конкретный процесс реализации? Подумайте об этом, не было бы лучше, если бы он не спрашивал, а вы ответили? На предпосылке, не забудьте передать простойmvvm
будет впечатлен~
Нечего сказать, нижеследующее основано на обучающем видео г-на Чжана Рэньяна, использующегоES6Грамматика, которая также содержит мое личное понимание, я буду очень рад, если она вам поможет. Если есть ошибки, поправьте меня, буду очень признательна~~~
Перед внедрением, пожалуйста, ознакомьтесь с основнымиmvvm
Процесс компиляции и использование
-
Скомпилированная блок-схема
-
Общий анализ
Его можно найтиnew MVVM()
Основная часть процесса пост-компиляции делится на две части:
- Частью этого является составление шаблонов
Compile
- Скомпилируйте элементы и текст и, наконец, отобразите на странице
- Только теги с директивами шаблона в тегах выполняют компиляцию.Например
<div>我很帅</div>
не компилировать
- Частично это захват данных
Observer
-
Dep
Публикуйте и подписывайтесь, всем нужно будет получать уведомления об измененияхdata
добавить в массив -
Watcher
Если данные изменяются, вObject
изdefineProperty
изset
вызов функцииWatcher
изupdate
метод
-
Уточните, для чего нужна бумага
-
Процесс реализации компиляции шаблона завершен
Vue
Свойства в экземпляре могут быть правильно привязаны к тегу и отображены на странице.- Работа: разбор инструкции, регулярная замена
{{}}
- содержимое узла
node.textContent
илиinput
изvalue
составлено
- Работа: разбор инструкции, регулярная замена
-
Полная двусторонняя привязка данных
- работа: пройти
observe
изменения данных захвата класса - Добавьте публикацию и подписку:
Object.defineProperty
существуетget
крюкaddSub
,set
Уведомлять об изменениях в хукахdep.notify()
-
dep.notify()
называетсяWatcher
изupdate
метод, так сказатьinput
вызов обновления при изменении
- работа: пройти
Давайте сначала уточним наши цели: просмотр рендеринга и двусторонняя привязка данных и уведомления об изменениях!шаг: Начните с пошагового анализа того, как использовать Vue, от начального класса Vue до цели компиляции [реализовать рендеринг представления], до этого следует наблюдать за захватом данных, а затем вызывать обновление представления, класс наблюдателя прослушивает изменения и, наконец, уведомляет обо всех обновлениях представления и т. д.
Разложение экземпляра Vue
С чего начать? Как использовать в первую очередьVue
Начинать. Давайте разберем его шаг за шагомVue
использование:
let vm = new Vue({
el: '#app'
data: {
message: 'hello world'
}
})
В приведенном выше коде можно увидеть использованиеVue
, мы первыеnew
ОдинVue
Экземпляр, передайте параметр объекта, включаяel
а такжеdata
.
хорошо, вышеизложенная информация получена, давайте реализуем ее дальшеЦель 1:БудуVue
примерdata
скомпилировано на странице
Процесс реализации Compile для компиляции шаблонов
Сначала посмотрите на использование страницы:index.html
<div id="app">
<input type="text" v-model="jsonText.text">
<div>{{message}}</div>
{{jsonText.text}}
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
message: 'gershonv',
jsonText:{
text: 'hello Vue'
}
}
})
</script>
Первым шагом, конечно же, является добавление
Vue
Класс действует как входной файл.
Класс Vue — добавление файла записи
создать новыйvue.js
файл со следующим кодом
определяется в конструкторе$el
а также$data
, потому что последующая компиляция будет использовать
class Vue {
constructor(options) {
this.$el = options.el; // 挂载
this.$data = options.data;
// 如果有要编译的模板就开始编译
if (this.$el) {
// 用数据和元素进行编译
new Compile(this.$el, this)
}
}
}
- Перехват данных еще не добавлен
obeserve
, для достижения цели 1 пока не используется и будет добавлено позже - Потребности в компиляции
el
и сопутствующие данные, приведенный выше код будет скомпилирован после выполнения, поэтому мы создаем новый файл для скомпилированного класса
здесь, в файле ввода
vue.js
серединаnew
взял одинCompile
Экземпляр, поэтому создайте новый следующийcompile.js
Compile class — добавление шаблонной компиляции
Compile
Что я должен делать?
Мы знаем операцию на страницеdom
будет потреблять производительность, поэтому вы можете поставитьdom
Перейдите к обработке памяти:
- поставь настоящий
dom
Перемещаться в память (действовать в памятиdom
Быстрее)- Как вставить в память? Может воспользоваться фрагментацией документа
fragment
- Как вставить в память? Может воспользоваться фрагментацией документа
- компилировать
compile(fragment){}
- Извлеките нужные узлы элементов и текстовые узлы
v-model
{{}}
, а затем выполните соответствующие операции.
- Извлеките нужные узлы элементов и текстовые узлы
- составлено
fragment
вернуться на страницу
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {// 如果这个元素能获取到 我们才开始编译
// 1.先把这些真实的DOM移入到内存中 fragment[文档碎片]
let fragment = this.node2fragment(this.el)
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment)
// 3.编译好的fragment在塞回页面里去
this.el.appendChild(fragment)
}
}
/* 专门写一些辅助的方法 */
isElementNode(node) { // 判断是否为元素及节点,用于递归遍历节点条件
return node.nodeType === 1;
}
/* 核心方法 */
node2fragment(el) { // 将el的内容全部放入内存中
// 文档碎片
let fragment = document.createDocumentFragment();
while (el.firstChild) { // 移动DOM到文档碎片中
fragment.appendChild(firstChild)
}
return fragment;
}
compile(fragment) {
}
}
Дополнение: будетel
Переместить содержимое документа во фрагмент документаfragment
In — это процесс входа и выхода из стека. дети Эль переезжают вfragment
После [хлопка],el
Следующий дочерний элемент станетfirstChild
.
Процесс компиляции заключается в отображении наших данных и отображении их в представлении.
Компиляция процесса компиляции (фрагмент)
- Шаг 1: Получить узел элемента, извлечь в нем инструкцию или шаблон
{{}}
- Во-первых, вам нужно пройти по узлам, используярекурсивный метод, потому что существует отношение вложенности узлов,
isElementNode
Представитель является элементом узла, а также условием завершения рекурсии.
- Во-первых, вам нужно пройти по узлам, используярекурсивный метод, потому что существует отношение вложенности узлов,
- Шаг 2: Способ классификации инструкций компиляции
compileElement
И составить текст{{}}
Методы-
compileElement
правильноv-model
,v-text
разбор команд -
compileText
скомпилировать текстовый узел{{}}
-
class Compile{
// ...
compile(fragment) {
// 遍历节点 可能节点套着又一层节点 所以需要递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 是元素节点 继续递归
// 这里需要编译元素
this.compileElement(node);
this.compile(node)
} else {
// 文本节点
// 这里需要编译文本
this.compileText(node)
}
})
}
}
compileElement && compileText
- Получить атрибуты элемента
node.attributes
Сначала определите, включена ли команда - Определить тип инструкции (
v-html v-text v-model...
) вызвать другой метод обновления данных- Скомпилированный объект инструмента извлекается здесь
CompileUtil
- Метод вызова:
CompileUtil[type](node, this.vm, expr)
CompileUtil.类型(节点,实例,v-XX 绑定的属性值)
- Скомпилированный объект инструмента извлекается здесь
class Compile{
// ...
// 判断是否是指令 ==> compileElement 中递归标签属性中使用
isDirective(name) {
return name.includes('v-')
}
compileElement(node) {
// v-model 编译
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
// 判断属性名是否包含 v-
if (this.isDirective(attrName)) {
// 取到对应的值,放到节点中
let expr = attr.value;
// v-model v-html v-text...
let [, type] = attrName.split('-')
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// 编译 {{}}
let expr = node.textContent; //取文本中的内容
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr)
}
}
// compile(fragment){...}
}
CompileUtil = {
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.'); // 处理 jsonText.text 的情况
return expr.reduce((prev, next) => {
return prev[next] // 譬如 vm.$data.jsonText.text、vm.$data.message
}, vm.$data)
},
getTextVal(vm, expr) { // 获取文本编译后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1])
})
},
text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
На данный момент привязка данных завершена, т.е.new Vue
в случаеdata
Он уже может корректно отображаться на странице, теперь решениеКак реализовать двустороннюю привязку
в сочетании с открытиемvue
График процесса компиляции показывает, что у нас на один меньшеobserve
захват данных,Dep
Сообщить об изменениях, добавитьWatcher
Прислушивайтесь к изменениям и, в конечном итоге, переписывайтеdata
Атрибуты
Реализовать двустороннюю привязку
Класс Observer — добавление наблюдателей
- существует
vue.js
захват данных
class Vue{
//...
if(this.$el){
new Observer(this.$data); // 数据劫持
new Compile(this.$el, this); // 用数据和元素进行编译
}
}
- новый
observer.js
документ
Шаги кода:
- Добавить прямо в конструктор
observe
- судить
data
Существует ли он, является ли он объектом (не может быть записан при использовании нового Vue)data
Атрибуты) - Захватите данные один за другим, получите
data
серединаkey
а такжеvalue
- судить
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 要对这个数据将原有的属性改成 set 和 get 的形式
if (!data || typeof data !== 'object') {
return
}
// 将数据一一劫持
Object.keys(data).forEach(key => {
// 劫持
this.defineReactive(data, key, data[key])
this.observe(data[key]) //递归深度劫持
})
}
defineReactive(obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 取值时调用的方法
return value
},
set(newValue) { // 当给data属性中设置的时候,更改属性的值
if (newValue !== value) {
// 这里的this不是实例
that.observe(newValue) // 如果是对象继续劫持
value = newValue
}
}
})
}
}
Хотя есть
observer
, но не связаны, и уведомления меняются. добавить нижеWatcher
Добрый
Добавление класса Watcher
новыйwatcher.js
документ
- Назначение наблюдателя — добавить наблюдателя к элементу, который необходимо изменить, и выполнить соответствующий метод при изменении данных.
запомни сначалаwatch
Применение:this.$watch(vm, 'a', function(){...})
Параметры, которые нам нужно передать при добавлении подписчика публикации:экземпляр vm, свойства связаны v-XX, функция обратного вызова cb(getVal
метод скопирован ранееCompileUtil
Метод, на самом деле, можно извлечь...)
class Watcher {
// 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
// this.$watch(vm, 'a', function(){...})
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取下老的值
this.value = this.get();
}
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.');
return expr.reduce((prev, next) => { //vm.$data.a
return prev[next]
}, vm.$data)
}
get() {
let value = this.getVal(this.vm, this.expr);
return value
}
// 对外暴露的方法
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value
if(newValue !== oldValue){
this.cb(newValue); // 对应 watch 的callback
}
}
}
Watcher
Определено, но еще не вызвано. Когда шаблон скомпилирован, наблюдайте за ним, когда вам нужно настроить наблюдение.Compile
class Compile{
//...
}
CompileUtil = {
//...
text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], () => {
// 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
},
//...
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
// 这里应该加一个监控,数据变化了,应该调用watch 的callback
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb 将newValue传递过来()
updateFn && updateFn(node, this.getVal(vm, expr))
});
node.addEventListener('input', e => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},
//...
}
После осуществления мониторинга обнаруживается, что уведомление об изменении было отправлено не всем шаблонам, привязанным к инструкции или{{}}
, поэтому нам нужноDep
Класс для мониторинга, свойства публикации-подписки экземпляра, которые мы можем добавить вobserver.js
середина
Добавление класса Dep
Обратите внимание, что он не будет вызываться при первой компиляции.Watcher
,dep.target
не существует,new Watcher
когдаtarget
стоит только
Это немного сбивает с толку, посмотрите на следующий код:
class Watcher {
constructor(vm, expr, cb) {
//...
this.value = this.get()
}
get(){
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value
}
//...
}
// compile.js
CompileUtil = {
model(node, vm, expr) { // 输入框处理
//...
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb 将newValue传递过来()
updateFn && updateFn(node, this.getVal(vm, expr))
});
}
}
class Observer{
//...
defineReactive(obj, key, value){
let that = this;
let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组存放所有更新的操作
Object.defineProperty(obj, key, {
//...
get(){
Dep.target && dep.addSub(Dep.target)
//...
}
set(newValue){
if (newValue !== value) {
// 这里的this不是实例
that.observe(newValue) // 如果是对象继续劫持
value = newValue;
dep.notify(); //通知所有人更新了
}
}
})
}
}
class Dep {
constructor() {
// 订阅的数组
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
Приведенный выше код выполненопубликовать подписчикаВыкройка, простая реализация. . То есть цель 2 двустороннего связывания достигнута.
Эпилог
У меня нет намерения выступать напоказ, это всего лишь статья из моих учебных записей. Если вы хотите поделиться им, вы можете добиться прогресса.
Буду очень рад, если эта статья вам поможет. Есть вопросы, чтобы задатьissue
, я надеюсь, что есть ошибки, и я надеюсь, что каждый может выдвинуть это, большое спасибо.
Я поместил конкретный исходный код на свой github, и я могу забрать его, если мне это нужно.Исходная ссылка