【第1979期】深入理解 Vue3 Reactivity API
前言
今日早读文章由字节跳动@HcySunYang授权分享。
正文从这开始~~
TOC:
effect() 和 reactive()
shallowReactive()
readonly()
shallowReadonly()
isReactive()
isReadonly()
isProxy()
markRaw()
哪些数据是可以被代理的
markRaw() 函数用于让数据不可被代理
toRaw()
ReactiveFlags
调度执行 effect - scheduler
watchEffect()
异步副作用和 invalidate
停止一个副作用(effect)
watchEffect() 与 effect() 的区别
track() 与 trigger()
ref()
isRef()
toRef()
toRefs()
自动脱 ref
customRef()
shallowRef()
triggerRef()
unref()
Lazy 的 effect()
computed()
effect 的其他选项 onTrack 和 onTrigger
effect() 和 reactive()
import { effect, reactive } from '@vue/reactivity'
// 使用 reactive() 函数定义响应式数据
const obj = reactive({ text: 'hello' })
// 使用 effect() 函数定义副作用函数
effect(() => {
document.body.innerText = obj.text
})
// 一秒后修改响应式数据,这会触发副作用函数重新执行
setTimeout(() => {
obj.text += ' world'
}, 1000)
reactive() 函数接收一个对象作为参数,并返回一个代理对象。
effect() 函数用于定义副作用,它的参数就是副作用函数,这个函数可能会产生副作用,例如上面代码中的 document.body.innerText = obj.text。在副作用函数内的响应式数据会与副作用函数之间建立联系,即所谓的依赖收集,当响应式数据变化之后,会导致副作用函数重新执行。
shallowReactive()
定义浅响应数据:
import { effect, shallowReactive } from '@vue/reactivity'
// 使用 shallowReactive() 函数定义浅响应式数据
const obj = shallowReactive({ foo: { bar: 1 } })
effect(() => {
console.log(obj.foo.bar)
})
obj.foo.bar = 2 // 无效
obj.foo = { bar: 2 } // 有效
readonly()
有些数据,我们要求对用户是只读的,此时可以使用 readonly() 函数,它的用法如下:
import { readonly } from '@vue/reactivity'
// 使用 reactive() 函数定义响应式数据
const obj = readonly({ text: 'hello' })
obj.text += ' world' // Set operation on key "text" failed: target is readonly.
shallowReadonly()
类似于浅响应,shallowReadonly() 定义浅只读数据,这意味着,深层次的对象值是可以被修改的,在 Vue 内部 props 就是使用 shallowReadonly() 函数来定义的,用法如下:
import { effect, shallowReadonly } from '@vue/reactivity'
// 使用 shallowReadonly() 函数定义浅只读数据
const obj = shallowReadonly({ foo: { bar: 1 } })
obj.foo = { bar: 2 } // Warn
obj.foo.bar = 2 // OK
isReactive()
判断数据对象是否是 reactive:
import { isReactive, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'
const reactiveProxy = reactive({ foo: { bar: 1 } })
console.log(isReactive(reactiveProxy)) // true
console.log(isReactive(reactiveProxy.foo)) // true
const shallowReactiveProxy = shallowReactive({ foo: { bar: 1 } })
console.log(isReactive(shallowReactiveProxy)) // true
console.log(isReactive(shallowReactiveProxy.foo)) // false
const readonlyProxy = readonly({ foo: 1 })
console.log(isReactive(readonlyProxy)) // false
const shallowReadonlyProxy = shallowReadonly({ foo: 1 })
console.log(isReactive(shallowReadonlyProxy)) // false
isReadonly()
用于判断数据是否是 readonly:
import { isReadonly, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'
console.log(isReadonly(readonly({}))) // true
console.log(isReadonly(shallowReadonly({}))) // true
console.log(isReadonly(reactive({}))) // false
console.log(isReadonly(shallowReactive({}))) // false
isProxy()
用于判断对象是否是代理对象(reactive 或 readonly):
import { isProxy, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'
console.log(isProxy(readonly({}))) // true
console.log(isProxy(shallowReadonly({}))) // true
console.log(isProxy(reactive({}))) // true
console.log(isProxy(shallowReactive({}))) // true
const shallowReactiveProxy = shallowReactive({ foo: {} })
console.log(isProxy(shallowReactiveProxy)) // true
console.log(isProxy(shallowReactiveProxy.foo)) // false
const shallowReadonlyProxy = shallowReadonly({ foo: {} })
console.log(isProxy(shallowReadonlyProxy)) // true
console.log(isProxy(shallowReadonlyProxy.foo)) // false
markRaw()
哪些数据是可以被代理的:
Object 、Array、Map、Set、WeakMap、WeakSet
非 Object.isFrozen
const obj = { foo: 1 }
Object.freeze(obj)
// Object.isFrozen(obj) ==> true
// proxyObj === obj
const proxyObj = reactiev(obj)
非 VNode,Vue3 的 VNode 对象带有 _vskip: true 标识,用于跳过代理(实际上,只要带有 _vskip 属性并且值为 true 的对象,都不会是被代理),例如:
// obj 是原始数据对象
const obj = reactive({
foo: 0,
__v_skip: true
})
markRaw() 函数用于让数据不可被代理:
实际上 markRaw 函数所做的事情,就是在数据对象上定义 _vskip 属性,从而跳过代理:
import { markRaw } from '@vue/reactivity'
const obj = { foo: 1 }
markRaw(obj) // { foo: 1, __v_skip: true }
toRaw()
接收代理对象作为参数,并获取原始对象:
import { toRaw, reactive, readonly } from '@vue/reactivity'
const obj1 = {}
const reactiveProxy = reactive(obj1)
console.log(toRaw(reactiveProxy) === obj1) // true
const obj2 = {}
const readonlyProxy = readonly(obj2)
console.log(toRaw(readonlyProxy) === obj2) // true
如果参数是非代理对象,则直接该值:
import { toRaw } from '@vue/reactivity'
const obj1 = {}
console.log(toRaw(obj1) === obj1) // true
console.log(toRaw(1) === 1) // true
console.log(toRaw('hello') === 'hello') // true
ReactiveFlags
ReactiveFlags 是一个枚举值:
它的定义如下:
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
它有什么用呢?举个例子,我们要定义一个不可被代理的对象:
import { ReactiveFlags, reactive, isReactive } from '@vue/reactivity'
const obj = {
[ReactiveFlags.skip]: true
}
const proxyObj = reactive(obj)
console.log(isReactive(proxyObj)) // false
实际上 markRaw() 函数就是使用类似的方式实现的。所以我们不必像如上代码那么做,但是在一些高级场景或许会用到这些值。
下面简单介绍一下 ReactiveFlags 中各个值得作用:
代理对象会通过 ReactiveFlags.raw 引用原始对象
原始对象会通过 ReactiveFlags.reactive 或 ReactiveFlags.readonly 引用代理对象
代理对象根据它是 reactive 或 readonly 的, 将 ReactiveFlags.isReactive 或 ReactiveFlags.isReadonly 属性值设置为 true。
调度执行 effect - scheduler
来看下面的例子:
const obj = reactive({ count: 1 })
effect(() => {
console.log(obj.count)
})
obj.count++
obj.count++
obj.count++
定义响应式对象 obj,并在 effect 内读取它的值,这样 effect 与数据之间就会建立“联系”,接着我们连续三次修改 obj.count 的值,会发现 console.log 语句共打印四次(包括首次执行)。
想像一下,假如我们只需要把数据的最终的状态应用到副作用中,而不是每次变化都重新执行一次副作用函数,这将对性能有所提升。实际上我们可以为 effect 传递第二个参数作为选项,可以指定“调度器”。所谓调度器就是用来指定如何运行副作用函数的:
const obj = reactive({ count: 1 })
effect(() => {
console.log(obj.count)
}, {
// 指定调度器为 queueJob
scheduler: queueJob
})
// 调度器实现
const queue: Function[] = []
let isFlushing = false
function queueJob(job: () => void) {
if (!queue.includes(job)) queue.push(job)
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(() => {
let fn
while(fn = queue.shift()) {
fn()
}
})
}
}
obj.count++
obj.count++
obj.count++
我们指定 effect 的调度器为 queueJob,job 实际上就是副作用函数,我们将副作用函数缓冲到 queue 队列中,并在 microtask 中刷新队列,由于队列不会重复缓冲相同的 job,因此最终只会执行一次副作用函数。
这实际上就是 watchEffect() 函数的实现思路。
watchEffect()
watchEffect() 函数并不在 @vue/reactivity 中提供,而是在 @vue/runtime-core 中提供,与 watch() 函数一起对外暴露。
const obj = reactive({ foo: 1 })
watchEffect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
obj.foo++
这与我们上面刚刚实现的自定义调度器的 effect 的效果实际上是一样的。
异步副作用 和 invalidate
异步副作用是很常见的,例如请求 API 接口:
watchEffect(async () => {
const data = await fetch(obj.foo)
})
当 obj.foo 变化后,意味着将会再次发送请求,那么之前的请求怎么办呢?是否应该将之前的请求标记为 invalidate?
实际上,副作用函数接收一个函数作为参数:
watchEffect(async (onInvalidate) => {
const data = await fetch(obj.foo)
})
我们可以调用它来注册一个回调函数,这个回调函数会在副作用无效时执行:
watchEffect(async (onInvalidate) => {
let validate = true
onInvalidate(() => {
validate = false
})
const data = await fetch(obj.foo)
if (validate){
/* 正常使用 data */
} else {
/* 说明当前副作用已经无效了,抛弃即可 */
}
})
如果不抛弃无效的副作用,那么就会产生竟态问题。实际上,我们很容易就能通过封装 effect() 函数支持注册“无效回调”的功能:
import { effect } from '@vue/reactivity'
function watchEffect(fn: (onInvalidate: (fn: () => void) => void) => void) {
let cleanup: Function
function onInvalidate(fn: Function) {
cleanup = fn
}
// 封装一下 effect
// 在执行副作用函数之前,先使上一次无作用无效
effect(() => {
cleanup && cleanup()
fn(onInvalidate)
})
}
如果我们再加上调用器,那实际上就非常接近 watchEffect 的真实实现了。
什么时候需要 invalidate 掉一个副作用函数呢?
在组件中定义的 effect,需要在组件卸载时将其 invalidate
在数据变化导致 effect 重新执行时,需要 invalidate 掉上一次的 effect 执行
用户手动 stop 一个 effect 时
停止一个副作用(effect)
@vue/reactivity 提供了 stop 函数用来停止一个副作用:
import { stop, reactive, effect } from '@vue/reactivity'
const obj = reactive({ foo: 1 })
const runner = effect(() => {
console.log(obj.foo)
})
// 停止一个副作用
stop(runner)
obj.foo++
obj.foo++
effect() 函数会返回一个值,这个值其实就是 effect 本身,我们通常命名它为 runner。
把这个 runner 传递给 stop() 函数,就可以停止掉这个 effect。后续对数据的变更不会触发副作用函数的重新执行。
watchEffect() 与 effect() 的区别
effect() 函数来自于 @vue/reactivity ,而 watchEffect() 函数来自于 @vue/runtime-core。它们的区别在于:effect() 是非常底层的实现,watchEffect() 是基于 effect() 的封装,watchEffect() 会维护与组件实例以及组件状态(是否被卸载等)的关系,如果一个组件被卸载,那么 watchEffect() 也将被 stop,但 effect() 则不会。举个例子:
watchEffect()
const obj = reactive({ foo: 1 })
const Comp = defineComponent({
setup() {
watchEffect(() => {
console.log(obj.foo)
})
return () => ''
}
})
// 挂载组件
render(h(Comp), document.querySelector('#app')!)
// 卸载组件
render(null, document.querySelector('#app')!)
obj.foo++ // 副作用函数不会重新执行
我们先挂载了组件,接着又卸载了组件,最后修改 obj.foo 的值,并不会导致 watchEffect 的副作用函数重新执行。
effect()
const obj = reactive({ foo: 1 })
const Comp = defineComponent({
setup() {
effect(() => {
console.log(obj.foo)
})
return () => ''
}
})
// 渲染组件
render(h(Comp), document.querySelector('#app')!)
// 卸载组件
render(null, document.querySelector('#app')!)
obj.foo++
但 effect() 的副作用函数仍然会被执行,但我们可以借助 onUnmounted API 解决这个问题:
const obj = reactive({ foo: 1 })
const Comp = defineComponent({
setup() {
const runner = effect(() => {
console.log(obj.foo)
})
// 组件卸载时,stop 掉 effect
onUnmounted(() => stop(runner))
return () => ''
}
})
// 渲染组件
render(h(Comp), document.querySelector('#app')!)
// 卸载组件
render(null, document.querySelector('#app')!)
obj.foo++
当然,在普通开发中不推荐直接用 effect() 啦,使用 watchEffect() 就好了。
track() 与 trigger()
track() 和 trigger() 是依赖收集的核心,track() 用来跟踪收集依赖(收集 effect),trigger() 用来触发响应(执行 effect),它们需要配合 effect() 函数使用:
const obj = { foo: 1 }
effect(() => {
console.log(obj.foo)
track(obj, TrackOpTypes.GET, 'foo')
})
obj.foo = 2
trigger(obj, TriggerOpTypes.SET, 'foo')
如上代码所示,obj 是一个普通的对象,注意它并非是响应式对象。接着使用 effect() 函数定义了一个副作用函数,读取并打印 obj.foo 的值,由于 obj 是一个普通对象,因此它并没有收集依赖的能力,为了收集到依赖,我们需要手动调用 track() 函数,track() 函数接收三个参数:
target:要跟踪的目标对象,这里就是 obj
跟踪操作的类型:obj.foo 是读取对象的值,因此是 'get'
key:要跟踪目标对象的 key,我们读取的是 foo,因此 key 是 foo
这样,我们本质上是手动建立一种数据结构:
// 伪代码
map : {
[target]: {
[key]: [effect1, effect2....]
}
}
简单的理解,effect 与对象和具体操作的 key,是以这种映射关系建立关联的:
[target]---->key1---->[effect1, effect2...]
[target]---->key2---->[effect1, effect3...]
[target2]---->key1---->[effect5, effect6...]
既然 effect 与目标对象 target 已经建立了联系,那么当然就可以想办法通过 target ----> key 进而取到 effect ,然后执行它们,而这就是 trigger() 函数做的事情,所以在调用 trigger 函数时我们要指定目标对象和相应的key值:
trigger(obj, TriggerOpTypes.SET, 'foo')
这大概就是依赖收集的原理,但是这个过程是可以自动完成的,而不需要开发者手动调用 track() 和 trigger() 函数,想要自定完成依赖收集,那么就需要拦截诸如:设置、读取等对值得操作方法才行。至于实现方式,无论是 Object.defineProperty 还是 Proxy 那就是具体的技术形式了。
ref()
reactive() 函数可以代理一个对象,但不能代理基本类型值,例如字符串、数字、boolean 等,这是 js 语言的限制,因此我们需要使用 ref() 函数来间接对基本类型值进行处理:
const refVal = ref(0)
refVal.value // 0
ref 是响应式的:
const refVal = ref(0)
effect(() => {
console.log(refVal.value)
})
refVal.value = 1 // 触发响应
已经了解了 track() 和 trigger() 函数的你,仔细思考一下,实现 ref() 函数是不是非常简单呢:
function myRef(val: any) {
let value = val
const r = {
isRef: true, // 随便加个标识以示区分
get value() {
// 收集依赖
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal: any) {
if (newVal !== value) {
value = newVal
// 触发响应
trigger(r, TriggerOpTypes.SET, 'value')
}
}
}
return r
}
现在去试试我们的 myRef() 函数吧:
const refVal = myRef(0)
effect(() => {
console.log(refVal.value)
})
refVal.value = 1
一切 OK。
isRef()
我们在实现 myRef() 函数时,可以看到为 ref 对象添加了一个标识 isRef: true。因此我们可以封装一个函数 isRef() 函数来判断一个值是不是 ref:
function isRef(val) {
return val.isRef
}
实际上在 Vue3 中使用的标识是 _visRef,这无关紧要嘛。
toRef()
丢失响应式 reactivity api 的一个问题:
const obj = reactive({ foo: 1 }) // obj 是响应式数据
const obj2 = { foo: obj.foo }
effect(() => {
console.log(obj2.foo) // 这里读取 obj2.foo
})
obj.foo = 2 // 设置 obj.foo 显然无效
为了解决这个问题,我们可以使用 toRef() 函数:
const obj = reactive({ foo: 1 })
const obj2 = { foo: toRef(obj, 'foo') } // 修改了这里
effect(() => {
console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value
})
obj.foo = 2 // 有效
toRef() 函数用来把一个响应式对象的的某个 key 值转换成 ref,它的实现本身很简单:
function toRef(target, key) {
return {
isRef: true,
get value() {
return target[key]
},
set value(newVal){
target[key] = newVal
}
}
}
可以看到 toRef() 函数比 ref() 函数要简单的多,这是因为 target 本身就是响应的,因此无需手动 track() 和 trigger()。
toRefs()
toRef() 的一个问题是定义起来极其麻烦,一次只能转换一个 key,因此我们可以封装一个函数,直接把一个响应式对象的所有key都转成 ref,这就是 toRefs()
function toRefs(target){
const ret: any = {}
for (const key in target) {
ret[key] = toRef(target, key)
}
return ret
}
这样我们就可以修改前例的代码为:
const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
const obj2 = { ...toRefs(obj) } // 代替上面注释这句代码
effect(() => {
console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value
})
obj.foo = 2 // 有效
自动脱 ref
但是我们发现,问题虽然解决了,但是带来了新的问题,即我们需要通过 .value 访问值才行,这就带来了另外一个问题:我们怎么知道一个值是不是 ref,需不需要通过 .value 来访问呢?因为上面的例子可能会给读者带来疑惑,我们为什么把问题弄得这么复杂?为什么弄了 obj 和 obj2 这两个变量,只用 obj 不就没问题了嘛?这是因为在 Vue 中,我们要暴露数据到渲染环境,怎么暴露呢?
const Comp = {
setup() {
const obj = reactive({ foo: 1 })
return { ...obj }
}
}
这就会导致丢失响应,因此我们需要 toRef() 还是和 toRefs() 函数。然而这带来了新的问题, 我们在 setup 中暴露出去的数据,是要在渲染环境中使用的:
<h1>{{ obj.foo }}</h1>
这里我们应该用 obj.foo 还是 obj.foo.value 呢?这时就需要你明确知道在 setup 中暴露出去的值,哪些是 ref 哪些不是 ref。因此为了减轻心智负担,干脆,在渲染环境都不需要 .value 去取值,即使是 ref 也不需要,这就极大的减少的心智负担,这就是自动脱 Ref
功能。而实现自动脱 ref也很简单,回看一下刚才的代码:
const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
const obj2 = { ...toRefs(obj) } // 代替上面注释这句代码
effect(() => {
console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value
})
obj.foo = 2 // 有效
为了自动摆脱 ref,我们可以:
const obj = reactive({ foo: 1 })
// const obj2 = { foo: toRef(obj, 'foo') }
const obj2 = reactive({ ...toRefs(obj) }) // 让 obj2 也是 reactive
effect(() => {
console.log(obj2.foo) // 即使 obj2.foo 是 ref,我们也不需要 .value 来取值
})
obj.foo = 2 // 有效
我们只需要让 obj2 也是 reactive 的即可,这样,即使 obj2.foo 是 ref,我们也不需要通过 .value 取值,其实现也很简单,当我们在对象上读取属性时,如果发现其值是 ref,那么直接返回 .value 就可以了:
get(target, key, receiver) {
// ...
const res = Reflect.get(target, key, receiver)
if (isRef(res)) return res.value
// ...
}
但对于由 ref 组成的数组,在渲染环境仍然需要 .value 访问。
customRef()
customRef() 实际上就是手动 track 和 trigger 的典型例子,参考上文中的 “track() 和 trigger()”一节。它的源码也极其简单,大家可以自行查看。
shallowRef()
通常我们使用 ref() 函数时,目的是为了引用原始类型值,例如:ref(false)。但我们仍然可以引用非基本类型值,例如一个对象:
const refObj = ref({ foo: 1 })
此时,refObj.value 是一个对象,这个对象依然是响应的,例如如下代码会触发响应:
refObj.value.foo = 2
shallowRef() 顾名思义,它只代理 ref 对象本身,也就是说只有 .value 是被代理的,而 .value 所引用的对象并没有被代理:
const refObj = shallowRef({ foo: 1 })
refObj.value.foo = 3 // 无效
triggerRef()
我们上面讲过了 ref() 函数的实现,在 trigger 一个 ref 的时候,它的操作类型都是 SET,并且操作的 key 都是 value:
trigger(r, TriggerOpTypes.SET, 'value')
这里唯一不同的就是 r,也就是 ref 本身。换句话说,如果一个 ref 被 track 了,那么我们可以手动调用 trigger 函数任意去触发响应:
const refVal = ref(0)
effect(() => {
refVal.value
})
// 任意次的 trigger
trigger(refVal, TriggerOpTypes.SET, 'value')
trigger(refVal, TriggerOpTypes.SET, 'value')
trigger(refVal, TriggerOpTypes.SET, 'value')
而 triggerRef() 函数实际上就是封装了这一操作:
export function triggerRef(ref: Ref) {
trigger(
ref,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: ref.value } : void 0
)
}
那它有什么用呢?上面我们讲过了,shallowRef() 函数不会代理 .value 所引用的对象,因此我们修改对象值的时候不会触发响应,这时我们可以通过 triggerRef() 函数强制触发响应:
const refVal = shallowRef({ foo: 1 })
effect(() => {
console.log(refVal.value.foo)
})
refVal.value.foo = 2 // 无效
triggerRef(refVal) // 强制 trigger
unref()
unref() 函数很简单:
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref
}
给它一个值,如果这个值是 ref 就返回 .value,否则原样返回。
Lazy 的 effect()
effect() 用来运行副作用函数,默认是立即执行的,但它可以是 lazy 的,这时我们可以手动执行它:
const runner = effect(
() => { console.log('xxx') },
{ lazy: true } // 指定 lazy
)
runner() // 手动执行副作用函数
它有什么用呢?实际上 computed() 就是一个 lazy 的 effect。
computed()
我们先来看看 computed() 怎么用:
const refCount = ref(0)
const refDoubleCount = computed(() => refCount.value * 2)
当我们通过 refDoubleCount.value 取值时,如果 refCount 的值没变,那么表达式 refCount.value * 2 只会计算一次。这也是 computed() 优于 methods 的地方。
上面说了 computed 就是一个 lazy 的 effect,接下来我们证明这个说法。
我们可以把问题简化一下,首先我们有一 refCount 和 doubleCount,其中 doubleCount 是根据 refCount.value * 2 计算得来的,如下:
const refCount = ref(1)
let doubleCount = 0
function getDoubleCount() {
doubleCount = refCount.value * 2
return doubleCount
}
这样,我们就可以通过执行 getDoubleCount() 函数来取值,这段代码实际上我们可以改写一下,例如:
const refCount = ref(1)
let doubleCount = 0
const runner = effect(() => {
doubleCount = refCount.value * 2
}, { lazy: true })
function getDoubleCount() {
runner()
return doubleCount
}
我们定义了一个 lazy 的 effect,然后在 getDoubleCount() 函数中手动执行 runner() 来计算值。不过无论怎么改,都存在一个问题:即使 refCount 的值没变,表达式 refCount.value * 2 都会执行计算。
实际上,我们可以通过一个标志变量 dirty 来避免这个问题:
const refCount = ref(1)
let doubleCount = 0
let dirty = true // 定义标志变量,默认为 true
const runner = effect(() => {
doubleCount = refCount.value * 2
}, { lazy: true })
function getDoubleCount() {
if(dirty) {
runner() // 只有 dirty 的时候才执行计算
dirty = false // 设置为 false
}
return doubleCount
}
如上代码所示,我们增加了 dirty 变量,默认为 true,代表脏值,需要计算,所以只有 dirty 为 true 的时候才执行 runner(),紧接着将 dirty 设置为 false,这样就避免了冗余的计算量。
但问题是,现在我们修改 refCount 的值,并在此执行 getDoubleCount() 函数,得到的仍然是上一次的值,这是不正确的,因为 refCount 已经变化了,这是因为 dirty 一直是 false 的缘故,因此问题的解决办法也很简单,当 refCount 值变化之后,我们将 dirty 再次设置为 true 就可以了:
const refCount = ref(1)
let doubleCount = 0
let dirty = true // 定义标志变量,默认为 true
const runner = effect(() => {
doubleCount = refCount.value * 2
}, { lazy: true, scheduler: () => dirty = true }) // 将 dirty 设置为 true
function getDoubleCount() {
if(dirty) {
runner() // 只有 dirty 的时候才执行计算
dirty = false // 设置为 false
}
return doubleCount
}
如上代码所示,当 refCount 变化之后,我们知道副作用函数会进行调度执行,因此我们提供调度器,在调度器中仅仅将 dirty 设置为 true 即可。
那其实我们可以把 getDoubleCount 函数封装为一个 getter:
const refDoubleCount = {
get value() {
if(dirty) {
runner() // 只有 dirty 的时候才执行计算
dirty = false // 设置为 false
}
return doubleCount
}
}
refDoubleCount.value
这其实机上计算属性的思路了。
effect() 的其他选项
onTrack()
const obj = reactive({ foo: 0 })
effect(() => {
obj.foo
}, {
onTrack({ effect, target, type, key }) {
// ...
}
})
参数介绍:
effect:track 谁?
target:谁 track 的?
type:因为啥 track?
key:哪个 key track 的?
onTrigger()
const map = reactive(new Map())
map.set('foo', 1)
effect(() => {
for (let item of map){}
}, {
onTrigger({ effect, target, type, key, newValue, oldValue }) {
// ...
}
})
map.set('bar', 2)
effect:trigger 谁?
target:谁 trigger 的?
type:因为啥 trigger
key:哪个 key trigger 的?可能是 undefined,例如:map.clear() 的时候。
newValue 和 oldValue:新旧值
onStop()
const runner = effect(() => {
// ...
}, {
onStop() {
console.log('stop...')
}
})
stop(runner)
关于本文 作者:@HcySunYang 原文:https://zhuanlan.zhihu.com/p/146097763
为你推荐
【第1893期】TypedArray 还是 DataView: 理解字节序
欢迎自荐投稿,前端早读课等你来