Vue 3 響應式原理及實現

從零開始實現你本身的響應式庫,從零開始實現 Vue 3 響應式模塊。
本文完整內容見buid-your-own-vue-nextjavascript

1. 實現響應式

響應基本類型變量

首先看一下響應式預期應該是什麼樣的,新建一個 demo.js 文件,內容以下:vue

// 這種寫成一行徹底是爲了節省空間,實際上我會一行一個變量
let a = 1, b = 2, c = a * b
console.log('c:' + c) // 2
a = 2
console.log('c:' + c) // 指望獲得4

思考一下,如何才能作到當 a 變更時 c 跟着變化?java

顯然,咱們須要作的就是從新執行一下 let c = a * b 便可,像這樣:react

let a = 1, b = 2, c = a * b
console.log('c:' + c) // 2
a = 2
c = a * b
console.log('c:' + c) // 指望獲得4

那麼,如今咱們把須要從新執行的代碼寫成一個函數,代碼以下:git

let a = 1, b = 2, c = 0
let effect = () => { c = a * b }
effect() // 首次執行更新c的值
console.log('c:' + c) // 2
a = 2
console.log('c:' + c) // 指望獲得4

如今仍然沒有達成預期的效果,實際上咱們還須要兩個方法,一個用來存儲全部須要依賴更新的 effect,咱們假設叫 track,一個用來觸發執行這些 effect 函數,假設叫作 triggergithub

注意: 這裏咱們的函數命名和 Vue 3 中保持一致,從而能夠更容易理解 Vue 3 源碼。編程

代碼相似這樣:數組

let a = 1, b = 2, c = 0
let effect = () => { c = a * b }

track() // 收集 effect 
effect() // 首次執行更新c的值
console.log('c:' + c) // 2
a = 2
trigger() // a變化時,觸發effect的執行
console.log('c:' + c) // 指望獲得4

那麼 tracktrigger 分別作了什麼,是如何實現的呢?咱們暫且能夠簡單理解爲一個「發佈-訂閱者模式」,track 就是不斷給一個數組 dep 添加 effecttrigger 用來遍歷執行 dep 的每一項 effect瀏覽器

如今來完成這兩個函數ide

let a = 1, b = 2, c = 0
let effect = () => { c = a * b }

let dep = new Set()
let track = () => { dep.add(effect) }
let trigger = () => { dep.forEach(effect => effect()) }

track()
effect() // 首次執行更新c的值
console.log('c:' + c) // 2
a = 2
trigger() // a變化時,觸發effect的執行
console.log('c:' + c) // 指望獲得4,實際獲得4

注意這裏咱們使用 Set 來定義 dep,緣由就是 Set 自己不能添加劇復的 key,讀寫都很是方便。

如今代碼的執行結果已經符合預期了。

c: 2
c: 4

響應對象的不一樣屬性

一般狀況,咱們定義的對象都有不少的屬性,每個屬性都須要有本身的 dep(即每一個屬性都須要把那些依賴了本身的effect記錄下來放進本身的 new Set() 中),如何來實現這樣的功能呢?

有一段代碼以下:

let obj = { a: 10, b: 20 }
let timesA = obj.a * 10
let divideA = obj.a / 10
let timesB = obj.b * 10
let divideB = obj.b / 10

// 100, 1, 200, 2
console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)
obj.a = 100
obj.b = 200
// 指望獲得 1000, 10, 2000, 20
console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)

這段代碼中,按照上文講解的,屬性abdep應該是以下:

let depA = [
  () => { timesA = obj.a * 10 },
  () => { divideA = obj.a / 10 }
]
let depB = [ 
  () => { timesB = obj.b * 10 },
  () => { divideB = obj.b / 10 }
]

若是代碼仍是按照前文的方式來寫顯然是不科學的,這裏就要開始作一點點抽象了,收集依賴咱們能夠假想用track('a') track('b')這種形式分別記錄對象不一樣key的依賴項,那麼顯然咱們還須要一個東西來存放這些 key 及相應的dep

如今咱們來實現這樣的 track 函數及對應的 trigger 函數,代碼以下:

const depsMap = new Map() // 每一項都是一個 Set 對象
function track(key) {
  let dep = depsMap.get(key)
  if(!dep) {
    depsMap.set(key, dep = new Set());
  }
  dep.add(effect)
}

function trigger(key) {
  let dep = depsMap.get(key)
  if(dep) {
    dep.forEach(effect => effect())
  }
}

這樣就實現了對一個對象不一樣屬性的依賴收集,那麼如今這個代碼最簡單的使用方法將是下面這樣:

const depsMap = new Map() // 每一項都是一個 Set 對象
function track(key) {
  ...
  
  // only for usage demo
  if(key === 'a'){
    dep.add(effectTimesA)
    dep.add(effectDivideA)
  }else if(key === 'b'){
    dep.add(effectTimesB)
    dep.add(effectDivideB)
  }
}

function trigger(key) {
  ...
}

let obj = { a: 10, b: 20 }
let timesA = 0
let divideA = 0
let timesB = 0
let divideB = 0
let effectTimesA = () => { timesA = obj.a * 10 }
let effectDivideA = () => { divideA = obj.a / 10 }
let effectTimesB = () => { timesB = obj.b * 10 }
let effectDivideB = () => { divideB = obj.b / 10 }

track('a')
track('b')

// 爲了省事直接改爲調用trigger,後文一樣
trigger('a')
trigger('b')

// 100, 1, 200, 2
console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)
obj.a = 100
obj.b = 200

trigger('a')
trigger('b')
// 指望獲得:1000, 10, 2000, 20 實際獲得:1000, 10, 2000, 20
console.log(`${timesA}, ${divideA}, ${timesB}, ${divideB}`)

代碼看起來仍然是臃腫無比,彆着急,後面的設計會優化這個問題。

響應多個對象

咱們已經實現了對一個對象的響應編程,那麼要對多個對象實現響應式編程該怎麼作呢?

腦殼一拍,繼續往外嵌套一層對象不就能夠了嗎?沒錯,你能夠用 ES6 中的 WeakMap 輕鬆實現,WeakMap 恰好能夠(只能)把對象看成 key。(題外話,Map 和 WeakMap 的區別

咱們假想實現後是這樣的效果:

let obj1 = { a: 10, b: 20 }
let obj2 = { c: 30, d: 40 }
const targetMap = new WeakMap()

// 省略代碼
// 獲取 obj1 的 depsMap
// 獲取 obj2 的 depsMap

targetMap.set(obj1, "obj1's depsMap")
targetMap.set(obj2, "obj2's depsMap")

這裏暫且不糾結爲何叫 targetMap,如今總體依賴關係以下:

名稱 類型 key
targetMap WeakMap object depsMap
depsMap Map property dep
dep Set effect
  • targetMap: 存放每一個響應式對象(全部屬性)的依賴項
  • targetMap: 存放響應式對象每一個屬性對應的依賴項
  • dep: 存放某個屬性對應的全部依賴項(當這個對象對應屬性的值發生變化時,這些依賴項函數會從新執行)

如今咱們能夠實現這個功能了,核心代碼以下:

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());
  }
  // 先忽略這個
  dep.add(effect)
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if(depsMap){
    let dep = depsMap.get(key)
    if(dep) {
      dep.forEach(effect => effect())
    }
  }
}

那麼如今這個代碼最簡單的使用方法將是下面這樣:

const targetMap = new WeakMap();

function track(target, key) {
  ...

  // only for usage demo
  if(key === 'a'){
    dep.add(effectTimesA)
    dep.add(effectDivideA)
  }
}

function trigger(target, key) {
  ...
}

let obj = { a: 10, b: 20 }
let timesA = 0
let divideA = 0
let effectTimesA = () => { timesA = obj.a * 10 }
let effectDivideA = () => { divideA = obj.a / 10 }

track(obj, 'a')
trigger(obj, 'a')

console.log(`${timesA}, ${divideA}`) // 100, 1
obj.a = 100

trigger(obj, 'a')
console.log(`${timesA}, ${divideA}`) // 1000, 10

至此,咱們對響應式的基本概念有了瞭解,咱們已經作到了收集全部響應式對象的依賴項,可是如今你能夠看到代碼的使用是極其繁瑣的,主要是由於咱們還沒實現自動收集依賴項、自動觸發修改。

2. Proxy 和 Reflect

上一節講到了咱們實現了基本的響應功能,可是咱們目前仍是手動進行依賴收集和觸發更新的。

解決這個問題的方法應該是:

  • 當訪問(GET)一個屬性時,咱們就調用 track(obj, <property>) 自動收集依賴項(存儲 effect
  • 當修改(SET)一個屬性時,咱們就調用 trigger(obj, <property> 自動觸發更新(執行存儲的effect

那麼如今問題就是,咱們如何在訪問或修改一個屬性時作到這樣的事情?也便是如何攔截這種 GETSET 操做?

Vue 2中咱們使用 ES5 中的 Object.defineProperty 來攔截 GETSET
Vue 3中咱們將使用 ES6 中的 ReflectProxy。(注意:Vue 3再也不支持IE瀏覽器,因此能夠用比較多的高級特性)

咱們先來看一下怎麼輸出一個對象的一個屬性值,能夠用下面這三種方法:

  • 使用 . => obj.a
  • 使用 [] => obj['a']
  • 使用 ES6 中的 Reflect => Reflect.get(obj, 'a')

這三種方法都是可行的,可是 Reflect 有很是強大的能力,後面會講到。

Proxy

咱們先來看看 ProxyProxy 是另外一個對象的佔位符,默認是對這個對象的委託。你能夠在這裏查看 Proxy 更詳細的用法。

let obj = { a: 1}
let proxiedObj = new Proxy(obj, {})
console.log(proxiedObj.a) // 1

這個過程能夠表述爲,獲取 proxiedObj.a 時,直接去從查找 obj.a而後返回給 proxiedObj,再輸出 proxiedObj.a

Proxy 的第二個參數被稱爲 handlerhandler就是包含捕捉器(trap)的佔位符對象,即處理器對象,捕捉器容許咱們攔截一些基本的操做,如:

  • 查找屬性
  • 枚舉
  • 函數的調用

如今咱們的示例代碼修改成:

let obj = { a: 1}
let proxiedObj = new Proxy(obj, {
  get(target, key) {
    console.log('Get')
    return target[key]
  }
})
console.log(proxiedObj.a) // 1

這段代碼中,咱們直接使用 target[key] 返回值,它直接返回了原始對象的值,不作任何其它操做,這對於這個簡單的示例來講沒任何問題,。

如今咱們看一下下面這段稍微複雜一點的代碼:

let obj = {
  a: 1,
  get b() { return this.a }
}

let proxiedObj = new Proxy(obj, {
  get(target, key, receiver) {
    return target[key] // 這裏的target是obj
  }
})

let childObj = Object.create(proxiedObj)
childObj.a = 2
console.log(childObj.b) // 指望獲得2 實際輸出1

這段代碼的輸出結果就是錯誤的,這是什麼狀況?難道是原型繼承寫錯了嗎?咱們嘗試把Proxy相關代碼去掉,發現輸出是正常的......
這個問題其實就出在 return target[key]這一行:

  1. 當讀取 childObj.b 時,childObj 上沒有屬性 b,所以會從原型鏈上查找
  2. 原型鏈是 proxiedObj
  3. 讀取 proxiedObj.b 時,會觸發Proxy捕捉器(trap)中的 get,這直接從原始對象中返回了 target[key]
  4. 這裏target[key]key 是一個 getter,所以這個 getter 中的上下文 this 即爲target,這裏的 target 就是 obj,所以直接返回了 1
參考 爲何要使用 Reflect

那麼咱們怎麼解決這個 this 出錯的問題呢?

Reflect

如今咱們就能夠講講 Reflect 了。你能夠在這裏查看 Reflect 更詳細的用法。

捕獲器 get 有第三個參數叫作 receiver

Proxyhandler.get(target, prop, receiver) 中的參數 receiverProxy 或者繼承 Proxy 的對象。

Reflect.get(target, prop, receiver) 中的參數 receiver :若是target 對象中指定了 getterreceiver 則爲 getter 調用時的 this 值。

這確保了當咱們的對象從另外一個對象繼承了值或函數時使用 this 值的正確性。

咱們修改剛纔的示例以下:

let obj = {
  a: 1,
  get b() { return this.a }
}

let proxiedObj = new Proxy(obj, {
  // 本例中這裏的receiver爲調用時的對象childOjb
  get(target, key, receiver) {
    // 這裏的target是obj
    // 這意思是把receiver做爲this去調用target[key]
    return Reflect.get(target, key, receiver)
  }
})

let childObj = Object.create(proxiedObj)
childObj.a = 2;
console.log(childObj.b) // 指望獲得2 實際輸出1

如今咱們弄清楚了爲何要結合 Reflect 來使用 Proxy,有了這些知識,就能夠繼續完善咱們的代碼了。

實現reactive函數

如今修改咱們的示例代碼爲:

let obj = { a: 1}
let proxiedObj = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('Get')
    return Reflect.get(target, key, receiver)
  }
  set(target, key, value, receiver) {
    console.log('Set')
    return Reflect.set(target, key, value, receiver)
  }
})
console.log(proxiedObj.a) // Get 1

接下來咱們要作的就是結合 Proxyhandler 和 以前實現了的 tracktrigger 來完成一個響應式模塊。

首先,咱們來封裝一下 Proxy 相關代碼,和Vue 3保持一致叫reactive

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      return Reflect.set(target, key, value, receiver)
    }
  }
  return new Proxy(target, handler)
}

這裏有一個問題,當咱們每次調用 reactive 時都會從新定義一個 handler 的對象,爲了優化這個,咱們把 handler 提出去,代碼以下:

const reactiveHandler = {
  get(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set(target, key, value, receiver) {
    return Reflect.set(target, key, value, receiver)
  }
}

function reactive(target) {
  return new Proxy(target, reactiveHandler)
}

如今把reactive引入到咱們的第一節中最後的示例代碼中。

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let divideA = 0
let effectTimesA = () => { timesA = obj.a * 10 }
let effectDivideA = () => { divideA = obj.a / 10 }

track(obj, 'a')
trigger(obj, 'a')

console.log(`${timesA}, ${divideA}`) // 100, 1
obj.a = 100

trigger(obj, 'a')
console.log(`${timesA}, ${divideA}`) // 1000, 10

如今咱們要作的是去掉示例代碼中的 tracktrigger

回到本節開頭提出的解決方案,咱們已經能夠攔截 GETSET 操做了,只須要在適當的時候調用 tracktrigger 方法便可,咱們修改 reactiveHandler 代碼以下:

const reactiveHandler = {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    track(target, key)
    return result
  },
  set(target, key, value, receiver) {
    const oldVal = target[key]
    const result = Reflect.set(target, key, value, receiver)

    // 這裏判斷條件不對,result爲一個布爾值
    if(oldVal !== result){
      trigger(target, key)
    }
    return result
  }
}

如今咱們的示例代碼能夠精簡爲這樣:

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let divideA = 0
let effectTimesA = () => { timesA = obj.a * 10 }
let effectDivideA = () => { divideA = obj.a / 10 }

// 恢復調用 effect 的形式
effectTimesA()
effectDivideA()

console.log(`${timesA}, ${divideA}`) // 100, 1
obj.a = 100

console.log(`${timesA}, ${divideA}`) // 1000, 10

咱們已經去掉了手動 tracktrigger 代碼,至此,咱們已經實現了 reactive 函數,看起來和Vue 3源碼差很少了。

但這還有點問題:

  • track 函數中的 effect 如今還沒處理,只能手動添加
  • reactive 如今只能做用於對象,基本類型變量怎麼處理?

下一個章節咱們將解決這個問題,讓咱們的代碼更加接近Vue 3。

3. activeEffect 和 ref

首先,咱們修改一下示例代碼:

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let effect = () => { timesA = obj.a * 10 }
effect()

console.log(timesA) // 100
obj.a = 100
// 新增一行,使用到obj.a
console.log(obj.a)
console.log(timesA) // 1000

由上節知識能夠知道,當 effect 執行時咱們訪問到了 obj.a,所以會觸發 track 收集該依賴 effect。同理,console.log(obj.a) 這一行也一樣觸發了 track,但這並非響應式代碼,咱們預期不觸發 track

咱們想要的是隻在 effect 中的代碼才觸發 track

能想到怎麼來實現嗎?

只響應須要依賴更新的代碼(effect)

首先,咱們定義一個變量 shouldTrack暫且認爲它表示是否須要執行 track,咱們修改 track 代碼,只須要增長一層判斷條件,以下:

const targetMap = new WeakMap();
let shouldTrack = null
function track(target, key) {
  if(shouldTrack){
    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());
    }

    // 這裏的 effect 爲使用時定義的 effect
    // shouldTrack 時應該把對應的 effect 傳進來
    dep.add(effect)
    // 若是有多個就手寫多個
    // dep.add(effect1)
    // ...
  }
}

如今咱們須要解決的就是 shouldTrack 賦值問題,當有須要響應式變更的地方,咱們就寫一個 effect 並賦值給 shouldTrack,而後 effect 執行完後重置 shouldTracknull,這樣結合剛纔修改的 track 函數就解決了這個問題,思路以下:

let shouldTrack = null
// 這裏省略 track trigger reactive 代碼
...

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let effect = () => { timesA = obj.a * 10 }
shouldTrack = effect // (*)
effect()
shouldTrack = null // (*)

console.log(timesA) // 100
obj.a = 100
console.log(obj.a)
console.log(timesA) // 1000

此時,執行到 console.log(obj.a) 時,因爲 shouldTrack 值爲 null,因此並不會執行 track,完美。

完美了嗎?顯然不是,當有不少的 effect 時,你的代碼會變成下面這樣:

let effect1 = () => { timesA = obj.a * 10 }
shouldTrack = effect1 // (*)
effect1()
shouldTrack = null // (*)

let effect2 = () => { timesB = obj.a * 10 }
shouldTrack = effect1 // (*)
effect2()
shouldTrack = null // (*)

咱們來優化一下這個問題,爲了和Vue 3保持一致,這裏咱們修改 shouldTrackactiveEffect,如今它表示當前運行的 effect

咱們把這段重複使用的代碼封裝成函數,以下:

let activeEffect = null
// 這裏省略 track trigger reactive 代碼
...

function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

同時咱們還須要修改一下 track 函數:

function track(target, key) {
  if(activeEffect){
    ...
    // 這裏不用再根據條件手動添加不一樣的 effect 了!
    dep.add(activeEffect)
  }
}

那麼如今的使用方法就變成了:

const targetMap = new WeakMap();
let activeEffect = null
function effect (eff) { ... }
function track() { ... }
function trigger() { ... }
function reactive() { ... }

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let timesB = 0
effect(() => { timesA = obj.a * 10 })
effect(() => { timesB = obj.b * 10 })

console.log(timesA) // 100
obj.a = 100
console.log(obj.a)
console.log(timesA) // 1000

現階段完整代碼

如今新建一個文件reactive.ts,內容就是當前實現的完整響應式代碼:

const targetMap = new WeakMap();
let activeEffect = null
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

function track(target, key) {
  if(activeEffect){
    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)
  }
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if(depsMap){
    let dep = depsMap.get(key)
    if(dep) {
      dep.forEach(effect => effect())
    }
  }
}

const reactiveHandler = {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    track(target, key)
    return result
  },
  set(target, key, value, receiver) {
    const oldVal = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if(oldVal !== result){
      trigger(target, key)
    }
    return result
  }
}

function reactive(target) {
  return new Proxy(target, reactiveHandler)
}

如今咱們已經解決了非響應式代碼也觸發track的問題,同時也解決了上節中留下的問題:track 函數中的 effect 只能手動添加。

接下來咱們解決上節中留下的另外一個問題:reactive 如今只能做用於對象,基本類型變量怎麼處理?

實現ref

修改 demo.js 代碼以下:

import { effect, reactive } from "./reactive"

let obj = reactive({ a: 10, b: 20 })
let timesA = 0
let sum = 0
effect(() => { timesA = obj.a * 10 })
effect(() => { sum = timesA + obj.b })

obj.a = 100
console.log(sum) // 指望: 1020

這段代碼並不能實現預期效果,由於當 timesA 正常更新時,咱們但願能更新 sum(即從新執行 () => { sum = timesA + obj.b }),而實際上因爲 timesA 並非一個響應式對象,沒有 track 其依賴,因此這一行代碼並不會執行。

那咱們如何才能讓這段代碼正常工做呢?其實咱們把基本類型變量包裝成一個對象去調用 reactive 便可。
看過 Vue composition API 的同窗可能知道,Vue 3中用一個 ref 函數來實現把基本類型變量變成響應式對象,經過 .value 獲取值,ref 返回的就是一個 reactive 對象。

實現這樣的一個有 value 屬性的對象有這兩種方法:

  1. 直接給一個對象添加 value 屬性
function ref(intialValue) {
  return reactive({
    value: intialValue
  })
}
  1. gettersetter 來實現
function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value)
    }
  }
  return r
}

如今咱們的示例代碼修改爲:

import { effect, reactive } from "./reactive"

function ref(intialValue) {
  return reactive({
    value: intialValue
  })
}

let obj = reactive({ a: 10, b: 20 })
let timesA = ref(0)
let sum = 0
effect(() => { timesA.value = obj.a * 10 })
effect(() => { sum = timesA.value + obj.b })

// 指望: timesA: 100  sum: 120 實際:timesA: 100  sum: 120
console.log(`timesA: ${timesA.value}  sum: ${sum}`)

obj.a = 100
// 指望: timesA: 1000  sum: 1020 實際:timesA: 1000  sum: 1020
console.log(`timesA: ${timesA}  sum: ${sum}`)

增長了 ref 處理基本類型變量後,咱們的示例代碼運行結果符合預期了。至此咱們已經解決了遺留問題:reactive 只能做用於對象,基本類型變量怎麼處理?

Vue 3中的 ref 是用第二種方法來實現的,如今咱們整理一下代碼,把 ref 放到 reactive.j 中。

現階段完整代碼

const targetMap = new WeakMap();
let activeEffect = null
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

function track(target, key) {
  if(activeEffect){
    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)
  }
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if(depsMap){
    let dep = depsMap.get(key)
    if(dep) {
      dep.forEach(effect => effect())
    }
  }
}

const reactiveHandler = {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    track(target, key)
    return result
  },
  set(target, key, value, receiver) {
    const oldVal = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if(oldVal !== result){
      trigger(target, key)
    }
    return result
  }
}

function reactive(target) {
  return new Proxy(target, reactiveHandler)
}

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value)
    }
  }
  return r
}

有同窗可能就要問了,爲何不直接用第一種方法實現 ref,而是選擇了比較複雜的第二種方法呢?
主要有三方面緣由:

  1. 根據定義,ref 應該只有一個公開的屬性,即 value,若是使用了 reactive 你能夠給這個變量增長新的屬性,這其實就破壞了 ref 的設計目的,它應該只用來包裝一個內部的 value 而不該該做爲一個通用的 reactive 對象;
  2. Vue 3中有一個 isRef 函數,用來判斷一個對象是 ref 對象而不是 reactive 對象,這種判斷在不少場景都是很是有必要的;
  3. 性能方面考慮,Vue 3中的 reactive 作的事情遠比第二種實現 ref 的方法多,好比有各類檢查。

4. Computed

回到上節中最後的示例代碼:

import { effect, reactive, ref } from "./reactive"

let obj = reactive({ a: 10, b: 20 })
let timesA = ref(0)
let sum = 0
effect(() => { timesA.value = obj.a * 10 })
effect(() => { sum = timesA.value + obj.b })

看到 timesAsum 兩個變量,有同窗就會說:「這不就是計算屬性嗎,不能像Vue 2同樣用 computed 來表示嗎?」 顯然是能夠的,看過 Vue composition API 的同窗可能知道,Vue 3中提供了一個 computed 函數。

示例代碼若是使用 computed 將變成這樣:

import { effect, reactive, computed } from "./reactive"

let obj = reactive({ a: 10, b: 20 })
let timesA = computed(() => obj.a * 10)
let sum = computed(() => timesA.value + obj.b)

如今的問題就是如何實現 computed

實現computed

咱們拿 timesA 先後的改動來講明,思考一下 computed 應該是什麼樣的?

  1. 返回響應式對象,也許是 ref()
  2. 內部須要執行 effect 函數以收集依賴
function computed(getter) {
  const result = ref();
  effect(() => result.value = getter())
  return result
}

如今測試一下示例代碼:

import { effect, reactive, ref } from "./reactive"

let obj = reactive({ a: 10, b: 20 })
let timesA = computed(() => obj.a * 10)
let sum = computed(() => timesA.value + obj.b)

// 指望: timesA: 1000  sum: 1020 實際:timesA: 1000  sum: 1020
console.log(`timesA: ${timesA.value}  sum: ${sum.value}`)

obj.a = 100
 // 指望: timesA: 1000  sum: 1020
console.log(`timesA: ${timesA.value}  sum: ${sum.value}`)

結果符合預期。
這樣實現看起來很容易,實際上Vue 3中的 computed 支持傳入一個 getter 函數或傳入一個有 getset 的對象,而且有其它操做,這裏咱們不作實現,感興趣能夠去看源碼

現階段完整代碼

至此咱們已經實現了一個簡易版本的響應式庫了,完整代碼以下:

const targetMap = new WeakMap();
let activeEffect = null
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}

function track(target, key) {
  if(activeEffect){
    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)
  }
}

function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if(depsMap){
    let dep = depsMap.get(key)
    if(dep) {
      dep.forEach(effect => effect())
    }
  }
}

const reactiveHandler = {
  get(target, key, receiver) {
    const result = Reflect.get(target, key, receiver)
    track(target, key)
    return result
  },
  set(target, key, value, receiver) {
    const oldVal = target[key]
    const result = Reflect.set(target, key, value, receiver)
    if(oldVal !== result){
      trigger(target, key)
    }
    return result
  }
}

function reactive(target) {
  return new Proxy(target, reactiveHandler)
}

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    }
  }
  return r
}

function computed(getter) {
  const result = ref();
  effect(() => result.value = getter())
  return result
}

尚存問題

咱們如今的代碼很是簡易,有不少細節還沒有實現,你均可以在源碼中學習到,好比:

  • 操做一些內置的屬性,如 Symbol.iteratorArray.length 等觸發了 track 如何處理
  • 嵌套的對象,如何遞歸響應
  • 對象某個 key 對應的 value 自己是一個 reactive 對象,如何處理

你也能夠本身嘗試着實現它們。

本文完整內容見從零開始建立你的Vue 3

相關文章
相關標籤/搜索