解析Vue3源码(二)——ref

前言

官方对ref的说明是
在这里插入图片描述

vue2的ref是用来操作某个元素节点的,可以说是document.getElementById的语法糖。vue3的ref延续了vue2的用法,还增加了一个作用就是创建响应式数据。相比reactive,ref可以监听基本类型数据。尤大大也是建议创建响应式数据更多使用ref来代替reactive。

ref使用

vue2

  1. 在组件上声明
    <el-form ref="form"></el-form>
  2. 然后通过this.$ref使用
    this.$refs.form.validate()
    this.$refs.form获取到对应element实例,就可以使用实例上的属性

vue3

第一种使用方式:操作元素节点

  1. 在组件上声明
    <el-form ref="form"></el-form>
  2. 需要手动声明再使用,Vue3基本取消this的使用,也就是更多的内容为声明式使用
    let form = ref();
  3. 然后通过form.value操作其内容
    form.value.validate()

需要注意的是在vue2中,通过ref是可以获取子组件的所有内容的,而vue3中如果是在组件上声明ref,是无法用ref获取子组件内部的元素属性的,子组件需要通过defineExpose手动暴露给外部组件

第二种使用方式:声明响应式数据

如下例子:

  1. 数字自增自减
<div>{
    
    {
    
     count }}</div>
<button @click="add"></button>
<button @click="subtract">-</button>

let count = ref(0);
const add =()=>{
    
    count.value++;}
const subtract = ()=>{
    
    count.value--;}
  1. 给对象添加自定义属性
<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暴露出去。

猜你喜欢

转载自blog.csdn.net/qq_41028148/article/details/127651661