一、v-model双向绑定原理
1、v-model是v-on和v-bind的语法糖
v-on用于事件绑定,给当前组件绑定一个input事件。
v-bind用于属性绑定,给当前组件绑定一个value属性。
2、v-model双向绑定的实现:
(1)在父组件内给子组件添加v-model,相当于给组件添加了个value 属性,传递的值就是绑定的值。和绑定了一个此绑定值的input事件。
(2)子组件中使用prop属性接受这个value值,名字必须是value。
(3)子组件内部更改value值的时候,必须通过$emit()事件派发个input事件,并携带最新的值。
(4)v-model绑定的input事件会自动监听这个input事件,把接收到的最近新值赋值给v-model绑定的变量。
二、vue 的双向绑定原理
1、vue数据双向绑定的框架:mvvm
- 数据层(Model):应用的数据以及业务逻辑,为开发者编写的业务代码;
- 视图层(View):应用的展示效果,各类UI组件,由template和css组成的代码;
- 业务逻辑层(ViewModel):负责将view和model连接起来。使View和Model层的同步工作完全是自动的。
2、vue2数据双向绑定原理——MVVM的核心
vue2数据双向绑定原理图
1、监听器、发布者(Observer):
主要应用Object.defineProperty()方法,对数据对象的属性进行遍历,修改属性的setter、getter方法。在getter方法中属性添加到订阅器中,在setter方法中通知消息订阅器发出通知。使用 defineRecative 函数对 data 做 Object.defineProperty 处理,使得可以拦截 data 中的每个数据的get/set。
代码:
class Observer {
constructor(data) {
this.observer(data);
}
observer(obj) {
if (obj && typeof obj === 'object') {
// 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
for (let key in obj) {
this.defineRecative(obj, key, obj[key])
}
}
}
// obj: 需要操作的对象
// attr: 需要新增get/set方法的属性
// value: 需要新增get/set方法属性的取值
defineRecative(obj, attr, value) {
this.observer(value); // 递归遍历所有子属性
// 1,创建了属于当前属性的依赖收集对象
let dep = new Dep();
Object.defineProperty(obj, attr, {
get() {
if (Dep.target) { // 判断是否需要添加订阅者
dep.addSub(Dep.target); // 在这里添加一个订阅者
}
return value;
},
set: (newValue) => {
if (value !== newValue) {
// 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
this.observer(newValue);
value = newValue;
// 通知到视图更新
dep.notify();
console.log('监听到数据的变化');
}
}
})
}
}
将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
拓展:object.defineProperty()方法介绍:
(1)Object.defineproperty 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性
(2)接收是三个参数Object.defineproperty(obj, key, desc):
obj:要操作的对象
key:添加或修改的属性名
desc:配置项,一般是一个对象 :
配置对象中一般有6个可配置修改的属性
- writable:是否可重写
- value:当前属性对应的值
- get:读取时内部自动调用的函数
- set:写入时 内部自动调用的函数
- enumerable:是否可遍历
- configurable:是否可再次修改配置项
注意:当使用了getter或setter方法,不允许使用writable和value这两个属性(如果使用,会直接报错滴)
get 是获取值的时候的方法,类型为 function ,获取值的时候会被调用,不设置时为undefined
set 是设置值的时候的方法,类型为 function ,设置值的时候会被调用,undefined
get或set不是必须成对出现,任写其一就可以
Object.defineProperty缺点:
- 无法监听数组的变化
- 需要深度遍历,浪费内存
- 无法监听新增属性/删除属性(Vue.set Vue.delete,未在 data 中定义的属性会报 undefined)
2、消息订阅器(Dep):
订阅器Dep主要负责收集订阅者,然后在属性变化的是,然后在属性变化的时候通知订阅这执行更新函数。
function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
3、订阅者(Watcher):
订阅者Watcher在初始化的时候需要将自己添加到订阅器中。
首先我们知道监听器observer在get函数中执行了添加订阅者操作,所以我们需要在订阅者Watcher初始化的时候触发对应的get函数去执行 添加订阅者操作。触发get 函数的核心就是使用了Object.defineProperty( )进行数据监听。只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。
代码实现:
/**
* 订阅者
*
*/
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 缓存自己
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
4、模版解析器(Compiler):
解析器实现对vue各个指令模版的解析,生成抽象语法树,编译成Virtual DOM,渲染视图。
解析器的实现步骤:
- 将模板元素提取到内存中,方便将数据渲染到模板后,再一次性挂载到页面中
- 模板提取到内存后,使用 buildTemplate 函数遍历该模板元素.:
(1)元素节点: 使用函数检查元素上是否有v-开头的属性
(2)文本节点:检查文本中是否有{{}}内容 - 创建 CompilerUtil 类,用于处理vue指令和 {{}},完成数据的渲染
- 到此就完成了首次数据渲染,接下来需要实现:数据改变时,自动更新视图。
代码:
Compiler :
class Compiler {
constructor(vm) {
this.vm = vm;
// 1.将网页上的元素放到内存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的数据编译内存中的元素
this.buildTemplate(fragment);
// 3.将编译好的内容重新渲染会网页上
this.vm.$el.appendChild(fragment);
}
node2fragment(app) {
// 1.创建一个空的文档碎片对象
let fragment = document.createDocumentFragment();
// 2.编译循环取到每一个元素
let node = app.firstChild;
while (node) {
// 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存储了所有元素的文档碎片对象
return fragment;
}
buildTemplate(fragment) {
let nodeList = [...fragment.childNodes];
nodeList.forEach(node => {
// 需要判断当前遍历到的节点是一个元素还是一个文本
if (this.vm.isElement(node)) {
// 元素节点
this.buildElement(node);
// 处理子元素
this.buildTemplate(node);
} else {
// 文本节点
this.buildText(node);
}
})
}
buildElement(node) {
let attrs = [...node.attributes];
attrs.forEach(attr => {
// v-model="name" => {name:v-model value:name}
let { name, value } = attr;
// v-model / v-html / v-text / v-xxx
if (name.startsWith('v-')) {
// v-model -> [v, model]
let [_, directive] = name.split('-');
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node) {
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if (reg.test(content)) {
CompilerUtil['content'](node, content, this.vm);
}
}
}
工具类CompilerUtil:
let CompilerUtil = {
getValue(vm, value) {
// 解析this.data.aaa.bbb.ccc这种属性
return value.split('.').reduce((data, currentKey) => {
return data[currentKey.trim()];
}, vm.$data);
},
getContent(vm, value) {
// 解析{{}}中的变量
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
return this.getValue(vm, args[1]);
});
return val;
},
// 解析v-model指令
model: function (node, value, vm) {
new Watcher(vm, value, (newValue, oldValue)=>{
node.value = newValue;
});
let val = this.getValue(vm, value);
node.value = val;
// 看这里
node.addEventListener('input', (e)=>{
let newValue = e.target.value;
this.setValue(vm, value, newValue);
})
},
// 解析v-html指令
html: function (node, value, vm) {
// 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
new Watcher(vm, value, (newValue, oldValue) => {
node.innerHTML = newValue;
});
let val = this.getValue(vm, value);
node.innerHTML = val;
},
// 解析v-text指令
text: function (node, value, vm) {
// 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
new Watcher(vm, value, (newValue, oldValue) => {
node.innerText = newValue;
});
let val = this.getValue(vm, value);
node.innerText = val;
},
// 解析{{}}中的变量
content: function (node, value, vm) {
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
new Watcher(vm, args[1], (newValue, oldValue) => {
node.textContent = this.getContent(vm, value);
});
return this.getValue(vm, args[1]);
});
node.textContent = val;
}
}
5、总结
vue接收一个模板和data参数。
vue解析模版的时候渲染视图,初始化对行的订阅者。
遍历data数据的时候初始化发布者,利用Object.defineProperty函数在get 中触发订阅器中的添加订阅者方法。当数据更新的时候再set 函数中触发订阅器中的通知函数,通过订阅器的update 方法更新视图。
6、observer都在什么时候执行
在Vue.js中,observer(观察者)的执行时机与Vue实例的生命周期紧密相关。具体来说,observer的执行主要发生在Vue实例初始化的过程中,特别是与数据响应式系统的建立有关。
以下是observer执行的关键时刻:
Vue实例初始化:当创建一个新的Vue实例时,Vue会遍历实例的data对象中的每个属性,并使用Object.defineProperty来使这些属性变得“响应式”。这个过程是由observer来完成的。它为每个属性创建一个getter和setter函数,从而能够在属性值改变时触发相应的更新。
数据属性访问与修改:每当Vue实例中的数据属性被访问或修改时,observer也会参与其中。通过之前设置的getter和setter函数,observer能够追踪数据的变化,并在必要时通知依赖这些数据的watcher进行更新。
需要注意的是,observer本身并不是在某个特定的“时间”执行,而是与Vue实例的数据访问和修改操作紧密相连。换句话说,observer的执行是数据驱动和事件触发的,它会在数据发生变化时自动执行相应的操作。
总的来说,observer在Vue.js中扮演着建立和维护数据响应式系统的关键角色,它使得Vue实例能够在数据发生变化时自动更新相关的视图或计算属性。
7、Watcher实例创建情况:
计算属性(Computed Properties):当一个组件定义了计算属性时,每个计算属性都会对应一个Watcher实例。这些Watcher实例在组件初始化时被创建,用于监控计算属性所依赖的数据,并在这些数据变化时重新计算属性的值。
侦听器(Watchers):开发者可以在Vue组件中使用watch选项来定义侦听器,这些侦听器会监控指定的数据属性。当这些属性变化时,侦听器的回调函数会被触发。每个这样的侦听器都会对应一个Watcher实例,这些实例在组件初始化时根据watch选项的内容被创建。
模板渲染:在Vue组件的模板中使用的数据属性,也会被Vue监控。对于每个在模板中使用的响应式数据,Vue都会创建一个与之关联的Watcher实例,以确保当数据变化时能够更新DOM。这个Watcher通常被称为“渲染Watcher”,它在组件挂载(mount)过程中被创建。
由开发者显式创建:除了Vue自动为计算属性、侦听器和模板渲染创建的Watcher实例外,开发者也可以显式地创建Watcher实例,以便更灵活地监控数据变化并执行自定义逻辑。
总的来说,Watcher实例的创建时机取决于它们的使用场景。在Vue组件的生命周期中,这些Watcher实例主要在组件初始化、挂载以及开发者显式调用时被创建。
8、Dep.target被赋值的过程:
具体来说,以下是Dep.target被赋值的过程:
**创建Watcher实例:**当Vue需要观察某个数据的变化时(例如,为了计算属性、侦听器或组件的重新渲染),它会创建一个Watcher实例。
开始依赖收集:在Watcher实例化的过程中,或者在其开始监控某个数据之前,它会调用一个方法来收集依赖。这个方法通常会涉及到访问数据对象的属性,从而触发属性的getter函数。
**设置Dep.target:**在调用getter函数之前,Watcher会将自己设置为当前全局唯一的Dep.target。这是通过全局变量来实现的,确保同一时间只有一个Watcher在进行依赖收集。
触发getter函数:当Watcher尝试访问数据对象的属性时,会触发该属性的getter函数。由于此时Dep.target已经被设置为当前的Watcher实例,因此在getter函数内部可以访问到这个Watcher。
**收集依赖:**在getter函数内部,Vue会检查Dep.target是否存在。如果存在,说明当前有一个Watcher正在进行依赖收集,于是Vue会将这个Watcher添加到该属性的依赖列表(即Dep实例的subs数组)中。
**清除Dep.target:**依赖收集完成后,Dep.target会被清除或重置,以确保不会错误地将后续的依赖收集与当前的Watcher关联起来。
这个过程确保了当数据对象的属性值发生变化时,Vue能够知道哪些Watcher实例依赖了这个属性,并通知它们进行相应的更新。通过这种方式,Vue实现了数据的响应式更新机制。
3、vue3的双向绑定原理
,vue3通过proxy进行双向数据绑定,proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),可以通过使用ref和reactive对数据实现响应式。
Proxy可以理解成,在目标对象之前架设一层 “拦截”,当外界对该对象访问的时候,都必须经过这层拦截,而Proxy就充当了这种机制,类似于代理的含义,它可以对外界访问对象之前进行过滤和改写该对象。
const obj = new Proxy(target, handler)
被代理之后返回的对象 = new Proxy(被代理对象,要代理对象的操作)
handler中常用的对象方法如下:
- get(target, propKey, receiver)
- set(target, propKey, value, receiver)
- has(target, propKey)
- construct(target, args):
- apply(target, object, args)
reflect 对象(详细描述可见es6中的 reflect)
// 创建响应式
function reactive(target {}) {
if (typeof target !== 'object' || typeof target == null) {
return target // 不是对象或数组直接返回
}
// 代理配置
const proxyConf = {
get(targe, key, receiver) {
// 只处理本身(非原型)的属性
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.include(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver) // 返回不做处理
return reactive(result) // 递归调用,这里所做的优化是只在调用到对象深层次的属性时才会触发递归
}
set(target, key, val, receiver) {
// 重复的数组,不处理
if(val === target[key]) {
return true;
}
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.include(key)) {
console.log('set 已有的属性', key) // 监听
} else {
console.log('新增的属性', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
}
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return result
}
})
// 生成代理 对象
const observed = new Proxy(target, proxyConf)
return observed;
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: e
}
}
}
}
}
vue3的未完后续补充…
参考:
https://juejin.cn/post/6844903942254510087#heading-13
https://juejin.cn/post/7065967379095748638
https://blog.csdn.net/weixin_57677300/article/details/126278467
vue3参考
https://blog.csdn.net/weixin_38318244/article/details/123601856