Vue 3 会为 data 建立一个 Proxy
物件,并在里面建立 getter
和 setter
来取值和更新值,藉此实现响应式。因此不用直接操作原本的 data,而是操作代理 data 的 Proxy 的物件。相反, Vue 2 因为使用 Object.defineProperty
来为 data 的最外层每一个属性加上 getter
和setter
,所以 Vue 2 是直接修改 data。
同时,因为现在是操作 Proxy
,所以即使 data 里有多层物件资料,我们仍然可以透过 Proxy
里建立的 setter
来修改多层物件资料。但 Vue 2 就不能,因为 Vue 2 只是为 data 的最外层属性逐一加上 getter
和 setter
,因此内层的资料并没有 setter
,无法被正确修改。
以下会详细解说 Vue 3 如何使用 Proxy 。承接上一篇讨论 Vue 实现响应式(reactivity)的原理。这篇集中整理有关 Vue 3 响应式的原理。内容主要是参考 Vue 3 官方教学影片以及文件,并以个人理解和整理来作解说。
Object.defineProperty()
逐一加上 get
和 set
Proxy 是代理的意思。就像在你要操作的资料前放置一个栏截器,每次操作资料时,都会先跑在 Proxy 里的程序。以下是一个简单例子:
const target = {
user: {
name: "Alysa",
jobTitle: "front-end developer",
},
};
const p = new Proxy(target, {
get(target, key) {
return target[key];
},
set(target, key, value) {
// key 是 name, value 是 Tom
target[key] = value;
return true;
},
});
p.user.name = "Tom"; // 触发 set
console.log(p.user.name); // Tom
透过建立一个 Proxy 物件,并指向代理 target
这个物件。之後触发 setter
来修改 user
里的 name
。
在讨论如何用 Proxy 实现响应式之前,简单重温要实现响应式所需要解决的问题。这一部分在上一篇已经提过,在这里再简单总结一次。重用上一篇的例子。假设现在有以下情景:
const data = {
price: 100,
quantity: 2,
}
const total = () => data.price * data.quantity
console.log(total()) // 200
当我修改了 price
,照理 total
也要随之然更新,所以 total
也要被重新执行,以下是错误示范:
let total;
const data = {
price: 100,
quantity: 2,
}
total = data.price * data.quantity
console.log(total) // 200
data.price = 200
console.log(total) // 仍然是 200,但我们期望是 400
正确做法是再次计算 total
的公式,才可以更新 total
:
let total;
const data = {
price: 100,
quantity: 2,
}
total = data.price * data.quantity
console.log(total) // 200
data.price = 200
// 再计算一次,才会更新 total
total = data.price * data.quantity
console.log(total) // 400
以上简单例子,可见所谓的响应式,要达成两件事才可以完成:
price
值的元件第一点,Vue 3 与 Vue 2 的原理是相同。同样是在修改值时,就会触发 set
。并在 set
里执行更新旧值的动作。但是, Vue 2 是在 Object.definedProperty()
里的所定义的 set
,而 Vue 3 就是在 Proxy
的 handler
函式里定义 set
。
const data = {
price: 100,
quantity: 2,
}
const proxyData = new Proxy(data, {
get(target, key, receiver) {
//...(先作省略)执行把依赖储存起来的程序码
let result = Reflect.get(target, key, receiver)
return result
},
set(target, key, value, receiver){
// 以下会回传布林值
let bol = Reflect.set(target, key, value, receiver)
//...(先作省略)执行所有依赖,更新所有受这依赖影响的值
// set handler 规定要回传 true
return bol
}
})
proxyData.price = 1000 // 触发 set()
console.log(proxyData.price) // 触发 get()。结果是 1000
以上就是 Vue 3 使用 Proxy
来实现更新的概念。利用 Proxy
把 data
包起来,并在 handler
里使用 set
和 get
。所以,每次我们操作 data
时,我们是操作由 Vue 产生出来的 Proxy
物件,而非直接修改本身data
这个物件,而它本身亦没有响应式(reactive)。拥有响应式的是经过 Vue 包装出来的Proxy
物件。相反,Vue 2 是直接修改 data
这物件,而且把它变成响应式。
另外,也补充说明Reflect
以及receiver
参数。Reflect
是等同於使用.
或 []
来访问物件:
Reflect.get(data, 'price')
// 等於
data.price
Reflect.set(data, 'price', 1000)
// 等於
data.price = 1000
receiver
参数并非必需。 只是为了确保当我们所访问的物件,如果它本身有继承其他物件时,this
是指向仍然是该物件本身。详细解说可参考这篇文章提及的 Reflect 和 receiver 例子。因此,Vue 使用了 Reflect
来取值和修改值,而非一般像是obj.a
或是 obj['a']
这样的写法。
price
值的元件第二点,因为 price
改变了,所以 total
就要重新计算,换言之所有用到 total
的元件都要重新渲染。但 Vue 怎麽知道哪一个元件有用到 total
这个值?同时又怎样知道当 price
更新时, total
要随之然更新?
这些问题,不论是 Vue 2 还是 Vue 3 所用的手法虽然不相同,但是概念是相同。概念一样是:
total = data.price * data.quantity
收集起来。data
里的属性做记录。记录它们会牵涉到哪些依赖。所以,当某个 data
里的属性的值更新时(即是 price
),就会按这个纪录,找出此值连带的所有依赖程序码,并重新执行这堆依赖(即是 total = ...
),从而更新所有有牵涉到此属性的值 (即是 total
)。第一点,Vue 2 的做法是把这些依赖都存放在每个 data
属性里所建立的 Dep
实体。但 Vue 3 的做法是用 Map
来存放。概念如下图:
值得一提是 Vue 3 是使用 Set
来存放所有依赖,Set
物件里只会存放唯一值,里面不会有重复的值。所以里面不会有重复的依赖。
第二点,Vue 2 与 3 的原理是一样。一样是透过触发 set
,并在 set
里面把之前储存起来的依赖取出来,并重新跑一次。但因为第一点提到,记录的手法不一样了,所以实际上程序码也会不一样。
Vue 2 是触发 dep.depend()
来储存依赖,并用 dep.notify()
来执行依赖并更新值。但 Vue 3 是触发 track()
来达成前者,trigger()
来达成後者。
接下来看看 trigger()
和 track()
的程序码及其概念:
// 储存不同响应式物件,例如是 "data"
const targetMap = new WeakMap()
// 触发 get 时会跑 track,储存依赖:
function track(target, key) {
// 取得 depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果还没建立 depsMap,就建立一个
targetMap.set(target, (depsMap = new Map()))
}
// 取得对应 dep 实体
let dep = depsMap.get(key)
if (!dep) {
// 如果没有 dep 实体,就建立一个
depsMap.set(key, (dep = new Set())) // Create a new Set
}
// 把依赖储存起来
dep.add(effect)
}
// 触发 set 时会跑 trigger,执行所有依赖来更新所有值:
function trigger(target, key) {
// 取得 depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 取得对应的 dep 实体
let dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
// 跑一次所有依赖
effect()
})
}
}
所以,如果把之前讨论到的 Proxy
部分,以及以上的track()
和 trigger()
合并起来,就是 Vue 3 实现响应式的概念。
https://codepen.io/alysachan/pen/VwWLGGJ
Proxy
来为资料实现响应式(reactivity),并在里面建立 setter
和 getter
。setter
里解决。第二点,涉及到 Vue 怎麽把依赖储存起来,以及怎样重新执行这些依赖。Vue 3 是使用 Map
、Set
,以及 dep
实体来储存依赖。当值被修改时,就储透过查找Map
,去找出对应的 dep
实体,把这实体里的所有依赖跑一次,从而更新所有受影响的值。最後,这篇文章只是简单示范了 Vue 3 响应式的概念,实际上 Vue 在实行此概念时是更加复杂。
Proxy and Reflect
一起来了解 Javascript 中的 Proxy 与 Reflect
>>: Day 15 - LocalStorage and Event Delegation
Scrum Kanban(看板) 有部美剧叫Silicon Valley (矽谷群瞎传),由HBO出...
import { useEffect, useState, useRef } from "...
If AOL is not working on your iPhone, you can try ...
Virtual Judge ZeroJudge 题意 输入第一组旅行团人数 S,输出酒店内第 D ...
在第17天,做了一个ISBN 查询 书本的书名、书本描述 那麽今天我希望做一个使用拍照扫描辨识出IS...