计算属性:computed Vue.js 3.0 提供了一个 computed 函数作为计算属性 API,我们先来看看它是如何使用的。
1 2 3 4 5 6 const count = ref(1 )const plusOne = computed(() => count.value + 1 )console .log(plusOne.value) plusOne.value++ count.value++ console .log(plusOne.value)
从代码中可以看到,我们先使用 ref API 创建了一个响应式对象 count,然后使用 computed API 创建了另一个响应式对象 plusOne,它的值是 count.value + 1,当我们修改 count.value 的时候, plusOne.value 就会自动发生变化。
注意,这里我们直接修改 plusOne.value 会报一个错误,这是因为如果我们传递给 computed 的是一个函数,那么这就是一个 getter 函数,我们只能获取它的值,而不能直接修改它。
在 getter 函数中,我们会根据响应式对象重新计算出新的值,这也就是它被叫做计算属性的原因,而这个响应式对象,就是计算属性的依赖。
当然,有时候我们也希望能够直接修改 computed 的返回值,那么我们可以给 computed 传入一个对象:
1 2 3 4 5 6 7 8 9 const count = ref(1 )const plusOne = computed({ get : () => count.value + 1, set : val => { count.value = val - 1 } }) plusOne.value = 1 console .log(count.value)
在这个例子中,结合上述代码可以看到,我们给 computed 函数传入了一个拥有 getter 函数和 setter 函数的对象,getter 函数和之前一样,还是返回 count.value + 1;而 setter 函数,请注意,这里我们修改 plusOne.value 的值就会触发 setter 函数,其实 setter 函数内部实际上会根据传入的参数修改计算属性的依赖值 count.value,因为一旦依赖的值被修改了,我们再去获取计算属性就会重新执行一遍 getter,所以这样获取的值也就发生了变化。
好了,我们现在已经知道了 computed API 的两种使用方式了,接下来就看看它是怎样实现的:
computed 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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__ ? () => { 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 }
computed接收3个参数,主要看第一个参数,观察其类型,发现可以传两种参数:一种是一个getter函数,一种是个包含get、set的对象。 首先从getterOrOptions中确定getter、setter(如果getterOrOptions是个function,说明computed是不可写的,所以会将setter设置为一个空函数),确定好之后,创建ComputedRefImpl的实例,并将其返回。
ComputedRefImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 export 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 _warnRecursive?: boolean constructor ( private 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() { 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) { triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect) } return self._value } set value(newValue: T) { this ._setter(newValue) } get _dirty() { return this .effect.dirty } set _dirty(v) { this .effect.dirty = v } }
侦听器:watch 1.watch API 可以侦听一个 getter 函数,但是它必须返回一个响应式对象,当该响应式对象更新后,会执行对应的回调函数。
1 2 3 4 5 import { reactive, watch } from 'vue' const state = reactive({ count : 0 })watch(() => state.count, (count, prevCount) => { })
2.watch API 也可以直接侦听一个响应式对象,当响应式对象更新后,会执行对应的回调函数。
1 2 3 4 5 import { ref, watch } from 'vue' const count = ref(0 )watch(count, (count, prevCount) => { })
3.watch API 还可以直接侦听多个响应式对象,任意一个响应式对象更新后,就会执行对应的回调函数。
1 2 3 4 5 6 import { ref, watch } from 'vue' const count = ref(0 )const count2 = ref(1 )watch([count, count2], ([count, count2], [prevCount, prevCount2]) => { })
watch 1 2 3 4 5 6 7 function watch <T = any , Immediate extends Readonly <boolean > = false >( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate>, ): WatchStopHandle { return doWatch(source as any, cb, options) }
我们看到watch内部调用了doWatch:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 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(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 = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { getter = () => { if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup], ) } } } else { getter = NOOP __DEV__ && warnInvalidSource(source) } 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 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 ssrCleanup: (( ) => void )[] | undefined if (__SSR__ && isInSSRComponentSetup) { } 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) { 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)) ) { if (cleanup) { cleanup() } callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, oldValue === INITIAL_WATCHER_VALUE ? undefined : isMultiSource && oldValue[0 ] === INITIAL_WATCHER_VALUE ? [] : oldValue, onCleanup, ]) oldValue = newValue } } else { effect.run() } } job.allowRecurse = !!cb let scheduler: EffectScheduler if (flush === 'sync' ) { scheduler = job as any } else if (flush === 'post' ) { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { 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) } } if (cb) { if (immediate) { job() } else { oldValue = effect.run() } } else if (flush === 'post' ) { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense, ) } else { effect.run() } if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch }
WatchEffect watchEffect API 的作用是注册一个副作用函数,副作用函数内部可以访问到响应式对象,当内部响应式对象变化后再立即执行这个函数。
1 2 3 4 import { ref, watchEffect } from 'vue' const count = ref(0 )watchEffect(() => console .log(count.value)) count.value++
watchEffect 和前面的 watch API 主要有三点不同:
侦听的源不同 。watch API 可以侦听一个或多个响应式对象,也可以侦听一个 getter 函数,而 watchEffect API 侦听的是一个普通函数,只要内部访问了响应式对象即可,这个函数并不需要返回响应式对象。
没有回调函数 。watchEffect API 没有回调函数,副作用函数的内部响应式对象发生变化后,会再次执行这个副作用函数。
立即执行 。watchEffect API 在创建好 watcher 后,会立刻执行它的副作用函数,而 watch API 需要配置 immediate 为 true,才会立即执行回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 export function watchEffect ( effect: WatchEffect, options?: WatchOptionsBase, ): WatchStopHandle { return doWatch(effect, null , options) } export function watchPostEffect ( effect: WatchEffect, options?: DebuggerOptions, ) { return doWatch( effect, null , __DEV__ ? extend({}, options as any, { flush : 'post' }) : { flush : 'post' }, ) } export function watchSyncEffect ( effect: WatchEffect, options?: DebuggerOptions, ) { return doWatch( effect, null , __DEV__ ? extend({}, options as any, { flush : 'sync' }) : { flush : 'sync' }, ) }
简易版实现原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 if (isFunction(source)) { getter = () => { if (cleanup) { cleanup() } return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup], ) } } let scheduler: EffectSchedulerif (flush === 'sync' ) { scheduler = job as any } else if (flush === 'post' ) { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { job.pre = true if (instance) job.id = instance.uid scheduler = () => queueJob(job) } const effect = new ReactiveEffect(getter, NOOP, scheduler)if (flush === 'post' ) { queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense, ) } else { effect.run() }
总结 以上就是计算属性computed和侦听器watch的实现原理,总的来说就是侦听器更适合用于在数据变化后执行某段逻辑的场景,而计算属性则用于一个数据依赖另外一些数据计算而来的场景。
文章里面涉及到任务队列异步更新的原理 在这。