抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

一:什么是双向数据绑定?

1.1 Vue中MVVM模型

img

  • 模型(Model)表示应用程序的数据和业务逻辑。这可以是从后端API获取的数据,或者在前端应用程序内部定义的数据。
  • 视图(View)是用户界面的可见部分,通常以HTML模板的形式存在。它负责将数据呈现给用户,并处理用户的输入事件。
  • 视图模型(ViewModel)是模型和视图之间的中间层,它负责管理视图所需的数据,并处理视图中发生的事件。视图模型通过双向数据绑定将模型的状态与视图保持同步。

MVVM即是“Model-View-ViewModel”,它是一种设计模式,用于实现用户界面的分离和交互。

主要职责

View中视图变化,通过ViewModel中的监听器反馈给model进行数据的更新

Model中数据的变化,通过ViewModel中的解析器反馈给View进行视图的更新

1.2 双向数据绑定原理

img

vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。

二:实现双向数据绑定

进行数据的准备,我们目的是为了实现双向数据绑定

  • 模板解析,姓名年龄渲染出来的内容是将括号内容替换成我们的数据
  • 数据绑定,文本框的内容和上方渲染的数据是一致的,通过修改文本框上方渲染的内容同步修改

image-20230727173115726

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>姓名:{{name}}</h3>
<h3>年龄:{{more.age}}</h3>
输入姓名:<input type="text" v-model="name">
<br>
输入年龄:<input type="text" v-model="more.age">
</div>
<!-- 引入自己的vue.js文件 -->
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: '张三',
more: {
age: 18
}
}
})
console.log(vm);
</script>
</body>
</html>

数据初始化

1
2
3
4
5
6
7
8
9
//定义vue类
class Vue {
//构造函数
constructor(obj_instance) {
//执行初始化
this.$data = obj_instance.data
console.log(this.$data);
}

image-20230727173929004

我们创建的vm实例已经传给vue类,为了模拟vue中$data,也在构造函数利用this.$data来存储我们创建的vm实例中的data数据

只是此时的数据都还不是响应式的

2.1 数据劫持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
//递归出口
if (!data_instance || typeof data_instance !== 'object') return;
//object.keys以数组形式返回对象中的属性
//遍历属性属性,通过obj.defineProperty来进行数据监视
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
//递归将 子属性的值进行数据劫持
Observer(value);
//三个参数,(对象, 监视的属性, 回调)
Object.defineProperty(data_instance, key, {
//可以枚举 属性描述符可以改变
enumerable: true,
configurable: true,
//通过getter 和 setter函数进行数据监视
get() {
//访问属性时候 调用getter函数 返回return 值
console.log(`访问了属性:${key} -> 值为${value}`);
// console.log(Dependency.temp);
return value;
},
//修改的新属性值
set(newValue) {
console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
value = newValue;
Observer(newValue);
},
});
});
}

  • Vue.js是通过Object.defineProperty来实现对数据的监视
  • data_instance是一个对象,通过Object.keys来实现对对象以数组形式放回

image-20230727180010190

  • 将数组中的每一项通过Object.defineProperty进行数据监视,设置getter和setter,当访问数据和修改数据时调用
  • 还要通过递归去将每一项的子属性的值都进行数据监听

image-20230727180218389

2.2 模板解析

对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

1
2
3
4
5
6
7
8
9
10
11
12
13
//创建vue类
class Vue {
//执行初始化
constructor(obj_instance) {
this.$data = obj_instance.data;
//调用Observe - 对data中的每个数据进行数据劫持
//对data中的每一项进行响应式处理
Observer(this.$data);

//解析模板
Compile(obj_instance.el, this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
//获取id为app的dom元素 绑定到vm.$el上
vm.$el = document.querySelector(element);
// console.log(vm.$el);
//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
const fragment = document.createDocumentFragment();
//循环将vm.$el中的dom元素 插入到fragment文档碎片中
let child;
while ((child = vm.$el.firstChild)) {
//使用fragment.append会将原先dom删除
fragment.append(child);
}
// console.log(fragment);
// console.log(fragment.childNodes);
//要将{{}}替换 所以节点类型为 1 和 3为h3
fragment_compile(fragment);
//替换文档碎片内容
function fragment_compile(node) {
//正则匹配 {{ 属性 }}
const pattern = /\{\{\s*(\S+)s*}\}/;
//如果节点为文本节点
if (node.nodeType === 3) {
const temp = node.nodeValue
//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
// console.log(vm.$data[result_regex[1]]);
const arr = result_regex[1].split('.');
//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
//将 {{name}} {{more.age}} 替换成value
node.nodeValue = temp.replace(pattern, value);
}
return;
}
//将文档碎片 fragment渲染到el中
vm.$el.appendChild(fragment);
}
  • 开辟一个内存空间,创建fragment文档碎片,不属于dom,属于内存区域,当所有数据更新完成时再渲染页面,避免过多操作dom
  • 将vm.$el中的dom元素 ,通过appendChild插入到fragment文档碎片中,原先dom中的元素会被移除,存放在fragment文档碎片之中

image-20230727181015293

  • 替换文档碎片中 括号中的内容,首先得遍历fragment中的node节点也要通过递归遍历, 通过正则表达式来匹配 内容
1
2
//递归遍历
node.childNodes.forEach((child) => fragment_compile(child));

image-20230727181310664

  • 要将name 和 more.age 替换成数据,通过reduce方法获取数据

  • reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值,无法通过vm.$data[more.age]来获取数据

1
2
3
4
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
  • vm.$data[more.age]

image-20230727181812004

  • 将括号内内容替换成value
1
node.nodeValue = temp.replace(pattern, value);
  • 最终将文档碎片 fragment渲染到el中
1
vm.$el.appendChild(fragment);

image-20230727181959274

2.3 订阅者-发布者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//依赖 --收集和通知订阅者
class Dependency {
constructor() {
//收集订阅者
this.subscribers = [];
}
//添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
//通知订阅者
notify() {
//遍历订阅者 让订阅者触发自己的update函数
this.subscribers.forEach((sub) => sub.update());
}
}
  • 数组用于收集订阅者
  • 添加订阅者的方法
  • 当数据修改时需要通知订阅者,触发自己的update更新函数来更新视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//订阅者
class Watcher {
//三个参数
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
//临时属性 --触发getter
//因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
}
//更新函数
update() {
//获取属性值
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data )
this.callback(value);
}
}

将watcher类实例添加到Dep数组中来实现数据视图的绑定

1
2
3
4
5
6
   //因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
  • 创建Dependency.temp用于临时存储创建的watcher实例,触发getter

  • 在observer类中触发getter时,将临时存储的watcher实例添加到Dependency的存储订阅者的数组之中

1
2
3
4
get() {
//将订阅者实例添加到订阅者数组中
Dependency.temp && dependency.addSub(Dependency.temp)
},
  • 同时为了避免多次重复,添加watcher实例,在添加该实例过后,赋空值

2.4 v-model数据绑定

视图与数据的绑定

在fragment_compile()函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//找v-model属性的元素 更改其nodeValue
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes)
attr.forEach(item => {
if(item.nodeName === 'v-model'){
// console.log(item.nodeValue);
//修改nodeValue
const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
// console.log(value);
node.value = value
//创建watcher实例
new Watcher(vm, item.nodeValue, newValue => {
node.value = newValue
})
//触发input事件来通过视图修改数据
node.addEventListener('input', e => {
const arr1 = item.nodeValue.split('.')
// console.log(arr1);
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => total[current], vm.$data)
// console.log(final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
  • 在文档碎片fragment中遍历node,通过node.attributes方法来找到属性值为v-model的node节点

  • 遍历的节点 item.nodeValue,是name, more.age

image-20230727184125900

  • 通过reduce方法来获取到vm.$data上对应属性的属性值
  • 将node.value 修改为属性值,此时将文本框中的内容和属性值相绑定

image-20230727184209298

  • 然后需要通过,文本框修改数据同时修改上方的视图,那就需要用到addEvetListener方法添加input事件
  • 然后通过文本框视图来修改数据
1
final[arr1[arr1.length - 1]] = e.target.value

三:完整代码

html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>姓名:{{name}}</h3>
<h3>年龄:{{more.age}}</h3>
输入姓名:<input type="text" v-model="name">
<br>
输入年龄:<input type="text" v-model="more.age">
</div>
<!-- 引入自己的vue.js文件 -->
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: '张三',
more: {
age: 18
}
}
})
console.log(vm);
</script>
</body>
</html>

vue.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
//创建vue类
class Vue {
//执行初始化
constructor(obj_instance) {
this.$data = obj_instance.data;
//调用Observe - 对data中的每个数据进行数据劫持
//对data中的每一项进行响应式处理
Observer(this.$data);

//解析模板
Compile(obj_instance.el, this);
}
}

//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
//递归出口
if (!data_instance || typeof data_instance !== 'object') return;
//创建订阅者实例
const dependency = new Dependency()
//object.keys以数组形式返回对象中的属性
// console.log(Object.keys(data_instance));
//遍历属性属性,通过obj.defineProperty来进行数据监视
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
//递归将 子属性的值进行数据劫持
Observer(value);
//三个参数,(对象, 监视的属性, 回调)
Object.defineProperty(data_instance, key, {
//可以枚举 属性描述符可以改变
enumerable: true,
configurable: true,
//通过getter 和 setter函数进行数据监视
get() {
//访问属性时候 调用getter函数 返回return 值
console.log(`访问了属性:${key} -> 值为${value}`);
// console.log(Dependency.temp);
//将订阅者实例添加到订阅者数组中
Dependency.temp && dependency.addSub(Dependency.temp)
return value;
},
//修改的新属性值
set(newValue) {
console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
value = newValue;
Observer(newValue);
dependency.notify()
},
});
});
}

//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
//获取id为app的dom元素 绑定到vm.$el上
vm.$el = document.querySelector(element);
// console.log(vm.$el);
//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
const fragment = document.createDocumentFragment();
//循环将vm.$el中的dom元素 插入到fragment文档碎片中
let child;
while ((child = vm.$el.firstChild)) {
//使用fragment.append会将原先dom删除
fragment.append(child);
}
// console.log(fragment);
// console.log(fragment.childNodes);
//要将{{}}替换 所以节点类型为 1 和 3为h3
fragment_compile(fragment);
//替换文档碎片内容
function fragment_compile(node) {
//正则匹配 {{ 属性 }}
const pattern = /\{\{\s*(\S+)s*}\}/;
//如果节点为文本节点
if (node.nodeType === 3) {
const temp = node.nodeValue
//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
const result_regex = pattern.exec(node.nodeValue);
// console.log(result_regex);
if (result_regex) {
// console.log(vm.$data[result_regex[1]]);
const arr = result_regex[1].split('.');
//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
//将 {{name}} {{more.age}} 替换成value
node.nodeValue = temp.replace(pattern, value);
//文档碎片替换的时候添加创建订阅者
new Watcher(vm, result_regex[1], newValue => {
//wacther的回调函数 会将文档碎片中的nodevalue更新为我们修改的newValue
node.nodeValue = temp.replace(pattern, newValue);
});
}
return;
}
//找v-model属性的元素 更改其nodeValue
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes)
attr.forEach(item => {
if(item.nodeName === 'v-model'){
console.log(item.nodeValue);
//修改nodeValue
const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
// console.log(value);
node.value = value
//创建watcher实例
new Watcher(vm, item.nodeValue, newValue => {
node.value = newValue
})
//触发input事件来通过视图修改数据
node.addEventListener('input', e => {
const arr1 = item.nodeValue.split('.')
// console.log(arr1);
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => total[current], vm.$data)
// console.log(final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
//递归遍历
node.childNodes.forEach((child) => fragment_compile(child));
}
//将文档碎片 fragment渲染到el中
vm.$el.appendChild(fragment);
}

//依赖 --收集和通知订阅者
class Dependency {
constructor() {
//收集订阅者
this.subscribers = [];
}
//添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
//通知订阅者
notify() {
//遍历订阅者 让订阅者触发自己的update函数
this.subscribers.forEach((sub) => sub.update());
}
}

//订阅者
class Watcher {
//三个参数
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
//临时属性 --触发getter
//因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
}
//更新函数
update() {
//获取属性值
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data )
this.callback(value);
}
}

评论