什么是Vue中的响应式
Vue中的响应式,简而言之就是当数据发生变化时,页面跟随变化。使用过Vue的v-model都有比较深刻的感受,我们在代码中修改双向绑定的数据后,页面上的数据也会自动更新,页面跟随变化
我们看个例子:
<div id="app">
<div>价格:¥{{ price }}</div>
<div>支付金额:¥{{ price * quantity }}</div>
<button @click="changePrice">改变价格</button>
</div>
var app = new Vue({
el: '#app',
data() {
return {
price: 5.0,
quantity: 2
};
},
methods: {
changePrice() {
this.price = 10;
}
}
})
在这个例子中,我们调用changePrice方法,修改price的值,页面上价格 、支付金额,都会自动改变。这就是Vue中的响应式
Vue中如何实现响应式
当数据发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?
想完成这个过程,我们需要完成这几个步骤:
(1)侦测数据的变化(数据劫持 / 数据代理)
(2)收集视图依赖了哪些数据(依赖收集)
(3)数据变化时,自动“通知”需要更新的视图部分,并进行更新(发布订阅模式)
1.侦测数据的变化(数据劫持 / 数据代理)
在Javascript中,如何侦测一个对象的变化?我们有两种办法可以侦测到变化:使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。
在Vue2中,使用的是Object.defineProperty的方式侦测数据的变化,
类似如下代码,当属性被读取或设置时,相应的getter或setter将被调用。修改obj.value的值时,会监听到obj.value,执行set方法,执行console.log(obj.value)时,又会调用get方法。
<script>
const obj = {};
// 定义属性'value',包含getter和setter
Object.defineProperty(obj, "value", {
get() {
console.log("get value");
return this._value;
},
set(newValue) {
console.log("set value");
this._value = newValue;
},
// 可以通过这个属性来控制属性的可枚举性
configurable: true,
// 可以通过这个属性来控制属性的可写性
enumerable: true,
});
obj.value = 5;
console.log(obj.value); // get value ,set value, 5
</script>
但在Vue3中,使用的是ES6的Proxy的方式侦测数据的变化。
Proxy对象允许你拦截并自定义对象的基本操作,包括属性访问和修改。这使得你可以创建一个代理对象,当访问或修改目标对象的属性时,会触发预定义的行为。如下代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>基础 ref()</title>
</head>
<body>
<button id="updateButton">点击+1</button>
<div id="message"></div>
<script>
const createRef = (initialValue) => {
return new Proxy(
{ value: initialValue },
{
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value;
if (key === "value") {
updateDisplay(); // 当.value被设置时,更新DOM
}
return true;
},
}
);
};
// 初始化ref
const numberRef = createRef(0);
// 更新DOM的函数
const updateDisplay = () => {
document.getElementById("message").innerText = numberRef.value;
};
// 绑定按钮点击事件
document.getElementById("updateButton").addEventListener("click", () => {
numberRef.value++; // 点击按钮时计数器加一
});
// 初始显示
updateDisplay();
</script>
</body>
</html>
2.收集视图依赖了哪些数据(依赖收集)
当模板渲染或计算属性计算时,Vue 会追踪哪些数据被访问了。这通过 Dep 类和 Watcher 类完成。Watcher 会在读取数据时将自身添加到数据的依赖列表中。
订阅者 Dep
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。
于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
Dep的简单实现
class Dep {
constructor () {
/* 用来存放Watcher对象的数组 */
this.subs = [];
}
/* 在subs中添加一个Watcher对象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher对象更新视图 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
以上代码主要做两件事情:
用 addSub 方法可以在目前的 Dep 对象中增加一个 Watcher 的订阅操作;
用 notify 方法通知目前 Dep 对象的 subs 中的所有 Watcher 对象触发更新操作
观察者 Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖。
watcher的简单实现:
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向自己
// 然后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 获得新值
this.value = this.obj[this.key]
// 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
this.cb(this.value)
}
}
3.数据变化时,自动“通知”需要更新的视图部分,并进行更新(发布订阅模式)
当数据被修改时,对应的 Watcher 会收到通知,并触发视图更新