vue3的双向绑定是通过使用ES6新增的Proxy对象来实现的。Proxy对象可以拦截对象的访问和修改操作,并在这些操作发生时执行自定义的函数。这样,vue3可以在数据对象被读取或修改时,触发相应的响应函数,从而实现数据和视图的同步更新。
具体来说,vue3会创建一个Proxy对象,作为数据对象的代理。Proxy对象会定义一些handler函数,用来拦截数据对象的get和set操作。当数据对象被读取时,Proxy对象会执行get函数,返回数据对象的值,并同时收集依赖该数据的视图组件。当数据对象被修改时,Proxy对象会执行set函数,更新数据对象的值,并同时通知依赖该数据的视图组件进行更新。
这种方式比vue2使用Object.defineProperty方法来实现双向绑定有一些优势,例如:
Proxy对象可以拦截整个对象,而不是单个属性,这样可以减少遍历和重写属性的开销。
Proxy对象可以拦截数组和嵌套对象的变化,而不需要额外的处理。
Proxy对象可以支持更多的操作,例如delete、has、ownKeys等,而不仅限于get和set。
响应式系统
Vue3的响应式系统是一种基于Proxy代理和effect函数的机制,它可以让数据和视图保持同步,实现交互效果。Vue3的响应式系统主要由三个部分组成:reactive,effect和track/trigger。reactive函数可以把一个普通的对象转化为一个响应式的对象,effect函数可以创建一个副作用函数,并执行该函数,track/trigger函数可以实现依赖收集和依赖触发的功能。
reactive函数
reactive函数接收一个普通的对象作为参数,然后返回一个Proxy代理对象。Proxy代理对象是一种特殊的对象,它可以拦截对原始对象的所有操作,比如读取或修改属性。Proxy代理对象有两个重要的函数:get和set。get函数会在我们访问原始对象的属性时触发,set函数会在我们修改原始对象的属性时触发。
effect函数
effect函数接收一个函数作为参数,然后执行该函数,并把该函数添加到一个栈中。这个栈叫做activeReactiveEffectStack,它用来存储当前正在执行的effect函数。effect函数通常用来更新视图或者执行一些副作用操作,比如发送请求或者打印日志等。
track/trigger函数
track/trigger函数是Vue3响应式系统的核心功能,它们可以实现依赖收集和依赖触发的功能。依赖收集就是把数据和视图之间的关系记录下来,依赖触发就是根据数据的变化来更新视图或者执行其他操作。
总结
当我们在effect函数中访问Proxy代理对象的属性时,就会触发get函数,这时就会调用track函数,把当前的effect函数添加到targetMap中。targetMap是一个Map对象,它存储了每个响应式对象及其对应的属性和依赖。这样就完成了依赖收集。
当我们修改Proxy代理对象的属性时,就会触发set函数,这时就会调用trigger函数,执行targetMap中存储的所有依赖于该属性的effect函数。这样就完成了依赖触发。
通过这样的机制,Vue3可以实现自动收集视图更新对应的依赖部分,当数据改变后,视图自动更新。
简单实现vue3双向绑定
- 创建一个vue.js 文件
// 存储effect函数的栈
const activeReactiveEffectStack = [];
// 把一个普通的对象转化为一个响应式的对象
function reactive(target) {
// 创建一个Proxy代理
const observed = new Proxy(target, {
// 设置get函数
get(target, key, receiver) {
// 获取属性值
const result = Reflect.get(target, key, receiver);
// 如果当前有effect函数,则添加到activeReactiveEffectStack中
if (activeReactiveEffectStack.length > 0) {
track(target, key);
}
return result;
},
// 设置set函数
set(target, key, value, receiver) {
// 设置属性值
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (result && oldValue !== value) {
// 触发更新
trigger(target, key);
}
return result;
}
});
return observed;
}
// 存储响应式对象及其对应的属性和依赖
const targetMap = new Map();
// 把当前的effect函数添加到targetMap中
function track(target, key) {
// 获取当前的effect函数
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
if (effect) {
// 获取target对应的Map对象
let depsMap = targetMap.get(target);
if (!depsMap) {
// 如果不存在,则创建一个新的Map对象,并添加到targetMap中
depsMap = new Map();
targetMap.set(target, depsMap);
}
// 获取key对应的Set对象
let dep = depsMap.get(key);
if (!dep) {
// 如果不存在,则创建一个新的Set对象,并添加到depsMap中
dep = new Set();
depsMap.set(key, dep);
}
// 把effect函数添加到Set对象中
dep.add(effect);
}
}
// 执行targetMap中存储的所有依赖于某个属性的effect函数
function trigger(target, key) {
// 获取target对应的Map对象
const depsMap = targetMap.get(target);
if (depsMap) {
// 获取key对应的Set对象
const dep = depsMap.get(key);
if (dep) {
// 遍历Set对象,执行每个effect函数
dep.forEach(effect => {
effect();
});
}
}
}
// 创建一个effect函数,并执行传入的函数
function effect(fn) {
// 把fn添加到activeReactiveEffectStack中
activeReactiveEffectStack.push(fn);
// 执行fn
fn();
// 把fn从activeReactiveEffectStack中移除
activeReactiveEffectStack.pop();
}
// 编译模板
function compile(el) {
// 获取根元素
let element = document.querySelector(el);
if (element) {
// 遍历根元素的子节点
compileNodes(element.childNodes);
}
}
// 遍历子节点
function compileNodes(nodes) {
[...nodes].forEach(node => {
if (node.nodeType === 1) {
// 如果是元素节点,解析指令
compileElement(node);
} else if (node.nodeType === 3) {
// 如果是文本节点,解析插值表达式
compileText(node);
}
// 如果有子节点,递归遍历
if (node.childNodes && node.childNodes.length > 0) {
compileNodes(node.childNodes);
}
});
}
// 解析元素节点中的指令
function compileElement(node) {
// 获取所有属性
let attrs = node.attributes;
[...attrs].forEach(attr => {
// 获取属性名和属性值
let attrName = attr.name;
let attrValue = attr.value;
if (attrName === 'v-model') {
// 如果是v-model指令,创建一个effect函数,并绑定input事件监听器
effect(() => {
node.value = data[attrValue];
});
node.addEventListener('input', e => {
data[attrValue] = e.target.value;
});
} else if (attrName.startsWith('v-on:')) {
// 如果是v-on指令,获取事件名和方法名,并绑定事件监听器
let eventName = attrName.slice(5);
let methodName = attrValue;
node.addEventListener(eventName, methods[methodName].bind(this));
}
});
}
// 解析文本节点中的插值表达式
function compileText(node) {
// 获取文本内容,并匹配插值表达式
let text = node.textContent;
let reg = /\{\{(.+?)\}\}/g;
let match = reg.exec(text);
if (match) {
// 获取第一个插值表达式的内容,并去除两端的空格
let exp = match[1].trim();
// 创建一个effect函数
effect(() => {
node.textContent = text.replace(match[0], data[exp]);
});
}
}
- 在页面中引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>双向绑定演示</title>
<!-- 引入vue.js文件 -->
<script src="vue.js"></script>
</head>
<body>
<div id="app">
<p>姓名:{{name}}</p>
<p>年龄:{{age}}</p>
<input type="text" v-model="name">
<input type="number" v-model="age">
<button v-on:click="sayHello">打招呼</button>
</div>
<script>
// 创建一个响应式对象
const data = reactive({
name: '张三',
age: 18
});
// 创建一个methods对象,存储方法
const methods = {
sayHello() {
alert(`你好,我叫${data.name},我今年${data.age}岁`);
}
};
// 编译模板
compile('#app');
</script>
</body>
</html>