1. 程式人生 > 其它 >Vue3響應式核心原理解析

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):個人理解就是,訪問targetkey屬性,但是this是指向receiver,所以實際是訪問的值是receiver的key的值,但是這可不是直接訪問receiver[key]屬性,大家要區分一下
  • set(target, key, value, receiver):個人理解就是,設定targetkey屬性為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)
    }
})