Vue 3.x

源码学习进度

响应式原理 reactivity

  • reactive: 为对象、数组、Set/WeakSetMap/WeakMap等做响应式转换,而不会对 Date、RegExp、Function 等做响应式处理
    • 源码位置: packages/reactivity/src/reactive.ts
    • 其他方法
      • isReactive()
      • isReadonly()
      • isProxy()
      • toRaw():递归地获取代理对象observed的原始对象
      • markRaw(value):标记value为跳转响应式转换,以后即使调用reactive(value)等操作返回的还是value本身
      • baseHandlers.ts: 对象/数组做代理时使用的处理器对象
  • ref(val): 将val转换为 Ref 对象,val可以是对象或代理对象
    • 源码位置: packages/reactivity/src/ref.ts
    • shallowRef()
    • isRef()
    • unref(): 获取 Ref 对象的原始值
    • toRef(obj, key): 将对象的单个key转为 ObjectRef 对象(类似于 Ref 对象),解决因提前触发值的get方法(比如解构赋值、扩展运算符)导致的响应式丢失问题
    • toRefs(): 遍历对象的每一个key调用toRef
    • triggerRef(): 手动触发依赖refeffect重新运行
    • customRef: 自定义refget/set函数
    • 【TODO】proxyRefs
  • 【done】effect.ts
    • effect()
    • track()
    • trigger()
  • computed方法
    • 源码位置: packages/reactivity/src/computed.ts
    • computed具有双重角色,自身既作为ReactiveEffect依赖外部的依赖项,也作为ComputedRef(类似于ref,专用于计算属性)成为外部ReactiveEffect的依赖项
    • computed的计算是惰性的,即:
      • 首次声明computed()时不会对传入的getter进行计算
      • 若只声明了computed但没有获取computed的值,当computed的依赖项改变时,computed不会进行计算新值,只会标记当前的计算值为dirty

以上这些内容位于vue-next/packages/reactivity目录下,会单独发包为@vue/reactivity,可独立于 Vue 3 使用。

响应式的计算和侦听

  • watchEffect/watch方法
    • 源码位置: packages/runtime-core/src/apiWatch.ts
    • watchEffect方法
      • 第一个参数只能是函数
      • 立即执行传入的函数fn,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数fn
    • watch
      • 第一个参数可以是:
        • getter函数
        • ref对象
        • 响应式对象
        • 以上这些组成的数组
      • watchEffect比较,watch允许我们:
        • 懒执行副作用;
        • 更具体地说明什么状态应该触发侦听器重新运行;
        • 访问侦听状态变化前后的值。

以上这些内容是 Vue 3 基于@vue/reactivity的封装,只在 Vue 3 内部使用。

runtime-core

watchEffect/watch

  • watchEffect/watch
    • 首次都会运行一次
    • 调用watchEffect()的返回值stop,可以停止副作用,且将该watcher从当前 Vue 实例instance.effects数组里移除
  • watchEffect(effect, options?)
  • watch(source, callback, options?)
    • source的依赖项改变导致source重新计算后,则满足以下条件之一,回调函数callback会运行
      • 第一个参数是ref,或是reactive的对象,或是指定了deep: true
      • 通过第一个参数获取的值改变了

为什么解构赋值、扩展运算符等会造成响应式丢失

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

effect(() => {
  console.log(obj2.foo) // 这里读取 obj2.foo
})

obj.foo = 2  // 设置 obj.foo 显然无效
1
2
3
4
5
6
7
8

2行获取obj.foo时会触发obj.fooget函数进行依赖收集,但是此时activeEffect为空,这导致obj.foo没有被任何的effect所依赖,且赋值结束后,obj2的值为{ foo: 1 }obj.foo的值是原始值,而不是响应式的,因此在effect()函数执行时不会将obj.foo收集为依赖项,因此最后一行给obj.foo赋值时不会触发effect()的重新计算。

解构赋值、扩展运算符也是同理,都是先将值获取到再赋值给变量,而这个过程中activeEffect都为空,不会进行依赖收集,而赋值后的变量不是响应式的。

参考文档