MVVM 原理
常见的面试问题:
- Vue 数据绑定的原理?
- MVVM 数据绑定的原理?
- Vue 双向数据绑定的原理?
- Vue 数据响应式原理?
- 数据响应式原理?
当前比较流行的前端框架都是采用的 MVVM 的方式:
什么是 MVVM?
简单一句话:数据驱动视图。
介绍
感受 MVVM
- 传统的 DOM 操作方式
- 模板引擎方式
- 数据驱动视图方式(MVVM)
什么是 MVVM
简单一句话:数据驱动视图
<!-- 视图 -->
<template>
<div>{{ message }}</div>
</template>
<!-- ViewModel -->
把普通的 JavaScript 对象和视图 DOM 之间建立了一种映射关系:
- 数据的改变影响视图
- 视图(表单元素)的改变影响数据
<script>
// Model 普通数据对象
export default {
data () {
return {
message: 'Hello World'
}
}
}
</script>
<style>
</style>
- Model(M):普通的 JavaScript 对象,例如 Vue 实例中的 data
- 普通数据
- View(V):视图
- HTML DOM 模板
- ViewModel(VM):Vue实例
- 负责数据和视图的更新
- 它是 Model数据 和 View 视图通信的一个桥梁
逻辑:
1.把数据绑定到视图,vm解析插值表达式和指令,找到模型中的数据,把数据在视图中呈现出来
2.视图发生变化,在DOM中注册input等事件进行触发事件,更改model中的数据
JavaScript 数据劫持
- 数据劫持?
- Observer 数据观察
- 数据拦截器
如何实现修改一个对象成员就修改了DOM?
const data = {
message: 'Hello World'
}
// 监视 data.message 的改变
// watch('data.message', () => {
// dom.xxx = xxx
// })
data.message = 'hello'
// data.message = xxx 不仅仅对数据进行了修改,还操作了 DOM
//
答案是:JavaScript 数据劫持,或者说是 JavaScript 对象属性拦截器。
什么是数据劫持(属性拦截器)?
说白了就是:观察数据的变化。
- Object.defineProperty
- ECMAScript 5 中的一个 API
- Vue 1 和 Vue 2 中使用的都是 Object.defineProperty
- Proxy
- ECMAScript 6 中的一个 API
- 即将升级的 Vue 3 会升级使用 Proxy
- Proxy 比 Object.defineProperty 性能要更好
深入响应式原理:https://cn.vuejs.org/v2/guide/reactivity.html
如何追踪变化:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的属性,并使用Object.defineProperty
把这些属性全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim (ES5之前的语法无法解析)的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
检测变化的注意事项
Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data
对象上存在才能让 Vue 将它转换为响应式的。例如:
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式属性。例如,对于:
Vue.set(vm.someObject, 'b', 2)
您还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject,'b',2)
Object.defineProperty
参考资料:
**Object.defineProperty()**
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法
Object.defineProperty(obj, prop, descriptor)
参数:
-
obj
要在其上定义属性的对象。 -
prop
要定义或修改的属性的名称。 -
descriptor
将被定义或修改的属性描述符。
返回值:
被传递给函数的对象。
属性描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值(默认值是在使用Object.defineProperty()定义属性的情况下):
-
configurable
当且仅当该属性的 configurable 为 true 时,该属性
描述符
才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。 -
enumerable
当且仅当该属性的
enumerable
为true
时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
-
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
-
writable
当且仅当该属性的
writable
为true
时,value
才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
-
get
一个给属性提供 getter 的方法,如果没有 getter 则为
undefined
。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。默认为 undefined。
-
set
一个给属性提供 setter 的方法,如果没有 setter 则为
undefined
。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined。
打印台显示:属性描述符赋值
赋值方法对比
属性的访问器 获取值get 和 修改值set
get set 和 value可以同时使用
get set 和 value可以同时使用 打印显示
1. enumerable 可枚举方法,同get获取数据 set修改数据
显示结果为:name
当enumerable:false
时,不可打印,页面不可打印,不显示内容
2. configurable 是否可以配置
打开页面,控制台打印,仍可以看到 name ,删除操作无效,但不报错,是静默错误,需要ES5语法开启严格模式优化JS
放在script最首行,开启严格模式,此时删除属性name会显示报错
configurable:true
时,删除有效,name不显示
严格模式
JavaScript 的严格模式是使用受限制的 JavaScript 的一种方式,从而隐式地退出“草率模式”。严格模式不仅仅是一个子集:这种模式有意地与普通情形下的代码有所区别。
通过在脚本文件/函数开头添加 "use strict"
; 声明,即可启用严格模式。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
严格模式对正常的 JavaScript语义做了一些更改:
- 严格模式通过抛出错误来消除了一些原有静默错误。
- 严格模式修复了一些导致 JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行得更快。
- 严格模式禁用了在ECMAScript的未来版本中可能会定义的一些语法。
3.writable 该属性是否是可写-只读状态
writable:false
且 去掉严格模式 ,在打印台输入 vm.name ,显示为: xxx ,不是hello,赋值失败;
说明此时是不可写的,writable:false
是只读状态
开启严格模式'use strict'
,就会报错
描述符全实例代码:
<body>
<script>
// 开启严格模式(es5中新增),需要在当前作用域的最上面
'use strict'
function fn () {
// 严格模式下 全局函数调用,this 指向 undefined
console.log(this)
}
fn()
// Array.prototype = {} // 只读
var obj = {
name: 'zs'
}
var vm = {}
Object.defineProperty(vm, 'name', {
// 是否可以配置(是否可以删除,是否可以重新被defineProperty)
configurable: false,
// 可枚举(遍历)
enumerable: true,
// 该属性是否是可写
// 不能和get/set一起使用 (只读的)
writable: false,
value: 'xxx'
// get () {
// console.log('get')
// return obj.name
// },
// set (value) {
// console.log('set')
// obj.name = value
// }
})
vm.name = 'hello'
// delete vm.name
// for (var key in vm) {
// console.log(key)
// }
</script>
</body>
Object.keys()方法 遍历对象中的所有属性(遍历数组中的所有参数)
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/keys
Object.keys()
方法会返回一个由一个给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for...in
循环遍历该对象时返回的顺序一致 。
实例1:
显示结果:
实例2:
显示结果:
实现对一个对象所有成员的代理——Object.defineProperty()方法中的描述符功能展示
需求:
const data = {
foo: 'bar',
user: {
name: '张三',
age: 18
}
}
// data.foo 被访问了
data.foo
// data.foo 被改变了
data.foo = xxx
// data.user 被改变了
data.user = xxx
// data.user.name 被访问了
data.user.name
实现:
// 普通数据源
var data = {
name: 'kindeng',
user: {
age: 18,
foo: 'bar'
},
count: 0
};
// 对 data 中所有数据成员进行数据劫持(观察)
observe(data);
// data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq
// data.user.name = 'zs'
// console.log(data.user.name)
function observe(data) {
// 如果 data 数据无效或者 data 不是一个对象,就停止处理
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历,对属性成员进行代理(拦截、观察)操作
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
};
/**
* data 是数据对象
* key 是属性名
* val 当前属性名对应的值
*/
function defineReactive(data, key, val) {
// observe('kindeng');
// observe(data.user);
observe(val); // 监听子属性
// 'name'
// 'age'
// 'foo'
// 'user'
// 'count'
console.log(key)
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function () {
return val;
},
set: function (newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}
// function fn (a) {
// return {
// getA () {
// return a
// },
// setA () {
// a++
// }
// }
// }
// const { getA, setA } = fn(100)
// console.log(getA())
// setA()
// setA()
// setA()
// console.log(getA())
Proxy-对象级别
Object.defineProperty()方法-属性级别
参考资料:
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
语法
let p = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中:
new Proxy()
表示生成一个Proxy
实例target
参数表示所要拦截的目标对象- 可以是任意类型的对象,包括原生数组,函数,甚至是另一个 Proxy
handler
参数也是一个对象,用来定制拦截行为。- 其属性是当执行一个操作时定义代理的行为的函数
实例:Proxy.html
中
<body>
<script>
var obj = {
name: 'zs',
age: 18
}
//obj-目标对象 , { }-给对象配置的选项
var p = new Proxy(obj, {
get (target, key) { //target-目标对象 , key-obj目标对象中的某个属性
console.log(key)
//属性中有name 和 age ,没有变量key, key里是属性数组成员
return target[key]
},
//target-目标对象 , key-obj目标对象中的某个属性 ,value-设置的新的值
set (target, key, value) {
console.log(key, value)
target[key] = value
}
})
</script>
</body>
控制台操作打印-Proxy方法操作值
注:
Object.defineProperty 和 Proxy,都可以实现描述符get(获取数据)和set(修改数据),区别是:
- Object.defineProperty()方法-属性级别,给属性设值;
Proxy-对象级别,给对象设值;给对象设值更方便,是ES6新增属性
给对象的多个属性设值,需要循环遍历各个属性值,需要分别调用defineProperty()方法,实现起来麻烦,性能也比Proxy差
如果有一个定义的实例对象-p代理了数据操作,后期操作目标对象-obj 通过 代理对象 设 属性值 即可
示例
const data = {}
var proxy = new Proxy(data, {
get: function(target, property) {
return 35;
// return property in target ? target[property] : 37;
}
});
注意:要使得
Proxy
起作用,必须针对Proxy
实例(上例是proxy
对象)进行操作,而不是针对目标对象(上例是data)进行操作。
示例
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
let target = {};
let p = new Proxy(target, {});
p.a = 37; // 操作转发到目标
console.log(target.a); // 37. 操作已经被正确地转发
上面代码中,handler
是一个空对象,没有任何拦截效果,访问proxy
就等同于访问target
。
set的用途-实现响应式,还有给目标对象 设置值的时候,对值进行合法性的校验
示例1:
<body>
<script>
var obj = {
name: 'zs',
age: 18
}
var p = new Proxy(obj, {
get (target, key) {
console.log(key)
return target[key]
},
set (target, key, value) {
console.log(key, value)
if (key === 'age') {
if ( value >= 0 && value <= 200 ) {
} else {
//用throw new Error()方法,这样可以停止js执行,但浏览器会显示有错误
throw new Error('age的值不合法')
}
}
target[key] = value
}
})
</script>
</body>
页面显示为:
示例2:
通过代理,你可以轻松地验证向一个对象的传值。这个例子使用了 set
。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age);
// 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid
差值表达式的处理
用正则表达式,匹配差值表达式的模式,实现值的修改
<body>
<script>
// 实现——用 name的值'zs' ,替换 Name 中的 插值表达式(其内可能存在空格)
// 方法——用正则表达式,匹配差值表达式的模式
var data = {
name: 'zs'
}
var value = 'Name: {{ name }}'
// 正则表达式,匹配差值表达式的模式
// 正则表达式中的() 分组
// . 是任意单个字符 , + 是量词,修饰前面的任意字符
var reg = /\{\{(.+)\}\}/
// console.log(RegExp.$1) ——>没调test,控制台打印显示 为 空
if (reg.test(value)) {
// 1.确保匹配到差值表达式
// console.log('....') ——>控制台打印显示 .... ,证明匹配正则成功
// 2.获取差值表达式中的内容name
// 当正则匹配后,获取分组匹配的结果 $1 就是匹配第一组(第一个小括号获取的结果)
// console.log(RegExp.$1) ——> 控制台打印显示 name ,但name前后有空格
// .trim() 去空格
var key = RegExp.$1.trim()
// replace() 替换
//释义:用data中的key属性,可以获取到 值,然后 用获取到的值 把正则表达式 匹配到的结果插值表达式{{ name }} 给替换掉
value = value.replace(reg, data[key])
console.log(value) // 控制台打印显示 Name:zs
}
</script>
</body>