Vue3源码之Refs
前言Vue3的响应性API除了reactive、readonly等基础的,还提供了相关Refs API其中包括ref、customRef等。在之前的[响应系统](https://blog.csdn.net/s1879046/article/details/118757873)的文章中,对reactive的逻辑有较为详细的分析,本文会对梳理相关API的逻辑,对比reactive API看看二者的不同
前言
Vue3的响应性API除了reactive、readonly等基础的,还提供了相关Refs API其中包括ref、customRef等。在之前的响应系统的文章中,对reactive的逻辑有较为详细的分析,本文会对梳理相关API的逻辑,对比reactive API看看二者的不同,加强对Refs API使用的理解(Vue 3.1.1版本)。
ref
官网对ref API的描述如下:
接受一个内部值并返回一个响应式且可变的 ref 对象,ref 对象具有指向内部值的单个属性value。
通过上面的描述总结下面几点关键信息:
- 内部值:可以是原始值、Object、Array、Map、Set等
- 响应式且可变:proxy对象
- ref对象存在value属性
结合源码来看看上面几点的处理逻辑:
function ref(value) {
return createRef(value);
}
function createRef(rawValue, shallow = false) {
// 判断是否已是ref对象
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
从上面可知,Ref API就是返回一个RefImpl实例对象,RefImpl类的具体逻辑如下:
class RefImpl {
constructor(_rawValue, _shallow) {
this._rawValue = _rawValue;
this._shallow = _shallow;
this.__v_isRef = true;
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);
}
}
}
使用class定义RefImpl类,其逻辑非常清晰。首先其构造函数中定义的属性:
- _rawValue:传入的原始数据
- _shallow:是否是浅层代理
- __v_isRef:标记ref对象
- _value:返回的ref对象
然后就是定义了value属性的getter和setter,这里解释了ref对象存在value属性的原因。实际上从上面RefImpl的整体处理逻辑,有3点核心:
- value属性的getter
- value属性的setter
- shallow与convert
value属性的getter
value属性的getter处理逻辑具体如下:
get value() {
track(toRaw(this), "get" /* GET */, 'value');
return this._value;
}
内部会调用track函数对value属性进行追踪,需要对target进行toRaw操作,即始终使用目标代理的原始对象。
value属性的setter
value属性的setter处理逻辑具体如下:
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);
}
}
内部会调用trigger函数来触发视图的更新,其次会替换内部属性_value的值为最新的值。ref API定义的代理对象是可变的,所谓的可变是指什么?实际上就是value属性值支持可变,即内部属性_value属性值的可变。
从_shallow可知,ref会存在浅层响应的处理,那convert函数的逻辑是什么呢?convert函数具体逻辑如下:
const convert = (val) => isObject(val) ? reactive(val) : val;
如果值是对象类型就会调用reactive实现完全代理,从这里就可以知道:
如果传入给ref的值是对象,实际上内部会调用reactive来实现完全代理,即value属性的值也会被代理
shallowRef
官方对其API的描述如下:
创建一个 ref,它跟踪自己的 .value 更改,但不会使其值成为响应式的
实际上shallowRef是浅层响应,该API的源码如下:
function shallowRef(value) {
return createRef(value, true);
}
而createRef就是new RefImpl,其中会存在_shallow实例属性,就是createRef函数的第2个参数值,即是否浅层,对于浅层的,实际上就是不会调用reactive来实现对value属性值的代理。这也是官网说的不会使其值成为响应式的本质。
toRef
官网文档中对toRef API的描述如下:
可以用来为源响应式对象上的 property 创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接
其调用语法是:toRef(Object, key),再结合官网文档对其的描述,实际上toRef只能用来对指定的对象属性创建一个ref对象。
toRef API相关源码具体如下:
class ObjectRefImpl {
constructor(_object, _key) {
this._object = _object;
this._key = _key;
this.__v_isRef = true;
}
get value() {
return this._object[this._key];
}
set value(newVal) {
this._object[this._key] = newVal;
}
}
function toRef(object, key) {
return isRef(object[key])
? object[key]
: new ObjectRefImpl(object, key);
}
toRef的逻辑非常简单,如果对象属性的值本身就是ref对象就不会再次处理,否则会调用ObjectRefImpl类创建对应实例。ObjectRefImpl中不会对属性做任何操作,仅仅是定义value属性,而value属性的getter、setter只是简单的获取和赋值操作:
toRef只是定义了value属性而已,之所以实现保持对 对象源属性的响应式连接,完全是属性所在对象自身必须是响应式
toRefs
function toRefs(object) {
// Object必须是代理对象
if (!isProxy(object)) {
console.warn(`toRefs() expects a reactive object but received a plain one.`);
}
const ret = isArray(object) ? new Array(object.length) : {};
for (const key in object) {
ret[key] = toRef(object, key);
}
return ret;
}
从上面的逻辑中可知如下的信息:
- toRefs实际上就是批量的执行toRef
- 支持数组类型
- 只是浅层处理,即只处理对象自身的属性,不会去处理所有嵌套属性
customRef
使用customRef创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。实际上
customRef背后实现的逻辑实现具体如下:
class CustomRefImpl {
constructor(factory) {
this.__v_isRef = true;
const { get, set } = factory(
() => track(this, "get" /* GET */, 'value'),
() => trigger(this, "set" /* SET */, 'value')
);
this._get = get;
this._set = set;
}
get value() {
return this._get();
}
set value(newVal) {
this._set(newVal);
}
}
function customRef(factory) {
return new CustomRefImpl(factory);
}
从上面可知customRef实际上是调用CustomRefImpl类创建一个实例,开发者需要传入一个函数类型的参数,这个函数类实例化过程中会被调用,而这个函数会被传入 track 和 trigger 函数作为参数,返回值需要是一个带有 get 和 set 的对象,返回值对应get和set会在value属性的getter和setter中执行。
通过源码逻辑的了解customRef的使用语法如下:
function createCustomRef(value) {
return customRef(function(track, trigger) {
return {
get() {
// 必须调用,否则无法实现属性的响应式
track();
return value;
},
set(newVal) {
value = newVal;
// 必须调用
trigger();
}
}
})
}
你可以在set中做一些异步操作,例如Ajax的请求等,具体使用完全取决于开发者了。
triggerRef
该API的源码逻辑非常简单:
function triggerRef(ref) {
trigger(toRaw(ref), "set" /* SET */, 'value', ref.value );
}
就是手动调用trigger触发,为什么会存在该API呢?实际上是搭配shallowRef来使用的。通过前面对ref、shallowRef的分析可以知道:
ref对象实际上就是存在value属性,Ref API不仅仅对value属性做代理,对value属性的值会调用reactive来处理;而shallowRef是不会对value属性的值做代理处理的
shallowRef处理的ref对象是浅层的,如果value属性的值是对象类型,对其做赋值是不会触发作用的,而通过triggerRef函数来手动调用trigger,就可以手动触发一次作用(视图更新或其他逻辑),即使value属性值的是多层对象结构也是有效的。
总结
通过对Refs相关API的源码阅读,实际上可以看出相关API背后所做的工作。相对于reactive,ref等API存在的意义是什么?个人觉得最核心的点是:
reactive实现对复杂类型(对象、数组等)的代理处理,ref等API支持对原始值的代理处理,而且在保持响应性的前提下通过value属性支持可变
至于ref处理复杂类型,本质上与reactive并没有什么区别。相对来说ref的使用更加轻量化,这是它的优点之一,ref有没有缺点呢?个人觉得还是存在的,对于不理解其背后逻辑前提下相关场景ref的使用相对来说会让人有些困惑。
这里概括下ref:
所谓的ref对象就是提供一个对value属性进行响应的普通对象(需要注意的是ref对象不是Proxy对象)。如果value属性的值是复杂类型,ref API会实现深度代理,shallowRef、customRef只是浅层代理,当然可以通过triggerRef手动触发响应作用来解决浅层代理的问题
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)