可能很多人面试会被问到MVVM是什么?
MVVM 由以下三个内容组成:
- view:页面(视图)
- model:数据模型(模型)
- viewModel:作为桥梁负责沟通view和model(视图模型)
第一阶段(原生阶段),直接用JavaScript操作DOM节点,使用浏览器提供的原生API:
var dom = document.getElementById('name');
dom.innerHTML = 'an';
dom.style.color = 'red';
第二阶段,由于原生API不好用,还要考虑浏览器兼容性,jQuery横空出世,以简洁的API迅速俘获了前端开发者的芳心:
$('#name').text('an').css('color', 'red');
第三阶段,MVC模式,需要服务器端配合,JavaScript可以在前端修改服务器渲染后的数据。
现在,随着前端页面越来越复杂,用户对于交互性要求也越来越高,仅仅用jQuery是远远不够的,所以MVVM模型就应运而生。
第四阶段,MVVM是微软提出,它借鉴了桌面应用程序的MVC思想,在前端页面中,把model用纯javascript对象表示,View负责显示,两者做到了最大限度的分离。把Model和View关联起来的就是ViewModel,ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。
通俗来讲:
在 MVVM 中,UI 是通过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 如果改变,也会改变对应的数据。
这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。
ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。
MVVM框架和jQuery操作DOM相比有什么区别?
jQuery实现修改两个DOM节点的例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
</script>
<script>
$(document).ready(function(){
$("#name").click(function(){
var name = 'an-an-s';
var age = 22;
$('#name').text(name);
$('#age').text(age);
});
});
</script>
</head>
<body>
<p>Hello, <span id="name">an</span>!</p>
<p>You are <span id="age">12</span>.</p>
</body>
</html>
MVVM框架(vue)来实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script></script>
</head>
<body>
<div id="app">
<p @click="test">Hello, <span id="name">{{userInfo.name}}</span>!</p>
<p>You are <span id="age">{{userInfo.age}}</span>.</p>
</div>
</body>
</html>
<script>
var app = new Vue({
el: '#app',
data: {
userInfo: {
name: "an",
age: 12
}
},
methods: {
test() {
this.userInfo.name = "an-an-s"
this.userInfo.age = 51
}
}
})
</script>
看代码并运行起来跑了看一下,我们可以得出:
jQuery是直接操作DOM
MVVM(vue)它是操作javascript对象(data里面的userInfo),这就是虚拟DOM(里面做了什么操作在深入vue原理计划讲)
Angluar脏数据检测
当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化。循环至少为二次 ,至多为十次。
脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,都可以完成任务,但是这在 Vue 中的双向绑定是存在问题的。并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,大大减少了操作 DOM 的次数。所以低效也是相对的,这就仁者见仁智者见智了。
Vue数据劫持
Vue 内部使用了 Object.defineProperty() 来实现双向绑定(vue3是使用Proxy),通过这个函数可以监听到 set 和 get 的事件。
Object.difineProperty
Object.defineProperty(obj, prop, descriptor)
obj:要定义属性的对象。
prop:要定义或修改的属性的名称或 Symbol 。
descriptor:要定义或修改的属性描述符。
入门例子:
let obj = {
name: 'an',
age: 18
}
console.log(obj)
Object.defineProperty(obj, "name", {
get: function() {
console.log("get---", name)
return name
},
set: function(newName) {
console.log("set---",newName, 'age---', this.age)
name = newName
}
})
obj.name = "an-an-s"
obj.age = 22
console.log(obj)
console.log(obj.name)
// { name: 'an', age: 18 }
// set--- an-an-s age--- 18
// { name: [Getter/Setter], age: 22 }
// get--- an-an-s
// an-an-s
加深一点的例子:
var data = { name: 'an' }
observe(data)
let name = data.name // -> get value
data.name = 'an-an-a' // -> change value
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
}
})
}
以上代码简单的实现了如何监听数据的 set 和 get 的事件,但是仅仅如此是不够的,还需要在适当的时候给属性添加发布订阅。
<div>{{name}}</div>
在解析如上模板代码时,遇到 {{name}} 就会给属性 name 添加发布订阅。
// 通过 Dep 解耦
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null
function update(value) {
document.querySelector('div').innerText = value
}
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)
}
}
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}
var data = { name: 'an' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'an'
以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。
Proxy
let p = new Proxy(target, handler);
target :需要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个对象,其属性是当执行一个操作时定义代理的行为的函数(可以理解为某种触发器)。具体的
入门例子:
let test = {
name: "an",
age: 18
};
console.log(test);
test = new Proxy(test, {
get: function(target, key) {
console.log('get---', target, key);
return target[key];
},
set: function(target, key, value) {
console.log('set---', target, key, value);
return Reflect.set(target, key, value);
}
});
test.name = "an-an-s";
test.age = 18;
console.log(test);
console.log(test.name);
// { name: 'an', age: 18 }
// set--- { name: 'an', age: 18 } name an-an-s
// set--- { name: 'an-an-s', age: 18 } age 18
// { name: 'an-an-s', age: 18 }
// get--- { name: 'an-an-s', age: 18 } name
// an-an-s
Proxy 与 Object.defineProperty 对比
Object.defineProperty是实现了双向绑定了,但是他还是有缺陷的:
- 只能对属性进行数据劫持,所以需要深度遍历整个对象
- 对于数组不能监听到数据的变化
虽然 Vue 中确实能检测到数组数据的变化,但是其实是使用了 hack 的办法(覆盖原型方法),并且也是有缺陷的。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 以下几个函数
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function(method) {
// 获得原生函数
const original = arrayProto[method]
def(arrayMethods, method, function mutator(...args) {
// 调用原生函数
const result = original.apply(this, args)
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()
return result
})
})
反观 Proxy 就没以上的问题,原生支持监听数组变化,并且可以直接对整个对象进行拦截,所以 Vue 也将在下个大版本中使用 Proxy 替换 Object.defineProperty
let test = [
{
name: "an",
age: 18
}
]
console.log(test);
test = new Proxy(test, {
get: function(target, key) {
console.log('get---', target, key);
return target[key];
},
set: function(target, key, value) {
console.log('set---', target, key, value);
return Reflect.set(target, key, value);
}
});
test[0].name = "an-an-s";
test[0].age = 18;
console.log(test);
console.log(test[0].name);
// [ { name: 'an', age: 18 } ]
// get--- [ { name: 'an', age: 18 } ] 0
// get--- [ { name: 'an-an-s', age: 18 } ] 0
// [ { name: 'an-an-s', age: 18 } ]
// get--- [ { name: 'an-an-s', age: 18 } ] 0
// an-an-s
参考:
https://yuchengkai.cn/docs/frontend/framework.html#mvvm
https://www.liaoxuefeng.com/wiki/1022910821149312/1108898947791072