VUE作为国内最火的 MVVM框架,必须学习一下框架的实现原理。我们通过一个小的demo来理解一下 VUE2.x到底是如何实现MVVM的。
什么是MVVM
在前端领域, MVVM的出现无疑极大的节省了开发人员的心智,用 jquery 操作dom渲染页面的日子一去不复返。
- Model:数据模型
- View:视图
- View-Model:Model和View的桥梁,通过它,数据模型的变化可以直接反应到视图上,同样在视图上修改数据也会反应到数据模型。
所以 MVVM的核心在于,数据驱动视图和双向数据绑定
数据驱动视图
数据驱动视图的关键在于,如何知道数据发生了变更,假如我们能监听到数据变更,然后再渲染相应的dom就可以实现数据驱动视图了。
Object.defineProperty
实际上官方提供了方法Object.defineProperty
.通过该方法,我们可以动态的创建对象属性,并且监听属性的读取和修改操作。vue2.x便是使用了该方法。
先看一个简单的例子:
var data = {}
Object.defineProperty(data,'name',{})
console.log(data)
可以看到我们给 data
对象增加了一个name
属性
那我们如何监听该属性的变化呢,defineProperty()
方法的第三个参数是属性描述,描述里的set
属性会在name
被读取时触发,get
属性会在name
属性被赋值时触发。
我们试一下
var data = {}
Object.defineProperty(data, 'name', {
get(){
console.log('我被读取了')
},
set(newVal){
console.log('我被赋值了')
}
})
//触发get
data.name
//触发set
data.name = '张三'
可以看到赋值和读取都可以触发相应的函数。
对于set
和get
的定义如下:
get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。
set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined。
需要注意标黄的部分,get
在不设置return
的情况下会始终返回undefined
,这意味着我们必须正确的使用get
才能取到属性正确的值。
看下错误的例子
var data = {
name: '李四'
}
Object.defineProperty(data, 'name', {
get(){
console.log('我被读取了')
},
set(newVal){
console.log('我被赋值了')
}
})
//触发set
data.name = '张三'
//触发get
console.log('赋值之后重新获取属性值:',data.name)
取到了undefined
。
那我们直接在 get
里return data.name
呢,这是不行的。因为return data.name
本身也会触发get
函数,这显然会导致死循环。
get和set联动
那如何实现set
和get
的联动呢?
导致死循环的根本原因在于我们使用了原对象,我们可以借助中间变量来解决这个问题。
var data = {
name: '李四'
}
var _name
Object.defineProperty(data, 'name', {
get(){
console.log('我被读取了')
return _name
},
set(newVal){
console.log('我被赋值了')
_name = newVal
}
})
//触发set
data.name = '张三'
//触发get
console.log('赋值之后重新获取属性值:',data.name)
借助中间变量 name
实现了set
和get
的联动。
接下来就是把属性值渲染到试图了。
数据渲染
拿到修改之后的数据直接渲染到页面上即可,之后data.name
的修改都会反应到视图上
<body>
<div id="app">
</div>
</body>
<script>
var data = {
name: '李四'
}
var _name
Object.defineProperty(data, 'name', {
get() {
console.log('我被读取了')
return _name
},
set(newVal) {
console.log('我被赋值了')
_name = newVal
document.getElementById('app').innerText = newVal
}
})
//触发set
data.name = '张三'
//触发get
console.log('赋值之后重新获取属性值:', data.name)
</script>
</html>
基本思路就是这些,接下来我们看怎么优化?
优化
要优化,自然要分析一下现在的实现方式的缺点是什么
- 实际的应用场景里,
data
的属性会有多个,我们要想办法遍历data
的所有属性进行绑定。 - 对于上面的例子,如果有新的元素使用了
data.name
属性,就需要在set
方法里新增加一条渲染语句,这显然是无法接受的。
通过Object.keys()
方法可以获取对象的所有属性,循环遍历就可以为每个属性。
每个属性需要渲染的元素不同,我们可以通过属性名称标识属性来区分。这便是指令的由来。指令就是元素的自定义属性,该属性用于建立元素和数据的联系。
比如我们规定 v-text
属性表示,我们会修改元素的innerText
,而innerText
的值由v-text
指向的数据模型决定
代码可以优化成
<body>
<div id="app">
<!-- 表示需要将该元素的 innerText 渲染成name的值 -->
<p v-text="name"></p>
<p v-text="age"></p>
</div>
</body>
<script>
var data = {
name: '张三',
age: 12
}
Object.keys(data).forEach((item) => {
//声明中间变量
let dataProperty = data[item]
Object.defineProperty(data, item, {
get() {
return dataProperty
},
set(newVal) {
//值没变不用重新渲染
if (dataProperty === newVal) {
return
}
dataProperty = newVal
//必须指定根节点,通过根节点寻找元素
document.getElementById('app').childNodes.forEach((ele) => {
//只找 元素类型的节点
if (ele.nodeType === 1) {
//将 属性 和 对应的元素建立联系
if (ele.getAttribute('v-text') === item) {
ele.innerText = newVal
}
}
})
}
})
})
data.name = '李四'
data.age = 15
</script>
这样我们在修改属性值的时候,对应的元素就会重新渲染。
当然这个版本还有需要优化的地方,我们每次修改属性,都会从根节点重新寻找目标元素,这显然是不合理的。我们先不去关注这些细节,只需要理解思路。
解决了数据驱动视图,接下来是双向数据绑定。
双向数据绑定
首先上面我们已经解决了从m到v的数据渲染,接下来要解决 v到m。
比如说我们修改了input
框的值,如何影响到 model
层面呢?
其实很简单,就是 事件监听,监听input
框onChange
事件。
<body>
<div id="app">
<!-- 表示需要将该元素的 innerText 渲染成name的值 -->
<p v-text="name"></p>
<p v-text="age"></p>
<input type="text" v-model="sex" />
</div>
</body>
<script>
var data = {
name: '张三',
age: 12,
sex: '男'
}
Object.keys(data).forEach((item) => {
//声明中间变量
let dataProperty = data[item]
Object.defineProperty(data, item, {
get() {
return dataProperty
},
set(newVal) {
//值没变不用重新渲染
if (dataProperty === newVal) {
return
}
dataProperty = newVal
//必须指定根节点,通过根节点寻找元素
document.getElementById('app').childNodes.forEach((ele) => {
//只找 元素类型的节点
if (ele.nodeType === 1) {
//将 属性 和 对应的元素建立联系
if (ele.getAttribute('v-text') === item) {
ele.innerText = newVal
}
//和 v-text 不同,修改的是元素的 value
if (ele.getAttribute('v-model') === item) {
ele.value = newVal
//元素 value变更影响 model
ele.onchange = function () {
data[item] = ele.value
}
}
}
})
}
})
})
data.name = '李四'
data.age = 15
data.sex = '女'
</script>
我们改data.sex
的时候input
框里的值会变,修改input
框的输入data.sex
也会变。
这便实现了双向数据绑定。
优化
思考一下上面方案的缺点,再进一步改良
- 使用面向对象的方式,暴露统一的构造函数。
- 使用观察者模式,对数据变化进行监听,用闭包保存需要渲染的节点,避免重复查找元素。
- 上面的例子里只支持层元素,可以使用递归遍历所有后代节点进行动态绑定。