vue3.x Ref VS Reactive学习记录

4,488 阅读7分钟

学习Vue3,refreactive是整个源码中的核心,通过这两个方法创建了响应式数据。要想完全吃透reactivity,必须先吃透这两个。

Reactive

接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

const obj = reactive({ count: 0 })

响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象__不等于__原始对象。建议仅使用代理对象而避免依赖原始对象

Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等等),都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作

类型定义

function reactive<T extends object>(raw: T): T

reactive 类的 api 主要提供了将复杂类型的数据处理成响应式数据的能力,其实这个复杂类型是要在object array map set weakmap weakset 这六种之中【如下源码,他会判断是否是五类以及是否被冻结】

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
const isObservableType = /*#__PURE__*/ makeMap(
	'Obeject,Array,Map,Set,WeakMap,WeakSet'
)

const canObserve = (value: Target): boolean => {
	return (
    	!value[ReactiveFlags.SKIP] &&
        isObservableType(toRawType(value)) &&
        !Object.isFrozen(value)
    )
}

因为是组合函数【对象】,所以必须始终保持对这个所返回对象的引用以保持响应性【不能解构该对象或者展开】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }

function useMousePosition() {
    const pos = reactive({
        x: 0,
        y: 0,
      })
    return pos
}

Ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。

const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换。

  • 模板中访问

    ref 作为渲染上下文的属性返回(即在setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value
      <template>
        <div>{{ count }}</div>
      </template>
      <script>
        export default {
          setup() {
            return {
              count: ref(0),
            }
          },
        }
      </script>
  • 作为响应式对象的属性访问

    当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性:
    const count = ref(0)
    const state = reactive({
      count,
    })
    console.log(state.count) // 0
    state.count = 1
    console.log(count.value) // 1

注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref:

	const otherCount = ref(2)
	state.count = otherCount
	console.log(state.count) // 2
	console.log(count.value) // 1

注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:

	const arr = reactive([ref(0)])
	// 这里需要 .value
	console.log(arr[0].value)
	const map = reactive(new Map([['foo', ref(0)]]))
	// 这里需要 .value
	console.log(map.get('foo').value)

ref最重要的作用,其实是提供了一套Ref类型,我们先来看,它到底是个怎么样的数据类型。

// 生成一个唯一key,开发环境下增加描述符 'refSymbol'
export const refSymbol = Symbol(__DEV__ ? 'refSymbol' : undefined)

// 声明Ref接口
export interface Ref<T = any> {
  // 用此唯一key,来做Ref接口的一个描述符,让isRef函数做类型判断
  [refSymbol]: true
  // value值,存放真正的数据的地方。关于UnwrapNestedRefs这个类型,我后续单独解释
  value: UnwrapNestedRefs<T>
}

// 判断是否是Ref数据的方法
// 对于is关键词,若不熟悉,见:http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates
export function isRef(v: any): v is Ref {
  return v ? v[refSymbol] === true : false
}

// 见下文解释
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

关于UnwrapNestedRefsUnwrapRef

// 不应该继续递归的引用数据类型
type BailTypes =
  | Function
  | Map<any, any>
  | Set<any>
  | WeakMap<any, any>
  | WeakSet<any>

// 递归地获取嵌套数据的类型
// Recursively unwraps nested value bindings.
export type UnwrapRef<T> = {
  // 如果是ref类型,继续解套
  ref: T extends Ref<infer V> ? UnwrapRef<V> : T
  // 如果是数组,循环解套
  array: T extends Array<infer V> ? Array<UnwrapRef<V>> : T
  // 如果是对象,遍历解套
  object: { [K in keyof T]: UnwrapRef<T[K]> }
  // 否则,停止解套
  stop: T
}[T extends Ref
  ? 'ref'
  : T extends Array<any>
    ? 'array'
    : T extends BailTypes
      ? 'stop' // bail out on types that shouldn't be unwrapped
      : T extends object ? 'object' : 'stop']

// 声明类型别名:UnwrapNestedRefs
// 它是这样的类型:如果该类型已经继承于Ref,则不需要解套,否则可能是嵌套的ref,走递归解套
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

Ref是这样的一种数据结构:它有个key为Symbol的属性做类型标识,有个属性value用来存储数据。这个数据可以是任意的类型,唯独不能是被嵌套了Ref类型的类型。 具体来说就是不能是这样 Array<Ref> 或者这样 { [key]: Ref }或者Ref<Ref>

另外,Map、Set、WeakMap、WeakSet也是不支持解套的。说明Ref数据的value也有可能是Map<Ref>这样的数据类型。

Ref类型的数据,是一种响应式的数据。然后我们看其具体实现:

// 从@vue/shared中引入,判断一个数据是否为对象
// Record<any, any>代表了任意类型key,任意类型value的类型
// 为什么不是 val is object 呢?可以看下这个回答:https://stackoverflow.com/questions/52245366/in-typescript-is-there-a-difference-between-types-object-and-recordany-any
export const isObject = (val: any): val is Record<any, any> =>
  val !== null && typeof val === 'object'

// 如果传递的值是个对象(包含数组/Map/Set/WeakMap/WeakSet),则使用reactive执行,否则返回原数据
// 从上文知道,这个reactive就是将我们的数据转成响应式数据
const convert = (val: any): any => (isObject(val) ? reactive(val) : val)

export function ref<T>(raw: T): Ref<T> {
  // 转化数据
  raw = convert(raw)
  const v = {
    [refSymbol]: true,
    get value() {
      // track的代码在effect中,暂时不看,能猜到此处就是监听函数收集依赖的方法。
      track(v, OperationTypes.GET, '')
      // 返回刚刚被转化后的数据
      return raw
    },
    set value(newVal) {
      // 将设置的值,转化为响应式数据,赋值给raw
      raw = convert(newVal)
      // trigger也暂时不看,能猜到此处就是触发监听函数执行的方法
      trigger(v, OperationTypes.SET, '')
    }
  }
  return v as Ref<T>
}

以上是关于RefReactive相关实现和使用方法,可以看出两者都是将普通类型数据转为响应式数据。

Ref VS Reactive

可以理解的是,用户会纠结用 ref 还是 reactive。而首先你要知道的是,这两者你都必须要了解,才能够高效地使用组合式 API。只用其中一个很可能会使你的工作无谓地复杂化,或反复地造轮子。

使用 ref 和 reactive 的区别,可以通过如何撰写标准的 JavaScript 逻辑来比较:

// 风格 1: 将变量分离
let x = 0
let y = 0

function updatePosition(e) {
  x = e.pageX
  y = e.pageY
}

// --- 与下面的相比较 ---

// 风格 2: 单个对象
const pos = {
  x: 0,
  y: 0,
}

function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}
  • 如果使用 ref,我们实际上就是将风格 (1) 转换为使用 ref (为了让基础类型值具有响应性) 的更细致的写法。
  • 使用 reactive 和风格 (2) 一致。我们只需要通过 reactive 创建这个对象。

而只使用 reactive 的问题是,使用组合函数时必须始终保持对这个所返回对象的引用以保持响应性。这个对象不能被解构或展开:

// 组合函数:
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0,
  })

  // ...
  return pos
}

// 消费者组件
export default {
  setup() {
    // 这里会丢失响应性!
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }

    // 这里会丢失响应性!
    return {
      ...useMousePosition(),
    }

    // 这是保持响应性的唯一办法!
    // 你必须返回 `pos` 本身,并按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
    return {
      pos: useMousePosition(),
    }
  },
}

toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref。

function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0,
  })

  // ...
  return toRefs(pos)
}

// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()

引入Ref 的心智负担

  1. 当使用组合式 API 时, 我们需要一直区别「响应式值引用」与普通的基本类型值与对象,这无疑增加了使用本套 API 的心智负担.
这一层心智负担可以通过名称规范来大大降低 (例如为所有的 ref 名加类似 xxxRef 的后缀),亦或者是使用类型系统。另外,由于代码组织方面的灵活性增加了,组件逻辑会更多地分解成一些短小精悍的函数,它们的上下文都比较简单,这些 ref 的上限也易于控制。
  1. 读写 ref 的操作比普通值的更冗余,因为需要访问 .value

参考文章

Vue组合式API

Vue3响应式系统源码解析-Ref篇

Vue3文档【Vue2迁移Vue3】