Vue3響應式核心原理解析
const data = {
name: '林三心',
age: 22
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
console.log(`訪問了${key}屬性`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`將${key}由->${target[key]}->設定成->${value}`)
Reflect.set(target, key, value, receiver)
}
}
return new Proxy(target, handler)
}
const proxyData = reactive(data)
console.log(proxyData.name)
// 訪問了name屬性
// 林三心
proxyData.name = 'sunshine_lin'
// 將name由->林三心->設定成->sunshine_lin
console.log(proxyData.name)
// 訪問了name屬性
// sunshine_lin
可以看到,其實效果與上面的Object.defineProperty
沒什麼差別,那為什麼尤大大要拋棄它,選擇Proxy
呢?注意了,最最最關鍵的來了,那就是物件新增屬性,來看看效果吧:
proxyData.hobby = '打籃球'
console.log(proxyData.hobby)
// 訪問了hobby屬性
// 打籃球
proxyData.hobby = '打遊戲'
// 將hobby由->打籃球->設定成->打遊戲
console.log(proxyData.hobby)
// 訪問了hobby屬性
// 打遊戲
所以現在大家知道Vue3的響應式比Vue2好在哪了吧?
Vue3響應式原理
說完Proxy
的好處,咱們正式來講講Vue3的響應式原理的核心部分吧。
前言
先看看下面這段程式碼
let name = '林三心', age = 22, money = 20
let myself = `${name}今年${age}歲,存款${money}元`
console.log(myself) // 林三心今年22歲,存款20元
money = 300
// 預期:林三心今年22歲,存款300元
console.log(myself) // 實際:林三心今年22歲,存款20元
大家想一下,我想要讓myself
跟著money
變,怎麼辦才行?嘿嘿,其實,只要讓myself = '${name}今年${age}歲,存款${money}元'
let name = '林三心', age = 22, money = 20
let myself = `${name}今年${age}歲,存款${money}元`
console.log(myself) // 林三心今年22歲,存款20元
money = 300
myself = `${name}今年${age}歲,存款${money}元` // 再執行一次
// 預期:林三心今年22歲,存款300元
console.log(myself) // 實際:林三心今年22歲,存款300元
effect
上面說了,每一次money
改變就得再執行一次myself = '${name}今年${age}歲,存款${money}元'
,才能使myself
更新,其實這麼寫不優雅,咱們可以封裝一個effect函式
let name = '林三心', age = 22, money = 20
let myself = ''
const effect = () => myself = `${name}今年${age}歲,存款${money}元`
effect() // 先執行一次
console.log(myself) // 林三心今年22歲,存款20元
money = 300
effect() // 再執行一次
console.log(myself) // 林三心今年22歲,存款300元
其實這樣也是有壞處的,不信你可以看看下面這種情況
let name = '林三心', age = 22, money = 20
let myself = '', ohtherMyself = ''
const effect1 = () => myself = `${name}今年${age}歲,存款${money}元`
const effect2 = () => ohtherMyself = `${age}歲的${name}居然有${money}元`
effect1() // 先執行一次
effect2() // 先執行一次
console.log(myself) // 林三心今年22歲,存款20元
console.log(ohtherMyself) // 22歲的林三心居然有20元
money = 300
effect1() // 再執行一次
effect2() // 再執行一次
console.log(myself) // 林三心今年22歲,存款300元
console.log(ohtherMyself) // 22歲的林三心居然有300元
增加了一個ohtherMyself
,就得再寫一個effect
,然後每次更新都執行一次,那如果增加數量變多了,那豈不是每次都要寫好多好多的effect函式
執行程式碼?
track和trigger
針對上面的問題,咱們可以這樣解決:用track函式
把所有依賴於money變數
的effect函式
都收集起來,放在dep
裡,dep
為什麼用Set
呢?因為Set
可以自動去重。蒐集起來之後,以後只要money變數
一改變,就執行trigger函式
通知dep
裡所有依賴money變數
的effect函式
執行,實現依賴變數的更新。先來看看程式碼吧,然後我再通過一張圖給大家展示一下,怕大家頭暈哈哈。
let name = '林三心', age = 22, money = 20
let myself = '', ohtherMyself = ''
const effect1 = () => myself = `${name}今年${age}歲,存款${money}元`
const effect2 = () => ohtherMyself = `${age}歲的${name}居然有${money}元`
const dep = new Set()
function track () {
dep.add(effect1)
dep.add(effect2)
}
function trigger() {
dep.forEach(effect => effect())
}
track() //收集依賴
effect1() // 先執行一次
effect2() // 先執行一次
console.log(myself) // 林三心今年22歲,存款20元
console.log(ohtherMyself) // 22歲的林三心居然有20元
money = 300
trigger() // 通知變數myself和otherMyself進行更新
console.log(myself) // 林三心今年22歲,存款300元
console.log(ohtherMyself) // 22歲的林三心居然有300元
物件呢?
上面都是講基礎資料型別的,那咱們來講講物件
吧,我先舉個例子,用最原始的方式去實現他的響應
const person = { name: '林三心', age: 22 }
let nameStr1 = ''
let nameStr2 = ''
let ageStr1 = ''
let ageStr2 = ''
const effectNameStr1 = () => { nameStr1 = `${person.name}是個大菜鳥` }
const effectNameStr2 = () => { nameStr2 = `${person.name}是個小天才` }
const effectAgeStr1 = () => { ageStr1 = `${person.age}歲已經算很老了` }
const effectAgeStr2 = () => { ageStr2 = `${person.age}歲還算很年輕啊` }
effectNameStr1()
effectNameStr2()
effectAgeStr1()
effectAgeStr2()
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// 林三心是個大菜鳥 林三心是個小天才 22歲已經算很老了 22歲還算很年輕啊
person.name = 'sunshine_lin'
person.age = 18
effectNameStr1()
effectNameStr2()
effectAgeStr1()
effectAgeStr2()
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// sunshine_lin是個大菜鳥 sunshine_lin是個小天才 18歲已經算很老了 18歲還算很年輕啊
複製程式碼
上面的程式碼,咱們也看出來了,感覺寫的很無腦。。還記得前面講的dep
收集effect
嗎?咱們暫且把person物件裡的name和age看成兩個變數,他們都有各自的依賴變數
- name:nameStr1和nameStr2
- age:ageStr1和ageStr2
所以name和age
應該擁有自己的dep
,並收集各自依賴變數所對應的effect
前面說了dep
是使用Set
,由於person擁有age和name
兩個屬性,所以擁有兩個dep
,那用什麼來儲存這兩個dep呢?咱們可以用ES6的另一個數據結構Map
來儲存
const person = { name: '林三心', age: 22 }
let nameStr1 = ''
let nameStr2 = ''
let ageStr1 = ''
let ageStr2 = ''
const effectNameStr1 = () => { nameStr1 = `${person.name}是個大菜鳥` }
const effectNameStr2 = () => { nameStr2 = `${person.name}是個小天才` }
const effectAgeStr1 = () => { ageStr1 = `${person.age}歲已經算很老了` }
const effectAgeStr2 = () => { ageStr2 = `${person.age}歲還算很年輕啊` }
const depsMap = new Map()
function track(key) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 這裡先暫且寫死
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
}
function trigger (key) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
track('name') // 收集person.name的依賴
track('age') // 收集person.age的依賴
effectNameStr1()
effectNameStr2()
effectAgeStr1()
effectAgeStr2()
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// 林三心是個大菜鳥 林三心是個小天才 22歲已經算很老了 22歲還算很年輕啊
person.name = 'sunshine_lin'
person.age = 18
trigger('name') // 通知person.name的依賴變數更新
trigger('age') // 通知person.age的依賴變數更新
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// sunshine_lin是個大菜鳥 sunshine_lin是個小天才 18歲已經算很老了 18歲還算很年輕啊
上面咱們是隻有一個person物件,那如果有多個物件呢?怎麼辦?我們都知道,每個物件會建立一個Map
來儲存此物件裡屬性的dep(使用Set來儲存)
,那如果有多個物件,該用什麼來儲存每個物件對應的Map
呢?請看下圖
其實ES6還有一個新的資料結構,叫做WeakMap
的,咱們就用它來儲存這些物件的Map
吧。所以咱們得對track函式
和trigger函式
進行改造,先看看之前他們長啥樣
const depsMap = new Map()
function track(key) {
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, dep = new Set())
}
// 這裡先暫且寫死
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
}
function trigger (key) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
之前的程式碼只做了單個物件的處理方案,但是現在如果要多個物件,那就得使用WeakMap
進行改造了(接下來程式碼可能有點囉嗦,但都會為了照顧基礎薄弱的同學)
const person = { name: '林三心', age: 22 }
const animal = { type: 'dog', height: 50 }
const targetMap = new WeakMap()
function track(target, key) {
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())
}
// 這裡先暫且寫死
if (target === person) {
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
} else if (target === animal) {
if (key === 'type') {
dep.add(effectTypeStr1)
dep.add(effectTypeStr2)
} else {
dep.add(effectHeightStr1)
dep.add(effectHeightStr2)
}
}
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
經過了上面的改造,咱們終於實現了多物件的依賴收集,咱們來試一試吧
const person = { name: '林三心', age: 22 }
const animal = { type: 'dog', height: 50 }
let nameStr1 = ''
let nameStr2 = ''
let ageStr1 = ''
let ageStr2 = ''
let typeStr1 = ''
let typeStr2 = ''
let heightStr1 = ''
let heightStr2 = ''
const effectNameStr1 = () => { nameStr1 = `${person.name}是個大菜鳥` }
const effectNameStr2 = () => { nameStr2 = `${person.name}是個小天才` }
const effectAgeStr1 = () => { ageStr1 = `${person.age}歲已經算很老了` }
const effectAgeStr2 = () => { ageStr2 = `${person.age}歲還算很年輕啊` }
const effectTypeStr1 = () => { typeStr1 = `${animal.type}是個大菜鳥` }
const effectTypeStr2 = () => { typeStr2 = `${animal.type}是個小天才` }
const effectHeightStr1 = () => { heightStr1 = `${animal.height}已經算很高了` }
const effectHeightStr2 = () => { heightStr2 = `${animal.height}還算很矮啊` }
track(person, 'name') // 收集person.name的依賴
track(person, 'age') // 收集person.age的依賴
track(animal, 'type') // animal.type的依賴
track(animal, 'height') // 收集animal.height的依賴
effectNameStr1()
effectNameStr2()
effectAgeStr1()
effectAgeStr2()
effectTypeStr1()
effectTypeStr2()
effectHeightStr1()
effectHeightStr2()
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// 林三心是個大菜鳥 林三心是個小天才 22歲已經算很老了 22歲還算很年輕啊
console.log(typeStr1, typeStr2, heightStr1, heightStr2)
// dog是個大菜鳥 dog是個小天才 50已經算很高了 50還算很矮啊
person.name = 'sunshine_lin'
person.age = 18
animal.type = '貓'
animal.height = 20
trigger(person, 'name')
trigger(person, 'age')
trigger(animal, 'type')
trigger(animal, 'height')
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// sunshine_lin是個大菜鳥 sunshine_lin是個小天才 18歲已經算很老了 18歲還算很年輕啊
console.log(typeStr1, typeStr2, heightStr1, heightStr2)
// 貓是個大菜鳥 貓是個小天才 20已經算很高了 20還算很矮啊
Proxy
通過上面的學習,我們已經可以實現當資料更新時,他的依賴變數也跟著改變,但是還是有缺點的,大家可以發現,每次我們總是得自己手動去執行track函式
進行依賴收集,並且當資料改變時,我麼又得手動執行trigger函式
去進行通知更新
那麼,到底有沒有辦法可以實現,自動收集依賴,以及自動通知更新呢?答案是有的,Proxy
可以為我們解決這個難題。咱們先寫一個reactive函式
,大家先照敲,理解好Proxy-track-trigger
這三者的關係,後面我會講為什麼這裡Proxy
需要搭配Reflect
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(receiver, key) // 訪問時收集依賴
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 設值時自動通知更新
}
}
return new Proxy(target, handler)
}
然後改一改之前的程式碼,把手動track
和手動trigger
去掉,發現也能實現之前的效果
const person = reactive({ name: '林三心', age: 22 }) // 傳入reactive
const animal = reactive({ type: 'dog', height: 50 }) // 傳入reactive
effectNameStr1()
effectNameStr2()
effectAgeStr1()
effectAgeStr2()
effectTypeStr1()
effectTypeStr2()
effectHeightStr1()
effectHeightStr2()
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// 林三心是個大菜鳥 林三心是個小天才 22歲已經算很老了 22歲還算很年輕啊
console.log(typeStr1, typeStr2, heightStr1, heightStr2)
// dog是個大菜鳥 dog是個小天才 50已經算很高了 50還算很矮啊
person.name = 'sunshine_lin'
person.age = 18
animal.type = '貓'
animal.height = 20
console.log(nameStr1, nameStr2, ageStr1, ageStr2)
// sunshine_lin是個大菜鳥 sunshine_lin是個小天才 18歲已經算很老了 18歲還算很年輕啊
console.log(typeStr1, typeStr2, heightStr1, heightStr2)
// 貓是個大菜鳥 貓是個小天才 20已經算很高了 20還算很矮啊
可能有的同學會有點懵逼,對上面的程式碼有點疑惑,也可能有點繞,我還以為通過一張圖給大家講解一下流程,圖可能會被壓縮,建議點開看看
解決寫死問題
在上面有一處地方,咱們是寫死的,大家都還記得嗎,就是在track函式
中
function track(target, key) {
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())
}
// 這裡先暫且寫死
if (target === person) {
if (key === 'name') {
dep.add(effectNameStr1)
dep.add(effectNameStr2)
} else {
dep.add(effectAgeStr1)
dep.add(effectAgeStr2)
}
} else if (target === animal) {
if (key === 'type') {
dep.add(effectTypeStr1)
dep.add(effectTypeStr2)
} else {
dep.add(effectHeightStr1)
dep.add(effectHeightStr2)
}
}
}
實際開發中,肯定是不止兩個物件的,如果每多加一個物件,就得多加一個else if
判斷,那是萬萬不行的。那我們要怎麼解決這個問題呢?其實說難也不難,Vue3的作者們想出了一個非常巧妙的辦法,使用一個全域性變數activeEffect
來巧妙解決這個問題,具體是怎麼解決呢?其實很簡單,就是每一個effect函式
一執行,就把自身放到對應的dep
裡,這就可以不需要寫死了。
我們怎麼才能實現這個功能呢?我們需要改裝一下effect函式
才行,並且要修改track函式
let activeEffect = null
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null // 執行後立馬變成null
}
function track(target, key) {
// 如果此時activeEffect為null則不執行下面
// 這裡判斷是為了避免例如console.log(person.name)而觸發track
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) // 把此時的activeEffect新增進去
}
// 每個effect函式改成這麼執行
effect(effectNameStr1)
effect(effectNameStr2)
effect(effectAgeStr1)
effect(effectAgeStr2)
effect(effectTypeStr1)
effect(effectTypeStr2)
effect(effectHeightStr1)
effect(effectHeightStr2)
實現ref
咱們在Vue3中是這麼使用ref
的
let num = ref(5)
console.log(num.value) // 5
然後num
就會成為一個響應式的資料,而且使用num
時需要這麼寫num.value
才能使用
實現ref其實很簡單,咱們上面已經實現了reactive
,只需要這麼做就可以實現ref
function ref (initValue) {
return reactive({
value: initValue
})
}
咱們可以來試試效果如何
let num = ref(5)
effect(() => sum = num.value * 100)
console.log(sum) // 500
num.value = 10
console.log(sum) // 1000
實現computed
咱們順便簡單實現一下computed
吧,其實也很簡單
function computed(fn) {
const result = ref()
effect(() => result.value = fn()) // 執行computed傳入函式
return result
}
咱們來看看結果
let num1 = ref(5)
let num2 = ref(8)
let sum1 = computed(() => num1.value * num2.value)
let sum2 = computed(() => sum1.value * 10)
console.log(sum1.value) // 40
console.log(sum2.value) // 400
num1.value = 10
console.log(sum1.value) // 80
console.log(sum2.value) // 800
num2.value = 16
console.log(sum1.value) // 160
console.log(sum2.value) // 1600
自此咱們就實現了本文章所有功能
最終程式碼
const targetMap = new WeakMap()
function track(target, key) {
// 如果此時activeEffect為null則不執行下面
// 這裡判斷是為了避免例如console.log(person.name)而觸發track
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) // 把此時的activeEffect新增進去
}
function trigger(target, key) {
let depsMap = targetMap.get(target)
if (depsMap) {
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
}
function reactive(target) {
const handler = {
get(target, key, receiver) {
track(receiver, key) // 訪問時收集依賴
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
trigger(receiver, key) // 設值時自動通知更新
}
}
return new Proxy(target, handler)
}
let activeEffect = null
function effect(fn) {
activeEffect = fn
activeEffect()
activeEffect = null
}
function ref(initValue) {
return reactive({
value: initValue
})
}
function computed(fn) {
const result = ref()
effect(() => result.value = fn())
return result
}
Proxy和Reflect
Proxy
const person = { name: '林三心', age: 22 }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
console.log(target) // 原來的person
console.log(key) // 屬性名
console.log(receiver) // 代理後的proxyPerson
},
set(target, key, value, receiver) {
console.log(target) // 原來的person
console.log(key) // 屬性名
console.log(value) // 設定的值
console.log(receiver) // 代理後的proxyPerson
}
})
proxyPerson.name // 訪問屬性觸發get方法
proxyPerson.name = 'sunshine_lin' // 設定屬性值觸發set方法
Reflect
在這列舉Reflect
的兩個方法
get(target, key, receiver)
:個人理解就是,訪問target
的key
屬性,但是this
是指向receiver
,所以實際是訪問的值是receiver的key
的值,但是這可不是直接訪問receiver[key]
屬性,大家要區分一下set(target, key, value, receiver)
:個人理解就是,設定target
的key
屬性為value
,但是this
是指向receiver
,所以實際是是設定receiver的key
的值為value
,但這可不是直接receiver[key] = value
,大家要區分一下
上面咱們強調了,不能直接receiver[key]
或者receiver[key] = value
,而是要通過Reflect.get和Reflect.set
,繞個彎去訪問屬性或者設定屬性,這是為啥呢?下面咱們舉個反例
const person = { name: '林三心', age: 22 }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return Reflect.get(receiver, key) // 相當於 receiver[key]
},
set(target, key, value, receiver) {
Reflect.set(receiver, key, value) // 相當於 receiver[key] = value
}
})
console.log(proxyPerson.name)
proxyPerson.name = 'sunshine_lin'
// 會直接報錯,棧記憶體溢位 Maximum call stack size exceeded
為什麼會這樣呢?看看下圖解答
現在知道為什麼不能直接receiver[key]
或者receiver[key] = value
了吧,因為直接這麼操作會導致無限迴圈,最終報錯。所以正確做法是
const person = { name: '林三心', age: 22 }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
}
})
console.log(proxyPerson.name) // 林三心
proxyPerson.name = 'sunshine_lin'
console.log(proxyPerson.name) // sunshine_lin
肯定有的同學就要問了,下面這麼寫也可以,為什麼也不建議呢?我放到下面一起說
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return Reflect.get(target, key)
},
set(target, key, value, receiver) {
Reflect.get(target, key, value)
}
})
為什麼要一起用
其實Proxy不搭配Reflect也是可以的。咱們可以這麼寫,也照樣能實現想要的效果
const person = { name: '林三心', age: 22 }
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
}
})
console.log(proxyPerson.name) // 林三心
proxyPerson.name = 'sunshine_lin'
console.log(proxyPerson.name) // sunshine_lin
那為什麼建議Proxy和Reflect
一起使用呢?因為Proxy和Reflect
的方法都是一一對應的,在Proxy
裡使用Reflect
會提高語義化
Proxy的get
對應Reflect.get
Proxy的set
對應Reflect.set
- 還有很多其他方法我就不一一列舉,都是一一對應的
還有一個原因就是,儘量把this放在receiver
上,而不放在target
上
為什麼要儘量把this放在代理物件receiver
上,而不建議放原物件target
上呢?因為原物件target
有可能本來也是是另一個代理的代理物件,所以如果this一直放target
上的話,出bug的概率會大大提高,所以之前的程式碼為什麼不建議,大家應該知道了吧?
const proxyPerson = new Proxy(person, {
get(target, key, receiver) {
return Reflect.get(target, key)
},
set(target, key, value, receiver) {
Reflect.get(target, key, value)
}
})