最近刚开始用 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 的返回值中直接使用数组 APIarr.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 的内容就这么多啦,希望对你有用。
本文正在参加「金石计划」