前言
这是vue3系列源码的第九章,使用的vue3版本是3.4.15
。
背景
前面我们看了vue3源码的,渲染,更新过程以及响应式的实现还通过一个特殊的案例看了props 的实现,这里看一下我们常用的监听功能的实现,也就是watch watchEffect computed函数的实现
前置
我们随便写一点内容,主要为了看一下这三个函数在源码中是如何实现的。
js
<template>
<div>{{ aa }}</div>
<div>{{ bb }}</div>
<button @click="aa = '小石'">点击</button>
</template>
<script setup>
import { ref, watch, watchEffect, computed } from 'vue'
const aa = ref('小石')
const bb = computed(() => aa.value + '谭记')
watch(aa, () => {console.log(11, aa.value)})
watchEffect(() => {console.log(22, aa.value)})
</script>
computed
首先看一下computed的实现。
ts
const computed: typeof _computed = (
getterOrOptions: any,
debugOptions?: any,
) => {
return _computed(getterOrOptions, debugOptions, isInSSRComponentSetup)
}
这里是调用了**_computed** 函数,如下:
ts
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__
? () => {
warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
最终得到的是new ComputedRefImpl得到的对象。
ComputedRefImpl
ts
class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
this.effect = new ReactiveEffect(
() => getter(this._value),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
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
const self = toRaw(this)
if (
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!))
) {
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
__DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
// #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
get _dirty() {
return this.effect.dirty
}
set _dirty(v) {
this.effect.dirty = v
}
// #endregion
}
在ComputedRefImpl这个类中,
- 先
new ReactiveEffect
得到一个副作用对象,ReactiveEffect类前面我们分析过,是vue3响应式的核心类。 - get 的时候,先运行
effect.run
,最终就是执行了() => aa.value + '谭记'
得到了bb 的值,这里会触发aa 的get
把当前的effect 加入到aa 的effect 的dep
中。同时,检查当前的值有没有变化,如何变了,会调用triggerRefValue 去更新相应的组件,最后执行了trackRefValue ,也就是绑定副作用函数或者称他为依赖收集
- set 的时候,会执行传入进来的
set
函数,这里面我们没有传。
当aa 的值发生变化的时候,会在aa 的dep
中找到这个effect ,并调用trigger
,也就是这里传给triggerRefValue
函数的第二个参数,也就是triggerRefValue 函数,也就是对依赖了computed的部分进行更新。
watch
下面看一下watch函数的实现
ts
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(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`,
)
}
return doWatch(source as any, cb, options)
}
这里实际是调用了doWatch
doWatch
ts
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, once, onTrack, onTrigger, }: WatchOptions = EMPTY_OBJ,
): WatchStopHandle {
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
unwatch()
}
}
const instance = currentInstance
const reactiveGetter = (source: object) =>
deep === true
? source // traverse will happen in wrapped getter below
: // for deep: false, only traverse root-level properties
traverse(source, deep === false ? 1 : undefined)
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
getter = () => reactiveGetter(source)
forceTrigger = 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 reactiveGetter(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 (cleanup) {
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
if (cb && deep) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: (() => void) | undefined
let onCleanup: OnCleanup = (fn: () => void) => {
cleanup = effect.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
cleanup = effect.onStop = undefined
}
}
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!effect.active || !effect.dirty) {
return
}
if (cb) {
// watch(source, cb)
const newValue = effect.run()
if (
deep ||
forceTrigger ||
(isMultiSource
? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
: hasChanged(newValue, oldValue)) ||
(__COMPAT__ &&
isArray(newValue) &&
isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
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 = 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') {
...
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, NOOP, scheduler)
const scope = getCurrentScope()
const unwatch = () => {
effect.stop()
if (scope) {
remove(scope.effects, effect)
}
}
// initial run
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
} else if (flush === 'post') {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
effect.run()
}
return unwatch
}
doWatch主要干了这几件事情:
- 首先根据source 的类型,来确定getter 的值,我们这里是
ref
,所以就是source.value
,source 的值还可以是reactive,array,function
- watch 函数第三个参数可以传入这些字段
{ immediate, deep, flush, once, onTrack, onTrigger}
,如果deep
为true
,会对我们传入的source进行递归,深度监听 - 接着定义了任务函数,这里会根据是否有cb ,来判断调用的是watch 还是watchEffect
- 然后根据任务的执行时机,添加到不同的任务队列中,这里默认是pre ,加入到queueJob队列中
- 根据getter 和任务函数创建副作用函数
- 如果定义了immediate ,那么会立即执行一次回调,否则也会执行一次getter ,把当前的值作为oldValue ,那么在此时又会触发aa 的get ,把aa 作为这个watch 的依赖进行收集,或者说把这个watch 添加到aa的副作用函数的通知队列中
当getter 发生变化的时候,会执行scheduler ,也就是定义的任务函数job ,也就是我们传入的回调函数cb
watchEffect
ts
function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase,
): WatchStopHandle {
return doWatch(effect, null, options)
}
watchEffect 走的也是doWatch ,和watch 没有本质的区别,这里的source会是一个函数,会直接执行,这个函数来进行依赖收集。
以上就是watch watchEffect compute的全部内容
总结
computed 每次读取数据的时候会先判断是否发生了值的变化,而且它返回的是和ref有点像的值。他的优点就是每次读取的时候会比较其依赖的响应式对象的值,如果没有变化,就不会触发更新流程。
watch watchEffect 都是调用的doWatch函数,没有本质的区别。