查看原文
其他

【第1979期】深入理解 Vue3 Reactivity API

HcySunYang 前端早读课 2020-10-12

前言

今日早读文章由字节跳动@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()

  1. import { effect, reactive } from '@vue/reactivity'

  2. // 使用 reactive() 函数定义响应式数据

  3. const obj = reactive({ text: 'hello' })

  4. // 使用 effect() 函数定义副作用函数

  5. effect(() => {

  6. document.body.innerText = obj.text

  7. })


  8. // 一秒后修改响应式数据,这会触发副作用函数重新执行

  9. setTimeout(() => {

  10. obj.text += ' world'

  11. }, 1000)

reactive() 函数接收一个对象作为参数,并返回一个代理对象。

effect() 函数用于定义副作用,它的参数就是副作用函数,这个函数可能会产生副作用,例如上面代码中的 document.body.innerText = obj.text。在副作用函数内的响应式数据会与副作用函数之间建立联系,即所谓的依赖收集,当响应式数据变化之后,会导致副作用函数重新执行。

shallowReactive()

定义浅响应数据:

  1. import { effect, shallowReactive } from '@vue/reactivity'

  2. // 使用 shallowReactive() 函数定义浅响应式数据

  3. const obj = shallowReactive({ foo: { bar: 1 } })


  4. effect(() => {

  5. console.log(obj.foo.bar)

  6. })


  7. obj.foo.bar = 2 // 无效

  8. obj.foo = { bar: 2 } // 有效

readonly()

有些数据,我们要求对用户是只读的,此时可以使用 readonly() 函数,它的用法如下:

  1. import { readonly } from '@vue/reactivity'

  2. // 使用 reactive() 函数定义响应式数据

  3. const obj = readonly({ text: 'hello' })

  4. obj.text += ' world' // Set operation on key "text" failed: target is readonly.

shallowReadonly()

类似于浅响应,shallowReadonly() 定义浅只读数据,这意味着,深层次的对象值是可以被修改的,在 Vue 内部 props 就是使用 shallowReadonly() 函数来定义的,用法如下:

  1. import { effect, shallowReadonly } from '@vue/reactivity'

  2. // 使用 shallowReadonly() 函数定义浅只读数据

  3. const obj = shallowReadonly({ foo: { bar: 1 } })


  4. obj.foo = { bar: 2 } // Warn

  5. obj.foo.bar = 2 // OK

isReactive()

判断数据对象是否是 reactive:

  1. import { isReactive, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'


  2. const reactiveProxy = reactive({ foo: { bar: 1 } })

  3. console.log(isReactive(reactiveProxy)) // true

  4. console.log(isReactive(reactiveProxy.foo)) // true


  5. const shallowReactiveProxy = shallowReactive({ foo: { bar: 1 } })

  6. console.log(isReactive(shallowReactiveProxy)) // true

  7. console.log(isReactive(shallowReactiveProxy.foo)) // false


  8. const readonlyProxy = readonly({ foo: 1 })

  9. console.log(isReactive(readonlyProxy)) // false


  10. const shallowReadonlyProxy = shallowReadonly({ foo: 1 })

  11. console.log(isReactive(shallowReadonlyProxy)) // false

isReadonly()

用于判断数据是否是 readonly:

  1. import { isReadonly, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'


  2. console.log(isReadonly(readonly({}))) // true

  3. console.log(isReadonly(shallowReadonly({}))) // true

  4. console.log(isReadonly(reactive({}))) // false

  5. console.log(isReadonly(shallowReactive({}))) // false

isProxy()

用于判断对象是否是代理对象(reactive 或 readonly):

  1. import { isProxy, reactive, readonly, shallowReactive, shallowReadonly } from '@vue/reactivity'


  2. console.log(isProxy(readonly({}))) // true

  3. console.log(isProxy(shallowReadonly({}))) // true

  4. console.log(isProxy(reactive({}))) // true

  5. console.log(isProxy(shallowReactive({}))) // true


  6. const shallowReactiveProxy = shallowReactive({ foo: {} })

  7. console.log(isProxy(shallowReactiveProxy)) // true

  8. console.log(isProxy(shallowReactiveProxy.foo)) // false


  9. const shallowReadonlyProxy = shallowReadonly({ foo: {} })

  10. console.log(isProxy(shallowReadonlyProxy)) // true

  11. console.log(isProxy(shallowReadonlyProxy.foo)) // false

markRaw()

哪些数据是可以被代理的:

  • Object 、Array、Map、Set、WeakMap、WeakSet

  • 非 Object.isFrozen

  1. const obj = { foo: 1 }

  2. Object.freeze(obj)


  3. // Object.isFrozen(obj) ==> true

  4. // proxyObj === obj

  5. const proxyObj = reactiev(obj)

非 VNode,Vue3 的 VNode 对象带有 _vskip: true 标识,用于跳过代理(实际上,只要带有 _vskip 属性并且值为 true 的对象,都不会是被代理),例如:

  1. // obj 是原始数据对象

  2. const obj = reactive({

  3. foo: 0,

  4. __v_skip: true

  5. })

markRaw() 函数用于让数据不可被代理:

实际上 markRaw 函数所做的事情,就是在数据对象上定义 _vskip 属性,从而跳过代理:

  1. import { markRaw } from '@vue/reactivity'

  2. const obj = { foo: 1 }

  3. markRaw(obj) // { foo: 1, __v_skip: true }

toRaw()

接收代理对象作为参数,并获取原始对象:

  1. import { toRaw, reactive, readonly } from '@vue/reactivity'


  2. const obj1 = {}

  3. const reactiveProxy = reactive(obj1)

  4. console.log(toRaw(reactiveProxy) === obj1) // true


  5. const obj2 = {}

  6. const readonlyProxy = readonly(obj2)

  7. console.log(toRaw(readonlyProxy) === obj2) // true

如果参数是非代理对象,则直接该值:

  1. import { toRaw } from '@vue/reactivity'


  2. const obj1 = {}

  3. console.log(toRaw(obj1) === obj1) // true

  4. console.log(toRaw(1) === 1) // true

  5. console.log(toRaw('hello') === 'hello') // true

ReactiveFlags

ReactiveFlags 是一个枚举值:

它的定义如下:

  1. export const enum ReactiveFlags {

  2. skip = '__v_skip',

  3. isReactive = '__v_isReactive',

  4. isReadonly = '__v_isReadonly',

  5. raw = '__v_raw',

  6. reactive = '__v_reactive',

  7. readonly = '__v_readonly'

  8. }

它有什么用呢?举个例子,我们要定义一个不可被代理的对象:

  1. import { ReactiveFlags, reactive, isReactive } from '@vue/reactivity'


  2. const obj = {

  3. [ReactiveFlags.skip]: true

  4. }


  5. const proxyObj = reactive(obj)


  6. console.log(isReactive(proxyObj)) // false

实际上 markRaw() 函数就是使用类似的方式实现的。所以我们不必像如上代码那么做,但是在一些高级场景或许会用到这些值。

下面简单介绍一下 ReactiveFlags 中各个值得作用:

  • 代理对象会通过 ReactiveFlags.raw 引用原始对象

  • 原始对象会通过 ReactiveFlags.reactive 或 ReactiveFlags.readonly 引用代理对象

  • 代理对象根据它是 reactive 或 readonly 的, 将 ReactiveFlags.isReactive 或 ReactiveFlags.isReadonly 属性值设置为 true。

调度执行 effect - scheduler

来看下面的例子:

  1. const obj = reactive({ count: 1 })

  2. effect(() => {

  3. console.log(obj.count)

  4. })


  5. obj.count++

  6. obj.count++

  7. obj.count++

定义响应式对象 obj,并在 effect 内读取它的值,这样 effect 与数据之间就会建立“联系”,接着我们连续三次修改 obj.count 的值,会发现 console.log 语句共打印四次(包括首次执行)。

想像一下,假如我们只需要把数据的最终的状态应用到副作用中,而不是每次变化都重新执行一次副作用函数,这将对性能有所提升。实际上我们可以为 effect 传递第二个参数作为选项,可以指定“调度器”。所谓调度器就是用来指定如何运行副作用函数的:

  1. const obj = reactive({ count: 1 })

  2. effect(() => {

  3. console.log(obj.count)

  4. }, {

  5. // 指定调度器为 queueJob

  6. scheduler: queueJob

  7. })


  8. // 调度器实现

  9. const queue: Function[] = []

  10. let isFlushing = false

  11. function queueJob(job: () => void) {

  12. if (!queue.includes(job)) queue.push(job)

  13. if (!isFlushing) {

  14. isFlushing = true

  15. Promise.resolve().then(() => {

  16. let fn

  17. while(fn = queue.shift()) {

  18. fn()

  19. }

  20. })

  21. }

  22. }


  23. obj.count++

  24. obj.count++

  25. obj.count++

我们指定 effect 的调度器为 queueJob,job 实际上就是副作用函数,我们将副作用函数缓冲到 queue 队列中,并在 microtask 中刷新队列,由于队列不会重复缓冲相同的 job,因此最终只会执行一次副作用函数。

这实际上就是 watchEffect() 函数的实现思路。

watchEffect()

watchEffect() 函数并不在 @vue/reactivity 中提供,而是在 @vue/runtime-core 中提供,与 watch() 函数一起对外暴露。

  1. const obj = reactive({ foo: 1 })

  2. watchEffect(() => {

  3. console.log(obj.foo)

  4. })


  5. obj.foo++

  6. obj.foo++

  7. obj.foo++

这与我们上面刚刚实现的自定义调度器的 effect 的效果实际上是一样的。

异步副作用 和 invalidate

异步副作用是很常见的,例如请求 API 接口:

  1. watchEffect(async () => {

  2. const data = await fetch(obj.foo)

  3. })

当 obj.foo 变化后,意味着将会再次发送请求,那么之前的请求怎么办呢?是否应该将之前的请求标记为 invalidate?

实际上,副作用函数接收一个函数作为参数:

  1. watchEffect(async (onInvalidate) => {

  2. const data = await fetch(obj.foo)

  3. })

我们可以调用它来注册一个回调函数,这个回调函数会在副作用无效时执行:

  1. watchEffect(async (onInvalidate) => {

  2. let validate = true

  3. onInvalidate(() => {

  4. validate = false

  5. })

  6. const data = await fetch(obj.foo)

  7. if (validate){

  8. /* 正常使用 data */

  9. } else {

  10. /* 说明当前副作用已经无效了,抛弃即可 */

  11. }

  12. })

如果不抛弃无效的副作用,那么就会产生竟态问题。实际上,我们很容易就能通过封装 effect() 函数支持注册“无效回调”的功能:

  1. import { effect } from '@vue/reactivity'


  2. function watchEffect(fn: (onInvalidate: (fn: () => void) => void) => void) {

  3. let cleanup: Function

  4. function onInvalidate(fn: Function) {

  5. cleanup = fn

  6. }

  7. // 封装一下 effect

  8. // 在执行副作用函数之前,先使上一次无作用无效

  9. effect(() => {

  10. cleanup && cleanup()

  11. fn(onInvalidate)

  12. })

  13. }

如果我们再加上调用器,那实际上就非常接近 watchEffect 的真实实现了。

什么时候需要 invalidate 掉一个副作用函数呢?

  • 在组件中定义的 effect,需要在组件卸载时将其 invalidate

  • 在数据变化导致 effect 重新执行时,需要 invalidate 掉上一次的 effect 执行

  • 用户手动 stop 一个 effect 时

停止一个副作用(effect)

@vue/reactivity 提供了 stop 函数用来停止一个副作用:

  1. import { stop, reactive, effect } from '@vue/reactivity'

  2. const obj = reactive({ foo: 1 })


  3. const runner = effect(() => {

  4. console.log(obj.foo)

  5. })

  6. // 停止一个副作用

  7. stop(runner)


  8. obj.foo++

  9. 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()
  1. const obj = reactive({ foo: 1 })


  2. const Comp = defineComponent({

  3. setup() {

  4. watchEffect(() => {

  5. console.log(obj.foo)

  6. })


  7. return () => ''

  8. }

  9. })


  10. // 挂载组件

  11. render(h(Comp), document.querySelector('#app')!)

  12. // 卸载组件

  13. render(null, document.querySelector('#app')!)

  14. obj.foo++ // 副作用函数不会重新执行

我们先挂载了组件,接着又卸载了组件,最后修改 obj.foo 的值,并不会导致 watchEffect 的副作用函数重新执行。

effect()
  1. const obj = reactive({ foo: 1 })


  2. const Comp = defineComponent({

  3. setup() {

  4. effect(() => {

  5. console.log(obj.foo)

  6. })


  7. return () => ''

  8. }

  9. })


  10. // 渲染组件

  11. render(h(Comp), document.querySelector('#app')!)

  12. // 卸载组件

  13. render(null, document.querySelector('#app')!)

  14. obj.foo++

但 effect() 的副作用函数仍然会被执行,但我们可以借助 onUnmounted API 解决这个问题:

  1. const obj = reactive({ foo: 1 })


  2. const Comp = defineComponent({

  3. setup() {

  4. const runner = effect(() => {

  5. console.log(obj.foo)

  6. })

  7. // 组件卸载时,stop 掉 effect

  8. onUnmounted(() => stop(runner))


  9. return () => ''

  10. }

  11. })


  12. // 渲染组件

  13. render(h(Comp), document.querySelector('#app')!)

  14. // 卸载组件

  15. render(null, document.querySelector('#app')!)

  16. obj.foo++

当然,在普通开发中不推荐直接用 effect() 啦,使用 watchEffect() 就好了。

track() 与 trigger()

track() 和 trigger() 是依赖收集的核心,track() 用来跟踪收集依赖(收集 effect),trigger() 用来触发响应(执行 effect),它们需要配合 effect() 函数使用:

  1. const obj = { foo: 1 }

  2. effect(() => {

  3. console.log(obj.foo)

  4. track(obj, TrackOpTypes.GET, 'foo')

  5. })


  6. obj.foo = 2

  7. trigger(obj, TriggerOpTypes.SET, 'foo')

如上代码所示,obj 是一个普通的对象,注意它并非是响应式对象。接着使用 effect() 函数定义了一个副作用函数,读取并打印 obj.foo 的值,由于 obj 是一个普通对象,因此它并没有收集依赖的能力,为了收集到依赖,我们需要手动调用 track() 函数,track() 函数接收三个参数:

  • target:要跟踪的目标对象,这里就是 obj

  • 跟踪操作的类型:obj.foo 是读取对象的值,因此是 'get'

  • key:要跟踪目标对象的 key,我们读取的是 foo,因此 key 是 foo

这样,我们本质上是手动建立一种数据结构:

  1. // 伪代码

  2. map : {

  3. [target]: {

  4. [key]: [effect1, effect2....]

  5. }

  6. }

简单的理解,effect 与对象和具体操作的 key,是以这种映射关系建立关联的:

  1. [target]---->key1---->[effect1, effect2...]


  2. [target]---->key2---->[effect1, effect3...]


  3. [target2]---->key1---->[effect5, effect6...]

既然 effect 与目标对象 target 已经建立了联系,那么当然就可以想办法通过 target ----> key 进而取到 effect ,然后执行它们,而这就是 trigger() 函数做的事情,所以在调用 trigger 函数时我们要指定目标对象和相应的key值:

  1. trigger(obj, TriggerOpTypes.SET, 'foo')

这大概就是依赖收集的原理,但是这个过程是可以自动完成的,而不需要开发者手动调用 track() 和 trigger() 函数,想要自定完成依赖收集,那么就需要拦截诸如:设置、读取等对值得操作方法才行。至于实现方式,无论是 Object.defineProperty 还是 Proxy 那就是具体的技术形式了。

ref()

reactive() 函数可以代理一个对象,但不能代理基本类型值,例如字符串、数字、boolean 等,这是 js 语言的限制,因此我们需要使用 ref() 函数来间接对基本类型值进行处理:

  1. const refVal = ref(0)

  2. refVal.value // 0

ref 是响应式的:

  1. const refVal = ref(0)

  2. effect(() => {

  3. console.log(refVal.value)

  4. })


  5. refVal.value = 1 // 触发响应

已经了解了 track() 和 trigger() 函数的你,仔细思考一下,实现 ref() 函数是不是非常简单呢:

  1. function myRef(val: any) {

  2. let value = val


  3. const r = {

  4. isRef: true, // 随便加个标识以示区分

  5. get value() {

  6. // 收集依赖

  7. track(r, TrackOpTypes.GET, 'value')

  8. return value

  9. },

  10. set value(newVal: any) {

  11. if (newVal !== value) {

  12. value = newVal

  13. // 触发响应

  14. trigger(r, TriggerOpTypes.SET, 'value')

  15. }

  16. }

  17. }


  18. return r

  19. }

现在去试试我们的 myRef() 函数吧:

  1. const refVal = myRef(0)


  2. effect(() => {

  3. console.log(refVal.value)

  4. })


  5. refVal.value = 1

一切 OK。

isRef()

我们在实现 myRef() 函数时,可以看到为 ref 对象添加了一个标识 isRef: true。因此我们可以封装一个函数 isRef() 函数来判断一个值是不是 ref:

  1. function isRef(val) {

  2. return val.isRef

  3. }

实际上在 Vue3 中使用的标识是 _visRef,这无关紧要嘛。

toRef()

丢失响应式 reactivity api 的一个问题:

  1. const obj = reactive({ foo: 1 }) // obj 是响应式数据

  2. const obj2 = { foo: obj.foo }


  3. effect(() => {

  4. console.log(obj2.foo) // 这里读取 obj2.foo

  5. })


  6. obj.foo = 2 // 设置 obj.foo 显然无效

为了解决这个问题,我们可以使用 toRef() 函数:

  1. const obj = reactive({ foo: 1 })

  2. const obj2 = { foo: toRef(obj, 'foo') } // 修改了这里


  3. effect(() => {

  4. console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value

  5. })


  6. obj.foo = 2 // 有效

toRef() 函数用来把一个响应式对象的的某个 key 值转换成 ref,它的实现本身很简单:

  1. function toRef(target, key) {

  2. return {

  3. isRef: true,

  4. get value() {

  5. return target[key]

  6. },

  7. set value(newVal){

  8. target[key] = newVal

  9. }

  10. }

  11. }

可以看到 toRef() 函数比 ref() 函数要简单的多,这是因为 target 本身就是响应的,因此无需手动 track() 和 trigger()。

toRefs()

toRef() 的一个问题是定义起来极其麻烦,一次只能转换一个 key,因此我们可以封装一个函数,直接把一个响应式对象的所有key都转成 ref,这就是 toRefs()

  1. function toRefs(target){

  2. const ret: any = {}

  3. for (const key in target) {

  4. ret[key] = toRef(target, key)

  5. }

  6. return ret

  7. }

这样我们就可以修改前例的代码为:

  1. const obj = reactive({ foo: 1 })

  2. // const obj2 = { foo: toRef(obj, 'foo') }

  3. const obj2 = { ...toRefs(obj) } // 代替上面注释这句代码


  4. effect(() => {

  5. console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value

  6. })


  7. obj.foo = 2 // 有效

自动脱 ref

但是我们发现,问题虽然解决了,但是带来了新的问题,即我们需要通过 .value 访问值才行,这就带来了另外一个问题:我们怎么知道一个值是不是 ref,需不需要通过 .value 来访问呢?因为上面的例子可能会给读者带来疑惑,我们为什么把问题弄得这么复杂?为什么弄了 obj 和 obj2 这两个变量,只用 obj 不就没问题了嘛?这是因为在 Vue 中,我们要暴露数据到渲染环境,怎么暴露呢?

  1. const Comp = {

  2. setup() {

  3. const obj = reactive({ foo: 1 })

  4. return { ...obj }

  5. }

  6. }

这就会导致丢失响应,因此我们需要 toRef() 还是和 toRefs() 函数。然而这带来了新的问题, 我们在 setup 中暴露出去的数据,是要在渲染环境中使用的:

  1. <h1>{{ obj.foo }}</h1>

这里我们应该用 obj.foo 还是 obj.foo.value 呢?这时就需要你明确知道在 setup 中暴露出去的值,哪些是 ref 哪些不是 ref。因此为了减轻心智负担,干脆,在渲染环境都不需要 .value 去取值,即使是 ref 也不需要,这就极大的减少的心智负担,这就是自动脱 Ref 功能。而实现自动脱 ref也很简单,回看一下刚才的代码:

  1. const obj = reactive({ foo: 1 })

  2. // const obj2 = { foo: toRef(obj, 'foo') }

  3. const obj2 = { ...toRefs(obj) } // 代替上面注释这句代码


  4. effect(() => {

  5. console.log(obj2.foo.value) // 由于 obj2.foo 现在是一个 ref,因此要访问 .value

  6. })


  7. obj.foo = 2 // 有效

为了自动摆脱 ref,我们可以:

  1. const obj = reactive({ foo: 1 })

  2. // const obj2 = { foo: toRef(obj, 'foo') }

  3. const obj2 = reactive({ ...toRefs(obj) }) // 让 obj2 也是 reactive


  4. effect(() => {

  5. console.log(obj2.foo) // 即使 obj2.foo 是 ref,我们也不需要 .value 来取值

  6. })


  7. obj.foo = 2 // 有效

我们只需要让 obj2 也是 reactive 的即可,这样,即使 obj2.foo 是 ref,我们也不需要通过 .value 取值,其实现也很简单,当我们在对象上读取属性时,如果发现其值是 ref,那么直接返回 .value 就可以了:

  1. get(target, key, receiver) {

  2. // ...

  3. const res = Reflect.get(target, key, receiver)

  4. if (isRef(res)) return res.value

  5. // ...

  6. }

但对于由 ref 组成的数组,在渲染环境仍然需要 .value 访问。

customRef()

customRef() 实际上就是手动 track 和 trigger 的典型例子,参考上文中的 “track() 和 trigger()”一节。它的源码也极其简单,大家可以自行查看。

shallowRef()

通常我们使用 ref() 函数时,目的是为了引用原始类型值,例如:ref(false)。但我们仍然可以引用非基本类型值,例如一个对象:

  1. const refObj = ref({ foo: 1 })

此时,refObj.value 是一个对象,这个对象依然是响应的,例如如下代码会触发响应:

  1. refObj.value.foo = 2

shallowRef() 顾名思义,它只代理 ref 对象本身,也就是说只有 .value 是被代理的,而 .value 所引用的对象并没有被代理:

  1. const refObj = shallowRef({ foo: 1 })


  2. refObj.value.foo = 3 // 无效

triggerRef()

我们上面讲过了 ref() 函数的实现,在 trigger 一个 ref 的时候,它的操作类型都是 SET,并且操作的 key 都是 value:

  1. trigger(r, TriggerOpTypes.SET, 'value')

这里唯一不同的就是 r,也就是 ref 本身。换句话说,如果一个 ref 被 track 了,那么我们可以手动调用 trigger 函数任意去触发响应:

  1. const refVal = ref(0)

  2. effect(() => {

  3. refVal.value

  4. })


  5. // 任意次的 trigger

  6. trigger(refVal, TriggerOpTypes.SET, 'value')

  7. trigger(refVal, TriggerOpTypes.SET, 'value')

  8. trigger(refVal, TriggerOpTypes.SET, 'value')

而 triggerRef() 函数实际上就是封装了这一操作:

  1. export function triggerRef(ref: Ref) {

  2. trigger(

  3. ref,

  4. TriggerOpTypes.SET,

  5. 'value',

  6. __DEV__ ? { newValue: ref.value } : void 0

  7. )

  8. }

那它有什么用呢?上面我们讲过了,shallowRef() 函数不会代理 .value 所引用的对象,因此我们修改对象值的时候不会触发响应,这时我们可以通过 triggerRef() 函数强制触发响应:

  1. const refVal = shallowRef({ foo: 1 })

  2. effect(() => {

  3. console.log(refVal.value.foo)

  4. })


  5. refVal.value.foo = 2 // 无效

  6. triggerRef(refVal) // 强制 trigger

unref()

unref() 函数很简单:

  1. export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {

  2. return isRef(ref) ? (ref.value as any) : ref

  3. }

给它一个值,如果这个值是 ref 就返回 .value,否则原样返回。

Lazy 的 effect()

effect() 用来运行副作用函数,默认是立即执行的,但它可以是 lazy 的,这时我们可以手动执行它:

  1. const runner = effect(

  2. () => { console.log('xxx') },

  3. { lazy: true } // 指定 lazy

  4. )


  5. runner() // 手动执行副作用函数

它有什么用呢?实际上 computed() 就是一个 lazy 的 effect。

computed()

我们先来看看 computed() 怎么用:

  1. const refCount = ref(0)

  2. const refDoubleCount = computed(() => refCount.value * 2)

当我们通过 refDoubleCount.value 取值时,如果 refCount 的值没变,那么表达式 refCount.value * 2 只会计算一次。这也是 computed() 优于 methods 的地方。

上面说了 computed 就是一个 lazy 的 effect,接下来我们证明这个说法。

我们可以把问题简化一下,首先我们有一 refCount 和 doubleCount,其中 doubleCount 是根据 refCount.value * 2 计算得来的,如下:

  1. const refCount = ref(1)

  2. let doubleCount = 0


  3. function getDoubleCount() {

  4. doubleCount = refCount.value * 2

  5. return doubleCount

  6. }

这样,我们就可以通过执行 getDoubleCount() 函数来取值,这段代码实际上我们可以改写一下,例如:

  1. const refCount = ref(1)

  2. let doubleCount = 0


  3. const runner = effect(() => {

  4. doubleCount = refCount.value * 2

  5. }, { lazy: true })


  6. function getDoubleCount() {

  7. runner()

  8. return doubleCount

  9. }

我们定义了一个 lazy 的 effect,然后在 getDoubleCount() 函数中手动执行 runner() 来计算值。不过无论怎么改,都存在一个问题:即使 refCount 的值没变,表达式 refCount.value * 2 都会执行计算。

实际上,我们可以通过一个标志变量 dirty 来避免这个问题:

  1. const refCount = ref(1)

  2. let doubleCount = 0

  3. let dirty = true // 定义标志变量,默认为 true


  4. const runner = effect(() => {

  5. doubleCount = refCount.value * 2

  6. }, { lazy: true })


  7. function getDoubleCount() {

  8. if(dirty) {

  9. runner() // 只有 dirty 的时候才执行计算

  10. dirty = false // 设置为 false

  11. }

  12. return doubleCount

  13. }

如上代码所示,我们增加了 dirty 变量,默认为 true,代表脏值,需要计算,所以只有 dirty 为 true 的时候才执行 runner(),紧接着将 dirty 设置为 false,这样就避免了冗余的计算量。

但问题是,现在我们修改 refCount 的值,并在此执行 getDoubleCount() 函数,得到的仍然是上一次的值,这是不正确的,因为 refCount 已经变化了,这是因为 dirty 一直是 false 的缘故,因此问题的解决办法也很简单,当 refCount 值变化之后,我们将 dirty 再次设置为 true 就可以了:

  1. const refCount = ref(1)

  2. let doubleCount = 0

  3. let dirty = true // 定义标志变量,默认为 true


  4. const runner = effect(() => {

  5. doubleCount = refCount.value * 2

  6. }, { lazy: true, scheduler: () => dirty = true }) // 将 dirty 设置为 true


  7. function getDoubleCount() {

  8. if(dirty) {

  9. runner() // 只有 dirty 的时候才执行计算

  10. dirty = false // 设置为 false

  11. }

  12. return doubleCount

  13. }

如上代码所示,当 refCount 变化之后,我们知道副作用函数会进行调度执行,因此我们提供调度器,在调度器中仅仅将 dirty 设置为 true 即可。

那其实我们可以把 getDoubleCount 函数封装为一个 getter:

  1. const refDoubleCount = {

  2. get value() {

  3. if(dirty) {

  4. runner() // 只有 dirty 的时候才执行计算

  5. dirty = false // 设置为 false

  6. }

  7. return doubleCount

  8. }

  9. }


  10. refDoubleCount.value

这其实机上计算属性的思路了。

effect() 的其他选项

onTrack()
  1. const obj = reactive({ foo: 0 })

  2. effect(() => {

  3. obj.foo

  4. }, {

  5. onTrack({ effect, target, type, key }) {

  6. // ...

  7. }

  8. })

参数介绍:

  • effect:track 谁?

  • target:谁 track 的?

  • type:因为啥 track?

  • key:哪个 key track 的?

onTrigger()
  1. const map = reactive(new Map())

  2. map.set('foo', 1)


  3. effect(() => {

  4. for (let item of map){}

  5. }, {

  6. onTrigger({ effect, target, type, key, newValue, oldValue }) {

  7. // ...

  8. }

  9. })


  10. map.set('bar', 2)


  • effect:trigger 谁?

  • target:谁 trigger 的?

  • type:因为啥 trigger

  • key:哪个 key trigger 的?可能是 undefined,例如:map.clear() 的时候。

  • newValue 和 oldValue:新旧值

onStop()
  1. const runner = effect(() => {

  2. // ...

  3. }, {

  4. onStop() {

  5. console.log('stop...')

  6. }

  7. })


  8. stop(runner)

关于本文 作者:@HcySunYang 原文:https://zhuanlan.zhihu.com/p/146097763

为你推荐


【第1823期】Git子仓库深入浅出


【第1893期】TypedArray 还是 DataView: 理解字节序


欢迎自荐投稿,前端早读课等你来

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存