VUE MVVM实现
详细代码请参考: https://github.com/osorso/VUE_MVVM
理解MVVM
mvvm - Model View ViewModel 数据 视图 视图模型
其中Model ---> data, View ---> template, ViewModel ---> new Vue({...})
view通过绑定事件的方式将model联系在一起, model可以通过数据绑定的形式影响view, 而这两种联系则是通过viewModel实现的
实现原理
创建html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue响应式实现</title>
</head>
<body>
<div id="app">
<h1>{{ person.name }} ----- {{ person.age }}</h1>
<h2>{{ person.fav }}</h2>
<ul>
<li>123</li>
<li>123</li>
<li>123</li>
</ul>
<h3>{{ msg }}</h3>
<div v-text="msg"></div>
<div v-html="htmlStr"></div>
<input type="text" v-model="msg" />
<button @click="handleClick">点击切换名称</button>
</div>
</body>
<script src="Observer.js"></script>
<script src="Mvue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: '这是一条提示信息',
htmlStr: '<h5>这是一个h5类型的数据</h5>',
person: {
name: '小明',
age: 16,
fav: '玩游戏'
}
},
methods: {
handleClick() {
const arr = ['小米', '小明', '校长', '学生', '小刘', '红色', '黄色', '白色', '黑色', '紫色', '蓝色', '绿色', '小花', '王二', '张三', '李四', '韩梅梅', '李雷']
const index = Math.ceil(Math.random() * (arr.length - 1))
this.person.name = arr[index]
}
}
})
</script>
</html>
创建一个VUE实例类
class Vue{
constructor(options) {
this.$el = options.el
this.$data = options.data
this.$options = options
if (this.$el) {
new Observer(this.$data)
new Compile(this.$el, this)
}
}
}
通过创建vue实例,将data, el存在时, 创建数据观察者Observe以及指令解析器Compile
实现compile(指令解析器)
class Compile{
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
const fragment = this.nodeFragment(this.el)
this.compile(fragment)
this.el.appendChild(fragment) // 追加子元素到根元素
}
}
fragment此方法为获取文档碎片对象,将其放入内存中,减少回流重绘
nodeFragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment()
let firstChild;
while (firstChild = el.firstChild) {
f.appendChild(firstChild)
}
return f
}
compile(编译模版)
compile(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
} else {
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
})
}
compileElement(node) {
const attributes = node.attributes
;[...attributes].forEach(attr => {
const { name, value } = attr
if (this.isDirective(name)) { // 获取到指令v-html v-text等
const [,dirctive] = name.split('-') // text, html, model on:click等
const [dirName, eventName] = dirctive.split(':') // text html model on
// 更新数据, 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName)
// 删除有指令的标签上的属性
node.removeAttribute('v-' + dirctive)
} else if(this.isEventName(name)) {
let [,eventName] = name.split('@')
compileUtil['on'](node, value, this.vm, eventName)
}
})
}
compileText(node) {
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm)
}
}
isEventName(attrName) {
return attrName.startsWith('@')
}
nodeFragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment()
let firstChild;
while (firstChild = el.firstChild) {
f.appendChild(firstChild)
}
return f
}
isDirective(attrName) {
return attrName.startsWith('v-')
}
isElementNode(node) {
return node.nodeType === 1
}
compile中解析模板的方法
const compileUtil = {
getVal(expr, vm) { // 获取div v-text="person.name"></div> 中的person.name这个属性--使得可以在$data顺利取得此值
return expr.split('.').reduce((data, currentVal) => {
currentVal = currentVal.trim()
return data[currentVal];
}, vm.$data)
},
setVal(expr, vm, inputVal) {
return expr.split('.').reduce((data, currentVal) => {
currentVal = currentVal.trim()
data[currentVal] = inputVal
}, vm.$data)
},
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1], vm)
})
},
text(node, expr, vm) { // expr---msg
let value;
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 绑定观察者,数据发生变化时触发 进行更新
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getVal(args[1], vm)
})
} else {
value = this.getVal(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal)
})
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
const value = this.getVal(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
// 实现双向数据绑定
model(node, expr, vm) {
const value = this.getVal(expr, vm)
// 绑定更新函数 数据=> 视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal)
})
// 视图 => 数据 => 视图
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm), false)
},
bind(node, expr, vm, attr) {},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
}
}
实现Observe(数据劫持)
class Observer{
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, value) {
// 递归遍历
this.observer(value)
const dep = new Dep()
// 劫持并监听所有属性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 订阅数据变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target)
return value
},
set: newVal => {
this.observer(value) // 如果设置新值时需重新劫持新值,确保不会因为改变值而导致未能劫持数据
if (newVal !== value) {
value = newVal
}
// 通知变化
dep.notify()
}
})
}
}
observe通过对数据对象的递归遍历, 结合Object.defineProperty()从而实现劫持各个属性的setter,getter, 从而实现当属性对应的数据变化时,发布消息给订阅者, 通知其变化!
订阅者实现
class Dep{
constructor() {
this.sub = []
}
// 收集观察者
addSub(watcher) {
this.sub.push(watcher)
}
// 通知观察者更新
notify() {
this.sub.forEach(w => w.update())
}
}
订阅者的功能主要是收集变化并通知观察者更新,架起了Observe与Watcher之间的桥梁
Watcher(监听器实现)
class Watcher{
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this // 将watcher挂在到dep上
const oldVal = compileUtil.getVal(this.expr, this.vm)
Dep.target = null // 获取到值后清除所挂载的watcher
return oldVal
}
update() {
const newVal = compileUtil.getVal(this.expr, this.vm)
if (newVal !== this.oldVal) {
this.cb(newVal)
}
}
}
通过Watcher,则可实现Observe与Compile之间的联系,从而实现数据变化驱动视图
实现原理: 往订阅者中添加Watcher,与Observe建立联系, 而Watcher自身有个update()方法,此方法与Compile建立联系,当属性变化时, Observe则会通知Watcher,从而Watcher调用update()方法, 触发Compile中的回调,实现更新