Во-первых, давайте сделаем снимок
Как видно из рисунка, МВВМ состоит из двух блоков.Observer
перехват слушателя реактивного иCompile
Разбор инструкции.
Давайте объединим эти два аспекта для реализации Vue.
Отзывчивый
Реализация наблюдателя
class Vue {
constructor(options) {
this.$options = options
this.$data = options.data
this.observe(this.$data)
// 运行created生命周期
this.$options.created && this.$options.created.call(this)
}
observe(data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach((key) => {
this.defineProperty(data, key, data[key])
this.poxyData(key)
})
}
defineProperty(obj, key, val) {
const dep = new Dep()
// 递归遍历
this.observe(val)
Object.defineProperty(obj, key, {
get() {
// 初始化时添加wather进行观察
Dep.target && dep.addWather(Dep.target)
return val
},
set(newVal) {
val = newVal
// 被赋值时,通知更新
dep.notify()
}
})
}
poxyData(key) {
this.$data[key] && Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
const pan = new Vue({
el: '#app',
data: {
name: "I am test.",
age: 12,
fag: {
bar: 'bar'
},
html: '<button @click="sex1">这是一个按钮</button>'
}
})
Здесь мы сначала используемObject.defineProperty()
Этот метод выполняет захват данных, которые мы передаем в конфигурацию Vue.defineProperty(obj, key, val)
метод, который мы вызываем сноваthis.observe(val)
, Вот поскольку данные могут быть более чем на один слой, нам нужно перехватить все данные ниже данных.poxyData(key)
Этот метод заключается в проксировании данных в data наthis
вверх, мы можемpan.html
доступ кpan.$data.html
данные в.
Реализация Dep && Watcher
class Dep {
constructor() {
this.wathers = []
}
addWather(wather) {
this.wathers.push(wather)
}
notify() {
this.wathers.forEach(wather => wather.update())
}
}
class Wather {
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
// 把新生成的wather附加到Dep.target
Dep.target = this
// 访问一次被代理的属性
this.vm[this.key]
// Dep.target 置为空,等待下一个wather的生成
Dep.target = null
}
update() {
this.cb && this.cb.call(this.vm, this.vm[this.key])
}
}
Dep
Это довольно просто, всего два метода одинaddWather
а такжеnotify
два метода,Wather
Просто метод обновления. Это то, что мы часто называем паттерном наблюдателя.Dep
сохранить несколькоwather
,когдаDep
ОбнаружитьWather
Когда будет обновление,Dep
позвонюnotify
способ уведомить всехwather
методupdate
обновить.
Компиляция шаблона
class Compile {
constructor(el, vm) {
this.$el = document.querySelector(el)
this.$vm = vm
// 模板移动到文档片段
this.$fragment = this.node2Fragment(this.$el)
// 编译
this.compile(this.$fragment)
// 把编译好的文档片段添加到el
this.$el.appendChild(this.$fragment)
}
node2Fragment(el) {
const fragment = document.createDocumentFragment()
let firstNode
while (firstNode = el.firstChild) {
fragment.appendChild(firstNode)
}
return fragment
}
compile(el) {
const nodes = el.childNodes
Array.from(nodes, (node) => {
// 标签
if (node.nodeType === 1) {
this.compileElement(node)
}
// 文本
else if (this.isInter(node)) {
this.compileText(node)
}
// 编译子节点
if (node.children && node.childNodes.length > 0) {
this.compile(node)
}
})
}
isInter(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileElement(node) {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs, (nodeAttr) => {
// 获取到标签内的属性名以及属性值
const attrName = nodeAttr.name
const attrValue = nodeAttr.value
// 匹配p-开头的属性名
if (attrName.includes('p-')) {
const dir = attrName.substring(2)
this[dir] && this[dir](node, attrValue)
}
// 匹配@开头的属性名
if (attrName.includes('@')) {
const dir = attrName.substring(1)
this[dir] && this[dir](node, attrValue)
}
})
}
compileText(node) {
// 拿取到文本标签里的{{xxx}}
// console.log(RegExp.$1); // {{name}}花括号内匹配的值
this.update(node, RegExp.$1, 'text')
}
update(node, key, dir) {
const updator = this[dir + 'Updator'].bind(this)
updator && updator(node, this.$vm[key])
new Wather(this.$vm, key, (value) => {
updator && updator(node, value)
})
}
eventListener(node, key, type) {
const options = this.$vm.$options
// 处理this指向问题
const eventFn = options.methods[key].bind(this.$vm)
eventFn && this.addEventListener(node, type, eventFn)
}
textUpdator(node, value) {
node.textContent = value
}
htmlUpdator(node, value) {
node.innerHTML = value
this.compile(node)
}
modelUpdator(node, value) {
node.value = value
}
text(node, key) {
this.update(node, key, 'text')
}
html(node, key) {
this.update(node, key, 'html')
}
model(node, key) {
this.update(node, key, 'model')
// 通过input 事件双向绑定表单数据
node.addEventListener('input', () => {
this.$vm[key] = node.value
})
}
click(node, key) {
this.eventListener(node, key, 'click')
}
dblclick(node, key) {
this.eventListener(node, key, 'dblclick')
}
addEventListener(node, key, fn) {
node.addEventListener(key, fn)
}
}
Идея компиляции нашего шаблона здесь в основном состоит из 3 шагов:
- Переместите el, переданный в конфигурацию параметров Vue, во фрагмент документа.
- Составление фрагментов документа
- Скомпилированная документация добавляется в html
Вот важный момент, зачем добавлять в документ фрагмент,Фрагмент документа используется в качестве временного заполнителя для размещения элементов, а затем добавляется в dom с помощью appendChild(), что сводит к минимуму обновления полей (одно обновление) и устраняет узкое место производительности непрерывных операций dom.
Давайте сосредоточимся на том, как скомпилировать фрагменты документа, сначала пройтись по всему нашему dom, а затем разделить наЭтикеткаа такжетекстдве штуки для анализа
Этикетка
-
Сначала получите атрибуты в теге, мы используем
node.attributes
Получить все свойства текущего узла -
Получить все имена атрибутов и значения атрибутов
-
Индивидуальное рассмотрение
p-
а также@
(Эта статья касается только этих двух случаев)p-textИзменять
node.textContent
значения и добавить воды для отслеживания последующих измененийp-htmlИзменять
node.innerHTML
значения и добавить воды для отслеживания последующих измененийp-modelИзменять
node.value
значение и добавить воды, чтобы отслеживать последующие изменения, и добавить к узлуinput
событие, реализующее двустороннюю привязку формы@clickпройти через
node.addEventListener('click', fn)
добавить событие клика@dblclickпройти через
node.addEventListener('dblclick', fn)
событие dblclick
текст
- Изменять
node.textContent
значение
Наконец, добавьте фрагмент скомпилированного документа в dom, чтобы завершить весь процесс. Подробный код см.github.
Добро пожаловать, чтобы оставить сообщение для руководства, спасибо.