Vue3 源码系列文章会持续更新,全部文章请查阅我的掘金专栏。
本系列的文章 demo 存放于我的 Github 仓库,推荐大家下载和调试,进而加深理解。
一. toRef 和 toRefs
通过 reactive
代理后的响应式对象是不支持解构的,例如下方的代码不会按预期执行:
<body>
<div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { reactive, effect } = Vue;
const div = document.querySelector('div');
const obj = reactive({msg: 'Hello!'});
effect(() => {
div.innerText = obj.msg
});
const { msg } = obj; // 解构
msg = 'Bye~'; // 不会触发副作用函数
</script>
toRef
和 toRefs
就是用来解决该问题的,它们可以把响应式对象内的单个或多个属性,转化为 ref
实例的形式,通过 value
属性来访问/修改对应的值,并触发相应的副作用函数。
像上面的代码可以利用这两个接口来处理:
<body>
<div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { reactive, effect, toRef, toRefs } = Vue;
const div = document.querySelector('div');
const obj = reactive({msg: 'Hello!'});
effect(() => {
div.innerText = obj.msg
});
const msg = toRef(obj, 'msg'); // 或者 const { msg } = toRefs(obj);
msg.value = 'Bye~'; // 可以触发副作用函数
</script>
它们的实现比较简单,依旧是利用 getter
和 setter
来拦截 value
属性的请求 —— 当访问 value
属性时,返回响应式对象对应的属性值;当修改 value
属性时,直接修改响应式对象。
接口实现如下:
/** ref.js **/
export function toRef(object, key, defaultValue) {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue))
}
export function toRefs(object) {
const ret = isArray(object) ? [] : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
class ObjectRefImpl {
constructor(object, key, defaultValue) { // defaultValue 是缺省值
this.__v_isRef = true;
this._object = object;
this._key = key;
this._defaultValue = defaultValue;
}
get value() {
const val = this._object[this._key]
return val === undefined ? this._defaultValue : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
着重关注 ObjectRefImpl
和 toRef
的实现即可,toRefs
不外乎是遍历传入的响应式对象,再复用 toRef
接口来映射全部的属性。
二. customRef
Vue 提供了一个自定义接口 customRef
,可以让用户决定 ref
实例执行 track
和 trigger
的时机。
customRef
接收一个工厂函数为参数,该工厂函数又包含 track
和 trigger
两个函数类型的参数,且必须返回带有 get
和 set
属性方法的对象。
下方示例通过 customRef
接口,自定义了一个只接受偶数赋值的 ref
:
<body>
<div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { effect, customRef } = Vue;
const div = document.querySelector('div');
// 只接受偶数的 ref
function useEvenRef(value = 0) {
return customRef((track, trigger) => {
return {
get() {
track();
return value
},
set(newValue) {
if (newValue % 2 === 0) { // 偶数才执行
value = newValue
trigger()
}
}
}
})
}
let num = useEvenRef();
effect(() => {
div.innerText = num.value
});
let count = 0;
setInterval(() => {
++count;
num.value = count;
}, 500);
</script>
customRef
的实现如下:
/** ref.js **/
export function customRef(factory) {
return new CustomRefImpl(factory)
}
class CustomRefImpl {
constructor(factory) {
this.__v_isRef = true;
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get;
this._set = set;
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
可以看到,在类 CustomRefImpl
中利用了闭包的能力,将传入工厂函数的执行结果(get
和 set
)封存为内部属性,并拦截用户对 value
属性的访问和修改来调用 get
和 set
方法。
另外在 CustomRefImpl
中也将工厂函数的两个参数定义为调用 trackRefValue
和 triggerRefValue
的方法,用户在外部执行这两个参数,即可调用依赖收集和触发副作用的能力。
常规要求在工厂函数返回的 get
中去调用 track
,在 set
中去调用 trigger
。
三. proxyRefs
如果一个对象中存在某个属性指向 ref
实例,每次我们在使用时(无论是访问,抑或修改),都需要访问其 value
属性,这是一个开发环节的心智负担:
const { ref, effect } = Vue;
const div = document.querySelector('div');
const info = ref('Hello');
const obj = { info };
effect(() => {
div.innerText = obj.info.value; // 需要多打一个 .value
});
obj.info.value = 'Bye~'; // 需要多打一个 .value
得益于 Proxy
的拦截机制,可以拦截用户对对象属性的访问,再在拦截器中返回/修改对应属性的 value
值即可。
我们基于该原理来新增一个 proxyRefs
接口:
/** ref.js **/
export function unref(ref) {
return isRef(ref) ? (ref.value) : ref
}
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
const shallowUnwrapHandlers = {
get: (target, key, receiver) => {
return unref(Reflect.get(target, key, receiver))
},
set: (target, key, value, receiver) => {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
}
另外为了保证代码的健壮性,我们在 proxyRefs
入口处新增一个判断,若传入的对象属于响应式对象,则直接返回:
export function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs) // 新增判断
? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
现在我们可以使用 proxyRefs
接口来快捷操作含有 ref
实例属性的对象了:
<body>
<div></div>
</body>
<script src="https://unpkg.com/vue@next"></script>
<script type="module">
const { ref, proxyRefs, effect } = Vue;
const div = document.querySelector('div');
const info = ref('Hello');
const obj = proxyRefs({ info });
effect(() => {
div.innerText = obj.info
});
obj.info = 'Bye~';
</script>
💡 Vue 官方并不推荐同时使用
Proxy
和getter / setter
的能力,所以proxyRefs
接口未收录在对外的官方文档中。