上一篇文章中介紹瞭如何調試
vue-next
。接下來開始解讀vue-next
的reactivity
模塊vue
vue3.0中比較大的改動之一就是響應式的實現有Object.defineProperty
改成Proxy
實現。閱讀以前能夠先提早了解下Proxy
。react
Object.defineProperty
對Object
偵聽須要遍歷遞歸全部的key
。因此在vue2.x中須要偵聽的數據須要先在data中定義,新增響應數據也須要使用$set
來添加偵聽。並且對Array
的偵聽也存在必定的問題。在vue3.0
就能夠不用考慮這些問題。bash
在vue3.0
中響應式代碼被放到單獨的模塊,代碼在/packages/reactivity
目錄下。每一個模塊的單元測試都放在__tests__
文件夾下。找到reactive.spec.ts
。代碼以下數據結構
import { reactive, isReactive, toRaw, markNonReactive } from '../src/reactive'
import { mockWarn } from '@vue/runtime-test'
describe('reactivity/reactive', () => {
mockWarn()
test('Object', () => {
const original = { foo: 1 }
const observed = reactive(original)
expect(observed).not.toBe(original)
expect(isReactive(observed)).toBe(true)
expect(isReactive(original)).toBe(false)
// get
expect(observed.foo).toBe(1)
// has
expect('foo' in observed).toBe(true)
// ownKeys
expect(Object.keys(observed)).toEqual(['foo'])
})
test('Array', () => {
const original: any[] = [{ foo: 1 }]
const observed = reactive(original)
expect(observed).not.toBe(original)
expect(isReactive(observed)).toBe(true)
expect(isReactive(original)).toBe(false)
expect(isReactive(observed[0])).toBe(true)
// get
expect(observed[0].foo).toBe(1)
// has
expect(0 in observed).toBe(true)
// ownKeys
expect(Object.keys(observed)).toEqual(['0'])
})
// ...
})
複製代碼
能夠大體看到reactive.ts提供了以下方法:app
Object|Array|Map|Set|WeakMap|WeakSet
常常和reactive結合起來使用的是effect
,它是偵聽到數據變化後的回調函數。effect
單元測試以下:函數
import {
reactive,
effect,
stop,
toRaw,
OperationTypes,
DebuggerEvent,
markNonReactive
} from '../src/index'
import { ITERATE_KEY } from '../src/effect'
describe('reactivity/effect', () => {
it('should run the passed function once (wrapped by a effect)', () => {
const fnSpy = jest.fn(() => {})
effect(fnSpy)
expect(fnSpy).toHaveBeenCalledTimes(1)
})
it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})
it('should observe multiple properties', () => {
let dummy
const counter = reactive({ num1: 0, num2: 0 })
effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))
expect(dummy).toBe(0)
counter.num1 = counter.num2 = 7
expect(dummy).toBe(21)
})
})
複製代碼
可總結出reactive + effect
的使用方法:post
import { reactive, effect } from 'dist/reactivity.global.js'
let dummy
<!-- reactive監聽對象 -->
const counter = reactive({ num: 0 })
<!-- 數據變更回調effect -->
effect(() => (dummy = counter.num))
複製代碼
從單元測試中能夠發現,reactive
函數和effect
分別在reactive.ts
和effect.ts
。接下來咱們從這兩個文件開始着手瞭解reactivity
的源碼。單元測試
參考下面這個例子,看看裏面都作了什麼。測試
import { reactive, effect } from 'dist/reactivity.global.js'
const counter = reactive({ num: 0, times: 0 })
effect(() => {console.log(counter.num)})
counter.num = 1
複製代碼
reactive()
會生成一個Proxy
對象counter
。effect()
時會默認調用一次內部函數() => {console.log(counter.num)}
(下文以fn
代替),運行fn
時會觸發counter.num
即get trap
。get trap
觸發track()
,會在targetMap
中增長num
依賴。// targetMap 存儲依賴關係,相似如下結構,這個結構會在 effect 文件中被用到
// {
// target: {
// key: Dep
// }
// }
// 解釋下三者究竟是什麼:target 就是被 proxy 的對象,key 是對象觸發 get 行爲之後的屬性
// export type Dep = Set<ReactiveEffect>
// export type KeyToDepMap = Map<string | symbol, Dep>
// export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
// get以後targetMap值
{
counter: {
num: [fn]
}
}
複製代碼
counter.num = 1
,會觸發counter
的set trap
trap,判斷num的值和oldValue不一致後,觸發trigger(),trigger中在targetMap中找到targetMap.counter.num的回調函數是fn。回調執行fn思考:若是改變了
counter.times
的值,回調函數fn:() => {console.log(counter.num)}
會不會執行呢?爲何?’ui
再次執行
counter.num = 1
即num
的值未改變,fn
會不會執行呢?
reactice
中核心代碼是createReactiveObject
,做用是建立一個proxy
對象
reactive(target: object) {
// 不是 readonly 就建立一個響應式對象,建立出來的對象和源對象不等
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
複製代碼
使用proxy建立一個代理對象。判斷對象的構造函數得出 handlers,集合類和別的類型用到的 handler 不同。collectionTypes的值爲
Set, Map, WeakMap, WeakSet
使用collectionHandlers。Object和Array使用baseHandlers
function createReactiveObject() {
// 判斷對象的構造函數得出 handlers,集合類和別的類型用到的 handler 不同
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 建立 proxy 對象,這裏主要要看 handlers 的處理了
// 因此咱們去 handlers 的具體實現文件夾吧,先看 baseHandlers 的
// 另外不熟悉 proxy 用法的,能夠先熟悉下文檔 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
observed = new Proxy(target, handlers)
return observed
}
複製代碼
mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
複製代碼
使用
Reflect.get
獲取get
的原始值,若是此值是對象,則遞歸返回具體的prox
y對象。track()
作的事情就是塞依賴到targetMap
中,用於下次尋找是否有這個依賴,另外就是把effect
的回調保存起來
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
// 得到結果
const res = Reflect.get(target, key, receiver)
// ....
// 這個函數作的事情就是塞依賴到 map 中,用於下次尋找是否有這個依賴
// 另外就是把 effect 的回調保存起來
track(target, OperationTypes.GET, key)
// 判斷get的值是否爲對象,是的話將對象包裝成 proxy(遞歸)
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
複製代碼
核心邏輯是trigger
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// ...
const result = Reflect.set(target, key, value, receiver)
// ...
// don't trigger if target is something up in the prototype chain of original // set 行爲核心邏輯是 trigger if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (value !== oldValue) { trigger(target, OperationTypes.SET, key) } return result } 複製代碼
targetMap
的數據結構以下,用來存儲依賴關係。 若是修改方式是CLEAR
,執行全部的回調。不然執行存儲的回調。另外ADD
、DELETE
會執行某些特殊的回調。
// targetMap 存儲依賴關係,相似如下結構,這個結構會在 effect 文件中被用到
// {
// target: {
// key: Dep
// }
// }
// 解釋下三者究竟是什麼:target 就是被 proxy 的對象,key 是對象觸發 get 行爲之後的屬性
// 好比 counter.num 觸發了 get 行爲,num 就是 key。dep 是回調函數,也就是 effect 中調用了 counter.num 的話
// 這個回調就是 dep,須要收集起來下次使用。
複製代碼
function trigger(
target: any,
type: OperationTypes,
key?: string | symbol,
extraInfo?: any
) {
const depsMap = targetMap.get(target)
// ...
const effects: Set<ReactiveEffect> = new Set()
const computedRunners: Set<ReactiveEffect> = new Set()
if (type === OperationTypes.CLEAR) {
// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {
// schedule runs for SET | ADD | DELETE
// depsMap.get(key) 取出依賴回調
if (key !== void 0) {
// 把依賴回調丟到 effects 中
addRunners(effects, computedRunners, depsMap.get(key as string | symbol))
}
// also run for iteration key on ADD | DELETE
if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}
const run = (effect: ReactiveEffect) => {
// 簡單點,就是執行回調函數
scheduleRun(effect, target, type, key, extraInfo)
}
// 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)
}
複製代碼
effect
在非lazy
的狀況下會直接調用effect
也就是傳入fn
,根據fn
生成targetMap
依賴。當依賴中的數據發生變化時會回調fn
。
export function effect(
fn: Function,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
// 判斷回調是否已經包裝過
if ((fn as ReactiveEffect).isEffect) {
fn = (fn as ReactiveEffect).raw
}
// 包裝回調,effect其實就是fn方法,在fn函數身上掛了不少屬性。
const effect = createReactiveEffect(fn, options)
// 不是 lazy 的話會直接調用一次。可是lazy狀況下,不調用effect,故而不會生成targetMap依賴。致使不能回調。不知道這是否是一個bug?
if (!options.lazy) {
effect()
}
// 返回值用以 stop
return effect
}
複製代碼