本篇文章同步發表在我的博客 Vue 3.0 —— Watch 與 Reactivity 代碼走讀react
若是對源碼查看沒有頭緒的能夠先參見參考文章數組
本篇文章爲梳理 scheduler、 effect、scheduler 與 proxy 之間的關係緩存
本篇文章以一個很簡單小例子打斷點入口開始分析,狀況很單一,僅僅是一個簡單的 object,沒有涉及到組件實例,目的也很簡單:搞清楚三者之間的工做流程、同時熟悉一些概念。app
例子代碼:函數
const { reactive, watch } = Vue
const a = reactive({ name: 'ym' })
watch(() => a.name, (val) => {
console.log(val)
}, { lazy: true })
setTimeout(() => {
a.name = 'cjh'
}, 1000)
複製代碼
咱們將 demo 代碼分爲 3個部分:post
因此代碼走讀也分爲三個部分,來分別參數這三個過程。性能
先用 reactive 初始化了對象 a,因此咱們看看 reactive 初始化過程ui
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
複製代碼
reactive 中有2個對象是須要理解的spa
rawToReactive = new WeakMap<any, any>()
普通對象與reactivity對象的映射reactiveToRaw = new WeakMap<any, any>()
reactivity對象與普通對象的映射利用 WeakMap 初始化的弱引用對象,弱引用對象在這裏的好處:調試
例如:
const wm = new WeakMap()
let arr = new Array(1024 * 1024)
wm.set(arr, 1)
// 這裏只用將arr的引用去除,而不用再將 wm 所引用的 arr 刪除
arr = null
複製代碼
這麼作是爲了緩存提升查找性能,由於對於一個嵌套對象,是須要遞歸遍歷每個屬性的。
reactive 本質是對 createReactiveObject 的包裹,其中傳入了 mutableHandlers,mutableHandlers 用來定義一個對象的屬性描述符,和 defineProperty 相似,這裏咱們只看 get 和 set。
// get 是由 createGetter 函數建立
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
// 避免循環引用
const res = Reflect.get(target, key, receiver)
// 排除關鍵字
if (typeof key === 'symbol' && builtInSymbols.has(key)) {
return res
}
// 自動 unwrap 屬性,因此對於一個 reactivity 對象的屬性,咱們直接 obj.property 便可
if (isRef(res)) {
return res.value
}
// 此處很是關鍵,屬於收集依賴的入口
track(target, OperationTypes.GET, key)
// 遞歸處理
return isObject(res) ? reactive(res) : res
}
}
// set
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)
// 當前僅當 target 和 調用對象相同時才作處理
// 關於 receiver 這裏能夠查看個人另一篇文章:熟悉 Proxy
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
// 觸發收集的依賴
trigger(target, OperationTypes.SET, key)
}
}
return result
}
複製代碼
初始化一個對象時,惟一值得說的就是遞歸對象,爲每個屬性都添加上 proxy,由於 proxy 的層級只有一層。
一樣,咱們發現全部的 Api 入口函數都只是內部函數的一個包裝,這樣利於邏輯的單一且反作用分隔。
// 針對 demo 的例子,咱們傳入 doWatch 的有三個參數,恰好對上
function doWatch(source, cb, WatchOptions): StopHandle {
let getter: Function
if (isArray(source)) {
// 保證 getter 拿到的始終是普通對象
getter = () =>
source.map(
s =>
// 這裏能夠發現 watch 數組時,也會自動 unwrap
isRef(s)
? s.value
: callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
)
} else if (isRef(source)) {
getter = () => source.value
} else if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[registerCleanup]
)
}
}
// 以上咱們能夠看到 watch 的對象有3種
// 數組
// ref包裹的對象
// 回調函數
// 最後的 else 實際上是錯誤處理
// callWithErrorHandling 是一個取值包裝函數,用來包裹取值時的錯誤處理
let oldValue = isArray(source) ? [] : undefined
// 包裹回調
// applyCb 是依賴更新後觸發的真正函數
const applyCb = cb
? () => {
const newValue = runner()
if (deep || newValue !== oldValue) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue,
registerCleanup
])
oldValue = newValue
}
}
: void 0
// 定義 scheduler,默認是值更新後再觸發
// 時機是 nextTick 後即下一個 task 隊列執行以前
let scheduler: (job: () => any) => void
scheduler = job => {
queuePostRenderEffect(job, suspense)
}
// 初始化 effect
const runner = effect(getter, {
lazy: true,
// so it runs before component update effects in pre flush mode
computed: true,
onTrack,
onTrigger,
scheduler: applyCb ? () => scheduler(applyCb) : scheduler
})
// 緩存舊值
oldValue = runner()
// 返回中止 watch 的句柄
return () => {
stop(runner)
}
}
複製代碼
watch 的初始化作了 2 件事
a.name = 'cjh'
的賦值,此時會觸發 set 的 trigger
trigger(target, OperationTypes.SET, key)
複製代碼
咱們先來看看 track 收集依賴的函數,由於 trigger 一定是依賴 track 收集後的數據的
export function track(target, type, key) {
// 初始化 reactive 時觸發 track
// activeReactiveEffectStack 是不會有值的,那麼這個依賴是何時注入的呢?
// 思考下,確定是在 watch 初始化的時候
// 咱們回到 watch,初始化舊值時,咱們初始化了 effect
// 在 run 函數中,activeReactiveEffectStack.push(effect)
// 因此這裏的依賴是存在的
const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
if (effect) {
// targetMap 是proxy遞歸時的用來存放的那層單一對象的鍵值對
// 其中 key 就是這個對象,值初始化空的 Map
// depsMap 是一個空的 Map
let depsMap = targetMap.get(target)
// dep 是一個 Set
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
if (!dep.has(effect)) {
// dep 用來存放全部對 watch 的 getter
dep.add(effect)
// ⚠️這一步不知道緣由???
// ️️⚠️包括 targetMap 什麼時候被 set ???否則的話 depsMap 永遠是個空的 Map
effect.deps.push(dep)
}
}
}
複製代碼
咱們再來看看 trigger 函數
export function trigger(target, type, key, extraInfo) {
// 由 trigger 添加的 dep 依賴的 Set
const depsMap = targetMap.get(target)
// 空的 Set
const effects = new Set<ReactiveEffect>()
const computedRunners = new Set<ReactiveEffect>()
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}
// also run for iteration key on ADD | DELETE
// 這是針對數組的 Proxy, push時會觸發屢次的 hack:一次是下標賦值,一次是 length 賦值
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)
}
複製代碼
trigger 函數作了 1 件事:添加 addRunners
runners ,再調用它們。
接下來再看看 addRunners
function addRunners(effects, computedRunners, effectsToAdd) {
if (effectsToAdd !== void 0) {
effectsToAdd.forEach(effect => {
// effect 就是 trigger 裏的 dep 數組
if (effect.computed) {
// 這裏應該是用來區分 computed 函數初始化的依賴
computedRunners.add(effect)
} else {
// 這是普通的 watch 依賴數組
effects.add(effect)
}
})
}
}
複製代碼
addRunners 區分 computed 分別爲 2 個數組 push 值。咱們這裏的 demo 沒有 computed,因此最終就是 forEach 數組調用 scheduleRun。
scheduleRun 就是調用 watch 初始化時的 applyCb
effect.scheduler(effect)
複製代碼
而初始化時
effect.scheduler = job => {
queuePostRenderEffect(job, suspense)
}
複製代碼
咱們看看 queuePostRenderEffect 函數,本質是調用的 queuePostFlushCb
export function queuePostFlushCb(cb: Function | Function[]) {
if (Array.isArray(cb)) {
// 這種寫法比 concat 優雅。。。
postFlushCbs.push.apply(postFlushCbs, cb)
} else {
postFlushCbs.push(cb)
}
if (!isFlushing) {
nextTick(flushJobs)
}
}
複製代碼
queuePostFlushCb 函數也比較簡單,收集回調函數,再 nextTick 後 flushJobs。
咱們能夠發現 scheduler 中有 2 個隊列:
對應的添加函數
很顯然,這是對應的 2 種更新時機的回調,而觸發這些回調都是由 flushJobs 完成:
function flushJobs(seenJobs?: JobCountMap) {
isFlushing = true
let job
while ((job = queue.shift())) {
try {
// queueJob
job()
} catch (err) {
handleError(err, null, ErrorCodes.SCHEDULER)
}
}
flushPostFlushCbs()
isFlushing = false
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length) {
flushJobs(seenJobs)
}
}
複製代碼
最後咱們回到回調函數 applyCb
() => {
// 獲取最新值
const newValue = runner()
// 若是值發生了改變
if (deep || newValue !== oldValue) {
// 觸發回調函數
// 能夠看到回調函數也能夠是一個 Promise
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue,
registerCleanup
])
oldValue = newValue
}
}
複製代碼
再回過頭來看這三個部分及它們的做用
Vue 3中還有 ref 和 computed ,我以爲熟悉完 reactivity 和 watch 後基本就能理解所有了。
固然其中還有不少細節沒有說到也不知道,由於必須有相應的場景你才能明白它這麼寫的做用,若是你連應用的場景都考慮不到或者說都沒用過,強行去理解就沒有太大意義了。
未完待續。