虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
<body>
<div id="v-bind-date">
单项数据流动: <input type="text" v-model="url">
<h1>{{this.$createElement.length}}h1>
div>
<script>
Vue.config.productionTip = false
let app = new Vue({
el: '#v-bind-date', // el用于指定当前Vue实例为哪个容器服务。
data: {
url: 'https://www.baidu.com'
}
})
script>
body>
MVVM模型:
1.M:模型(Model):对应data中的数据
2.V:视图(View):模板
3.VM:视图模型(ViewModel):Vue实例对象 观察发现:
1.data中所有的属性,最后都出现在VM身上。
2.VM身上的所有属性即Vue原型上所有属性,在Vue模板中都可以直接使用。
<script>
let person = {
name: 'zhaoshuai-lc',
sex: 'male'
}
Object.defineProperty(person, 'age', {
value: 18
})
console.log(person)
for (let personKey in person) {
console.log(person[personKey])
}
script>
通过遍历对象可以发现 - age 是不可以被枚举的
Object.defineProperty(person, 'age', {
value: 18,
enumerable:true, //控制属性是否可以枚举,默认值为false
})
可以被枚举,但是不可以被修改
Object.defineProperty(person, 'age', {
value: 18,
enumerable:true, //控制属性是否可以枚举,默认值为false
writable:true, //控制属性是否可以被修改,默认值为false
configurable:true //控制属性是否可以被删除,默认值为false
})
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<script type="text/javascript">
let number = 19
let person = {
name: 'zhaoshuai-lc',
sex: 'male'
}
Object.defineProperty(person, 'age', {
// 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
get() {
console.log('有人读取age属性了')
return number
},
// 当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
set(value){
console.log('有人修改了age属性,且值是',value)
number = value
}
})
console.info("person", person)
console.info("number", number)
script>
body>
html>
数据代理:通过一个对象代理对另一个对象中属性的操作 (读/写)
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<script type="text/javascript">
let obj1 = {
x: 100
}
let obj2 = {
y: 200
}
Object.defineProperty(obj2, 'x', {
get() {
return obj1.x
},
set(value) {
obj1.x = value
}
})
script>
body>
html>
将鼠标放到实例中的数据上,可以看到也提示了“Invoke property getter”。也就是说当有人访问name的时候,getter就在工作。
vm._data === data 为ture。也就是说_data完全来自于data:
1.Vue创建实例对象vm—>2.Vue收集data数据—>3.Vue往vm的_data上添加name、address(通过getter)等属性:
数据代理:通过vm.xxx来代理vm._data.xxx的操作(读/写),目的就是为了编码更方便。
_data
中的属性做了一个数据劫持,把data里面的属性做了修改/升级,以便更好地完成响应式操作。
基本原理:
通过Object.defineProperty()把_data对象中所有属性添加到vm上。为每一个添加到vm上的属性,都指定一个getter/setter。在getter/setter内部去操作(读/写)_data中对应的属性。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js">script>
head>
<body>
<div id="v-for">
<button @click="updateMei">更新马冬梅的信息button>
<ul>
<li v-for="(item, index) of personList" :key="item.id">
{{ item.name }} - {{ item.age }}
li>
ul>
div>
<script type="text/javascript">
let vm = new Vue({
el: '#v-for',
data: {
personList: [
{id: '001', name: '马冬梅', age: 19, sex: '女'},
{id: '002', name: '周冬雨', age: 24, sex: '女'},
{id: '003', name: '周杰伦', age: 55, sex: '男'},
{id: '004', name: '温兆伦', age: 12, sex: '男'}
]
},
methods: {
updateMei() {
this.personList[0].name = '马保国'
this.personList[0].age = 50
this.personList[0].sex = '男'
}
}
})
script>
body>
html>
以上代码是起作用的,这是为什么呢?原因如下所示【getter setter】
但是,我们修改为如下代码就不起作用了:
结果如下所示:这是为什么呢?
这是因为,如下所示:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js">script>
head>
<body>
<div id="v-school">
学校名称:{{ name }} <br/>
学校地址:{{ address }}
div>
<script type="text/javascript">
let vm = new Vue({
el: '#v-school',
data: {
name: '北京大学',
address: '北京'
}
})
script>
body>
html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="text/javascript">
let data = {
name: '尚硅谷',
address: '北京',
}
// 创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data)
console.log(obs)
// 准备一个vm实例对象
let vm = {}
vm._data = data = obs
function Observer(obj) {
// 汇总对象中所有的属性
const keys = Object.keys(obj)
// 遍历
keys.forEach((k) => {
Object.defineProperty(this, k, {
get() {
return obj[k]
},
set(val) {
console.log('${k}被改了,我要去解析模板,生成虚拟DOM...我要开始忙了')
obj[k] = val
}
})
})
}
script>
body>
html>
Vue监测数据的原理,就是靠setter。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>title>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js">script>
head>
<body>
<div id="v-set">
<h1>学校信息h1>
<h2>学校名称:{{ name }}h2>
<h2>学校地址:{{ address }}h2>
<hr/>
<h1>学生信息h1>
<button @click="addSex">添加一个性别属性,默认值是男button>
<h2>姓名:{{ student.name }}h2>
<h2>性别:{{ student.sex }}h2>
<h2>年龄:真实{{ student.age.rAge }}, 对外{{ student.age.sAge }}h2>
<h2>朋友们:h2>
<ul>
<li v-for="(f,index) in student.friends" :key="index">
{{ f.name }}--{{ f.age }}
li>
ul>
div>
<script type="text/javascript">
Vue.config.productionTip = false
let vm = new Vue({
el: '#v-set',
data: {
name: '北京大学',
address: '北京',
student: {
name: 'tom',
age: {
rAge: 40,
sAge: 29
},
friends: [
{name: 'jerry', age: 35},
{name: 'tony', age: 36}
]
}
},
methods: {
addSex() {
// Vue.set(this.student,'sex','男')
this.$set(this.student, 'sex', '男')
}
},
})
script>
body>
html>
原生Javascript数组使用的方法,例如push,就是从Array原型中找到的。可用 arr.push === Array.prototype.push
验证。 而Vue中的push却不等于 Array.prototype.push ,因为Vue中的push是经过包装的。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>title>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js">script>
head>
<body>
<div id="ddd">
<h1>学生信息h1>
<h2>爱好:h2>
<ul>
<li v-for="(item, index) in student.hobby" :key="index">
{{ item }}
li>
ul>
div>
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el: '#ddd',
data: {
student: {
name: 'tom',
age: 18,
hobby: ['抽烟', '喝酒', '烫头'],
friends: [
{name: 'jerry', age: 35},
{name: 'tony', age: 36}
]
}
},
methods: {
},
})
script>
body>
html>
针对于上面的数组,有了解决方案:如下
以后对于数组的修改,使用如下方法:
Vue监视数据的原理:
通过setter实现监视,且要在new Vue时就传入要监测的数据。
(1)对象中后追加的属性,Vue默认不做响应式处理。
(2)如需给后添加的属性做响应式,请使用如下API:
vm.set(target,propertyName/index,value) 或
vm.$set(target,propertyName/index,value)
通过包裹数组更新元素的方法实现,本质就是做了两件事:
(1)调用原生对应的方法对数组进行更新。
(2)重新解析模板,进而更新页面。
(1)使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
(2)Vue.set() 或 vm.$set()
特别注意:Vue.set() 和 vm.$set() 不能给 vm 或 vm的根数据对象添加属性!
定义:vue中双向绑定就是指v-model指令,可以绑定一个响应式数据到视图,同时视图中变化能同步改变该值。
通过Object.defineProperty( )
对属性设置一个set
函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。
我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。
如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。
因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。
接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript" src="./vue.js"></script>
<div id="app">
<h1>{{ name }}</h1>
{{ title }}
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'zhaoshuai-lc',
title: 'inspur'
}
})
</script>
</body>
</html>
我们自定义vue.js源码:
class Vue {
constructor(options) {
this.$el = document.querySelector(options.el)
this.$data = options.data
this.compile(this.$el)
}
compile(node) {
node.childNodes.forEach((item, index) => {
if (item.nodeType == 1) {
this.compile(item)
}
if (item.nodeType == 3) {
let reg = /\{\{(.*?)\}\}/g;
let text = item.textContent;
item.textContent = text.replace(reg, (match,vmKey) => {
vmKey = vmKey.trim()
return this.$data[vmKey]
})
}
})
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript" src="./vue.js"></script>
<div id="app">
<h1>{{ name }}</h1>
{{ title }}
<button @click="btn">点击事件</button>
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
name: 'zhaoshuai-lc',
title: 'inspur'
},
methods: {
btn(e) {
console.log('手写点击事件ok ... ')
console.log(e)
}
}
})
</script>
</body>
</html>
class Vue {
constructor(options) {
this.$optinos = options
this.$el = document.querySelector(options.el)
this.$data = options.data
this.compile(this.$el)
}
compile(node) {
node.childNodes.forEach((item, index) => {
if (item.nodeType == 1) {
if (item.hasAttribute('@click')) {
let vmKey = item.getAttribute('@click').trim();
item.addEventListener('click', (event) => {
this.eventFn = this.$optinos.methods[vmKey].bind(this);
this.eventFn(event)
})
}
if (item.childNodes.length > 0) {
this.compile(item)
}
}
if (item.nodeType == 3) {
let reg = /\{\{(.*?)\}\}/g;
let text = item.textContent;
item.textContent = text.replace(reg, (match,vmKey) => {
vmKey = vmKey.trim()
return this.$data[vmKey]
})
}
})
}
}
引出一个问题:
我们在绑定事件的方法 btn() 方法中打印 this:data里面的数据我们需要使用 this.$data.xxx方可获取到,但是我们vue中直接通过this.xxx就可以获取到数据这里就引出了本章节要讨论的问题了- 数据劫持的问题
我们这里的解决问题的思路是:Object.defineProperty()
class Vue {
constructor(options) {
this.$optinos = options
this.$el = document.querySelector(options.el)
this.$data = options.data
this.proxyData()
this.compile(this.$el)
}
// 使用来自于data里面的数据给Vue赋值属性
// data中的属性值和Vue对象的属性保持双向(劫持)
proxyData() {
for (let key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(v) {
this.$data[key] = v
}
})
}
}
compile(node) {
node.childNodes.forEach((item, index) => {
if (item.nodeType == 1) {
if (item.hasAttribute('@click')) {
let vmKey = item.getAttribute('@click').trim();
item.addEventListener('click', (event) => {
this.eventFn = this.$optinos.methods[vmKey].bind(this);
this.eventFn(event)
})
}
if (item.childNodes.length > 0) {
this.compile(item)
}
}
if (item.nodeType == 3) {
let reg = /\{\{(.*?)\}\}/g;
let text = item.textContent;
item.textContent = text.replace(reg, (match,vmKey) => {
vmKey = vmKey.trim()
return this.$data[vmKey]
})
}
})
}
}