Vue3 源码系列文章会持续更新,全部文章请查阅我的掘金专栏。
本系列的文章 demo 存放于我的 Github 仓库,推荐大家下载和调试,进而加深理解。
一. ref 的基础实现
在前三章我们介绍了引用类型的响应式实现,其底层采用了 Proxy
去拦截用户对各属性的操作。
然而 Proxy
接口只能代理引用类型,如果希望对一个原始类型实现响应式操作,只能另辟蹊径。其中一个取巧的办法,是将原始类型包裹为一个对象,再通过 getter
和 setter
的方法对其操作进行拦截:
const ref = (rawValue) => {
return {
get value() { // 拦截访问操作
// TODO: track
console.log('这里需要追踪依赖...');
return rawValue;
},
set value(newVal) { // 拦截设置操作
// TODO: trigger
console.log('这里需要触发副作用函数...');
rawValue = newVal;
}
}
}
const msg = ref('Hello!');
console.log(msg.value);
msg.value = 'Bye~!';
console.log(msg.value);
这里没有使用 Proxy
的原因也很简单 —— 原始类型不像引用类型那样需要操作各种属性,常规只会访问/设置其本身。
我们还需要在 getter
访问器中对原始类型进行依赖收集,并在 setter
设置其中触发收集到的副作用函数。不过这块的处理,只需要复用前几章已经封装好的 trackEffects
和 triggerEffects
方法即可。
我们进一步封装 ref
接口,让其返回一个类的示例,方便定义一些内部属性。
然后引入 trackEffects
和 triggerEffects
方法来追踪和触发依赖:
/** ref.js **/
import {
trackEffects,
triggerEffects
} from './effect.js'
import { hasChanged } from './shared.js'
export function ref(value) {
return new RefImpl(value)
}
class RefImpl {
constructor(value) {
this._value = value;
}
get value() {
trackRefValue(this); // 依赖收集
return this._value;
}
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal;
triggerRefValue(this); // 触发收集到的副作用函数
}
}
}
export function trackRefValue(ref) {
trackEffects(ref.dep || (ref.dep = new Set()))
}
export function triggerRefValue(ref) {
triggerEffects(ref.dep)
}
为了避免重复处理传入的原始类型,可以在 ref
入口处新增判断:
/** ref.js **/
export function isRef(r) { // 新增方法
return !!(r && r.__v_isRef === true)
}
export function ref(value) {
if (isRef(value)) { // 新增判断
return value
}
return new RefImpl(value)
}
class RefImpl {
constructor(value) {
this.__v_isRef = true; // 新增标记
this._value = value;
}
// 略...
}
如上,我们为 ref
实例(准确的说,是 RefImpl
的实例)新增了一个 __v_isRef
标记,在 ref
初始化时先判断传入值是否含有该标记,若有则表示传入值已是 ref
实例,直接返回即可。
至此我们便实现了一个最基础的原始类型响应式处理接口,读者可以点击这里获取示例代码。
二. 兼容非原始类型
2.1 RefImpl 中的兼容处理
在前面我们只考虑了对原始类型的处理,如果用户传入了一个引用类型,还需要在 RefImpl
中对其进行兼容处理,将实例属性 value
指向经由 reactive
接口处理过的代理对象。
改动如下:
/** ref.js **/
import { toRaw, toReactive } from './reactive.js' // 新增
class RefImpl {
constructor(value) {
this.__v_isRef = true;
this._rawValue = toRaw(value); // 新增 _rawValue 属性用于保管原始值
this._value = toReactive(value); // 调用 toReactive
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = toRaw(newVal); // 新增
if (hasChanged(newVal, this._rawValue)) { // 修改 this._value 为 this._rawValue
this._rawValue = newVal; // 修改 this._value 为 this._rawValue
this._value = toReactive(newVal); // 新增
triggerRefValue(this);
}
}
}
可以看到,在构造函数中,我们使用了 toRaw
接口来获取传入参数的原始值,并存入 _rawValue
属性中,方便在 setter
中对比新旧的值是否一致。
这样即使用户传入 ref
的是一个被 reactive
代理过的对象,也不会有问题。
另外 this._value
都指向了由 toReactive
处理过的值,此举也是为了兼容用户传入对象,甚至是被 reactive
接口代理过的对象的场景。
现在我们试试往 ref
里传入一个对象/响应式对象,会看到它们都能按预期正常执行:
<body>
<div></div>
<div></div>
</body>
<script type="module">
import { ref } from 'https://codepen.io/vajoy/pen/mdxqzzP.js';
import { effect } from 'https://codepen.io/vajoy/pen/ExEoxPB.js';
import { reactive } from 'https://codepen.io/vajoy/pen/VwXywKa.js';
const divs = document.querySelectorAll('div');
const msg1 = ref({info: 'Hello!'});
const msg2 = ref(reactive({info: 'Hello!'}));
effect(() => {
divs[0].innerText = msg1.value.info;
divs[1].innerText = msg2.value.info;
});
setTimeout(() => {
msg1.value = {info: 'Bye!'};
msg2.value = {info: 'Bye!'};
}, 1000)
</script>
2.2 trackRefValue 中的兼容处理
从上面的示例可以知道,如果直接重置 ref
实例的 value
方法,是可以正常执行的。
但如果用户传入了一个对象,且要修改 value
指向的响应式对象属性,会出现报错:
<body>
<div></div>
</body>
<script type="module">
import { ref } from './ref.js';
import { effect } from './effect.js';
const div = document.querySelector('div');
const msg = ref({info: 'Hello!'});
effect(() => {
div.innerText = msg.value.info;
});
setTimeout(() => {
// 直接修改响应式对象的 info 属性,
// 会报错 Cannot read properties of undefined (reading 'deps')
msg.value.info = 'Bye~';
}, 1000)
</script>
这是因为我们已经把传入对象交给 toReactive
接口处理,它会经由 reactive
接口去收集依赖和触发副作用函数,当执行 msg.value.info
时,会先触发 info
属性收集到的副作用函数,并重置 activeEffect
:
/** effect.js **/
export let activeEffect;
class ReactiveEffect {
// 略...
run() {
try {
// 略...
} finally {
activeEffect = this.parent; // 重置
}
}
}
紧接着 msg.value
被访问时会触发 trackEffects
方法,该方法内找不到 activeEffect.deps
进而报错。
因此我们需要在执行 trackEffects
方法前,先判断 reactive
是否已经先对传入对象做了响应处理,如果是,则不再多此一举。
改动如下:
/** ref.js **/
import {
shouldTrack, // 新增
activeEffect, // 新增
trackEffects,
triggerEffects
} from './effect.js'
export function trackRefValue(ref) {
if (shouldTrack && activeEffect) { // 新增
trackEffects(ref.dep || (ref.dep = new Set()))
}
}
其中判断 shouldTrack
是为了避免数组栈方法循环递归的问题,具体可以回顾《reactive 的实现(上)》3.3.2 小节的内容。
三. shallowRef 的实现
往 ref
里传入一个对象并非 Vue 所提倡的行为,因为那样一方面使用了 getter
和 setter
,一方面又会调用 reative
接口使用 Proxy
去深层代理该对象。
另外,ref
代理后的实例,本质应该是结构非常简单的数据,只关注其 value
属性的获取和修改即可。
下面的写法有悖 ref
的理念:
refInstance.value.certainProp = 'xxx';
对此 Vue 提供了一个 shallowRef
的接口,当传入对象时,不再会被 Proxy
代理,也只允许用户修改 value
属性。
官方 demo 如下:
const state = shallowRef({ count: 1 })
state.value.count = 2 // 无法触发 trigger
state.value = { count: 2 } // 可以触发 trigger
shallowRef
的实现其实很简单,只是在 RefImpl
中新增了 isShallow
参数并做判断,如果是 shallow 模式则绕过 toReactive
接口:
/** ref.js **/
class RefImpl {
constructor(value, isShallow) { // 新增 isShallow 参数
this.__v_isRef = true;
// this._rawValue = newVal;
// this._value = toReactive(newVal);
this._rawValue = isShallow ? value : toRaw(value); // 新增
this._value = isShallow ? value : toReactive(value) // 新增
this.__v_isShallow = isShallow; // 新增
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
// newVal = toRaw(newVal);
newVal = this.__v_isShallow ? newVal : toRaw(newVal); // 新增
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
// this._value = toReactive(newVal);
this._value = this.__v_isShallow ? newVal : toReactive(newVal); // 新增
triggerRefValue(this);
}
}
}
调用 RefImpl
的两个接口 ref
和 shallowRef
传入对应的 isShallow
参数即可:
/** ref.js **/
export function ref(value) {
// if (isRef(value)) {
// return value
// }
return createRef(value, false) // 新增 false 参数
}
// 新增
export function shallowRef(value) {
return createRef(value, true)
}
// 新增
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}