1、watch监听器
1.1、使用watch
watch
需要侦听特定的数据源,并在单独的回调函数中执行副作用,然后下面就是使用watch的一个说明。 小声逼逼:我还是习惯用监听,后面的监听也就是侦听。
/**
* @param source 监听对象
* 监听单个 a
* 监听多个 [a,b]
* 监听reactive单个值 ()=>{}
* @param cb 回调函数 (newVal, oldVal)得到变换前后的值
* @param options 配置项
* immediate 是否立即执行
* deep 是否深度监听
* once 是否只执行一次
* flush 回调执行时机
*/
watch(source,(newVal, oldVal)=>{},{})
1.2、创建watch
直接在源码当中找到watch,路径是packages/runtime-core/src/apiWatch.ts
,他本身就是一个函数,之后回去执行doWatch,同时我们可以看一下options配置的类型WatchOptions,这个我们先放在着,等下看一下配置项是如何生效的。
export 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)
}
//
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
once?: boolean
}
1.3、执行监听
- 首先对once配置项是否只执行一次进行判断,然后提前先对deep深层监听判断
- 监听源判断,单值监听、多值监听、回调监听。同时需要去判断ref和reactive。
- 根据前两步去判断是单层还是深层监听,执行traverse
- 创建一个job,在job当中去更新新旧值
- 判断flush,用来确定值变换与dom更新的时机(先后顺序)
- 判断immediate,是否立即执行一次(执行一次job)
- 最后通过
effect.run()
开始依赖收集整体调度 - doWatch返回了unwatch,这也就是
const A =watch(a,()=>{}); A();
再调用一个A就可以去除监听的原因
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{
immediate,
deep,
flush,
once,
onTrack,
onTrigger,
}: WatchOptions = EMPTY_OBJ,
// EMPTY_OBJ是一个Object.freeze({})冻结的空对象,也就是说当没有传options过来时,这个配置都会从这个{}解构得到
): WatchStopHandle {
// 当指定了once只执行一次,会执行一次cb(callback)然后unwatch结束监听
if (cb && once) {
const _cb = cb
cb = (...args) => {
_cb(...args)
unwatch()
}
}
const instance = currentInstance
const reactiveGetter = (source: object) =>
deep === true
? source // 遍历将发生在下面的包装getter中
: // 对于deep:false,仅遍历根级属性
traverse(source, deep === false ? 1 : undefined)
let getter: () => any
let forceTrigger = false
let isMultiSource = false
if (isRef(source)) {
// ref对象的get直接访问value属性
getter = () => source.value
// 判断是否是浅层ref
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 对于reactive对象来说,是通过deep去控制是否需要深层监听的
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 {
// 不能监听
}
})
} 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
}
// 有回调并且是单层监听
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
}
}
// 在SSR中,不需要设置实际效果,它应该是noop
// 除非它很急切或同步刷新
let ssrCleanup: (() => void)[] | undefined
if (__SSR__ && isInSSRComponentSetup) {
// 我们也不会调用 invalide 回调(没有设置+runner)
onCleanup = NOOP
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
isMultiSource ? [] : undefined,
onCleanup,
])
}
if (flush === 'sync') {
const ctx = useSSRContext()!
ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])
} else {
return NOOP
}
}
// isMultiSource 用来标记是否是多数据监听
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()
// 有真则真
// 深层监听 || 对象监听 || (多数据监听 遍历通过Object.is(value, oldValue)去比较值是否改变)|| (是数组类型 && ?)
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))
) {
// 再次运行cb之前的清理
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// 第一次更改时将undefined作为旧值传递,到这里oldVal才会有值
oldValue === INITIAL_WATCHER_VALUE
? undefined
: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
? []
: oldValue,
onCleanup,
])
oldValue = newValue
}
} else {
// watchEffect
effect.run()
}
}
// 将job标记为观察程序回调,以便调度程序知道,它被允许自触发 先设置为false
job.allowRecurse = !!cb // !!undefined false
let scheduler: EffectScheduler
/**
* flush: 'pre' | 'post' | 'sync'
* pre 在侦听器的回调函数运行之前立即运行更新函数
* post ------------------之后-------------
* sync 同步
* */
if (flush === 'sync') {
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
// 把当前示例的id作为job任务id
if (instance) job.id = instance.uid
// 开始调度
scheduler = () => queueJob(job)
}
// 和响应式那块是一样的,收集依赖
const effect = new ReactiveEffect(getter, NOOP, scheduler)
const scope = getCurrentScope()
const unwatch = () => {
// 停止依赖收集,并且把这个effect剔除出去
effect.stop()
if (scope) {
remove(scope.effects, effect)
}
}
// initial run
if (cb) {
// immediate 是否先调度一次
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
}
1.4、traverse对象值收集
在上面有调用traverse(baseGetter())
,把getter传给了这个函数,简单看一下这个函数,其实就是把所有getter能拿到的值全部给加到seen(set集合当中)之后要是有watch的变更也直接从这里面去掉即可
export function traverse(
value: unknown,
depth = Infinity,
seen?: Set<unknown>,
) {
if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
}
seen = seen || new Set()
if (seen.has(value)) {
return value
}
seen.add(value)
depth--
// 看是不是ref还包了ref
// 后面判断递归都是同理,把所有值都给加到seen当中
if (isRef(value)) {
traverse(value.value, depth, seen)
} else if (isArray(value)) {
for (let i = 0; i < value.length; i++) {
traverse(value[i], depth, seen)
}
} else if (isSet(value) || isMap(value)) {
value.forEach((v: any) => {
traverse(v, depth, seen)
})
} else if (isPlainObject(value)) {
for (const key in value) {
traverse(value[key], depth, seen)
}
for (const key of Object.getOwnPropertySymbols(value)) {
if (Object.prototype.propertyIsEnumerable.call(value, key)) {
traverse(value[key as any], depth, seen)
}
}
}
return value
}
1.5、watchEffect
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。官网说明:前往 https://cn.vuejs.org/api/reactivity-core.html#watcheffect
// use,当在watchEffect当中使用了的变量,就会自动追踪哪个属性,当使用了objEffect对象,他里面所有的属性都会被监听到
const objEffect = reactive({a: 1, b: {c: 1, d: {e: 2}}});
watchEffect(() => {
console.log(' =====', objEffect.a);
});
// 源码
export function watchPostEffect(
effect: WatchEffect,
options?: DebuggerOptions,
) {
return doWatch(
effect,
null,
__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },
)
}
// 这个去执行doWatch时,也就是数据源是一个函数,它执行的逻辑就是这一块
getter = () => {
if (cleanup) {
// 通过这个将依赖收集起来
cleanup()
}
return callWithAsyncErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onCleanup],
)
}
1.6、扩展:watchPostEffect & watchSyncEffect
本质上就是指定了flush,flush默认值是pre,在侦听器的回调函数运行之前立即运行更新函数,也就是watchEffect,而这两个的意义还是用来在语义上对前后、同步调用的一个区分
-
watchPostEffect : 把flush指定为post,也就是回调函数运行之后运行更新函数
-
watchSyncEffect : 同步执行
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' },
)
}
2、computed 计算属性
2.1、使用computed
计算属性是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。
使用computed有以下两种方式,一个是传函数直接返回,一个可以通过传递对象给定get/set函数进去控制。
const A = computed(() => {
return `A:${a.age}`;
});
const B = computed({
get: () => {
return a.age;
},
set: (value) => {
a.age = value;
}
});
2.2、创建computed
先看一下computed是怎么创建的,源码位置:packages/reactivity/src/computed.ts
。在这里通过getterOrOptions接收computed传递的参数,也就是可以拿到上面函数和对象两种方式传递的值,直接分别去取对应的get、set方法,通过ComputedRefImpl创建一个实现实例。
export function computed<T>(
// getterOrOptions传递值 ComputedGetter就是一个回调函数,WritableComputedOptions是一个对象包了get/set方法
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false,
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 判断传递过来是那种方式
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
// 将回调给到getter,并且不设置setter
getter = getterOrOptions
setter = __DEV__
? () => {
warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// 传递的是对象形式,直接去对象里面拿get/set方法
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
}
2.2、ComputedRefImpl实例
- 直接从构造开始看起,主要关注get/set两个方法。在构造创建了一个ReactiveEffect,也就是响应式的实现方式。并且指定了依赖触发
- set值在计算属性当中不关系,主要是使用别的值改变后,怎么获取计算之后的值,也就是这的get
- 在get当中通过了_cacheable是否缓存先执行一遍依赖触发和依赖收集的过程,而后则通过脏值判断是否需要使用缓存当中的值还是用新值
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
// 从构造开始,先不关心isReadonly和isSSR,就看get/set
constructor(
private getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean,
) {
// 创建了ReactiveEffect,也就是前面说到的reactive响应式,并且指定了triggerRefValue(依赖触发)
this.effect = new ReactiveEffect(
() => getter(this._value),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect // 4 === 2
? DirtyLevels.MaybeDirty_ComputedSideEffect // 2
: DirtyLevels.MaybeDirty, // 3
),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR // isSSR === false 添加缓存
this[ReactiveFlags.IS_READONLY] = isReadonly // isReadonly === false
}
get value() {
// 计算出的ref可能会被其他代理封装,例如readonly() toRaw 转换成原始对象
const self = toRaw(this)
/**
* self._cacheable 变量是否可缓存
* self.effect.dirty 表示该变量是否被修改过
* */
if (
(!self._cacheable || self.effect.dirty) &&
hasChanged(self._value, (self._value = self.effect.run()!))
) {
// 值变换之后会触发依赖更新DOM
triggerRefValue(self, DirtyLevels.Dirty)
}
// 触发之后重新收集
trackRefValue(self)
// 这里通过 DirtyLevels.MaybeDirty_ComputedSideEffect 脏标记级别用来控制是否需要重新执行依赖触发去更新DOM等操作
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
}
}
2.3、简单实现computed计算属性
- 首先还是要前面实现的MyReactive响应式的依赖收集依赖触发等方法(
MyReactive响应式简单示例案例,单击前往),这里就不往下面贴了,但是对应依赖收集、触发(effect、trigger)需要进行细微调整,- 依赖收集就是在收集的时候添加了一个options,挂在effect上
- 依赖触发本质上就是在遍历deps的时候去看一下那些值有scheduler调度。这个调度是我们创建computed去给他添加的。
- 创建computed,这里简单实现就只接收一个函数入参,之后进行依赖收集,在这里就将scheduler调度给挂载到effect副作用函数上。
- 当trigger响应式触发之后,会去尝试执行一个scheduler调度。执行了调度之后再去更新DOM时会触发effect,在computed当中get值,在这里判断了dirty,也就是新旧值是否相等(是否去更新cacheValue缓存值)。
const effect = (fn: Function, options: Options) => {
const _effect = () => {
activeEffect = _effect;
return fn();
};
_effect.options = options;
_effect();
return _effect;
};
const trigger = (target: object, key: any) => {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (!deps) return;
deps.forEach((effect: { (): void; (): void; options: any; }) => {
if (effect?.options?.scheduler) {
effect?.options.scheduler?.();
} else {
effect();
}
});
};
const myComputed = (getter: Function) => {
let _value = effect(getter, {
scheduler: () => {
_dirty = true;
}
});
let _dirty = true;
let catchValue: any;
class MyComputerRefImpl {
get value() {
if (_dirty) {
catchValue = _value();
_dirty = false;
}
return catchValue;
}
}
return new MyComputerRefImpl();
};