學習 Vue3.0 源碼必須對如下知識有所瞭解:vue
這些知識能夠看一下阮一峯老師的《ES6 入門教程》。react
若是不會 ts,我以爲影響不大,瞭解一下泛型就能夠了。由於我就沒用過 TS,可是不影響看代碼。es6
閱讀源碼,建議先過一遍該模塊下的 API,瞭解一下有哪些功能。而後再看一遍相關的單元測試,單元測試通常會把全部的功能細節都測一邊。對源碼的功能有所瞭解後,再去閱讀源碼的細節,效果更好。數組
const p = new Proxy(target, handler)
在閱讀源碼的過程當中,要時刻問本身三個問題:數據結構
正所謂知其然,知其因此然。app
閱讀源碼除了要了解一個庫具備什麼特性,還要了解它爲何要這樣設計,而且要問本身能不能用更好的方式去實現它。
若是隻是單純的停留在「是什麼」這個階段,對你可能沒有什麼幫助。就像看流水帳似的,看完就忘,你得去思考,才能理解得更加深入。less
reactivity 模塊是 Vue3.0 的響應式系統,它有如下幾個文件:函數
baseHandlers.ts collectionHandlers.ts computed.ts effect.ts index.ts operations.ts reactive.ts ref.ts
接下來按重要程度順序來說解一下各個文件的 API 用法和實現。oop
在 Vue.2x 中,使用 Object.defineProperty()
對對象進行監聽。而在 Vue3.0 中,改用 Proxy
進行監聽。Proxy
比起 Object.defineProperty()
有以下優點:post
reactive()
的做用主要是將目標轉化爲響應式的 proxy 實例。例如:
const obj = { count: 0 } const proxy = reactive(obj)
若是是嵌套的對象,會繼續遞歸將子對象轉爲響應式對象。
reactive()
是向用戶暴露的 API,它真正執行的是 createReactiveObject()
函數:
// 根據 target 生成 proxy 實例 function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { if (!isObject(target)) { if (__DEV__) { console.warn(`value cannot be made reactive: ${String(target)}`) } return target } // target is already a Proxy, return it. // exception: calling readonly() on a reactive object if ( target[ReactiveFlags.raw] && !(isReadonly && target[ReactiveFlags.isReactive]) ) { return target } // target already has corresponding Proxy if ( hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive) ) { return isReadonly ? target[ReactiveFlags.readonly] : target[ReactiveFlags.reactive] } // only a whitelist of value types can be observed. if (!canObserve(target)) { return target } const observed = new Proxy( target, // 根據是否 Set, Map, WeakMap, WeakSet 來決定 proxy 的 handler 參數 collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers ) // 在原始對象上定義一個屬性(只讀則爲 "__v_readonly",不然爲 "__v_reactive"),這個屬性的值就是根據原始對象生成的 proxy 實例。 def( target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive, observed ) return observed }
這個函數的處理邏輯以下:
__v_readonly
,不然爲 __v_reactive
),指向這個 proxy 實例,最後返回這個實例。添加這個屬性就是爲了在第 2 步作判斷用的,防止對同一對象重複監聽。其中第 三、4 點須要單獨拎出來說一講。
const canObserve = (value: Target): boolean => { return ( !value[ReactiveFlags.skip] && isObservableType(toRawType(value)) && !Object.isFrozen(value) ) }
canObserve()
函數就是用來判斷 value 是不是可觀察的對象,知足如下條件纔是可觀察的對象:
__v_skip
,__v_skip
是用來定義這個對象是否可跳過,即不監聽。Object,Array,Map,Set,WeakMap,WeakSet
纔可被監聽。根據上面的代碼能夠看出來,在生成 proxy 實例時,處理器對象是根據一個三元表達式產生的:
// collectionTypes 的值爲 Set, Map, WeakMap, WeakSet collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
這個三元表達式很是簡單,若是是普通的對象 Object
或 Array
,處理器對象就使用 baseHandlers;若是是 Set, Map, WeakMap, WeakSet
中的一個,就使用 collectionHandlers。
collectionHandlers 和 baseHandlers 是從 collectionHandlers.ts
和 baseHandlers.ts
處引入的,這裏先放一放,接下來再講。
createReactiveObject()
根據不一樣的參數,能夠建立多種不一樣的 proxy 實例:
reactive()
。淺層響應的 proxy 實例是什麼?
之因此有淺層響應的 proxy 實例,是由於 proxy 只代理對象的第一層屬性,更深層的屬性是不會代理的。若是確實須要生成徹底響應式的 proxy 實例,就得遞歸調用 reactive()
。不過這個過程是內部自動執行的,用戶感知不到。
// 判斷 value 是不是響應式的 export function isReactive(value: unknown): boolean { if (isReadonly(value)) { return isReactive((value as Target)[ReactiveFlags.raw]) } return !!(value && (value as Target)[ReactiveFlags.isReactive]) } // 判斷 value 是不是隻讀的 export function isReadonly(value: unknown): boolean { return !!(value && (value as Target)[ReactiveFlags.isReadonly]) } // 判斷 value 是不是 proxy 實例 export function isProxy(value: unknown): boolean { return isReactive(value) || isReadonly(value) } // 將響應式數據轉爲原始數據,若是不是響應數據,則返回源數據 export function toRaw<T>(observed: T): T { return ( (observed && toRaw((observed as Target)[ReactiveFlags.raw])) || observed ) } // 給 value 設置 skip 屬性,跳過代理,讓數據不可被代理 export function markRaw<T extends object>(value: T): T { def(value, ReactiveFlags.skip, true) return value }
在 baseHandlers.ts
文件中針對 4 種 proxy 實例定義了不對的處理器。
因爲它們之間差異不大,因此在這隻講解徹底響應式的處理器對象:
export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys }
處理器對五種操做進行了攔截,分別是:
其中 ownKeys 可攔截如下操做:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
Reflect.ownKeys()
其中 get、has、ownKeys 操做會收集依賴,set、deleteProperty 操做會觸發依賴。
get 屬性的處理器是用 createGetter()
函數建立的:
// /*#__PURE__*/ 標識此爲純函數 不會有反作用 方便作 tree-shaking const get = /*#__PURE__*/ createGetter() function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { // target 是不是響應式對象 if (key === ReactiveFlags.isReactive) { return !isReadonly // target 是不是隻讀對象 } else if (key === ReactiveFlags.isReadonly) { return isReadonly } else if ( // 若是訪問的 key 是 __v_raw,而且 receiver == target.__v_readonly || receiver == target.__v_reactive // 則直接返回 target key === ReactiveFlags.raw && receiver === (isReadonly ? (target as any).__v_readonly : (target as any).__v_reactive) ) { return target } const targetIsArray = isArray(target) // 若是 target 是數組而且 key 屬於三個方法之一 ['includes', 'indexOf', 'lastIndexOf'],即觸發了這三個操做之一 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。 // 若是不用 Reflect 來獲取,在監聽數組時能夠會有某些地方會出錯 // 具體請看文章《Vue3 中的數據偵測》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10 const res = Reflect.get(target, key, receiver) // 若是 key 是 symbol 而且屬於 symbol 的內置方法之一,或者訪問的是原型對象,直接返回結果,不收集依賴。 if ((isSymbol(key) && builtInSymbols.has(key)) || key === '__proto__') { return res } // 只讀對象不收集依賴 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 淺層響應當即返回,不遞歸調用 reactive() if (shallow) { return res } // 若是是 ref 對象,則返回真正的值,即 ref.value,數組除外。 if (isRef(res)) { // ref unwrapping, only for Objects, not for Arrays. return targetIsArray ? res : res.value } if (isObject(res)) { // 因爲 proxy 只能代理一層,因此 target[key] 的值若是是對象,就繼續對其進行代理 return isReadonly ? readonly(res) : reactive(res) } return res } }
這個函數的處理邏輯看代碼註釋應該就能明白,其中有幾個點須要單獨說一下:
Reflect.get()
builtInSymbols.has(key)
爲 true 或原型對象不收集依賴Reflect.get()
方法與從對象 (target[key])
中讀取屬性相似,但它是經過一個函數執行來操做的。
爲何直接用 target[key]
就能獲得值,卻還要用 Reflect.get(target, key, receiver)
來多倒一手呢?
先來看個簡單的示例:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { target[key] = value } }) p.push(100)
運行這段代碼會報錯:
Uncaught TypeError: 'set' on proxy: trap returned falsish for property '3'
但作一些小改動就可以正常運行:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { return target[key] }, set(target, key, value, receiver) { target[key] = value return true // 新增一行 return true } }) p.push(100)
這段代碼能夠正常運行。爲何呢?
區別在於新的這段代碼在 set()
方法上多了一個 return true
。我在 MDN 上查找到的解釋是這樣的:
set()
方法應當返回一個布爾值。
true
表明屬性設置成功。set()
方法返回 false
,那麼會拋出一個 TypeError
異常。這時我又試了一下直接執行 p[3] = 100
,發現能正常運行,只有執行 push
方法才報錯。到這一步,我心中已經有答案了。爲了驗證個人猜測,我在代碼上加了 console.log()
,把代碼執行過程的一些屬性打印出來。
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { console.log('get: ', key) return target[key] }, set(target, key, value, receiver) { console.log('set: ', key, value) target[key] = value return true } }) p.push(100) // get: push // get: length // set: 3 100 // set: length 4
從上面的代碼能夠發現執行 push
操做時,還會訪問 length
屬性。推測執行過程以下:根據 length
的值,得出最後的索引,再設置新的置,最後再改變 length
。
結合 MDN 的解釋,個人推測是數組的原生方法應該是運行在嚴格模式下的(若是有網友知道真相,請在評論區留言)。由於在 JS 中不少代碼在非嚴格模式和嚴格模式下都能正常運行,只是嚴格模式會給你報個錯。就跟此次狀況同樣,最後設置 length
屬性的時候報錯,但結果仍是正常的。若是不想報錯,就得每次都返回 true
。
而後再看一下 Reflect.set()
的返回值說明:
返回一個 Boolean 值代表是否成功設置屬性。
因此上面代碼能夠改爲這樣:
const p = new Proxy([1, 2, 3], { get(target, key, receiver) { console.log('get: ', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set: ', key, value) return Reflect.set(target, key, value, receiver) } }) p.push(100)
另外,無論 Proxy 怎麼修改默認行爲,你總能夠在 Reflect 上獲取默認行爲。
經過上面的示例,不難理解爲何要經過 Reflect.set()
來代替 Proxy 完成默認操做了。同理,Reflect.get()
也同樣。
// 若是 target 是數組而且 key 屬於三個方法之一 ['includes', 'indexOf', 'lastIndexOf'],即觸發了這三個操做之一 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) }
在執行數組的 includes
, indexOf
, lastIndexOf
方法時,會把目標對象轉爲 arrayInstrumentations
再執行。
const arrayInstrumentations: Record<string, Function> = {} ;['includes', 'indexOf', 'lastIndexOf'].forEach(key => { arrayInstrumentations[key] = function(...args: any[]): any { // 若是 target 對象中指定了 getter,receiver 則爲 getter 調用時的 this 值。 // 因此這裏的 this 指向 receiver,即 proxy 實例,toRaw 爲了取得原始數據 const arr = toRaw(this) as any // 對數組的每一個值進行 track 操做,收集依賴 for (let i = 0, l = (this as any).length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '') } // we run the method using the original args first (which may be reactive) // 參數有多是響應式的,函數執行後返回值爲 -1 或 false,那就用參數的原始值再試一遍 const res = arr[key](...args) if (res === -1 || res === false) { // if that didn't work, run it again using raw values. return arr[key](...args.map(toRaw)) } else { return res } } })
從上述代碼能夠看出,Vue3.0 對 includes
, indexOf
, lastIndexOf
進行了封裝,除了返回原有方法的結果外,還會對數組的每一個值進行依賴收集。
builtInSymbols.has(key)
爲 true 或原型對象不收集依賴const p = new Proxy({}, { get(target, key, receiver) { console.log('get: ', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set: ', key, value) return Reflect.set(target, key, value, receiver) } }) p.toString() // get: toString // get: Symbol(Symbol.toStringTag) p.__proto__ // get: __proto__
從 p.toString()
的執行結果來看,它會觸發兩次 get,一次是咱們想要的,一次是咱們不想要的(我還沒搞明白爲何會有 Symbol(Symbol.toStringTag)
,若是有網友知道,請在評論區留言)。因此就有了這個判斷: builtInSymbols.has(key)
爲 true
就直接返回,防止重複收集依賴。
再看 p.__proto__
的執行結果,也觸發了一次 get 操做。通常來講,沒有場景須要單獨訪問原型,訪問原型都是爲了訪問原型上的方法,例如 p.__proto__.toString()
這樣使用,因此 key 爲 __proto__
的時候也要跳過,不收集依賴。
const set = /*#__PURE__*/ createSetter() // 參考文檔《Vue3 中的數據偵測》——https://juejin.im/post/5d99be7c6fb9a04e1e7baa34#heading-10 function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) // 若是原來的值是 ref,但新的值不是,將新的值賦給 ref.value 便可。 if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { // in shallow mode, objects are set as-is regardless of reactive or not } const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { if (!hadKey) { // 若是 target 沒有 key,就表明是新增操做,須要觸發依賴 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 若是新舊值不相等,才觸發依賴 // 何時會有新舊值相等的狀況?例如監聽一個數組,執行 push 操做,會觸發屢次 setter // 第一次 setter 是新加的值 第二次是因爲新加的值致使 length 改變 // 但因爲 length 也是自身屬性,因此 value === oldValue trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }
set()
的函數處理邏輯反而沒那麼難,看註釋便可。track()
和 trigger()
將放在下面和 effect.ts 文件一塊兒講解。
function deleteProperty(target: object, key: string | symbol): boolean { const hadKey = hasOwn(target, key) const oldValue = (target as any)[key] const result = Reflect.deleteProperty(target, key) // 若是刪除結果爲 true 而且 target 擁有這個 key 就觸發依賴 if (result && hadKey) { trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } function has(target: object, key: string | symbol): boolean { const result = Reflect.has(target, key) track(target, TrackOpTypes.HAS, key) return result } function ownKeys(target: object): (string | number | symbol)[] { track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target) }
這三個函數比較簡單,看代碼便可。
等把 effect.ts 文件講解完,響應式模塊基本上差很少結束了。
effect()
主要和響應式的對象結合使用。
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { // 若是已是 effect 函數,取得原來的 fn if (isEffect(fn)) { fn = fn.raw } const effect = createReactiveEffect(fn, options) // 若是 lazy 爲 false,立刻執行一次 // 計算屬性的 lazy 爲 true if (!options.lazy) { effect() } return effect }
真正建立 effect 的是 createReactiveEffect()
函數。
let uid = 0 function createReactiveEffect<T = any>( fn: (...args: any[]) => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { // reactiveEffect() 返回一個新的 effect,這個新的 effect 執行後 // 會將本身設爲 activeEffect,而後再執行 fn 函數,若是在 fn 函數裏對響應式屬性進行讀取 // 會觸發響應式屬性 get 操做,從而收集依賴,而收集的這個依賴函數就是 activeEffect const effect = function reactiveEffect(...args: unknown[]): unknown { if (!effect.active) { return options.scheduler ? undefined : fn(...args) } // 爲了不遞歸循環,因此要檢測一下 if (!effectStack.includes(effect)) { // 清空依賴 cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn(...args) } finally { // track 將依賴函數 activeEffect 添加到對應的 dep 中,而後在 finally 中將 activeEffect // 重置爲上一個 effect 的值 effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] } } } as ReactiveEffect effect.id = uid++ effect._isEffect = true effect.active = true // 用於判斷當前 effect 是否激活,有一個 stop() 來將它設爲 false effect.raw = fn effect.deps = [] effect.options = options return effect }
其中 cleanup(effect)
的做用是讓 effect 關聯下的全部 dep 實例清空 effect,即清除這個依賴函數。
function cleanup(effect: ReactiveEffect) { const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } }
從代碼中能夠看出來,真正的依賴函數是 activeEffect。執行 track()
收集的依賴就是 activeEffect。
趁熱打鐵,如今咱們再來看一下 track()
和 trigger()
函數。
// 依賴收集 export function track(target: object, type: TrackOpTypes, key: unknown) { // activeEffect 爲空,表明沒有依賴,直接返回 if (!shouldTrack || activeEffect === undefined) { return } // targetMap 依賴管理中心,用於收集依賴和觸發依賴 let depsMap = targetMap.get(target) // targetMap 爲每一個 target 創建一個 map // 每一個 target 的 key 對應着一個 dep // 而後用 dep 來收集依賴函數,當監聽的 key 值發生變化時,觸發 dep 中的依賴函數 // 相似於這樣 // targetMap(weakmap) = { // target1(map): { // key1(dep): (fn1,fn2,fn3...) // key2(dep): (fn1,fn2,fn3...) // }, // target2(map): { // key1(dep): (fn1,fn2,fn3...) // key2(dep): (fn1,fn2,fn3...) // }, // } if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = new Set())) } if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) // 開發環境下會觸發 onTrack 事件 if (__DEV__ && activeEffect.options.onTrack) { activeEffect.options.onTrack({ effect: activeEffect, target, type, key }) } } }
targetMap 是一個 WeakMap
實例。
WeakMap 對象是一組鍵/值對的集合,其中的鍵是弱引用的。其鍵必須是對象,而值能夠是任意的。
弱引用是什麼意思呢?
let obj = { a: 1 } const map = new WeakMap() map.set(obj, '測試') obj = null
當 obj 置爲空後,對於 { a: 1 }
的引用已經爲零了,下一次垃圾回收時就會把 weakmap 中的對象回收。
但若是把 weakmap 換成 map 數據結構,即便把 obj 置空,{ a: 1 }
依然不會被回收,由於 map 數據結構是強引用,它如今還被 map 引用着。
// 觸發依賴 export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target) // 若是沒有收集過依賴,直接返回 if (!depsMap) { // never been tracked return } // 對收集的依賴進行分類,分爲普通的依賴或計算屬性依賴 // effects 收集的是普通的依賴 computedRunners 收集的是計算屬性的依賴 // 兩個隊列都是 set 結構,爲了不重複收集依賴 const effects = new Set<ReactiveEffect>() const computedRunners = new Set<ReactiveEffect>() const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { // effect !== activeEffect 避免重複收集依賴 if (effect !== activeEffect || !shouldTrack) { // 計算屬性 if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } } else { // the effect mutated its own dependency during its execution. // this can be caused by operations like foo.value++ // do not trigger or we end in an infinite loop } }) } } // 在值被清空前,往相應的隊列添加 target 全部的依賴 if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target depsMap.forEach(add) } else if (key === 'length' && isArray(target)) { // 當數組的 length 屬性變化時觸發 depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { add(dep) } }) } else { // schedule runs for SET | ADD | DELETE // 若是不符合以上兩個 if 條件,而且 key !== undefined,往相應的隊列添加依賴 if (key !== void 0) { add(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET const isAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && !isArray(target)) if ( isAddOrDelete || (type === TriggerOpTypes.SET && target instanceof Map) ) { add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) } if (isAddOrDelete && target instanceof Map) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) } } const run = (effect: ReactiveEffect) => { if (__DEV__ && effect.options.onTrigger) { effect.options.onTrigger({ effect, target, key, type, newValue, oldValue, oldTarget }) } if (effect.options.scheduler) { // 若是 scheduler 存在則調用 scheduler,計算屬性擁有 scheduler effect.options.scheduler(effect) } else { effect() } } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. computedRunners.forEach(run) // 觸發依賴函數 effects.forEach(run) }
對依賴函數進行分類後,須要先運行計算屬性的依賴,由於其餘普通的依賴函數可能包含了計算屬性。先執行計算屬性的依賴能保證普通依賴執行時能獲得最新的計算屬性的值。
這個 type 取值範圍就定義在 operations.ts
文件中:
// track 的類型 export const enum TrackOpTypes { GET = 'get', // get 操做 HAS = 'has', // has 操做 ITERATE = 'iterate' // ownKeys 操做 } // trigger 的類型 export const enum TriggerOpTypes { SET = 'set', // 設置操做,將舊值設置爲新值 ADD = 'add', // 新增操做,添加一個新的值 例如給對象新增一個值 數組的 push 操做 DELETE = 'delete', // 刪除操做 例如對象的 delete 操做,數組的 pop 操做 CLEAR = 'clear' // 用於 Map 和 Set 的 clear 操做。 }
type 主要用於標識 track()
和 trigger()
的類型。
if (key !== void 0) { add(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET const isAddOrDelete = type === TriggerOpTypes.ADD || (type === TriggerOpTypes.DELETE && !isArray(target)) if ( isAddOrDelete || (type === TriggerOpTypes.SET && target instanceof Map) ) { add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) } if (isAddOrDelete && target instanceof Map) { add(depsMap.get(MAP_KEY_ITERATE_KEY)) }
在 trigger()
中有這麼一段連續判斷的代碼,它們做用是什麼呢?其實它們是用於判斷數組/集合這種數據結構比較特別的操做。
看個示例:
let dummy const counter = reactive([]) effect(() => (dummy = counter.join())) counter.push(1)
effect(() => (dummy = counter.join()))
生成一個依賴,而且自執行一次。
在執行函數裏的代碼 counter.join()
時,會訪問數組的多個屬性,分別是 join
和 length
,同時觸發 track()
收集依賴。也就是說,數組的 join
length
屬性都收集了一個依賴。
當執行 counter.push(1)
這段代碼時,其實是將數組的索引 0 對應的值設爲 1。這一點,能夠經過打 debugger 從上下文環境看出來,其中 key 爲 0,即數組的索引,值爲 1。
設置值後,因爲是新增操做,執行 trigger(target, TriggerOpTypes.ADD, key, value)
。但由上文可知,只有數組的 key 爲 join
length
時,纔有依賴,key 爲 0 是沒有依賴的。
從上面兩個圖能夠看出來,只有 join
length
屬性纔有對應的依賴。
這個時候,trigger()
的一連串 if 語句就起做用了,其中有一個 if 語句是這樣的:
if ( isAddOrDelete || (type === TriggerOpTypes.SET && target instanceof Map) ) { add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY)) }
若是 target 是一個數組,就添加 length
屬性對應的依賴到隊列中。也就是說 key 爲 0 的狀況下使用 length
對應的依賴。
另外,還有一個巧妙的地方。待執行依賴的隊列是一個 set 數據結構。若是 key 爲 0 有對應的依賴,同時 length
也有對應的依賴,就會添加兩次依賴,但因爲隊列是 set,具備自動去重的效果,避免了重複執行。
僅看代碼和文字,是很難理解響應式數據和 track()
trigger()
是怎麼配合的。因此咱們要配合示例來理解:
let dummy const counter = reactive({ num: 0 }) effect(() => (dummy = counter.num)) console.log(dummy == 0) counter.num = 7 console.log(dummy == 7)
上述代碼執行過程以下:
{ num: 0 }
進行監聽,返回一個 proxy 實例,即 counter。effect(fn)
建立一個依賴,而且在建立時會執行一次 fn
。fn()
讀取 num 的值,並賦值給 dummy。counter.num = 7
這個操做會觸發 proxy 的屬性設置攔截操做,在這個攔截操做裏,除了把新的值返回,還會觸發剛纔收集的依賴。在這個依賴裏把 counter.num 賦值給 dummy(num 的值已經變爲 7)。用圖來表示,大概這樣的:
collectionHandlers.ts 文件包含了 Map
WeakMap
Set
WeakSet
的處理器對象,分別對應徹底響應式的 proxy 實例、淺層響應的 proxy 實例、只讀 proxy 實例。這裏只講解對應徹底響應式的 proxy 實例的處理器對象:
export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = { get: createInstrumentationGetter(false, false) }
爲何只監聽 get 操做,set has 等操做呢?不着急,先看一個示例:
const p = new Proxy(new Map(), { get(target, key, receiver) { console.log('get: ', key) return Reflect.get(target, key, receiver) }, set(target, key, value, receiver) { console.log('set: ', key, value) return Reflect.set(target, key, value, receiver) } }) p.set('ab', 100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]
運行上面的代碼會報錯。其實這和 Map Set 的內部實現有關,必須經過 this 才能訪問它們的數據。可是經過 Reflect 反射的時候,target 內部的 this 實際上是指向 proxy 實例的,因此就不難理解爲何會報錯了。
那怎麼解決這個問題?經過源碼能夠發現,在 Vue3.0 中是經過代理的方式來實現對 Map Set 等數據結構監聽的:
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { const instrumentations = shallow ? shallowInstrumentations : isReadonly ? readonlyInstrumentations : mutableInstrumentations return ( target: CollectionTypes, key: string | symbol, receiver: CollectionTypes ) => { // 這三個 if 判斷和 baseHandlers 的處理方式同樣 if (key === ReactiveFlags.isReactive) { return !isReadonly } else if (key === ReactiveFlags.isReadonly) { return isReadonly } else if (key === ReactiveFlags.raw) { return target } return Reflect.get( hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver ) } }
把最後一行代碼簡化一下:
target = hasOwn(instrumentations, key) && key in target? instrumentations : target return Reflect.get(target, key, receiver);
其中 instrumentations 的內容是:
const mutableInstrumentations: Record<string, Function> = { get(this: MapTypes, key: unknown) { return get(this, key, toReactive) }, get size() { return size((this as unknown) as IterableCollections) }, has, add, set, delete: deleteEntry, clear, forEach: createForEach(false, false) }
從代碼能夠看到,原來真正的處理器對象是 mutableInstrumentations。如今再看一個示例:
const proxy = reactive(new Map()) proxy.set('key', 100)
生成 proxy 實例後,執行 proxy.set('key', 100)
。proxy.set
這個操做會觸發 proxy 的屬性讀取攔截操做。
打斷點能夠看到,此時的 key 爲 set
。攔截了 set
操做後,調用 Reflect.get(target, key, receiver)
,這個時候的 target 已經不是原來的 target 了,而是 mutableInstrumentations 對象。也就是說,最終執行的是 mutableInstrumentations.set()
。
接下來再看看 mutableInstrumentations 的各個處理器邏輯。
// 若是 value 是對象,則返回一個響應式對象(`reactive(value)`),不然直接返回 value。 const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value get(this: MapTypes, key: unknown) { // this 指向 proxy return get(this, key, toReactive) } function get( target: MapTypes, key: unknown, wrap: typeof toReactive | typeof toReadonly | typeof toShallow ) { target = toRaw(target) const rawKey = toRaw(key) // 若是 key 是響應式的,額外收集一次依賴 if (key !== rawKey) { track(target, TrackOpTypes.GET, key) } track(target, TrackOpTypes.GET, rawKey) // 使用 target 原型上的方法 const { has, get } = getProto(target) // 原始 key 和響應式的 key 都試一遍 if (has.call(target, key)) { // 讀取的值要使用包裝函數處理一下 return wrap(get.call(target, key)) } else if (has.call(target, rawKey)) { return wrap(get.call(target, rawKey)) } }
get 的處理邏輯很簡單,攔截 get 以後,調用 get(this, key, toReactive)
。
function set(this: MapTypes, key: unknown, value: unknown) { value = toRaw(value) // 取得原始數據 const target = toRaw(this) // 使用 target 原型上的方法 const { has, get, set } = getProto(target) let hadKey = has.call(target, key) if (!hadKey) { key = toRaw(key) hadKey = has.call(target, key) } else if (__DEV__) { checkIdentityKeys(target, has, key) } const oldValue = get.call(target, key) const result = set.call(target, key, value) // 防止重複觸發依賴,若是 key 已存在就不觸發依賴 if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 若是新舊值相等,也不會觸發依賴 trigger(target, TriggerOpTypes.SET, key, value, oldValue) } return result }
set 的處理邏輯也較爲簡單,配合註釋一目瞭然。
還有剩下的 has
add
delete
等方法就不講解了,代碼行數比較少,邏輯也很簡單,建議自行閱讀。
const convert = <T extends unknown>(val: T): T => isObject(val) ? reactive(val) : val export function ref(value?: unknown) { return createRef(value) } function createRef(rawValue: unknown, shallow = false) { // 若是已是 ref 對象了,直接返回原值 if (isRef(rawValue)) { return rawValue } // 若是不是淺層響應而且 rawValue 是個對象,調用 reactive(rawValue) let value = shallow ? rawValue : convert(rawValue) const r = { __v_isRef: true, // 用於標識這是一個 ref 對象,防止重複監聽 ref 對象 get value() { // 讀取值時收集依賴 track(r, TrackOpTypes.GET, 'value') return value }, set value(newVal) { if (hasChanged(toRaw(newVal), rawValue)) { rawValue = newVal value = shallow ? newVal : convert(newVal) // 設置值時觸發依賴 trigger( r, TriggerOpTypes.SET, 'value', __DEV__ ? { newValue: newVal } : void 0 ) } } } return r }
在 Vue2.x 中,基本數值類型是不能監聽的。但在 Vue3.0 中經過 ref()
能夠實現這一效果。
const r = ref(0) effect(() => console.log(r.value)) // 打印 0 r.value++ // 打印 1
ref()
會把 0 轉成一個 ref 對象。若是給 ref(value)
傳的值是個對象,在函數內部會調用 reactive(value)
將其轉爲 proxy 實例。
export function computed<T>( options: WritableComputedOptions<T> ): WritableComputedRef<T> export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> // 若是 getterOrOptions 是個函數,則是不可被配置的,setter 設爲空函數 if (isFunction(getterOrOptions)) { getter = getterOrOptions setter = __DEV__ ? () => { console.warn('Write operation failed: computed value is readonly') } : NOOP } else { // 若是是個對象,則可讀可寫 getter = getterOrOptions.get setter = getterOrOptions.set } // dirty 用於判斷計算屬性依賴的響應式屬性有沒有被改變 let dirty = true let value: T let computed: ComputedRef<T> const runner = effect(getter, { lazy: true, // lazy 爲 true,生成的 effect 不會立刻執行 // mark effect as computed so that it gets priority during trigger computed: true, scheduler: () => { // 調度器 // trigger 時,計算屬性執行的是 effect.options.scheduler(effect) 而不是 effect() if (!dirty) { dirty = true trigger(computed, TriggerOpTypes.SET, 'value') } } }) computed = { __v_isRef: true, // expose effect so computed can be stopped effect: runner, get value() { if (dirty) { value = runner() dirty = false } track(computed, TrackOpTypes.GET, 'value') return value }, set value(newValue: T) { setter(newValue) } } as any return computed }
下面經過一個示例,來說解一下 computed 是怎麼運做的:
const value = reactive({}) const cValue = computed(() => value.foo) console.log(cValue.value === undefined) value.foo = 1 console.log(cValue.value === 1)
computed()
生成計算屬性對象,當對 cValue 進行取值時(cValue.value
),根據 dirty 判斷是否須要運行 effect 函數進行取值,若是 dirty 爲 false,直接把值返回。() => value.foo
) 取值。在取值過程當中,讀取 foo 的值(value.foo
)。value.foo = 1
),就會 trigger 這個 activeEffect 函數。scheduler()
將 dirty 設爲 true,這樣 computed 下次求值時會從新執行 effect 函數進行取值。index.ts 文件向外導出 reactivity 模塊的 API。