Vue动态绑定原理简单入门
Vue动态绑定原理简单入门
在vue2中,整个响应式系统构建在js特性 Object.defineProperty
,可以说它是vue2的MVVM架构的核心。在vue3中,Object.defineProperty
被Proxy
替代。
Object.defineProperty
Object.defineProperty
是一个在ECMAScript 5(ES5)标准中引入的方法。主要的作用是定义属性是否可写、可枚举、可配置,以及定义属性的getter和setter。
一个简单的例子:
const obj = {
internalValue: 0
};
Object.defineProperty(obj, 'number', {
get() {
console.log('getter: 获取值');
return this.internalValue;
},
set(value) {
console.log('setter: 设置值为 ' + value);
this.internalValue = value;
},
enumerable: true,
configurable: true
});
obj.number = 37; // setter: 设置值为 37
console.log(obj.number); // getter: 获取值; 输出 37
Object.defineProperty
为obj定义了一个number属性,以及定义了这个属性的get方法和set方法。当我们调用obj.number的时候,js就会执行get方法,get方法返回值就是获取到的值;同样的执行obj.number = 123;的时候, set方法就会被调用。
这就是vue2中双向绑定数据的基础。一个简单的例子,用了观察者模式:
class Dep {
constructor() {
this.subscribers = [];
}
depend() {
if (Dep.target && !this.subscribers.includes(Dep.target)) {
this.subscribers.push(Dep.target);
}
}
notify() {
this.subscribers.forEach(sub => sub.update());
}
}
Dep.target = null;
class Watcher {
constructor(obj, key, cb) {
Dep.target = this;
this.cb = cb;
this.obj = obj;
this.key = key;
this.value = this.get(); // 这将触发依赖收集
Dep.target = null; // 收集完毕后清除
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.obj, this.value, oldValue);
}
get() {
return this.obj[this.key]; // 访问属性,触发 getter
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
dep.depend();
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify();
}
});
}
function observe(value) {
if (value && typeof value === 'object') {
Object.keys(value).forEach(key => {
defineReactive(value, key, value[key]);
});
}
}
// 假设的组件的 data
let data = { message: 'Hello Vue' };
// 响应式化
observe(data);
// 创建一个 Watcher 来观察 data.message
// 在这个例子中,我们简化了回调函数,只是简单地打印新旧值
new Watcher(data, 'message', (newVal, oldVal) => {
console.log(`data.message数据变化:${oldVal} -> ${newVal}`);
});
data.message = 'Hello Vue.js1';
const a = data.message
console.log(a)
上面的例子需要格外注意Dep.target,它是一个静态属性,目的是将Watcher添加到通知列表
执行流程:
创建Dep类
↓
创建Watcher类
↓
定义defineReactive函数
↓
定义observe函数
↓
初始化数据对象data
↓
调用observe(data) -> 使data成为响应式对象
| ↓
| 遍历data的属性,对每个属性调用defineReactive
| ↓
| 对每个属性创建一个Dep实例
| ↓
| 使用Object.defineProperty设置getter和setter
↓
创建Watcher实例 (依赖收集开始)
↓
Watcher构造函数执行
| ↓
| 设置Dep.target为当前Watcher实例
| ↓
| 调用Watcher的get方法 -> 读取data.message
| | ↓
| | 触发data.message的getter
| | ↓
| | getter内调用dep.depend() -> 收集依赖
| ↓
| 还原Dep.target为null (依赖收集结束)
↓
data.message值发生改变
↓
触发data.message的setter
| ↓
| setter内调用dep.notify() -> 通知变化
| ↓
| dep.notify遍历所有订阅者,调用它们的update方法
| ↓
| Watcher的update方法被调用
| ↓
| update内调用Watcher的get方法 -> 重新读取data.message
| ↓
| 调用回调cb -> 打印新旧值
↓
程序继续执行...
当执行了`data.message = 123` 的时候,马上触发get方法,get方法通知一个包含渲染方法的`Watcher`观察者,然后通过这个执行渲染方法更新Html页面上的数据。
当v-mode双向绑定数据的时候(v-model是语法糖,实际是绑定了一个事件,官网有解释),触发事件执行set方法,然后也是执行上面一样的流程
Proxy
在Vue3中,Object.definProperty被替换成`Proxy`, `Proxy`是 ES6 中引入的一个新特性,它允许你创建一个对象的代理,从而可以拦截并自定义对象的基本操作,例如属性访问、赋值、枚举、函数调用等。因为 Proxy
可以拦截整个对象的操作,所以 Vue 3 不需在初始化时遍历对象的每个属性。这样可以减少初始化响应式系统时的开销。
let target = {};
let handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37; // 如果属性不存在,返回37
}
};
let p = new Proxy(target, handler);
console.log(p.a); // 输出 37,因为 'a' 不是 target 的属性, 实际上执行了handler的get方法
模拟vue3的简单例子
简化的 Vue 3 响应式系统的例子:
import { reactive, effect } from 'vue';
// reactive 用于创建响应式对象
const state = reactive({ count: 0 });
// effect 接收一个函数,这个函数会立即执行一次
// 并且当函数中依赖的响应式数据变化时,函数会再次执行
effect(() => {
console.log(state.count);
});
state.count++; // 修改数据,effect 中的函数会自动重新执行
一个简单的观察者模式来模拟实现这种效果:
function reactive(target) {
const handler = {
get(target, prop, receiver) {
const result = Reflect.get(target, prop, receiver);
track(target, prop); // 收集依赖
return result;
},
set(target, prop, value, receiver) {
const oldValue = target[prop];
const result = Reflect.set(target, prop, value, receiver);
if (oldValue !== value) {
trigger(target, prop); // 触发更新
}
return result;
}
};
return new Proxy(target, handler);
}
// 存储副作用函数的依赖关系
const targetMap = new WeakMap();
function track(target, key) {
// 在实际的 Vue 3 中,会有一个全局的活跃的 effect
// 这里我们简化为一个全局变量
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (depsMap) {
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effect => {
effect();
});
}
}
}
let activeEffect = null;
function effect(eff) {
activeEffect = eff;
activeEffect();
activeEffect = null;
}
// 使用示例
const state = reactive({ count: 0 });
effect(() => {
console.log(state.count);
});
state.count++; // 控制台输出:1
state.count++; // 控制台输出:2
effect
类似于 Vue 2 中的 Watcher
。当 state.count
被读取时,读取操作被 Proxy
拦截,effect
被订阅到这个状态的变化上。当 state.count
被修改时,修改操作同样被 Proxy
拦截,并通知到所有订阅了这个状态的 effect
,从而引起重新执行和更新。
它的执行流程如下:
[创建响应式对象]
↓
[reactive(target)] --> 返回 Proxy 对象
↓
[执行 effect 函数]
↓
[副作用函数 eff 执行]
│
├─[读取 Proxy 属性] --> [触发 get 陷阱]
│ ↓
│ [track 依赖收集]
│
└─[设置 Proxy 属性] --> [触发 set 陷阱]
↓
[trigger 触发更新]
↓
[重新执行相关的副作用函数 eff]
reactjs中也有一个useEffect函数,也是监听数据变化,然后执行特定的方法,不过用在系统调用和外部api调用比较多
与React.js的对比
React 通过状态更新引发组件的重新渲染来响应数据的变化,需要显式调用 setState 来触发。而 Vue.js 使用数据劫持机制,自动侦测数据变动并更新 DOM。
React 倾向于使用不可变数据,Vue.js 的数据是可变的,它依赖于其响应式系统来跟踪变化.
#vue(1)评论