Vue:模板 & MVVM
- 模板
- 插值语法
- 指令语法
- MVVM
- defineProperty
- 数据代理
模板
Vue
实例绑定一个容器,想要向容器中填入动态的值,就需要使用模板语法。模板语法分为插值语法
和指令语法
。
插值语法
插值语法很简单,使用{{}}
包含一个表达式,Vue
会把表达式的结果替代这个值渲染到HTML
中。
示例:
<div id="root">
<h3>hello,{{name}}</h3>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
name:'jack',
}
})
</script>
以上代码,最终会生成hello,jack
标题,{{name}}
被替换为了data.name
。
此外,任意表达式都可以填入{{}}
中,比如:
<div id="root">
<h3>hello,{{1 + 1}}</h3>
<h3>hello,{{x === y ? 'a' : 'b'}}</h3>
</div>
比如{{1 + 1}}
最终会变成2
,三元表达式会进行计算,或者说函数调用也可以在{{}}
内部执行。
另外的,{{}}
内的表达式,可以读取到Vue
示例的data
下的所有属性。
指令语法
插值语法能处理的情况是非常有限的,比如标签属性。
现在有一个<a>
标签,要动态的指定url
,如果使用插值语法,可能写出如下内容:
<div id="root">
<a href="{{addr.url}}">{{addr.name}}</a>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
addr:{
name:'百度',
url:'http://www.baidu.com',
}
}
})
</script>
但是这是一个错误的语法,{{}}
作为标签属性时,不会被解析。
此时就要使用指令语法,v-bind
指令可以将一个属性值解析为表达式。
<div id="root">
<a v-bind:href="addr.url">{{addr.name}}</a>
</div>
以上代码中,herf
属性前面加上了v-bind:
,那么后面的""
内部不再被解析为字符串,而是当作表达式处理,此时addr.url
就会被解析了。
一个v-bind
只能作用于其后面的那个属性,比如以下情况:
<div id="root">
<a v-bind:href="addr.url" id="addr.name">{{addr.name}}</a>
</div>
在id="name"
中,addr.name
不会被当作表达式,而是当作字符串,因为其前面没用v-bind
指令。
v-bind
指令可以简写为一个冒号:
。
<div id="root">
<a :href="addr.url">{{addr.name}}</a>
</div>
- 数据绑定
刚才的v-bind
称为单向数据绑定,简单来说就是如果Vue
实例中的属性值改变了,那么HTML
的内容也会改变。但是如果HTML
的内容改变了,Vue
实例的内容不会改变。
示例:
<div id="root">
单向数据绑定:<input type="text" :value="inner"><br/>
</div>
<script type="text/javascript">
new Vue({
el:'#root',
data:{
inner:'hello world'
}
})
</script>
这是一个输入框,输入框的value
属性被绑定为了inner
的内容。
初始时,inner
和输入框的内容相同,尝试修改inner
的值:
修改后,输入框的内容也随之改变了。
但是如果修改输入框的内容:
此时inner
的内容不会改变,这就是单向数据绑定,Vue
实例的值会影响HTML
,但是HTML
不会影响Vue
实例。
指令v-model
可以使用双向数据绑定:
<div id="root">
双向数据绑定:<input type="text" v-model:value="inner"><br/>
</div>
此时修改输入框,也会反过来修改Vue
实例的值:
但是不是所有标签都可以使用v-model
,只有可以输入元素,类似表单的元素可以使用,其他标签使用会导致报错,并且标签会丢失。
v-model:
也可以简写,因为v-model:
后面的属性往往是value
,表示元素内容,所以可以省略:value
,直接写为v-model
:
<div id="root">
双向数据绑定:<input type="text" v-model="inner"><br/>
</div>
MVVM
MVVM
是一种软件开发模型,在Vue
设计时参考了这个模型,其分为三个组成部分:
Model
:模型View
:视图ViewModel
:视图模型
Model
就是一个JavaScript
的表达式,简单来说就是数据,比如在{{}}
中包含一个表达式,在v-bind
后面的" "
内部的表达式,这些表达式都会产生一个值,变成具体的数据。
View
对应上图中最左侧的DOM
,它表示一个可以被用户看到的页面,这个页面通过Vue
解析产生。
ViewModel
是上图的中间部分,它表示Vue
实例对象,这也是整个MVVM
的核心,其分为两个区域。
首先是Data Bindings
数据绑定,它接收Model
产生的值,并把它填入到View
视图中,这样就产生了最终的页面。这对应了之前的v-bind
指令,或者{{}}
插值,他们都是接收JavaScript
表达式产生的值,并填入到元素中。
另一个是DOM Listeners
元素监听,它监听页面中的元素,当元素发生变化时,返过来影响Model
内部的值。这对应的了v-model
双向数据绑定,当DOM
元素发生更改时,修改Vue.data
的内容。
在Vue
中,JavaScript
表达式可以看到Vue
实例的所有属性和方法,包括原型链。
输出一下Vue
实例:
const vm = new Vue({
el:'#root',
data:{
name:'hello world',
num: 123
}
})
console.log(vm)
由于Vue
实例在MVVM
中对应VM
,所以接收时常把变量命名为vm
。
输出结果:
可以看到,name
、num
这两个属性最后都进入到了Vue
实例中,构造时会把data
下面的所有属性都添加到Vue
实例中,这就是为什么在{{}}
内部可以看到data
下面的数据,
仔细看name
、num
的属性值是(...)
,这涉及到数据代理的问题。
defineProperty
Object.defineProperty
可以给一个对象添加指定的值,并且可以进行属性的复杂配置。
语法:
Object.defineProperty(对象, 属性, {配置对象})
对象
:被修改的对象属性
:要添加的属性{配置对象}
:添加属性相关的配置
例如给一个对象"张三"
添加一个age
属性:
let person = {
name:'张三',
sex:'男',
}
Object.defineProperty(person,'age',{
value:18,
enumerable:true, //控制属性是否可以枚举,默认值是false
writable:true, //控制属性是否可以被修改,默认值是false
configurable:true //控制属性是否可以被删除,默认值是false
})
在配置对象中,value
就是配置的属性值,剩下三个属性用于对元素做一些限制。
比如说enumerable
控制元素是否可以被遍历,如果为false
,那么for in
语法就无法获取到这个值,或者通过keys
方法无法获得属性名。
后两个比较好理解,就是能否修改与删除。
这些都不是重点,在配置对象中还可以配置函数,其中set
和get
方法非常重要。
get
方法在该属性被读取时调用,并且读取元素值时,返回指定值。
Object.defineProperty(person,'age',{
get(){
console.log('有人读取age属性了')
return 123
}
})
输出结果:
此时可以看到,age
属性的值变成了(...)
,表示暂时未知。当访问person.age
,调用了get
函数并拿到了返回值123
。
注意:get
方法和value
属性不能同时设置。
set
方法在该属性被设置时调用,接收一个参数value
,这就是要设置的值。
实例:
Object.defineProperty(person,'age',{
set(value){
console.log('有人修改了age属性,值是',value)
number = value
}
})
输出结果:
在修改值时,调用了对应的set
函数。
注意:set
不能和writable
属性一起指定。
基于这两个特性,它可以完成一个功能:让一个对象外部的变量与该属性完全同步。
let number = 18
let person = {
name:'张三',
sex:'男',
}
Object.defineProperty(person,'age',{
get(){
return number
},
set(value){
number = value
}
})
以上代码,实现了person.age
与变量number
的完全同步。当每次读取age
时,返回number
的值,每次设置age
时,把number
一起修改。
基于以上操作,就可以完成数据代理。
数据代理
数据代理:通过一个对象代理对另一个对象中属性的操作
示例:
let A = {x:100}
let B = {}
Object.defineProperty(B,'x',{
get(){
return A.x
},
set(value){
A.x = value
}
})
以上就是一个数据代理,A.x
与B.x
是完全同步的,操作B.x
对应的A.x
也会修改,任何时候A.x === B.x
。
现在再回看之前Vue
实例中name
和num
的变量值为(...)
,这其实就是一个数据代理。
在Vue
实例的底部,可以看到四个方法:
这四个方法分别就是name
和num
的get
和set
,此处将vm.name
和vm.num
进行了数据代理,代理到data.name
和data.num
,这样可以保证vm
和构造时传入的data
完全同步。
而传入data
时,有可能传了个匿名对象,data
本身也要进行保存,其实vm
把data
保存为了一个vm._data
的对象。
真正被代理的其实是vm._data
,如果不做这层代理,那么用户访问数据时就要写{{ _data.name }}
、{{ _data.num }}
,这样有点麻烦,因此做了依次数据代理,将他直接放到vm
下,这样就可以直接访问了。
另外的,可以发现在_data
中,这两个属性的值也是(...)
,看着很想数据代理,但这并不是。这是数据劫持,当_data
的数据发生改变,就要渲染到HTML
页面中,这个过程是由数据劫持完成的。