Vue3源碼解讀computed和watch
computed
computed和watch在面試中經(jīng)常被問到他們的區(qū)別,那么我們就從源碼的實現(xiàn)來看看他們的具體實現(xiàn)
// packages/reactivity/src/computed.ts
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// new ComputedRefImpl
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
// 返回ComputedRefImpl實例
return cRef as any
}
可以看到computed內(nèi)部只是先處理getter和setter,然后new一個ComputedRefImpl返回,如果你知道ref API的實現(xiàn),可以發(fā)現(xiàn)他們的實現(xiàn)有很多相同之處
ComputedRefImpl
// packages/reactivity/src/computed.ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined // 存儲effect的集合
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _dirty = true // 是否需要重新更新value
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// 創(chuàng)建effect
this.effect = new ReactiveEffect(getter, () => {
// 調(diào)度器執(zhí)行 重新賦值_dirty為true
if (!this._dirty) {
this._dirty = true
// 觸發(fā)effect
triggerRefValue(this)
}
})
// 用于區(qū)分effect是否是computed
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
// computed ref可能被其他代理包裝,例如readonly() #3376
// 通過toRaw()獲取原始值
const self = toRaw(this)
// 收集effect
trackRefValue(self)
// 如果是臟的,重新執(zhí)行effect.run(),并且將_dirty設(shè)置為false
if (self._dirty || !self._cacheable) {
self._dirty = false
// run()方法會執(zhí)行g(shù)etter方法 值會被緩存到self._value
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
可以看到ComputedRefImplget的get實現(xiàn)基本和ref的get相同(不熟悉ref實現(xiàn)的請看上一章),唯一的區(qū)別就是_dirty值的判斷,這也是我們常說的computed會緩存value,那么computed是如何知道value需要更新呢?
可以看到在computed構(gòu)造函數(shù)中,會建立一個getter與其內(nèi)部響應(yīng)式數(shù)據(jù)的關(guān)系,這跟我們組件更新函數(shù)跟響應(yīng)式數(shù)據(jù)建立關(guān)系是一樣的,所以與getter相關(guān)的響應(yīng)式數(shù)據(jù)發(fā)生修改的時候,就會觸發(fā)getter effect 對應(yīng)的scheduler,這里會將_dirty設(shè)置為true并去執(zhí)行收集到的effect(這里通常是執(zhí)行g(shù)et里收集到的函數(shù)更新的effect),然后就會去執(zhí)行函數(shù)更新函數(shù),里面會再次觸發(fā)computed的get,此時dirty已經(jīng)被置為true,就會重新執(zhí)行g(shù)etter獲取新的值返回,并將該值緩存到_vlaue。
小結(jié):
所以computed是有兩層的響應(yīng)式處理的,一層是computed.value和函數(shù)的effect之間的關(guān)系(與ref的實現(xiàn)相似),一層是computed的getter和響應(yīng)式數(shù)據(jù)的關(guān)系。
注意:如果你足夠細(xì)心就會發(fā)現(xiàn)函數(shù)更新函數(shù)的effect觸發(fā)和computed getter的effect的觸發(fā)之間可能存在順序的問題。假如有一個響應(yīng)式數(shù)據(jù)a不僅存在于getter中,還在函數(shù)render中早于getter被訪問,此時a對應(yīng)的dep中更新函數(shù)的effect就會早于getter的effect被收集,如果此時a被改變,就會先執(zhí)行更新函數(shù)的effect,那么此時render函數(shù)訪問到computed.value的時候就會發(fā)現(xiàn)_dirty依然是false,因為getter的effect還沒有被執(zhí)行,那么此時依然會是舊值。vue3中對此的處理是執(zhí)行effects的時候會優(yōu)先執(zhí)行computed對應(yīng)的effect(此前章節(jié)也有提到):
// packages/reactivity/src/effect.ts
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
// computed的effect會先執(zhí)行
// 防止render獲取computed值得時候_dirty還沒有置為true
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
watch
watch相對于computed要更簡單一些,因為他只用建立getter與響應(yīng)式數(shù)據(jù)之間的關(guān)系,在響應(yīng)式數(shù)據(jù)變化時調(diào)用用戶傳過來的回調(diào)并將新舊值傳入即可
// packages/runtime-core/src/apiWatch.ts
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
if (__DEV__ && !isFunction(cb)) {
warn(...)
}
// watch 具體實現(xiàn)
return doWatch(source as any, cb, options)
}
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
if (__DEV__ && !cb) {
...
}
const warnInvalidSource = (s: unknown) => {
warn(...)
}
const instance =
getCurrentScope() === currentInstance?.scope ? currentInstance : null
// const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false
// 根據(jù)不同source 創(chuàng)建不同的getter函數(shù)
// getter 函數(shù)與computed的getter函數(shù)作用類似
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// source是reactive對象時 自動開啟deep=true
getter = () => source
deep = true
} else if (isArray(source)) {
isMultiSource = true
forceTrigger = source.some(s => isReactive(s) || isShallow(s))
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
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 callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
// 2.x array mutation watch compat
// 兼容vue2
if (__COMPAT__ && cb && !deep) {
const baseGetter = getter
getter = () => {
const val = baseGetter()
if (
isArray(val) &&
checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
) {
traverse(val)
}
return val
}
}
// 深度監(jiān)聽
if (cb && deep) {
const baseGetter = getter
// traverse會遞歸遍歷對象的所有屬性 以達(dá)到深度監(jiān)聽的目的
getter = () => traverse(baseGetter())
}
let cleanup: () => void
// watch回調(diào)的第三個參數(shù) 可以用此注冊一個cleanup函數(shù) 會在下一次watch cb調(diào)用前執(zhí)行
// 常用于競態(tài)問題的處理
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager or sync flush
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
// ssr處理 ...
}
// oldValue 聲明 多個source監(jiān)聽則初始化為數(shù)組
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
// 調(diào)度器調(diào)用時執(zhí)行
const job: SchedulerJob = () => {
if (!effect.active) {
return
}
if (cb) {
// watch(source, cb)
// 獲取newValue
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) =>
hasChanged(v, (oldValue as any[])[i])
)
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
// 執(zhí)行onCleanup傳過來的函數(shù)
cleanup()
}
// 調(diào)用cb 參數(shù)為newValue、oldValue、onCleanup
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup
])
// 更新oldValue
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步更新 即每次響應(yīng)式數(shù)據(jù)改變都會回調(diào)一次cb 通常不使用
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// job放入pendingPostFlushCbs隊列中
// pendingPostFlushCbs隊列會在queue隊列執(zhí)行完畢后執(zhí)行 函數(shù)更新effect通常會放在queue隊列中
// 所以pendingPostFlushCbs隊列執(zhí)行時組件已經(jīng)更新完畢
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
// 默認(rèn)異步更新 關(guān)于異步更新會和nextTick放在一起詳細(xì)講解
scheduler = () => queueJob(job)
}
// 創(chuàng)建effect effect.run的時候建立effect與getter內(nèi)響應(yīng)式數(shù)據(jù)的關(guān)系
const effect = new ReactiveEffect(getter, scheduler)
if (__DEV__) {
effect.onTrack = onTrack
effect.onTrigger = onTrigger
}
// initial run
if (cb) {
if (immediate) {
// 立馬執(zhí)行一次job
job()
} else {
// 否則執(zhí)行effect.run() 會執(zhí)行g(shù)etter 獲取oldValue
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense
)
} else {
effect.run()
}
// 返回一個取消監(jiān)聽的函數(shù)
const unwatch = () => {
effect.stop()
if (instance && instance.scope) {
remove(instance.scope.effects!, effect)
}
}
if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)
return unwatch
}以上就是Vue3源碼解讀computed和watch的詳細(xì)內(nèi)容,更多關(guān)于Vue3 computed watch的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文搞懂Vue3中toRef和toRefs函數(shù)的使用
這篇文章主要為大家介紹了Vue3中toRef和toRefs函數(shù)的使用方法,文中通過示例為大家進(jìn)行了詳細(xì)的講解,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-07-07
VSCode寫vue項目一鍵生成.vue模版,修改定義其他模板的方法
這篇文章主要介紹了VSCode寫vue項目一鍵生成.vue模版,修改定義其他模板的方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04
vue.js源代碼core scedule.js學(xué)習(xí)筆記
這篇文章主要為大家詳細(xì)介紹了vue.js源代碼core scedule.js的學(xué)習(xí)筆記,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
Vue3使用ref解決GetElementById為空的問題
今天遇到一個問題,就是在Vue3組件中需要獲取template中的元素節(jié)點,使用GetElementById返回的卻是null,網(wǎng)上查找了好些資料,才發(fā)需要使用ref,所以本文給大家介紹了Vue3組件中如何使用ref解決GetElementById為空的問題,需要的朋友可以參考下2023-12-12
vue在同一個頁面重復(fù)引用相同組件如何區(qū)分二者
這篇文章主要介紹了vue在同一個頁面重復(fù)引用相同組件如何區(qū)分二者,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08

