vue双向数据绑定原理
vue双向数据绑定原理
一:什么是双向数据绑定?
1.1 Vue中MVVM模型

- 模型(Model)表示应用程序的数据和业务逻辑。这可以是从后端API获取的数据,或者在前端应用程序内部定义的数据。
- 视图(View)是用户界面的可见部分,通常以HTML模板的形式存在。它负责将数据呈现给用户,并处理用户的输入事件。
- 视图模型(ViewModel)是模型和视图之间的中间层,它负责管理视图所需的数据,并处理视图中发生的事件。视图模型通过双向数据绑定将模型的状态与视图保持同步。
MVVM即是“Model-View-ViewModel”,它是一种设计模式,用于实现用户界面的分离和交互。
主要职责
View中视图变化,通过ViewModel中的
监听器反馈给model进行数据的更新Model中数据的变化,通过ViewModel中的
解析器反馈给View进行视图的更新
1.2 双向数据绑定原理

vue.js是采用
数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
二:实现双向数据绑定
进行数据的准备,我们目的是为了实现双向数据绑定
- 模板解析,姓名年龄渲染出来的内容是将括号内容替换成我们的数据
- 数据绑定,文本框的内容和上方渲染的数据是一致的,通过修改文本框上方渲染的内容同步修改

<!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类
class Vue {
//构造函数
constructor(obj_instance) {
//执行初始化
this.$data = obj_instance.data
console.log(this.$data);
}

我们创建的vm实例已经传给vue类,为了模拟vue中 d a t a ,也在构造函数利用 t h i s . data,也在构造函数利用this. data,也在构造函数利用this.data来存储我们创建的vm实例中的data数据
只是此时的数据都还不是响应式的
2.1 数据劫持
//数据劫持 - 监听实例中的数据
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来实现对对象以数组形式放回

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

2.2 模板解析
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
//创建vue类
class Vue {
//执行初始化
constructor(obj_instance) {
this.$data = obj_instance.data;
//调用Observe - 对data中的每个数据进行数据劫持
//对data中的每一项进行响应式处理
Observer(this.$data);
//解析模板
Compile(obj_instance.el, this);
}
}
//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文档碎片之中

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

-
要将name 和 more.age 替换成数据,通过reduce方法获取数据
-
reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm. d a t a 是初始值, t o t a l 的初始值,无法通过 v m . data是初始值,total的初始值,无法通过vm. data是初始值,total的初始值,无法通过vm.data[more.age]来获取数据
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
- vm.$data[more.age]

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

2.3 订阅者-发布者模式
//依赖 --收集和通知订阅者
class Dependency {
constructor() {
//收集订阅者
this.subscribers = [];
}
//添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
//通知订阅者
notify() {
//遍历订阅者 让订阅者触发自己的update函数
this.subscribers.forEach((sub) => sub.update());
}
}
- 数组用于收集订阅者
- 添加订阅者的方法
- 当数据修改时需要通知订阅者,触发自己的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);
}
}
将watcher类实例添加到Dep数组中来实现数据视图的绑定
//因为想要将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的存储订阅者的数组之中
get() {
//将订阅者实例添加到订阅者数组中
Dependency.temp && dependency.addSub(Dependency.temp)
},
- 同时为了避免多次重复,添加watcher实例,在添加该实例过后,赋空值
2.4 v-model数据绑定
视图与数据的绑定
在fragment_compile()函数中
//找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

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

- 然后需要通过,文本框修改数据同时修改上方的视图,那就需要用到addEvetListener方法添加input事件
- 然后通过文本框视图来修改数据
final[arr1[arr1.length - 1]] = e.target.value
三:完整代码
html
<!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
//创建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);
}
}
更多推荐


所有评论(0)