從零開始實現你本身的響應式庫,從零開始實現 Vue 3 響應式模塊。
本文完整內容見buid-your-own-vue-nextjavascript
首先看一下響應式預期應該是什麼樣的,新建一個 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
函數,假設叫作 trigger
。github
注意: 這裏咱們的函數命名和 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
那麼 track
和 trigger
分別作了什麼,是如何實現的呢?咱們暫且能夠簡單理解爲一個「發佈-訂閱者模式」,track
就是不斷給一個數組 dep
添加 effect
,trigger
用來遍歷執行 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}`)
這段代碼中,按照上文講解的,屬性a
和b
的dep
應該是以下:
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 |
如今咱們能夠實現這個功能了,核心代碼以下:
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
至此,咱們對響應式的基本概念有了瞭解,咱們已經作到了收集全部響應式對象的依賴項,可是如今你能夠看到代碼的使用是極其繁瑣的,主要是由於咱們還沒實現自動收集依賴項、自動觸發修改。
上一節講到了咱們實現了基本的響應功能,可是咱們目前仍是手動進行依賴收集和觸發更新的。
解決這個問題的方法應該是:
GET
)一個屬性時,咱們就調用 track(obj, <property>)
自動收集依賴項(存儲 effect
)SET
)一個屬性時,咱們就調用 trigger(obj, <property>
自動觸發更新(執行存儲的effect
)那麼如今問題就是,咱們如何在訪問或修改一個屬性時作到這樣的事情?也便是如何攔截這種 GET
和 SET
操做?
Vue 2中咱們使用 ES5 中的 Object.defineProperty
來攔截 GET
和 SET
。
Vue 3中咱們將使用 ES6 中的 Reflect
和 Proxy
。(注意:Vue 3再也不支持IE瀏覽器,因此能夠用比較多的高級特性)
咱們先來看一下怎麼輸出一個對象的一個屬性值,能夠用下面這三種方法:
.
=> obj.a
[]
=> obj['a']
Reflect
=> Reflect.get(obj, 'a')
這三種方法都是可行的,可是 Reflect
有很是強大的能力,後面會講到。
咱們先來看看 Proxy
,Proxy
是另外一個對象的佔位符,默認是對這個對象的委託。你能夠在這裏查看 Proxy 更詳細的用法。
let obj = { a: 1} let proxiedObj = new Proxy(obj, {}) console.log(proxiedObj.a) // 1
這個過程能夠表述爲,獲取 proxiedObj.a
時,直接去從查找 obj.a
而後返回給 proxiedObj
,再輸出 proxiedObj.a
。
Proxy
的第二個參數被稱爲 handler
,handler
就是包含捕捉器(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]
這一行:
childObj.b
時,childObj
上沒有屬性 b
,所以會從原型鏈上查找proxiedObj
proxiedObj.b
時,會觸發Proxy
捕捉器(trap)中的 get
,這直接從原始對象中返回了 target[key]
target[key]
中 key
是一個 getter
,所以這個 getter
中的上下文 this
即爲target,這裏的 target
就是 obj
,所以直接返回了 1
。參考 爲何要使用 Reflect
那麼咱們怎麼解決這個 this
出錯的問題呢?
如今咱們就能夠講講 Reflect
了。你能夠在這裏查看 Reflect 更詳細的用法。
捕獲器 get
有第三個參數叫作 receiver
。
Proxy
中handler.get(target, prop, receiver)
中的參數receiver
:Proxy
或者繼承Proxy
的對象。
Reflect.get(target, prop, receiver)
中的參數receiver
:若是target
對象中指定了getter
,receiver
則爲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
,有了這些知識,就能夠繼續完善咱們的代碼了。
如今修改咱們的示例代碼爲:
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
接下來咱們要作的就是結合 Proxy
的 handler
和 以前實現了的 track
、trigger
來完成一個響應式模塊。
首先,咱們來封裝一下 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
如今咱們要作的是去掉示例代碼中的 track
和 trigger
。
回到本節開頭提出的解決方案,咱們已經能夠攔截 GET
和 SET
操做了,只須要在適當的時候調用 track
和 trigger
方法便可,咱們修改 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
咱們已經去掉了手動 track
和 trigger
代碼,至此,咱們已經實現了 reactive
函數,看起來和Vue 3源碼差很少了。
但這還有點問題:
track
函數中的 effect
如今還沒處理,只能手動添加reactive
如今只能做用於對象,基本類型變量怎麼處理?下一個章節咱們將解決這個問題,讓咱們的代碼更加接近Vue 3。
首先,咱們修改一下示例代碼:
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
。
能想到怎麼來實現嗎?
首先,咱們定義一個變量 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
執行完後重置 shouldTrack
爲 null
,這樣結合剛纔修改的 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保持一致,這裏咱們修改 shouldTrack
爲 activeEffect
,如今它表示當前運行的 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
如今只能做用於對象,基本類型變量怎麼處理?
修改 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
屬性的對象有這兩種方法:
value
屬性function ref(intialValue) { return reactive({ value: intialValue }) }
getter
和 setter
來實現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
,而是選擇了比較複雜的第二種方法呢?
主要有三方面緣由:
ref
應該只有一個公開的屬性,即 value
,若是使用了 reactive
你能夠給這個變量增長新的屬性,這其實就破壞了 ref
的設計目的,它應該只用來包裝一個內部的 value
而不該該做爲一個通用的 reactive
對象;isRef
函數,用來判斷一個對象是 ref
對象而不是 reactive
對象,這種判斷在不少場景都是很是有必要的;reactive
作的事情遠比第二種實現 ref
的方法多,好比有各類檢查。回到上節中最後的示例代碼:
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 })
看到 timesA
和 sum
兩個變量,有同窗就會說:「這不就是計算屬性嗎,不能像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
?
咱們拿 timesA
先後的改動來講明,思考一下 computed
應該是什麼樣的?
ref()
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
函數或傳入一個有 get
和 set
的對象,而且有其它操做,這裏咱們不作實現,感興趣能夠去看源碼。
至此咱們已經實現了一個簡易版本的響應式庫了,完整代碼以下:
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.iterator
、Array.length
等觸發了 track
如何處理key
對應的 value
自己是一個 reactive
對象,如何處理你也能夠本身嘗試着實現它們。
本文完整內容見從零開始建立你的Vue 3