MVVM
MVVM 由以下三个内容组成
-
View:界面
-
Model:数据模型
-
ViewModel:作为桥梁负责沟通 View 和 Model ;
在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。
这种方式就可以在业务处理中只关心数据的流转,而 无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理 数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要 改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复 用这个 ViewModel。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。
Vue:双向绑定
比如,要html中是这样写的:
<div>
{{name}}
</div>
下面就来说说大概是怎么实现的;
observer类:数据劫持
Vue 内部使用了 Obeject.defineProperty() 来实现双向绑定,通过这个函数可以 监听到 set 和 get 的事件。
如下,调用observe类可以获取data里面的信息;
var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value
实现observer类
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
//Object.defineProperty只能更新数据,它做不到深度监听对象。
//那么如何实现深度监听对象?
//或许你听说过对象的深拷贝,实现深拷贝的方法是递归,
//所以我们劫持和监听data,也同样可以用递归来实现。
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
//下面这个是函数改造后的,第一次看文章先不用管
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value');
//下面三行这个是函数改造后的,第一次看文章先不用管-> 将 Watcher 添加到订阅
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
//下面这个是函数改造后的,第一次看文章先不用管-> 执行 watcher 的 update 方法
dep.notify();
}
})
}
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够 的,还需要在适当的时候给属性添加发布订阅 。
可以理为,一个页面,肯定不仅仅要监听一个数据,而每一个要监听的数据,都是一个订阅者,那么这些订阅者需要一个订阅者容器,这里面把它定义为Dep类;
Dep可以用来干嘛呢?
在解析如上模板代码时(上面给的html例子),遇到 {{name}} 就会给属性 name 添加发布订阅。
class Dep {
constructor() {
//容器
this.subs = []
}
//添加定订阅者
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub)
}
//通知watcher更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
//这是更新的函数,向div里面加入value
function update(value) {
document.querySelector('div').innerText = value
}
watcher类:添加订阅者、通知视图更新
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
这时候:
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'
Obeject.defineProperty 虽然已经能够实现数据的支持的,但是他还是有缺陷的。
它只能对属性进行数据劫持,所以需要深度遍历整个对象,对于数组不能监听到数据的变化;
虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法,并且也是有缺陷的。
所以Vue3.0里面是采用了proxy去做的;
上面提到了hack去检测数组的变化,hack是什么呢?
通过vue源码可以看出,vue重写了数组的 push、pop、shift、unshift、splice、sort、reverse七种方法 ;
hack中创建了一个空对象,并把空对象的原型指向Array.prototype; 重写方法在实现时除了将数组方法名对应的原始方法调用一遍并将执行结果返回外,还通过执行ob.dep.notify()
将当前数组的变更通知给其订阅者,这样当使用重写后方法改变数组后,数组订阅者会将这边变化更新到页面中。
// 获取数组的原型Array.prototype,上面有我们常用的数组方法
const arrayProto = Array.prototype
// 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 列出需要重写的数组方法名
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
methodsToPatch.forEach(function (method) {
// 保存一份当前的方法名对应的数组原始方法
const original = arrayProto[method]
// 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
def(arrayMethods, method, function mutator (...args) {
// 调用数组原始方法,并传入参数args,并将执行结果赋给result
const result = original.apply(this, args)
// 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 将当前数组的变更通知给其订阅者
ob.dep.notify()
// 最后返回执行结果result
return result
})
})
Proxy 与 Obeject.defineProperty 对比
反观 Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象 进行拦截,所以 Vue 也将在下个大版本中使用 Proxy 替换 Obeject.defineProperty ;
Proxy的优势:
- 数据被更改时返回一个proxy对象,从而不会破坏原对象
- 多个属性同时监听(传入一个对象),而defineProperty做不到,需要用for循环逐一监听
- 数组变化也能监听到
- 不需要深度遍历监听
参考文章: https://blog.csdn.net/zhenghaohan1999/article/details/100975584