在10月05日凌晨Vue3的源代碼正式發佈了,來自官方的消息:vue
目前的版本是 Pre-Alpha
, 倉庫地址: Vue-next, 能夠經過 Composition API瞭解更多新版本的信息, 目前版本單元測試相關狀況 vue-next-coverage。react
文章大綱:git
Vue 的核心之一就是響應式系統,經過偵測數據的變化,來驅動更新視圖。github
經過可響應對象,實現對數據的偵測,從而告知外界數據變化。實現可響應對象的方式:typescript
關於前兩個 API 的使用方式很少贅述,單一的訪問器 getter/setter
功能相對簡單,而做爲 Vue2.x 實現可響應對象的 API - defineProperty
, API 自己存在較多問題。api
Vue2.x 中,實現數據的可響應,須要對 Object
和 Array
兩種類型採用不一樣的處理方式。 Object
類型經過 Object.defineProperty
將屬性轉換成 getter/setter
,這個過程須要遞歸偵測全部的對象 key
,來實現深度的偵測。數組
爲了感知 Array
的變化,對 Array
原型上幾個改變數組自身的內容的方法作了攔截,雖然實現了對數組的可響應,但一樣存在一些問題,或者說不夠方便的狀況。 同時,defineProperty
經過遞歸實現 getter/setter
也存在必定的性能問題。函數
更好的實現方式是經過 ES6
提供的 Proxy API
。性能
Proxy API 具備更增強大的功能, 相比舊的 defineProperty
API ,Proxy
能夠代理數組,而且 API 提供了多個 traps
,能夠實現諸多功能。單元測試
這裏主要說兩個trap: get
、 set
, 以及其中的一些比較容易被忽略的細節。
let data = { foo: 'foo' }
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value // ?
}
})
p.foo = 123
// set value
複製代碼
經過 proxy
返回的對象 p
代理了對原始數據的操做,當對 p
設置時,即可以偵測到變化。可是這麼寫其實是有問題, 當代理的對象數據是數組時,會報錯。
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
}
})
p.push(4) // VM438:12 Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
複製代碼
將代碼更改成:
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
return target[key]
},
set(target, key, value, receiver) {
console.log('set value')
target[key] = value
return true
}
})
p.push(4)
// set value // 打印2次
複製代碼
實際上,當代理對象是數組,經過 push
操做,並不僅是操做當前數據,push
操做還觸發數組自己其餘屬性更改。
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return target[key]
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
target[key] = value
return true
}
})
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
複製代碼
先看 set
操做,從打印輸出能夠看出,push
操做除了給數組的第 3
位下標設置值 1
,還給數組的 length
值更改成 4
。 同時這個操做還觸發了 get 去獲取 push
和 length
兩個屬性。
咱們能夠經過 Reflect
來返回 trap 相應的默認行爲,對於 set 操做相對簡單,可是一些比較複雜的默認行爲處理起來相對繁瑣得多,Reflect
的做用就顯現出來了。
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.push(1)
// get value: push
// get value: length
// set value: 3 1
// set value: length 4
複製代碼
相比本身處理 set
的默認行爲,Reflect
就方便得多。
從前面的例子中能夠看出,當代理對象是數組時,push
操做會觸發屢次 set
執行,同時,也引起 get
操做,這點很是重要,vue3 就很好的使用了這點。 咱們能夠從另外一個例子來看這個操做:
let data = [1,2,3]
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.unshift('a')
// get value: unshift
// get value: length
// get value: 2
// set value: 3 3
// get value: 1
// set value: 2 2
// get value: 0
// set value: 1 1
// set value: 0 a
// set value: length 4
複製代碼
能夠看到,在對數組作 unshift
操做時,會屢次觸發 get
和 set
。 仔細觀察輸出,不難看出,get
先拿數組最末位下標,開闢新的下標 3
存放原有的末位數值,而後再將原數值都日後挪,將 0
下標設置爲了 unshift
的值 a
,由此引起了屢次 set
操做。
而這對於 通知外部操做 顯然是不利,咱們假設 set
中的 console
是觸發外界渲染的 render
函數,那麼這個 unshift
操做會引起 屢次 render
。
咱們後面會講述如何解決相應的這個問題,繼續。
let data = { foo: 'foo', bar: { key: 1 }, ary: ['a', 'b'] }
let p = new Proxy(data, {
get(target, key, receiver) {
console.log('get value:', key)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set value:', key, value)
return Reflect.set(target, key, value, receiver)
}
})
p.bar.key = 2
// get value: bar
複製代碼
執行代碼,能夠看到並無觸發 set
的輸出,反而是觸發了 get
,由於 set
的過程當中訪問了 bar
這個屬性。 因而可知,proxy
代理的對象只能代理到第一層,而對象內部的深度偵測,是須要開發者本身實現的。一樣的,對於對象內部的數組也是同樣。
p.ary.push('c')
// get value: ary
複製代碼
一樣只走了 get
操做,set
並不能感知到。
咱們注意到 get/set
還有一個參數:receiver
,對於 receiver
,其實接收的是一個代理對象:
let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
get(target, key, receiver) {
console.log(receiver)
const res = Reflect.get(target, key, receiver)
return res
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
})
// Proxy {a: {…}}
複製代碼
這裏 receiver
輸出的是當前代理對象,注意,這是一個已經代理後的對象。
let data = { a: {b: {c: 1 } } }
let p = new Proxy(data, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(res)
return res
},
set(target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
})
// {b: {c: 1} }
複製代碼
當咱們嘗試輸出 Reflect.get
返回的值,會發現,當代理的對象是多層結構時,Reflect.get
會返回對象的內層結構。
記住這一點,Vue3 實現深度的proxy ,即是很好的使用了這點。
前面提到了使用 Proxy
來偵測數據變化,有幾個細節問題,包括:
Reflect
來返回 trap
默認行爲set
操做,可能會引起代理對象的屬性更改,致使 set
執行屢次proxy
只能代理對象中的一層,對於對象內部的操做 set
未能感知,可是 get
會被執行接下來,咱們將先本身嘗試解決這些問題,後面再分析 Vue3 是如何解決這些細節的。
function reactive(data, cb) {
let timer = null
return new Proxy(data, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
clearTimeout(timer)
timer = setTimeout(() => {
cb && cb()
}, 0);
return Reflect.set(target, key, value, receiver)
}
})
}
let ary = [1, 2]
let p = reactive(ary, () => {
console.log('trigger')
})
p.push(3)
// trigger
複製代碼
程序輸出結果爲一個: trigger
這裏實現了 reactive
函數,接收兩個參數,第一個是被代理的數據 data
,還有一個回調函數 cb
, 咱們這裏先簡單的在 cb
中打印 trigger 操做,來模擬通知外部數據的變化。
解決重複的 cb
調用有不少中方式,比方經過標誌,來決定是否調用。而這裏是使用了定時器 setTimeout
, 每次調用 cb
以前,都清除定時器,來實現相似於 debounce
的操做,一樣能夠解決重複的 callback
問題。
目前還有一個問題,那即是深度的數據偵測,咱們可使用遞歸代理的方式來實現:
function reactive(data, cb) {
let res = null
let timer = null
res = data instanceof Array ? []: {}
for (let key in data) {
if (typeof data[key] === 'object') {
res[key] = reactive(data[key], cb)
} else {
res[key] = data[key]
}
}
return new Proxy(res, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, val) {
let res = Reflect.set(target, key, val)
clearTimeout(timer)
timer = setTimeout(() => {
cb && cb()
}, 0)
return res
}
})
}
let data = { foo: 'foo', bar: [1, 2] }
let p = reactive(data, () => {
console.log('trigger')
})
p.bar.push(3)
// trigger
複製代碼
對代理的對象進行遍歷,對每一個 key
都作一次 proxy
,這是遞歸實現的方式。 同時,結合前面提到的 timer
避免重複 set 的問題。
這裏咱們能夠輸出代理後的對象 p
:
能夠看到深度代理後的對象,都攜帶 proxy
的標誌。
到這裏,咱們解決了使用 proxy
實現偵測的系列細節問題,雖然這些處理方式能夠解決問題,但彷佛並不夠優雅,尤爲是遞歸 proxy
是一個性能隱患, 當數據對象比較大時,遞歸的 proxy 會消耗比較大的性能,而且有些數據並不是須要偵測,咱們須要對數據偵測作更細的控制。
接下來咱們就看下 Vue3 是如何使用 Proxy
實現數據偵測的。
Vue3 項目結構採用了 lerna
作 monorepo
風格的代碼管理,目前比較多的開源項目切換到了 monorepo 的模式, 比較顯著的特徵是項目中會有個 packages/
的文件夾。
Vue3 對功能作了很好的模塊劃分,同時使用 TS 。咱們直接在 packages 中找到響應式數據的模塊:
其中,reactive.ts
文件提供了 reactive
函數,該函數是實現響應式的核心。 同時這個函數也掛載在了全局的 Vue 對象上。
這裏對源代碼作一點程度的簡化:
const rawToReactive = new WeakMap()
const reactiveToRaw = new WeakMap()
// utils
function isObject(val) {
return typeof val === 'object'
}
function hasOwn(val, key) {
const hasOwnProperty = Object.prototype.hasOwnProperty
return hasOwnProperty.call(val, key)
}
// traps
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
return isObject(res) ? reactive(res) : res
}
}
function set(target, key, val, receiver) {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
val = reactiveToRaw.get(val) || val
const result = Reflect.set(target, key, val, receiver)
if (!hadKey) {
console.log('trigger ...')
} else if(val !== oldValue) {
console.log('trigger ...')
}
return result
}
// handler
const mutableHandlers = {
get: createGetter(),
set: set,
}
// entry
function reactive(target) {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
)
}
function createReactiveObject(target, toProxy, toRaw, baseHandlers) {
let observed = toProxy.get(target)
// 原數據已經有相應的可響應數據, 返回可響應數據
if (observed !== void 0) {
return observed
}
// 原數據已是可響應數據
if (toRaw.has(target)) {
return target
}
observed = new Proxy(target, baseHandlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
複製代碼
rawToReactive
和 reactiveToRaw
是兩個弱引用的 Map
結構,這兩個 Map
用來保存 原始數據
和 可響應數據
,在函數 createReactiveObject
中,toProxy
和 toRaw
傳入的即是這兩個 Map
。
咱們能夠經過它們,找到任何代理過的數據是否存在,以及經過代理數據找到原始的數據。
除了保存了代理的數據和原始數據,createReactiveObject
函數僅僅是返回了 new Proxy
代理後的對象。 重點在 new Proxy
中傳入的handler參數 baseHandlers
。
還記得前面提到的 Proxy
實現數據偵測的細節問題吧,咱們嘗試輸入:
let data = { foo: 'foo', ary: [1, 2] }
let r = reactive(data)
r.ary.push(3)
複製代碼
打印結果:
能夠看到打印輸出了一次 trigger ...
深度偵測數據是經過 createGetter
函數實現的,前面提到,當對多層級的對象操做時,set
並不能感知到,可是 get 會觸發, 於此同時,利用 Reflect.get()
返回的「多層級對象中內層」 ,再對「內層數據」作一次代理。
function createGetter() {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
return isObject(res) ? reactive(res) : res
}
}
複製代碼
能夠看到這裏判斷了 Reflect 返回的數據是否仍是對象,若是是對象,則再走一次 proxy
,從而得到了對對象內部的偵測。
而且,每一次的 proxy
數據,都會保存在 Map
中,訪問時會直接從中查找,從而提升性能。
當咱們打印代理後的對象時:
能夠看到這個代理後的對象內層並無代理的標誌,這裏僅僅是代理外層對象。
輸出其中一個存儲代理數據的 rawToReactive
:
對於內層 ary: [1, 2]
的代理,已經被存儲在了 rawToReactive
中。
由此實現了深度的數據偵測。
function hasOwn(val, key) {
const hasOwnProperty = Object.prototype.hasOwnProperty
return hasOwnProperty.call(val, key)
}
function set(target, key, val, receiver) {
console.log(target, key, val)
const hadKey = hasOwn(target, key)
const oldValue = target[key]
val = reactiveToRaw.get(val) || val
const result = Reflect.set(target, key, val, receiver)
if (!hadKey) {
console.log('trigger ... is a add OperationType')
} else if(val !== oldValue) {
console.log('trigger ... is a set OperationType')
}
return result
}
複製代碼
關於屢次 trigger
的問題,vue 處理得很巧妙。
在 set
函數中 hasOwn
前打印 console.log(target, key, val)
。
輸入:
let data = ['a', 'b']
let r = reactive(data)
r.push('c')
複製代碼
輸出結果:
r.push('c')
會觸發 set
執行兩次,一次是值自己 'c'
,一次是 length
屬性設置。
設置值 'c'
時,傳入的新增索引 key
爲 2
,target
是原始的代理對象 ['a', 'c']
,hasOwn(target, key)
顯然返回 false
,這是一個新增的操做,此時能夠執行 trigger ... is a add OperationType
。
當傳入 key
爲 length
時,hasOwn(target, key)
,length
是自身屬性,返回 true
,此時判斷 val !== oldValue
, val
是 3
, 而 oldValue
即爲 target['length']
也是 3
,此時不執行 trigger
輸出語句。
因此經過 判斷 key 是否爲 target 自身屬性,以及設置val是否跟target[key]相等 能夠肯定 trigger
的類型,而且避免多餘的 trigger
。
實際上本文主要集中講解 Vue3 中是如何使用 Proxy
來偵測數據的。 而在分析源碼以前,須要講清楚 Proxy
自己的一些特性,因此講了不少 Proxy
的前置知識。同時,咱們也經過本身的方式來解決這些問題。
最後,咱們對比了 Vue3 中, 是如何處理這些細節的。能夠看出,Vue3 並不是簡單的經過 Proxy
來遞歸偵測數據, 而是經過 get
操做來實現內部數據的代理,而且結合 WeakMap
來對數據保存,這將大大提升響應式數據的性能。
有興趣的小夥伴能夠針對 遞歸Proxy 和 這種Vue3的這種實現方式作相應的 benchmark , 這二者的性能差距比較大。
文章仍是對 reactive
作了很大程度的簡化,實際上要處理的細節要複雜得多。 更多的細節仍是須要查看源碼得到。