Vue.js響應(yīng)式原理回顧
- Proxy對象實現(xiàn)屬性監(jiān)聽
- 多層屬性嵌套,在訪問屬性過程中處理下一級屬性
- 默認(rèn)監(jiān)聽動態(tài)添加的屬性
- 默認(rèn)監(jiān)聽屬性的刪除操作
- 默認(rèn)監(jiān)聽數(shù)組索引和 length屬性
- 可以作為單獨的模塊使用
核心方法
- reactive/ref/toRefs/computed
- effect watch/watchEffect是vue3 runtime.core中實現(xiàn)的岛蚤,內(nèi)部使用effect底層函數(shù)
- track 收集依賴
- trigger 觸發(fā)更新
響應(yīng)式系統(tǒng)原理——Proxy
Proxy和Reflect是ES6 為了操作對象而提供的新 API
proxy中有兩個需要注意的地方:
-
set 和 deleteProperty 中需要返回布爾類型的值
<script> 'use strict' // set 和 deleteProperty 中需要返回布爾類型的值 // 在嚴(yán)格模式下兵多,如果返回 false 的話會出現(xiàn) Type Error 的異常 const target = { foo: 'xxx', bar: 'yyy' } // Reflect.getPrototypeOf()相當(dāng)于Object.getPrototypeOf() const proxy = new Proxy(target, { // receiver代表當(dāng)前的的Proxy對象或者繼承Proxy的對象 get (target, key, receiver) { // return target[key] // Reflect反射牌捷,代碼運行期間獲取對象中的成員 return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { // target[key] = value // Reflect.set設(shè)置成功返回true 設(shè)置失敗返回false return Reflect.set(target, key, value, receiver) }, deleteProperty (target, key) { // delete target[key] return Reflect.deleteProperty(target, key) } }) proxy.foo = 'zzz' // delete proxy.foo </script>
如果set和deleteProperty返回false時,頁面會報錯
image-20210414080553080.png -
Proxy 和 Reflect 中使用的 receiver指向
// Proxy 中 receiver:Proxy 或者繼承 Proxy 的對象 // Reflect 中 receiver:如果 target 對象中設(shè)置了 getter桐绒,getter 中的 this 指向 receiver const obj = { get foo() { console.log(this) return this.bar }, } const proxy = new Proxy(obj, { get(target, key, receiver) { if (key === 'bar') { return 'value - bar' } return Reflect.get(target, key, receiver) }, }) console.log(proxy.foo)
不傳遞receiver時,可以看到this返回的是obj對象聚唐,proxy.foo返回undefined
image-20210414080743227.png當(dāng)傳遞了receiver時辫继,this指向Proxy對象
image-20210414080825068.png
響應(yīng)式系統(tǒng)原理——reactive
接收一個參數(shù),判斷這參數(shù)是否是對象锌半,不是直接返回,只能轉(zhuǎn)換對象為響應(yīng)式對象
創(chuàng)建攔截器對象handler,設(shè)置get/set/deleteProperty
-
返回Proxy 對象
// reactivily/index.js const isObject = (val) => val !== null && typeof val === 'object' export function reactive(target) { if (!isObject(target)) return const handler = { get(target, key, receiver) { console.log('get', key, target) }, set(target, key, value, receiver) { console.log('set', key, value) return value }, deleteProperty(target, key) { console.log('delete', key) return target }, } return new Proxy(target, handler) }
測試set和delete刊殉,結(jié)果如下
image-20210414082410979.png
reactive實現(xiàn)思路:
- 定義handler對象殉摔,用于Proxy的第二個參數(shù)(攔截器對象)
- get方法實現(xiàn)
- 收集依賴
- 返回target中對于key的value
- 如果value為對象,需要再次轉(zhuǎn)為響應(yīng)式對象
- set方法中實現(xiàn)
- 獲取key屬性的值记焊,判斷新舊值是否相同逸月,相同時返回true
- 不同時,先將target中的key對應(yīng)的value修改為新值
- 最后觸發(fā)更新
- deleteProperty方法實現(xiàn)
- 首先判斷target本身是否存在key
- 刪除target中的key遍膜,并返回成功或失敗
- 刪除成功碗硬,觸發(fā)更新
代碼示例:
const isObject = (val) => val !== null && typeof val === 'object'
const convert = (val) => (isObject(val) ? reactive(val) : val)
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
export function reactive(target) {
if (!isObject(target)) return
const handler = {
get(target, key, receiver) {
// 收集依賴
const value = Reflect.get(target, key, receiver)
return convert(value)
},
set(target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
let result = Reflect.set(target, key, value, receiver)
// 觸發(fā)更新
}
return result
},
deleteProperty(target, key) {
const hasKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hasKey && result) {
// 觸發(fā)更新
}
return result
},
}
return new Proxy(target, handler)
}
測試,創(chuàng)建html文件進(jìn)行測試:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { reactive } from './reactivity/index.js'
const obj = reactive({
name: 'zs',
age: 18
})
obj.name = 'lisi'
delete obj.age
console.log(obj)
</script>
</body>
</html>
響應(yīng)式系統(tǒng)原理——收集依賴
- 依賴收集過程中會創(chuàng)建3個集合瓢颅,分別是targetMap恩尾、depsMap和dep
- targetMap作用是記錄目標(biāo)對象和一個字典(depsMap),使用WeakMap弱引用挽懦,當(dāng)目標(biāo)對象失去引用之后翰意,可以銷毀
- targetMap的值是depsMap,depsMap的key是目標(biāo)對象的屬性名稱信柿,值是一個set集合dep
- dep中存儲的是effect函數(shù)冀偶,因為可以多次調(diào)用一個effect,在effect中訪問同一個屬性渔嚷,這時該屬性會收集多次依賴进鸠,對應(yīng)多個effect函數(shù)
- 通過這種結(jié)構(gòu),可以存儲目標(biāo)對象形病,目標(biāo)對象屬性客年,以及屬性對應(yīng)的effect函數(shù)
- 一個屬性可能對應(yīng)多個函數(shù),當(dāng)觸發(fā)更新時窒朋,在這個結(jié)構(gòu)中根據(jù)目標(biāo)對象屬性找到effect函數(shù)然后執(zhí)行
- 收集依賴的track函數(shù)內(nèi)部搀罢,首先根據(jù)當(dāng)前targetMap對象找到depsMap,如果沒找到要給當(dāng)前對象創(chuàng)建一個depsMap侥猩,并添加到targetMap中榔至,如果找到了再根據(jù)當(dāng)前使用的屬性在depsMap找到對應(yīng)的dep,dep中存儲的是effect函數(shù)欺劳,如果沒有找到時唧取,為當(dāng)前屬性創(chuàng)建對應(yīng)的dep集合,并且存儲到depsMap中划提,如果找到當(dāng)前屬性對應(yīng)的dep集合枫弟,就把當(dāng)前的effect函數(shù)存儲到集合中
effect方法實現(xiàn)
實現(xiàn)思路:
- effect接收函數(shù)作為參數(shù)
- 執(zhí)行函數(shù)并返回響應(yīng)式對象去收集依賴,收集依賴過程中將callback存儲起來鹏往,需要在后面的track函數(shù)中能夠訪問到這里的callback
- 依賴收集完畢設(shè)置activeEffect為null
代碼實現(xiàn):
let activeEffect = null
export function effect (callback) {
activeEffect = callback
callback() // 訪問響應(yīng)式對象屬性淡诗,去收集依賴
activeEffect = null
}
track方法實現(xiàn)
實現(xiàn)思路:
- track接收兩個參數(shù),目標(biāo)對象target和需要跟蹤的屬性key
- 內(nèi)部需要將target存儲到targetMap中,targetMap定義在外面韩容,除了track使用外款违,trigger函數(shù)也要使用
- activeEffect不存在直接返回,否則需要在targetMap中根據(jù)當(dāng)前target找depsMap
- 判斷是否找到depsMap群凶,因為target可能還沒有收集依賴
- 未找到插爹,為當(dāng)前target創(chuàng)建depsMap去存儲對應(yīng)的鍵和dep對象,并添加到targetMap中
- 根據(jù)屬性查找對應(yīng)的dep對象请梢,dep是個集合赠尾,存儲effect函數(shù)
- 判斷是否存在,未找到時創(chuàng)建新的dep集合并添加到depsMap中
- 將effect函數(shù)添加到dep集合中
- 在收集依賴的get中調(diào)用這個函數(shù)
代碼實現(xiàn):
let targetMap = new WeakMap()
export function track(target, key) {
if (!activeEffect) return
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)
}
此時毅弧,整個依賴收集過程已經(jīng)完成
trigger方法實現(xiàn)
依賴收集完成后需要觸發(fā)更新
實現(xiàn)思路:
- 參數(shù)target和key
- 根據(jù)target在targetMap中找到depsMap
- 未找到時气嫁,直接返回
- 再根據(jù)key找對應(yīng)的dep集合,effect函數(shù)
- 如果dep有值形真,遍歷dep集合執(zhí)行每一個effect函數(shù)
- 在set和deleteProperty中觸發(fā)更新
代碼實現(xiàn):
export function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach((effect) => {
effect()
})
}
}
依賴收集和觸發(fā)更新代碼完成杉编,創(chuàng)建html文件進(jìn)行測試
<body>
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = 0
effect(() => {
total = product.price * product.count
})
console.log(total)
product.price = 4000
console.log(total)
product.count = 1
console.log(total)
</script>
</body>
打開瀏覽器控制臺,可以看到輸出結(jié)果如下
響應(yīng)式系統(tǒng)原理——ref
ref vs reactive
- ref可以把基本數(shù)據(jù)類型數(shù)據(jù)咆霜,轉(zhuǎn)成響應(yīng)式對象
- ref返回的對象邓馒,重新賦值成對象也是響應(yīng)式的
- reactive返回的對象,重新賦值丟失響應(yīng)式
- reactive返回的對象不可以解構(gòu)
實現(xiàn)原理:
- 判斷 raw 是否是ref 創(chuàng)建的對象蛾坯,如果是的話直接返回
- 判斷 raw是否是對象光酣,如果是對象調(diào)用reactive創(chuàng)建響應(yīng)式對象,否則返回原始值
- 創(chuàng)建ref對象并返回脉课,標(biāo)識是否是ref對象救军,這個對象只有value屬性,并且這個value屬性具有set和get
- get中調(diào)用track收集依賴倘零,收集依賴的對象是剛創(chuàng)建的r對象唱遭,屬性是value,也就是當(dāng)訪問對象中的值呈驶,返回的是內(nèi)部的變量value
- set中判斷新舊值是否相等拷泽,不相等時將新值存儲到raw中,并調(diào)用convert處理raw袖瞻,最終把結(jié)果存儲到value中司致,如果給value重新賦值為一個對象依然是響應(yīng)式的,當(dāng)raw是對象時聋迎,convert里調(diào)用reactive轉(zhuǎn)換為響應(yīng)式對象
- 最后觸發(fā)更新
代碼實現(xiàn):
export function ref(raw) {
// 判斷 raw 是否是ref 創(chuàng)建的對象脂矫,如果是的話直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw)
const r = {
__v_isRef: true,
get value() {
track(r, 'value')
return value
},
set value(newValue) {
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
},
}
return r
}
創(chuàng)建html文件進(jìn)行測試:
<body>
<script type="module">
import { reactive, effect, ref } from './reactivity/index.js'
const price = ref(5000)
const count = ref(3)
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
打開控制臺可以看到輸出結(jié)果和上面的相同
響應(yīng)式系統(tǒng)原理——toRefs
實現(xiàn)思路:
- 接收參數(shù)proxy,判斷參數(shù)是否為reactive創(chuàng)建的對象霉晕,如果不是發(fā)出警告
- 判斷傳入?yún)?shù)庭再,如果是數(shù)組創(chuàng)建長度是length的數(shù)組捞奕,否則返回空對象,因為傳入的proxy可能是響應(yīng)式數(shù)組或響應(yīng)式對象
- 接著遍歷proxy對象的所有屬性佩微,如果是數(shù)組遍歷索引缝彬,將每一個屬性都轉(zhuǎn)換為類似ref返回的對象
- 創(chuàng)建toProxyRef函數(shù),接收proxy和key哺眯,創(chuàng)建對象并最終返回對象(類似ref返回的對象)
- 創(chuàng)建標(biāo)識屬性__v_isRef,這里的get中不需要收集依賴扒俯,因為這里訪問的是響應(yīng)式對象奶卓,當(dāng)訪問屬性時,內(nèi)部的getter回去收集依賴撼玄,set不需要觸發(fā)更新夺姑,調(diào)用代理對象內(nèi)部的set觸發(fā)更新
- 調(diào)用toProxyRef,將所有屬性轉(zhuǎn)換并存儲到ret中
- toRefs將reactive返回的對象的所有屬性都轉(zhuǎn)換成一個對象掌猛,所以當(dāng)對響應(yīng)式對象進(jìn)行解構(gòu)的時候盏浙,解構(gòu)出的每一個屬性都是對象,而對象是引用傳遞荔茬,所以解構(gòu)的屬性依然是響應(yīng)式的
代碼實現(xiàn):
export function toRefs(proxy) {
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef(proxy, key) {
const r = {
__v_isRef: true,
get value() {
return proxy[key]
},
set value(newValue) {
proxy[key] = newValue
},
}
return r
}
創(chuàng)建html進(jìn)行測試:
<body>
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
function useProduct () {
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
return toRefs(product)
}
const { price, count } = useProduct()
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
</body>
打開控制臺可以看到輸出結(jié)果和上面的相同
響應(yīng)式系統(tǒng)原理——computed
實現(xiàn)原理:
- 接收一個有返回值的函數(shù)作為參數(shù)废膘,函數(shù)的返回值就是計算屬性的值
- 監(jiān)聽這個函數(shù)內(nèi)部的響應(yīng)式數(shù)據(jù)變化,最后將函數(shù)執(zhí)行結(jié)果返回
- computed內(nèi)部會通過effect監(jiān)聽getter內(nèi)部的響應(yīng)式數(shù)據(jù)變化慕蔚,因為在effect中執(zhí)行g(shù)etter訪問響應(yīng)式數(shù)據(jù)的getter會去收集依賴丐黄,當(dāng)數(shù)據(jù)變化后,回去重新執(zhí)行effect函數(shù)將getter結(jié)果在存儲到result中
代碼實現(xiàn):
export function computed(getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}
創(chuàng)建html文件進(jìn)行測試:
<body>
<script type="module">
import { reactive, effect, computed } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = computed(() => {
return product.price * product.count
})
console.log(total.value)
product.price = 4000
console.log(total.value)
product.count = 1
console.log(total.value)
</script>
</body>
打開控制臺可以看到輸出結(jié)果和上面的相同