解析Vue3源码(二)——ref
ref源码解析
前言
官方对ref的说明是
vue2的ref是用来操作某个元素节点的,可以说是document.getElementById的语法糖。vue3的ref延续了vue2的用法,还增加了一个作用就是创建响应式数据。相比reactive,ref可以监听基本类型数据。尤大大也是建议创建响应式数据更多使用ref来代替reactive。
ref使用
vue2
- 在组件上声明
<el-form ref="form"></el-form>
- 然后通过
this.$ref
使用
this.$refs.form.validate()
this.$refs.form获取到对应element实例,就可以使用实例上的属性
vue3
第一种使用方式:操作元素节点
- 在组件上声明
<el-form ref="form"></el-form>
- 需要手动声明再使用,Vue3基本取消this的使用,也就是更多的内容为声明式使用
let form = ref();
- 然后通过form.value操作其内容
form.value.validate()
需要注意的是在vue2中,通过ref是可以获取子组件的所有内容的,而vue3中如果是在组件上声明ref,是无法用ref获取子组件内部的元素属性的,子组件需要通过
defineExpose
手动暴露给外部组件
第二种使用方式:声明响应式数据
如下例子:
- 数字自增自减
<div>{{ count }}</div>
<button @click="add"></button>
<button @click="subtract">-</button>
let count = ref(0);
const add =()=>{count.value++;}
const subtract = ()=>{count.value--;}
- 给对象添加自定义属性
<div>{{obj}}</div>
<input v-model="key">
<button @click="add">+</button>
let obj = ref({});
let key = ref('');
const add =()=>{
obj.value[key.value] = key.value;
}
结果如下
当然这里的obj
也可以用reactive
响应式,但是key
就不能使用了,原因参考之前讲的reactive
原理。
下面看看ref的实现原理
ref源码
function ref(value) {
return createRef(value, false);
}
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
//判断当前数据是否已经是ref数据,是则直接返回。否则返回一个新new的RefImpl实例
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;//标志位来表明是否是浅响应数据(shallowRef时该值为true)
this.dep = undefined;
this.__v_isRef = true;//标志位来表明是ref类型数据。
this._rawValue = __v_isShallow ? value : toRaw(value);//toRaw返回变量的__v_raw属性或者变量本身
this._value = __v_isShallow ? value : toReactive(value);//如果是浅响应对象(shallowRef)则返回对象本身,否则进行监听,返回代理对象。
}
get value() {
trackRefValue(this);//依赖收集
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) { //判断数据是否发生变化
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal); //通知各依赖更新数据
}
}
}
//可以看到 返回的跟reactive的数据是一样的。
const toReactive = (value) => isObject(value) ? reactive(value) : value;
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if ((process.env.NODE_ENV !== 'production')) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get" /* GET */,
key: 'value'
});
}
else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
if ((process.env.NODE_ENV !== 'production')) {
triggerEffects(ref.dep, {
target: ref,
type: "set" /* SET */,
key: 'value',
newValue: newVal
});
}
else {
triggerEffects(ref.dep);
}
}
}
可以看到实例内部的value
属性的get,set
方法,这也就是为什么使用ref
数据时要用.value
来获取其数据。
在返回的实例的构造函数内对当前数据进行了reactive返回了代理对象给_value
,在get value
时直接返回_value
。
再看他的get
方法,是先进行了依赖收集,再返回实例的_value
属性
set
方法中判断数据是否改变,如果改变对数据再次reactive
响应,并调用triggerEffects
,更新相关依赖。
那么是如何区分ref创建的是元素节点还是纯响应式数据呢。
在patch时,会从标签的props中拿到ref属性。然后把当前节点挂载到ref的value属性
const patch = (n1, n2, container, ...){
//...
const { type, ref, shapeFlag } = n2;
//...
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
}
function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) {
//...
const refValue = vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */
? getExposeProxy(vnode.component) || vnode.component.proxy
: vnode.el;
const value = isUnmount ? null : refValue;
// ...
ref.value = value;
}
总结
在声明基础类型的响应式数据或者操作元素节点时ref都是一个很好的使用方式,需要注意的是在对子组件进行操作时,如果获取不到子组件的某些属性,就要查看子组件内部是否有将该属性通过defineExpose
暴露出去。
更多推荐
所有评论(0)