Vue3 computed和watch源码分析
computed
computed和watch在面试中经常被问到他们的区别,那么我们就从源码的实现来看看他们的具体实现
// 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内部只是先处理getter和setter,然后new一个ComputedRefImpl返回,如果你知道ref API的实现,可以发现他们的实现有很多相同之处
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 ) { // 创建effect this.effect = new ReactiveEffect(getter, () => { // 调度器执行 重新赋值_dirty为true if (!this._dirty) { this._dirty = true // 触发effect triggerRefValue(this) } }) // 用于区分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) // 如果是脏的,重新执行effect.run(),并且将_dirty设置为false if (self._dirty || !self._cacheable) { self._dirty = false // run()方法会执行getter方法 值会被缓存到self._value self._value = self.effect.run()! } return self._value } set value(newValue: T) { this._setter(newValue) } }
可以看到ComputedRefImplget的get实现基本和ref的get相同(不熟悉ref实现的请看上一章),唯一的区别就是_dirty值的判断,这也是我们常说的computed会缓存value,那么computed是如何知道value需要更新呢?
可以看到在computed构造函数中,会建立一个getter与其内部响应式数据的关系,这跟我们组件更新函数跟响应式数据建立关系是一样的,所以与getter相关的响应式数据发生修改的时候,就会触发getter effect 对应的scheduler,这里会将_dirty设置为true并去执行收集到的effect(这里通常是执行get里收集到的函数更新的effect),然后就会去执行函数更新函数,里面会再次触发computed的get,此时dirty已经被置为true,就会重新执行getter获取新的值返回,并将该值缓存到_vlaue。
小结:
所以computed是有两层的响应式处理的,一层是computed.value和函数的effect之间的关系(与ref的实现相似),一层是computed的getter和响应式数据的关系。
注意:如果你足够细心就会发现函数更新函数的effect触发和computed getter的effect的触发之间可能存在顺序的问题。假如有一个响应式数据a不仅存在于getter中,还在函数render中早于getter被访问,此时a对应的dep中更新函数的effect就会早于getter的effect被收集,如果此时a被改变,就会先执行更新函数的effect,那么此时render函数访问到computed.value的时候就会发现_dirty依然是false,因为getter的effect还没有被执行,那么此时依然会是旧值。vue3中对此的处理是执行effects的时候会优先执行computed对应的effect(此前章节也有提到):
// 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会先执行 // 防止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与响应式数据之间的关系,在响应式数据变化时调用用户传过来的回调并将新旧值传入即可
// 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 具体实现 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 // 根据不同source 创建不同的getter函数 // getter 函数与computed的getter函数作用类似 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 } } // 深度监听 if (cb && deep) { const baseGetter = getter // traverse会递归遍历对象的所有属性 以达到深度监听的目的 getter = () => traverse(baseGetter()) } let cleanup: () => void // watch回调的第三个参数 可以用此注册一个cleanup函数 会在下一次watch cb调用前执行 // 常用于竞态问题的处理 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监听则初始化为数组 let oldValue: any = isMultiSource ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE // 调度器调用时执行 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) { // 执行onCleanup传过来的函数 cleanup() } // 调用cb 参数为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') { // 同步更新 即每次响应式数据改变都会回调一次cb 通常不使用 scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') { // job放入pendingPostFlushCbs队列中 // pendingPostFlushCbs队列会在queue队列执行完毕后执行 函数更新effect通常会放在queue队列中 // 所以pendingPostFlushCbs队列执行时组件已经更新完毕 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' job.pre = true if (instance) job.id = instance.uid // 默认异步更新 关于异步更新会和nextTick放在一起详细讲解 scheduler = () => queueJob(job) } // 创建effect effect.run的时候建立effect与getter内响应式数据的关系 const effect = new ReactiveEffect(getter, scheduler) if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger } // initial run if (cb) { if (immediate) { // 立马执行一次job job() } else { // 否则执行effect.run() 会执行getter 获取oldValue oldValue = effect.run() } } else if (flush === 'post') { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ) } else { effect.run() } // 返回一个取消监听的函数 const unwatch = () => { effect.stop() if (instance && instance.scope) { remove(instance.scope.effects!, effect) } } if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch }