通过源码深入了解 vue3 的 ref vs reactive

最近刚开始用 vue3,其中组合式 API 的 ref 和 reactive 两者让我有些困惑:

  • 它们都返回响应式的数据,那么它们两者的区别在哪?
  • 它们的原理是怎样的?

于是就有了看源码的想法,源码是直接从 https://unpkg.com/vue@3/dist/vue.global.js 上保存下来的。后续就可以在源码上面调试学习啦。

结论

先说结论!(我知道很多朋友不喜欢看过程,只要结论。比如我自己 0,0)

  • 参数
    • ref() 函数的参数既可以是原始类型(string、number、boolean)也可以是对象类型(对象、数组、Set、Map)。
    • 如果将一个对象类型的数据赋值给 ref() 函数,这个对象将通过 reactive() 转为具有深层次响应式的对象。
    • reactive() 函数只有在接收对象类型是响应式的。它也可以接收 ref 函数返回的对象,不过如果需要解构就需要使用对象包裹。如 { a: refObj }
  • 返回值
    • ref() 接受一个内部值,并返回一个响应式的、可更改的 ref 对象。该对象通过内部值 .value 的 setter 和 getter 来获取和修改内部数据,如 count.value = 4
    • reactive() 函数返回一个对象的深层次响应式代理。

他们最终的目的都是能响应式渲染模板(即数据变化后网页内容也随之变化)。

ref

源码

先看下 ref 的源码,ref() 函数执行了 createRef() 函数,而 createRef() 中实例化了 RefImpl 类。

function ref(value) {
  return createRef(value, false)
}

function createRef(rawValue, shallow) {
  // 如果已经是 ref 则直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

RefImpl 类中除了构造函数,只有一个 value 内部值的 setter 和 getter 函数。在构造函数中 _rawValue 是原始数据,而 _value 是响应数据(如果数据是对象类型则为 Proxy)。

那么 _value 是如何来的?如果不是浅层响应式,则会调用 toReactive 函数。

class RefImpl {
  constructor(value, __v_isShallow) {
    this.__v_isShallow = __v_isShallow
    this.dep = undefined
    this.__v_isRef = true
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

toRaw() 函数中,递归获取数据的原始数据。(reactive() 函数返回的代理对象中带有 __v_raw 标签,它会让 getter 函数返回原始数据)

function toRaw(observed) {
  const raw = observed && observed['__v_raw' /* ReactiveFlags.RAW */]
  return raw ? toRaw(raw) : observed
}

toReactive() 函数中,就可以看到已经使用 reactive() 函数的逻辑了。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。

const toReactive = (value) => (isObject(value) ? reactive(value) : value)

顺便瞅一眼 isObject() 函数,对象类型的判定就是 typeof val === 'object'。不过由于 JavaScript 的缺陷,所以 typeof null 也是 object,需要排除掉。

const isObject = (val) => val !== null && typeof val === 'object'

小实验

实验出真知

const a = ref('123')
a.value += '456'
// '123456'
const b = ref(6)
b.value += 8
// 14
const c = ref(false)
c.value = !c.value
// true

const r3 = ref(false)
r3.value = true
r3.value = 'oh li gei' // value 是不限定类型的
// oh li gei

const d = ref(null)
// null
const e = ref(undefined)
// undefined
const f = ref(Symbol())
// Symbol()

// 这里打赢 ref 返回的对象
const g = ref({ a: [1, 2, 3] })
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
const h = ref([3, 4, 5, 6])
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Array(4), _value: Proxy}
const i = ref(new Set())
// RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: Set(1), _value: Proxy}

// 验证 toRaw 函数的 __v_raw 属性
const x = reactive({ a: 1, b: { c: { d: 3 } } })
const y = ref(x)
console.log('x', x.__v_raw) // { a: 1, b: { c: { d: 3 } } }
console.log('y', y.__v_raw) // undefined
  • 对于字符串、数字、布尔类型来说,ref 函数可以让这些数据变成响应式的。
  • 对于 null、undefined、symbol 这类特殊数据,ref 函数返回值还是其本身,无意义。
  • 对于对象类型数据,正如源码所说,对数据使用了 reactive() 来进行深层响应式代理。从 ref 返回的对象可以看出,_rawValue 是原始数据,而 _value 是数据的代理。
  • reactive 函数返回的代理对象中带有 __v_raw 标签,会返回原始数据

reactive

源码

reactive()

reactive() 函数除了判断只读外就只是调用了 createReactiveObject() 函数。

createReactiveObject() 函数中,排除了各种不需要代理的情况,并根据数据类型不同进行不同的代理逻辑处理。最后将代理结构记录到一个 Map 中。

function reactive(target) {
  // 如果 target 是 Readonly 的代理,返回自身
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap,
  )
}

function createReactiveObject(
  target,
  isReadonly,
  baseHandlers,
  collectionHandlers,
  proxyMap,
) {
  // target 不是对象类型,返回自身
  if (!isObject(target)) {
    {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // target 已经是代理,返回自身
  if (
    target['__v_raw' /* ReactiveFlags.RAW */] &&
    !(isReadonly && target['__v_isReactive' /* ReactiveFlags.IS_REACTIVE */])
  ) {
    return target
  }
  // target 已经有响应的代理,返回代理
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  /**
   * 判断 target 数据类型
   * 0 无效,直接返回
   * 1 COMMON 类型,使用 baseHandlers 代理配置
   * 2 COLLECTION 类型,使用 collectionHandlers 代理配置
   */
  const targetType = getTargetType(target)
  if (targetType === 0 /* TargetType.INVALID */) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === 2 /* TargetType.COLLECTION */
      ? collectionHandlers
      : baseHandlers,
  )
  // 记录代理关系
  proxyMap.set(target, proxy)
  return proxy
}

数据类型判断

判断数据类型的代码如下,根据不同的数据分为:

  • 0 无效数据类型,不进行代理返回自身。
  • 1 普通对象类型
  • 2 收集器类型

类型的获取是通过 Object.prototype.toString.call(target) 获取到 '[object Set]' 这类字符串,并截取 Set 这段有效字符串返回。

function getTargetType(value) {
  return value['__v_skip' /* ReactiveFlags.SKIP */] ||
    !Object.isExtensible(value)
    ? 0 /* TargetType.INVALID */
    : targetTypeMap(toRawType(value))
}

function targetTypeMap(rawType) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return 1 /* TargetType.COMMON */
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return 2 /* TargetType.COLLECTION */
    default:
      return 0 /* TargetType.INVALID */
  }
}

普通类数据代理

reactive() 函数中可以看到,代理配置分别使用的是 mutableHandlers 和 mutableCollectionHandlers。

普通类型的代理配置 mutableHandlers 如下。这里代码量较大,暂时就只贴出 getter 和 settter 函数。

const mutableHandlers = {
  get: get$1, // get 方法用于拦截某个属性的读取操作
  set: set$1, // set 方法用来拦截某个属性的赋值操作
  deleteProperty, // deleteProperty 方法用于拦截 delete 操作
  has: has$1, // has() 方法用来拦截HasProperty操作
  ownKeys, //  ownKeys() 方法用来拦截对象自身属性的读取操作
}

在 createGetter 中的处理逻辑如下:

  • 如果是标签 __v_raw 等则返回响应的值;
  • 如果是数组 API 的关键字 inclueds push 等就用 arrayInstrumentations 进行处理;(所以可以在 reactive 的返回值中直接使用数组 API arr.push()
  • 通过 Reflect.get() 获取到目标返回值。
  • 如果返回值是一个对象,且不是只读数据。那么就以递归的方式对这个子对象使用 reactive() 函数继续绑定响应式代理。(即深层响应式转换)
  • 返回最终结果。
const get$1 = /*#__PURE__*/ createGetter()

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
      return !isReadonly
    } else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
      return isReadonly
    } else if (key === '__v_isShallow' /* ReactiveFlags.IS_SHALLOW */) {
      return shallow
    } else if (
      key === '__v_raw' /* ReactiveFlags.RAW */ &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }
    const targetIsArray = isArray(target)
    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }
    const res = Reflect.get(target, key, receiver) // Reflect.get 方法查找并返回target对象的name属性
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }
    if (!isReadonly) {
      track(target, 'get' /* TrackOpTypes.GET */, key)
    }
    if (shallow) {
      return res
    }
    if (isRef(res)) {
      // ref 解构取值
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }
    if (isObject(res)) {
      // 递归生成响应式代理
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

在 set 函数中

  • 首先是调用 toRaw() 函数将 value 和 oldValue 递归从代理变为原始数据。原理大致如 reactive({ a: 1 }).__v_raw // output: { a: 1}
  • 如果 oldValue 是 ref() 函数返回的,则进行解构赋值。
  • 通过 Reflect.set() 函数对代理目标 target 进行赋值。
const set$1 = /*#__PURE__*/ createSetter()

function createSetter(shallow = false) {
  return function set(target, key, value, receiver) {
    let oldValue = target[key]
    // 不需要更新的情况
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue) // 递归转为原始数据
        value = toRaw(value) // 递归转为原始数据
      }
      // ref 解构赋值
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    }
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver) // Reflect.set 方法设置 target 对象的 name 属性等于value。
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
      }
    }
    return result
  }
}

收集器类数据代理

收集器类型的代理配置只有一个 getter 函数,它对收集器类型数据的 API 进行了定义。

如果调用 set.add() map.get() 这类 API,就会去调用 instrumentations 对象中相应的函数。否则就返回代理目标 target 自身。

const mutableCollectionHandlers = {
  get: /*#__PURE__*/ createInstrumentationGetter(false, false),
}

function createInstrumentationGetter(isReadonly, shallow) {
  const instrumentations = shallow
    ? isReadonly
      ? shallowReadonlyInstrumentations
      : shallowInstrumentations
    : isReadonly
    ? readonlyInstrumentations
    : mutableInstrumentations
  return (target, key, receiver) => {
    if (key === '__v_isReactive' /* ReactiveFlags.IS_REACTIVE */) {
      return !isReadonly
    } else if (key === '__v_isReadonly' /* ReactiveFlags.IS_READONLY */) {
      return isReadonly
    } else if (key === '__v_raw' /* ReactiveFlags.RAW */) {
      return target
    }
    return Reflect.get(
      hasOwn(instrumentations, key) && key in target
        ? instrumentations
        : target,
      key,
      receiver,
    )
  }
}

const mutableInstrumentations = {
  get(key) {
    return get(this, key)
  },
  get size() {
    return size(this)
  },
  has,
  add,
  set,
  delete: deleteEntry,
  clear,
  forEach: createForEach(false, false),
}

function add(value) {
  value = toRaw(value) // 去代理
  const target = toRaw(this) // 去代理
  const proto = getProto(target)
  const hadKey = proto.has.call(target, value)
  if (!hadKey) {
    target.add(value) // 执行原生函数
    trigger(target, 'add' /* TriggerOpTypes.ADD */, value, value)
  }
  return this
}

function set(key, value) {
  value = toRaw(value)
  const target = toRaw(this)
  const { has, get } = getProto(target)
  let hadKey = has.call(target, key)
  if (!hadKey) {
    key = toRaw(key)
    hadKey = has.call(target, key)
  } else {
    checkIdentityKeys(target, has, key)
  }
  const oldValue = get.call(target, key)
  target.set(key, value)
  if (!hadKey) {
    trigger(target, 'add' /* TriggerOpTypes.ADD */, key, value)
  } else if (hasChanged(value, oldValue)) {
    trigger(target, 'set' /* TriggerOpTypes.SET */, key, value, oldValue)
  }
  return this
}

源码小结

  • reactive 函数只对 Object Array Map Set WeakMap WeakSet 类型的数据生效,且分为了两种处理方式。
  • reactive 函数会排除各种不符合条件的数据,返回数据本身。
  • reactive 函数是通过代理 Proxy 实现数据的存取的。
  • reactive 中的 __v_raw __v_isShallow 并不是属性值,而是判断标签。会根据标签返回相应结果。
  • reactive 对于 ref 对象的解构其实就是在 get 的时候取 .value 值,而在 set 的时候将值传给 .value
  • reactive 对于 Set、Map 这类数据,仅提供了 getter 方法。如果调用这类数据 API 函数,vue 在做了数据处理后会去调用它的原生函数。如果是获取数据内容,则直接返回数据本身。
  • 对象类型数据想要变成响应式的,就必须用 reactive 函数代理。
  • 上面代码中用到了 ES6 的 Reflect 和 Proxy ,关于它们的更多内容可以访问 Proxy - ECMAScript 6 入门Reflect - ECMAScript 6 入门 了解。

小实验

以下写法 reactive() 返回值是其自身,但不是响应式的。而且 vue 会发出警告:value cannot be made reactive: 123

var a = reactive('123')
// 123

function add() {
  a += '456' // 变量 a 有变化,但是 HTML 无变化
}

var b = reactive(6)
b += 8
// 16

const c = reactive(false)
// false
setTimeout(() => {
  c = true // c 变为 true,但是 HTML 无变化
}, 1000)

const d = reactive(null)
// null

const e = reactive(undefined)
// undefined

const f = reactive(Symbol())
// Synbol()

下面这些情况可以正常使用 reactive() 函数。

const g = reactive({ a: 1, b: { c: 3 } })
g.a++
// Proxy: { a: 2, b: { c: 3 } }
setInterval(() => {
  // 网页会每秒变化数据
  g.a++
  g.b.c += 2
}, 1000)

const h = reactive([3, 4, 5, 6])
h.push(8)
// Proxy: {0: 3, 1: 4, 2: 5, 3: 6, 4: 8}

const i = reactive(new Set())
i.add('2')
i.add({ b: 3 })
i.add(321)
// Proxy: { "Set(4)": [ "2", { "b": 3 }, 321 ] }
setTimeout(() => {
  i.add(null)
  // Proxy: { "Set(4)": [ "2", { "b": 3 }, 321, null ] }
}, 1000)

const j = reactive(new Map())
j.set('yo', 'good')
j.set('x', { b: 3 })
setTimeout(() => {
  j.delete('x')
  // Proxy: { "Map(1)": { "yo =>": "good" } }
}, 1000)

既然 reactive 函数可以解构 ref,那么进行一些尝试。以下是官网的原话。

值得注意的是,当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包。

但在实际实验下来发现这句话并不严谨。

var a = ref(new Map())
var b = reactive(a)
a.value.set('a', 1)
b.value.set('b', 2) // 需要加上 value
// { "Map(2)": { "a =>": 1, "b =>": 2 } }

console.log('a === b', a.value === b.value) // true

var a = ref(new Set())
var b = reactive({ a })
a.value.add(1)
b.a.add(2) // ! 被对象包裹的 Map 是可以被解构的
// { "Map(2)": { "a =>": 1, "b =>": 2 } }
console.log('a === b', a.value === b.a) // true

尝试了 Object、Array、Set 后发现,被 ref 函数返回的对象如果直接传给 reactive 函数是不会被解构的,但如果 ref 对象被对象符号包裹 reactive({ ref: ref(new Set()) }) 的情况下是可以被解构的。

最后

本文我们先提出了 ref 和 reactive 的疑问,然后给出结果。再从源码层面逐步分析了 ref 和 reactive 函数。也算是基本掌握其原理了。

关于 ref 和 reactive 的内容就这么多啦,希望对你有用。

本文正在参加「金石计划」

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容