前言
版本:3.0.2
说明:通过源码对Vue3的响应式API-ref进行阐述
一、响应式API
ref
作用:
- 实现值类型的数据响应式
- 绑定模板的
DOM
元素
使用方式:
html
<div id="app">
<div>{{ count }}</div>
<div ref="eleRef">ref</div>
<button @click="add">添加</button>
</div>
复制代码
JavaScript
const { ref, createApp, onMounted } = Vue;
const app = createApp({
setup() {
const count = ref(1);
// 1、在这里修改count的值,必须通过.value设置
count.value++;
const eleRef = ref(null);
// 页面挂载完毕后执行
onMounted(() => {
console.log(eleRef.value); // => <div>ref</div>
});
// 2、将页面中添加了ref属性的元素与setup返回的ref对象绑定
return {
count,
eleRef
}
},
methods: {
add() {
// 3、这里不能通过.value进行赋值
this.count += 1;
}
}
}).mount("#app");
复制代码
二、源码解析
通过对源码的分析,剖析上面JS代码中的三点重要注释。
1、在这里修改count的值,必须通过.value设置
创建ref对象。
function ref(value) {
return createRef(value);
}
function createRef(rawValue, shallow = false) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
复制代码
其中isRef
为检查值是否为一个 ref 对象。
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
复制代码
RefImpl
为ref对象的实现类。通过源码我们可以得知,在该类中定义了get value()
和set value(newVal)
分别用来取值和赋值,这就解释了第一点注释。
class RefImpl {
constructor(_rawValue, _shallow = false) {
this._rawValue = _rawValue;
this._shallow = _shallow;
this.__v_isRef = true;
// 如果为浅层的,返回原始数据,否则将_value转成响应式对象
this._value = _shallow ? _rawValue : convert(_rawValue);
}
get value() {
track(toRaw(this), "get" /* GET */, 'value');
return this._value;
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal;
this._value = this._shallow ? newVal : convert(newVal);
trigger(toRaw(this), "set" /* SET */, 'value', newVal);
}
}
}
复制代码
convert
方法将对象类型的参数转成响应式对象。
关于track
和trigger
方法,涉及到响应式原理部分,推荐阅读我的前一篇文章。
传送门:Vue3响应式原理详解
2、将页面中添加了ref属性的元素与setup返回的ref对象绑定
处理setup
函数的返回结果。
function handleSetupResult(instance, setupResult, isSSR) {
// 省略部分源码...
instance.setupState = proxyRefs(setupResult);
}
复制代码
为ref数据添加拦截。
function unref(ref) {
// 如果为ref类型,则返回ref的value
return isRef(ref) ? ref.value : ref;
}
const shallowUnwrapHandlers = {
// 如果获取的属性为ref类型,则触发响应,更新界面
get: (target, key, receiver) => 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);
}
}
};
function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
复制代码
为setupState
添加ref对象。下方代码中的setupState[ref] = value;
部分,会将eleRef的value设置为页面中添加了ref="eleRef"
的DOM
。
const setRef = (rawRef, oldRawRef, parentComponent, parentSuspense, vnode) => {
// 省略部分源码...
let value;
// ...
// value为ref绑定的DOM元素
value = vnode.el;
// 为setupState添加ref对象。
const doSet = () => {
refs[ref] = value;
if (hasOwn(setupState, ref)) {
// 将DOM元素和ref绑定
// 设置值时会触发shallowUnwrapHandlers的set拦截,即:将ref对象的value属性设置为新值
setupState[ref] = value;
}
};
}
复制代码
3、这里不能通过.value进行赋值
暴露setupState属性到Vue实例中。
所以在第三点中,直接用this.count += 1;
,等同于this._.setupState.count
,取值操作会触发shallowUnwrapHandlers
方法的get方法,返回ref的value。
// 通过vm.[setupState]取值时,取的是instance.setupState[key]
function exposeSetupStateOnRenderContext(instance) {
const { ctx, setupState } = instance;
Object.keys(toRaw(setupState)).forEach(key => {
if (key[0] === '$' || key[0] === '_') {
// setup返回的对象属性中不能以`$`或`_`开头
warn(`setup() return property ${JSON.stringify(key)} should not start with "$" or "_" ` +
`which are reserved prefixes for Vue internals.`);
return;
}
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP
});
});
}
复制代码
看完3件事
1、如果文章对你有帮助,可以给博主点个赞。
2、如果你觉得文章还不错,可以动动你的小手,收藏一下。
3、如果想看更多的源码详解,可以添加关注博主。
附录:
1、Vue3.x完整版源码解析:github.com/fanqiewa/vu…
2、其它源码解析:www.fanqiewa.xyz/
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END