MVVM框架的实现
2019-04-23 本文已影响0人
MickeyMcneil
最近总被问到vue双向绑定的原理,所以整理实现一下
MVVM框架
- M:Model,模型层
- V:View,视图层
- VM:ViewModel,视图模型,VM连接的桥梁
M层修改时,VM层会监测到变化,并通知V层修改;V层修改则会通知M层数据进行修改。
![](https://img.haomeiwen.com/i13406581/217498f024b12b6c.png)
双向数据绑定的方式
发布-订阅者模式
通过pub、sub来实现数据和视图的绑定,使用麻烦。
脏值检查
通过定时器轮训检测数据是否发生变化。angular.js用此方法。
数据劫持
通过Object.defineProperty()
来劫持各个属性的setter
、getter
。vue.js采用数据劫持 + 发布-订阅者模式,在数据变动时发布消息给订阅者。
实现思路
- Compile:模板解析器,对模板中的指令和插值表达式进行解析,赋予不同的操作
- Observe:数据监听器,对数据对象的所有属性进行监听
- Watcher:将Compile的解析结果,与Observe的观察对象连接起来
Compile
对模板中的指令和插值表达式进行解析,并赋予不同的操作。
-
document.createDocumentFragment()
-
[].slice.call(likeArr)
将伪数组转换为数组的方法。具体参考这里 -
getVMValue
方法主要为了解决复杂数据类型带来的问题 -
代码
compile.js
// 负责解析模板内容
class Compile {
constructor(el, vm) {
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
// 编译模板
if (this.el) {
// 1.把子节点存入内存 -- fragment
let fragment = this.node2fragment(this.el)
// 2.在内存中编译fragment
this.compile(fragment)
// 3.把fragment一次性添加到页面
this.el.appendChild(fragment)
}
}
// 核心方法
node2fragment(node) { // 把el中的子节点添加到文档碎片中
let fragment = document.createDocumentFragment()
let childNodes = node.childNodes
this.toArray(childNodes).forEach(element => {
fragment.appendChild(element)
});
return fragment
}
compile(fragment) { // 编译文档碎片
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
// 元素节点 - 解析指令
if (this.isElementNode(node)) {
this.compileElement(node)
}
// 文本节点 - 解析插值表达式
if (this.isTextNode(node)) {
this.compileText(node)
}
// 若还有子节点,递归解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
compileElement(node) {
// 获取当前节点所有属性
let attr = node.attributes
this.toArray(attr).forEach(attr => {
// 解析vue指令
let attrName = attr.name
if (this.isDirective(attrName)) {
let type = attrName.slice(2)
let attrVal = attr.value
if (this.isEventDirective(type)) {
CompileUtil["eventHandler"](node, this.vm, type, attrVal)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, attrVal)
}
}
})
}
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
// 工具方法
toArray(likeArr) { // 把伪数组转换成数组
return [].slice.call(likeArr)
}
isElementNode(node) { // 判断元素节点
return node.nodeType === 1
}
isTextNode(node) { // 判断文本节点
return node.nodeType === 3
}
isDirective(attrName) { // 判断指令
return attrName.startsWith('v-')
}
isEventDirective(attrName) { // 判断事件
return attrName.split(":")[0] === "on"
}
}
let CompileUtil = {
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)){
let expr = RegExp.$1
node.textContent = txt.replace(reg, CompileUtil.getVMValue(vm, expr))
}
},
text(node, vm, attrVal) {
node.textContent = this.getVMValue(vm, attrVal)
},
html(node, vm, attrVal) {
node.innerHTML = this.getVMValue(vm, attrVal)
},
model(node, vm, attrVal) {
node.value = this.getVMValue(vm, attrVal)
},
eventHandler(node, vm, type, attrVal) {
let eventType = type.split(":")[1]
let fn = vm.$methods[attrVal]
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm))
}
},
// 获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
});
return data
}
}
vue.js
class Vue {
constructor(options = {}) {
// 给vue增加实例属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
if (this.$el) {
new Compile(this.$el, this)
}
}
}
index.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>Document</title>
</head>
<body>
<div id="app">
<p>复杂数据</p>
<p>{{car.color}}</p>
<div v-text="car.brand"></div>
<p>简单数据</p>
<p>大家好,{{text}}</p>
<p>{{msg}}</p>
<div v-text="msg" title="hhhh"></div>
<div v-html="msg" title="aaaa"></div>
<input type="text" v-model="msg">
<button v-on:click="clickFn">点击</button>
</div>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello world',
text: 'hello text',
car: {
color: 'red',
brand: 'polo'
}
},
methods: {
clickFn () {
console.log(this.$data.msg)
}
}
})
console.log(vm)
</script>
</body>
</html>
observe
数据劫持主要使用了object.defineProperty(obj, prop, descriptor)
,MDN链接
结合下面的小案例,来综合说明过程
<!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>Document</title>
</head>
<body>
<script>
let obj = {
name: 'fxd'
}
let temp = obj.name
Object.defineProperty(obj, 'name', {
configurable: true, // 属性可配置
enumerable: true, // 属性可遍历
get () {
// 每次获取属性时,会被此方法劫持到
console.log('获取属性')
return temp
},
set (newValue) {
console.log('改变属性')
temp = newValue
}
})
</script>
</body>
</html>
执行结果如下
![](https://img.haomeiwen.com/i13406581/048e3a9322dfabbb.png)
在vue的源码中,大概实现思路如下
- 新建
observe.js
/*
observe给data中的所有数据添加getter和setter
在获取或设置data数据时,方便实现逻辑
*/
class Observe {
constructor(data) {
this.data = data
this.walk(data)
}
// 核心方法
walk (data) { // 遍历数据,添加上getter和setter
if (!data || typeof data !== "object") {
return
}
Object.keys(data).forEach(key => {
// 给key设置getter和setter
this.defineReactive(data, key, data[key])
// 如果data是复杂类型,递归walk
this.walk(data[key])
})
}
// 数据劫持
defineReactive(obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
console.log('获取',value)
return value
},
set (newValue) {
if (value === newValue) {
return
}
console.log('设置', newValue)
value = newValue
// 如果value是对象
that.walk(newValue)
}
})
}
}
- 在
index.html
中引入observe.js
<script src="./src/observe.js"></script>
- 在
vue.js
中添加
// 监视data中的数据
new Observe(this.$data)
Watcher
-
update
是watcher.js
中对外暴露的更新页面的方法,在observe.js
的set
中调用。因为set
劫持的是数据改变,这样当数据改变时,就会调用update
实现页面更新 - 在
compile.js
的指令/插值表达式处理的部分,new
了Watcher
,用来将Watcher
中新值填入对应的指令/插值表达式中
上述方法的缺点:不同的指令/插值表达式各自
new
了不同Watcher
,这样在在observe.js
的set
中不确定要调用哪一个Watcher
的update
方法
解决方法:发布-订阅者模式
发布-订阅者模式
订阅者:只需要订阅
发布者:状态改变时,通知并自动更新给所有的订阅者
优点:解耦合
![](https://img.haomeiwen.com/i13406581/db3e2c2fb74510e1.png)
基本思路如下:
-
watch.js
中设置Dep
对象,用来管理、添加、通知订阅者;将Watcher
中的this
存储到Dep.target
中 -
observe.js
的数据劫持中new
了Dep
,判断并调用添加和通知订阅者的方法
最后,优化下复杂数据的更新,
model
指令中input
双向绑定,以及把data
和methods
中的数据挂载到vm
实例上即可
index.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>Document</title>
</head>
<body>
<div id="app">
<p>复杂数据</p>
<p>{{car.color}}</p>
<div v-text="car.brand"></div>
<p>简单数据</p>
<p>大家好,{{text}}</p>
<p>{{msg}}</p>
<div v-text="msg" title="hhhh"></div>
<div v-html="msg" title="aaaa"></div>
<input type="text" v-model="msg">
<button v-on:click="clickFn">点击</button>
</div>
<script src="./src/watcher.js"></script>
<script src="./src/observe.js"></script>
<script src="./src/compile.js"></script>
<script src="./src/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello world',
text: 'hello text',
car: {
color: 'red',
brand: 'polo'
}
},
methods: {
clickFn () {
// this.$data.msg = '2222'
this.msg = '2222222'
}
}
})
console.log(vm)
</script>
</body>
</html>
observe.js
/*
observe给data中的所有数据添加getter和setter
在获取或设置data数据时,方便实现逻辑
*/
class Observe {
constructor(data) {
this.data = data
this.walk(data)
}
// 核心方法
walk (data) { // 遍历数据,添加上getter和setter
if (!data || typeof data !== "object") {
return
}
Object.keys(data).forEach(key => {
// 给key设置getter和setter
this.defineReactive(data, key, data[key])
// 如果data是复杂类型,递归walk
this.walk(data[key])
})
}
// 数据劫持
// data中的每一个数据都维护一个dep对象,保存了所有订阅了该数据的订阅者
defineReactive(obj, key, value) {
let that = this
let dep = new Dep()
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get () {
// 如果Dep.target中有watcher,存储到订阅者数组中
Dep.target && dep.addSub(Dep.target)
return value
},
set (newValue) {
if (value === newValue) {
return
}
value = newValue
// 如果value是对象
that.walk(newValue)
// 发布通知,让所有的订阅者更新内容
dep.notify()
}
})
}
}
watch.js
/*
watcher负责将compile和observe关联起来
*/
class Watcher {
// 参数分别是:当前实例,data中的名字,数据改变时的回调函数
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 将this存储到Dep.target上
Dep.target = this
// 将expr的旧值存储
this.oldVal = this.getVMValue(vm, expr)
// 清空Dep.target
Dep.target = null
}
// 对外暴露更新页面的方法
update () {
let oldVal = this.oldVal
let newVal = this.getVMValue(this.vm, this.expr)
if (oldVal != newVal) {
this.cb(newVal, oldVal)
}
}
// 获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
});
return data
}
}
// dep对象 - 管理订阅者,通知订阅者
class Dep {
constructor () {
// 管理订阅者
this.subs = []
}
// 添加订阅者
addSub (watcher) {
this.subs.push(watcher)
}
// 通知订阅者
notify () {
// 遍历所有订阅者,调用watcher的update方法
this.subs.forEach(sub => {
sub.update()
})
}
}
compile.js
// 负责解析模板内容
class Compile {
constructor(el, vm) {
this.el = typeof el === 'string' ? document.querySelector(el) : el
this.vm = vm
// 编译模板
if (this.el) {
// 1.把子节点存入内存 -- fragment
let fragment = this.node2fragment(this.el)
// 2.在内存中编译fragment
this.compile(fragment)
// 3.把fragment一次性添加到页面
this.el.appendChild(fragment)
}
}
// 核心方法
node2fragment(node) { // 把el中的子节点添加到文档碎片中
let fragment = document.createDocumentFragment()
let childNodes = node.childNodes
this.toArray(childNodes).forEach(element => {
fragment.appendChild(element)
});
return fragment
}
compile(fragment) { // 编译文档碎片
let childNodes = fragment.childNodes
this.toArray(childNodes).forEach(node => {
// 元素节点 - 解析指令
if (this.isElementNode(node)) {
this.compileElement(node)
}
// 文本节点 - 解析插值表达式
if (this.isTextNode(node)) {
this.compileText(node)
}
// 若还有子节点,递归解析
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node)
}
})
}
compileElement(node) {
// 获取当前节点所有属性
let attr = node.attributes
this.toArray(attr).forEach(attr => {
// 解析vue指令
let attrName = attr.name
if (this.isDirective(attrName)) {
let type = attrName.slice(2)
let attrVal = attr.value
if (this.isEventDirective(type)) {
CompileUtil["eventHandler"](node, this.vm, type, attrVal)
} else {
CompileUtil[type] && CompileUtil[type](node, this.vm, attrVal)
}
}
})
}
compileText(node) {
CompileUtil.mustache(node, this.vm)
}
// 工具方法
toArray(likeArr) { // 把伪数组转换成数组
return [].slice.call(likeArr)
}
isElementNode(node) { // 判断元素节点
return node.nodeType === 1
}
isTextNode(node) { // 判断文本节点
return node.nodeType === 3
}
isDirective(attrName) { // 判断指令
return attrName.startsWith('v-')
}
isEventDirective(attrName) { // 判断事件
return attrName.split(":")[0] === "on"
}
}
let CompileUtil = {
mustache(node, vm) {
let txt = node.textContent
let reg = /\{\{(.+)\}\}/
if (reg.test(txt)){
let expr = RegExp.$1
node.textContent = txt.replace(reg, CompileUtil.getVMValue(vm, expr))
new Watcher(vm, expr, newVal => {
node.textContent = txt.replace(reg, newVal)
})
}
},
text(node, vm, attrVal) {
node.textContent = this.getVMValue(vm, attrVal)
// 通过Watcher监听attrVal,一旦变化,执行回调
new Watcher(vm, attrVal, newVal => {
node.textContent = newVal
})
},
html(node, vm, attrVal) {
node.innerHTML = this.getVMValue(vm, attrVal)
new Watcher(vm, attrVal, newVal => {
node.innerHTML = newVal
})
},
model(node, vm, attrVal) {
let that = this
node.value = this.getVMValue(vm, attrVal)
// 实现双向的数据绑定
node.addEventListener('input', function () {
that.setVMValue(vm, attrVal, this.value)
})
new Watcher(vm, attrVal, newVal => {
node.value = newVal
})
},
eventHandler(node, vm, type, attrVal) {
let eventType = type.split(":")[1]
let fn = vm.$methods[attrVal]
if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm))
}
},
// 获取VM中的数据
getVMValue(vm, expr) {
let data = vm.$data
expr.split(".").forEach(key => {
data = data[key]
});
return data
},
setVMValue(vm, expr, value) {
let data = vm.$data
let arr = expr.split(".")
arr.forEach((key, index) => {
if (index < arr.length - 1) {
data = data[key]
} else {
data[key] = value
}
})
}
}
vue.js
class Vue {
constructor(options = {}) {
// 给vue增加实例属性
this.$el = options.el
this.$data = options.data
this.$methods = options.methods
// 监视data中的数据
new Observe(this.$data)
// 将data和methods中的数据代理到vm上
this.proxy(this.$data)
this.proxy(this.$methods)
if (this.$el) {
new Compile(this.$el, this)
}
}
proxy (data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newVal) {
if (data[key] == newVal) {
return
}
data[key] = newVal
}
})
})
}
}