Proxy
提供了強大的 Javascript 元編程,儘管他不像其餘 ES6 功能用的廣泛,但Proxy
有許多功能,包括運算符重載,對象模擬,簡潔而靈活的API建立,對象變化事件,甚至Vue 3背後的內部響應系統提供動力。 javascript
Proxy
用於修改某些操做的默認行爲,也能夠理解爲在目標對象以前架設一層攔截,外部全部訪問都先通過這層攔截,因此咱們叫它爲代理模式。 vue
ES6原生提供了Proxy
構造函數,用來生成Proxy
實例。java
var proxy = new Proxy(target, handler);
Proxy
對象的全部用法,都是上面這種形式,不一樣的只是handle
參數的寫法。其中new Proxy
用來生成Proxy實例,target是表示所要攔截的對象,handle是用來定製攔截行爲的對象。
例子:react
const target = {} const proxy = new Proxy(target, { get: (obj, prop) => { console.log('設置 get 操做') return obj[prop]; }, set: (obj, prop, value) => { console.log('set 操做') obj[prop] = value; } }); proxy.a = 2 // set 操做 proxy.a // 設置 get 操做
當給目標對象進行賦值或獲取屬性時,就會分別觸發get
和set
方法,get
和set
就是咱們設置的代理,覆蓋了默認的賦值或獲取行爲。
固然,除了get
和set
,Proxy
還能夠攔截其餘共計13種操做編程
/* handler.get handler.set handler.has handler.apply handler.construct handler.ownKeys handler.deleteProperty handler.defineProperty handler.isExtensible handler.preventExtensions handler.getPrototypeOf handler.setPrototypeOf handler.getOwnPropertyDescriptor */ var target = function (a,b) { return a + b; }; const proxy = new Proxy(target, { apply: (target, thisArg, argumentsList) => { console.log('apply function', argumentsList) return target(argumentsList[0], argumentsList[1]) * 10; } }); proxy(1, 2)
let validator = { set: (obj, prop, value) => { if(prop === 'age') { if(!Number.isInteger(value)) { throw new TypeError('The age is not an integer') } if(value > 200) { throw new TypeError('The age is seems invalid') } } obj[prop] = value; return true; } }; let p = new Proxy({}, validator); p.age = '11' // Uncaught TypeError: The age is not an integer p.age = 2000 // Uncaught TypeError: The age is seems invalid p.age = 18 // true
咱們有時候可能會對一個對象的某些屬性進行一些限制,好比年齡age,只能是字符串並且不超過 200 歲,當不知足這些要求時咱們就能夠經過代理拋出錯誤app
10月出的時候,vue3公佈了源碼,其中數據響應式系統核心就是採用 Proxy
代理模式,咱們來看看它的源碼, reactivity
的源碼位置在packages
的文件內,
如下是簡化後的源碼。函數
// 代碼通過刪減 import { mutableHandlers, readonlyHandlers } from './baseHandlers' // rawToReactive 和 reactiveToRaw 是兩個弱引用的 Map 結構 // 這兩個 Map 用來保存原始數據 和 可響應數據 // 建立完 Proxy 後須要把原始數據和 Proxy對象分別保存到這兩個Map結構 const rawToReactive = new WeakMap() // 鍵是原始數據,值是響應數據 const reactiveToRaw = new WeakMap() // 鍵是響應數據,值是原始數據 export const targetMap = new WeakMap<any, KeyToDepMap>() // entry function reactive(target) { // if trying to observe a readonly proxy, return the readonly version. // 若是是隻讀proxy,直接返回 if (readonlyToRaw.has(target)) { return target } // target is explicitly marked as readonly by user // 若是目標被用戶標記爲只讀,那麼經過 readonly 建立一個只讀的Proxy if (readonlyValues.has(target)) { return readonly(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) // 把原數據當作key保存在targetMap,value值是一個 Map 類型 // if (!targetMap.has(target)) { targetMap.set(target, new Map()) } return observed }
reactive
方法就是暴露給外面的入口方法,方法裏面只作了一件事情,就是判斷是否要生成只讀的Proxy
對象,若是是則調用readonly
建立,不是則直接使用createReactiveObject
來生成響應是數據。 性能
createReactiveObject
裏面第一步嘗試在toProxy
中獲取是否已經有這個target
的響應式數據,若是有則直接把獲取到的返回出去,第二步判斷target
裏面是否已是可響應數據,第三步就是經過new Proxy
建立可響應數據,其中baseHandlers
在./baseHandlers.ts
這個文件下定義。建立完成後,把數據保存到toProxy
和toRaw
,這樣方便下次建立時使用。 this
咱們知道響應式數據是如何建立,接下來咱們看一下baseHandlers.ts
裏面定義的handler
實現代理
先看一段代碼,
let handler = { get: (obj, prop) => { console.log('get 操做') return obj[prop]; }, set: (obj, prop, value) => { console.log('set 操做') return true; } }; let p = new Proxy({ a: {} }, handler); p.a.c = 1 // get 操做
這時候咱們對target裏面的a對象進行賦值,可是咱們的set
裏面是不能觸發深度的數據賦值,可是這時候是會觸發get
,那麼這裏就會出現一個問題,較深層次的數據就沒法被代理到了。解決辦法很簡單,就是經過get
判斷值是否爲對象,若是是則把值再走一遍Proxy
。
function createGetter(isReadonly: boolean) { return function get(target: any, key: string | symbol, receiver: any) { const res = Reflect.get(target, key, receiver) // track(target, OperationTypes.GET, key) return isObject(res) ? isReadonly ? // need to lazy access readonly and reactive here to avoid // circular dependency readonly(res) : reactive(res) : res } } let handler = { get: createGetter(false), set: (obj, prop, value) => { console.log('set 操做') return true; } }; let p = new Proxy({ a: {} }, handler); p.a.c = 1 // get 操做
在vue3
中使用createGetter
方法來返回get
,createGetter
裏面判斷經過Reflect.get
獲取到的數據若是是Object
,則繼續調用reactive
生成Proxy
對象,從而得到了對對象內部的偵測。而且,每一次的 proxy 數據,都會保存在 WeakMap
中,訪問時會直接從中查找,從而提升性能。 track
方法和effect
有關,咱們下文再說。
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean { const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // 是否新增 key // trigger 是用來觸發回調 if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } return result }
對於 set
函數來講,有主要兩個做用,第一個就是設置值,第二個是調用 trigger
,這也是 effect
中的內容。
簡單來講,若是某個 effect
回調中有使用到 value.num
,那麼這個回調會經過track
方法被收集起來,並在調用 value.num = 2
時經過trigger
觸發。
那麼怎麼收集這些內容呢?這就要說說 targetMap
這個對象了。targetMap
是在reactive
裏面建立的WeakMap
類型,
它用於存儲依賴關係。
// effect.ts import { targetMap } from './reactive' // track用來把回調保存在 targetMap 中 export function track( target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack) { return } // activeReactiveEffectStack 的用處是保持依賴函數的存在 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] if (effect) { // 這個函數作的事情就是塞依賴到 map 中,用於下次尋找是否有這個依賴 // 另外就是把 effect 的回調保存起來 // 經過獲取targetMap上保存的 Map 類型數據 let depsMap = targetMap.get(target) if (depsMap === void 0) { // 什麼都沒有,設置空的map給它 targetMap.set(target, (depsMap = new Map())) } // 獲取target中的依賴 let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) } } }
咱們再瞭解一下effect
的組成
function createReactiveEffect( fn: Function, options: ReactiveEffectOptions ): ReactiveEffect { // 一系列賦值操做,重點看 run 的實現 const effect = function effect(...args): any { return run(effect as ReactiveEffect, fn, args) } as ReactiveEffect effect.isEffect = true effect.active = true effect.raw = fn effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.onStop = options.onStop effect.computed = options.computed // 用於收集依賴函數 effect.deps = [] return effect } function run(effect: ReactiveEffect, fn: Function, args: any[]): any { if (!effect.active) { return fn(...args) } if (activeReactiveEffectStack.indexOf(effect) === -1) { cleanup(effect) // 執行回調 push,回調執行結束 pop // activeReactiveEffectStack 的用處是保持依賴函數的存在 // 舉個例子: // const counter = reactive({ num: 0 }) // effect(() => { // console.log(counter.num) // }) // counter.num = 7 // effect 回調在執行的過程當中會觸發 counter 的 get 函數 // get 函數會觸發 track,在 track 函數調用的過程當中會執行 effect.deps.push(dep) 而且將 // 也就是把回調 push 到了回調的 deps 屬性上 // 這樣在下次 counter.num = 7 的時候會觸發 counter 的 set 函數 // set 函數會觸發 trigger,在 trigger 函數中會 effects.forEach(run),把須要執行的回調都執行一遍 try { activeReactiveEffectStack.push(effect) return fn(...args) } finally { activeReactiveEffectStack.pop() } } }
咱們最後把流程再回顧一下,首先經過createReactiveObject
建立Proxy
對象,建立完成後把這個Proxy
對象看成key
保存在targetMap
中。當觸發get
方法時調用 track
函數,把依賴函數保存到targetMap
中。觸發set
的時候在調用trigger
運行回調。