在 Vue 2,我们需要使用 .set()
等 Vue 语法来修改在 data 里的物件或阵列资料里的值。这是因为 Vue 2 是使用 Object.defineProperty()
实现响应式(reactivity)。在此机制下,Vue 只会为 data 里最外层的属性加上 getter
和 setter
,因此只会侦测到最外层资料的变动,无法侦测到下一层的资料,并作出更新。
以下会再详细解说当中原理。
这篇文章会针对 Vue 2 作解释,下一篇才会讨论 Vue 3。这里的概念主要是参考官方教学影片和文件,并稍微修改例子便於说明。
当我们在写 Options API 时,我们会把页面用到的资料通通放在data
里,当资料有变动,并一并更新有用到该资料的画面。这是因为 Vue 2 和 Vue 3 分别使用了JavaScript 的 Object.defineProperty()
和 Proxy
方法来完成。
在 Vue 2 里,之所以无法实现响应式来更新资料,原因通常有这两个:
.
、[]
来改变物件的属性,以及用 .length
改变阵列的长度、用[]
指定阵列的索引来改变阵列中的某个值第一种情况很易理解,就是建立 Vue 实体时,没有把资料写在data
属性里。详情见官方文件就很快能理解。
第二种情况,就是新手刚刚写 Vue 时所犯的错。而 Vue 官方文件也有说明,Vue 不能侦测阵列或物件的变化。
以下面的资料作例子,示范一些错误的写法:
data() {
return {
obj: {
a: 1,
},
arr: [1, 2, 3],
};
},
修改物件:
this.obj.b = 2 // 结果是 obj 不会新增 b 属性
delete this.obj.a; // 结果是 a 属性不会被删除
修改阵列:
this.arr[0] = 100 // 结果是 arr[0] 仍然是 1
this.arr.length = 1 // 结果是 arr 仍然是 [1,2,3]
对於以上情况,Vue官方提供了 Vue.set()
、 this.$set()
等方法来解决。以下是正确的写法:
修改物件:
Vue.set(this.obj, 'b', 2)
Vue.delete(this.obj, 'a')
修改阵列:
// 修改阵列中某个值
Vue.set(this.arr, 0, 100)
// 或
this.arr.splice(0, 1, 100);
// 截短阵列
this.arr.splice(1);
当我们平常在 data
物件里写上需要实现响应式的资料时,Vue 就会把 data
里的所有属性都跑一遍,透过Object.defineProperty()
,在 data
里为这些属性逐一加上getter
和setter
。
简单说明如下,例如有一件 T-shirt 商品的资料:
// 想像为 Vue 里面的 data 属性
let data = {
price: 100,
quantity: 2,
sizes: ["XS", "S", "M", "L", "XL"],
info: {
title: "纯色T-shirt",
color: "白色",
},
};
// 为每个属性加上getter、setter
Object.keys(data).forEach((key) => {
let internalValue = data[key];
Object.defineProperty(data, key, {
get() {
console.log(`Get ${key}: ${internalValue}`);
return internalValue;
},
set(newValue) {
console.log(`Set ${key} from ${internalValue} to ${newValue}`);
internalValue = newValue;
},
});
});
data.price = 200;
// Set price from 100 to 200
console.log(data.price);
// Get price: 200
// 200
以上只是一个简化版本,说明官方文件所指的「Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。」是什麽意思。
虽然以上例子还不完整,因为在 getter
和 setter
里,其实 Vue 还会执行其他函式来更新资料。虽然在这里还没作解释,但我们现在至少可以知道,Vue 只会为data
最外层的属性逐一加上getter
和setter
。当该值是一个阵列或物件,它们里面的值是不会被加上getter
和setter
。
以下示范用之前错误的写法,就会发现这样写法只会触发getter
,没有setter
:
// Get sizes: XS,S,M,L,XL
// 没触发 set 函式来更新 sizes
data.sizes[0] = 'XXS'
// Get info: [object Object]
// 没触发 set 函式来更新 info
data.info.color = '黑色'
以下写法才会触发set
,原因刚才已提过,因为只有最外层的属性才会有set
函式:
data.sizes = ['XSS', 'S', 'M', 'L', 'XL']
data.info = {
title: '纯色T-shirt',
color: '黑色',
}
为什麽一定要执行set
才行?因为 Vue 是不只是依赖getter
,还有在setter
里的其他程序来实现更新资料,实现响应式。所以如果没有正确透过 setter
来写入资料,就没法达成响应式。从下图可以见到setter
的作用。
图片来源:https://cn.vuejs.org/v2/guide/reactivity.html
以上概念可见,一定要由setter
去更新资料。
Vue 的官方影片有很详细示范如何用程序码实现上图的概念。以下会以官方例子稍作简化,并用我自己的了解去做总结。
当我们谈及响应式时,我们需要完成两项功能,才能真正达成响应式。举例说,我修改了price
这个值後,我就要做以下两件事:
data
里的 price
这个属性的值price
值的元件第一点很简单,当我写data.price = 200
时,我就会期望 data
里的 price
属性的值会由 100 改为 200。
第二点,举例说,我的元件某部分,以const total = data.price * data.quantity
来计算总额。这段程序码涉及了 price
这个值。因此total
也照理需要被更新,换言之,total
需要被重新计算。
data
里的 price
值要更新在 data
里的值,就是用上文提到的 setter
去处理。
price
值的元件在重新渲染所有有涉及到 price
值的元件前,我们需要知道哪些元件有用到 price
这个值。Vue 的做法就用 Watcher
函式记录下来。Vue 在建立元件的时候,会为这元件建立相应的 Watcher
函式,把此元件所有「依赖」(dependency) 的程序码都纪录下来。在我们的例子中,就是为根元件新增Watcher
函式,并在里面记录此元件使用了total = data.price * data.quantity
这段程序码。
let total, target
// 先不用理解这个 watcher 函式的内容
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
total = data.price * data.quantity;
});
先不用理解这个 watcher 函式的内容,我们先知道 Vue 会为此元件建立 watcher 来记录依赖即可。
当我们执行最後那一段 watcher
函式时,里面的data.price
以及 data.quantity
就会分别触发 price
和 quantity
里的 getter
函式。因为上文提及过,每个 data 属性的值,都会有 getter
这个函式。
而在 getter
里,Vue 就会把那些依赖的程序码储存起来,示范如下:
let target, total;
// Dep 实体
class Dep {
constructor() {
this.subscribers = [];
}
// 4. 当触发 getter 时,就会执行 depend()
depend() {
if (target && !this.subscribers.includes(target)) {
// 5. 把依赖的程序码储存起来
this.subscribers.push(target);
}
}
}
Object.keys(data).forEach((key) => {
let internalValue = data[key];
const dep = new Dep();
// 1. 使用 Object.definProperty 为 data 的每个值加上 get 和 set
Object.defineProperty(data, key, {
get() {
// 3. 把依赖收集起来
dep.depend();
return internalValue;
},
set(newValue) {
internalValue = newValue;
},
});
});
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
// 2. 触发 price 和 quantity 里的 getter
total = data.price * data.quantity;
});
以上程序码中,加上了 Dep
这个实体。在这实体里,我们把传进来的依赖储存在 subscribers
里。为什麽我们要储存这个依赖?因为我们的初衷是,当 price
有变动时,total
就要被重新计算。做法就是把所有用到 price
的依赖都储存起来,当侦测到 price
有变动时,我们就会跑一次此值所用到的所有依赖,也就是概念图中提到的 "Notify" 步骤,示范如下:
let data = {
...
};
let target, total;
// Dep 实体
class Dep {
constructor() {
this.subscribers = [];
}
// 4. 当触发 getter 时,就会执行 depend()
depend() {
if (target && !this.subscribers.includes(target)) {
// 3. 把依赖收集起来
this.subscribers.push(target);
}
}
notify() {
// 7. 跑一次之前储存在 subscribers 里的依赖,更新资料
this.subscribers.forEach((sub) => sub());
}
}
Object.keys(data).forEach((key) => {
let internalValue = data[key];
const dep = new Dep();
// 1. 使用 Object.defineProperty 为 data 的每个值加上 getter 和 setter
Object.defineProperty(data, key, {
get() {
// 3. 把依赖收集起来
dep.depend();
return internalValue;
},
set(newValue) {
internalValue = newValue;
// 6. 触发 notify
dep.notify();
},
});
});
function watcher(myFunc) {
target = myFunc;
target();
target = null;
}
watcher(() => {
// 2. 触发 price 和 quantity 里的 getter
total = data.price * data.quantity;
});
console.log(total); // 200
// 5. 修改 price
data.price = 200;
console.log(total); // 400
当我们修改 price
时,关键就在於 Dep
里的 notify()
函式。透过执行 notify()
,把之前储存在 Dep
实体的 subscribers
里的依赖,即是 total = data.price * data.quantity
跑一次,就能重新计算 total
,达成响应式的效果。
https://codepen.io/alysachan/pen/MWoYvGG
Vue.set()
或Vue.delete()
等语法。Object.defineProperty
,把在 data
物件里最外层的属性逐一加上 getter
和 setter
。getter/setter
、Watcher
函式、Dep
实体来处理。当修改一个值时,会触发setter
来更新该值。Watcher()
函式,把所有依赖(dependency)都记录起来,利用Dep
实体里的depend()
的方法,储存到对应的 Dep
实体里。当该值出现变化时,就会触发setter
,以及执行在Dep
里的 notify()
,重新执行所有储存起来的「依赖」,从而得出所有需要更新的值,并重新渲染到画面,达成响应式的效果。JavaScript Reactivity Explained Visually
没百度帐号,想下载百度档案,,请问可以帮我吗? 万分感谢 ...
这是我第一次参加铁人赛,如果内容有不清楚或有误欢迎大家指正,影片皆是我自学Unity并且规划的教学内...
属性列举与原型的关系 自订原型 与 原生原型 最大的不同是在可列举(enumerable)的部分 原...
CSS的选择器分为基础选择器以及复合选择器 本日将将继续说明复合选择器 复合选择器可以更准确更高效的...
创建App-联络客服 由於本App的联络客服功能的延伸界面使用最简介的方式来设计,因为本App的原始...