ref函数

Vue2 中的ref

首先我们回顾一下 Vue2 中的 ref。

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:

<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>

其实也就是给元素或者是子组件打上标记,然后通过在父组件中 通过 this.refs.xxx拿到这个 DOM元素或者是组件实例,进而操作 DOM 或者访问组件实例。

在官方文档上声明,ref是一个特殊的属性,$refs是一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。

Vue3 setup 中直接定义的数据为什么改变之后,视图不同步

 如果我按照上一节 setup 中的模式 ,直接创建变量且赋基础值,然后试图改变变量的值,我们看看会发生什么

<template>
  <p>{{ name }}</p>
  <button @click="changeName">改变名称</button>
</template>

<script>
export default {
  name: "App",
 
  setup() {
    let name = "al";

    function changeName() {
      name = '汤圆仔'
    }

    return {
      changeName,
      name,
    };
  },
};
</script>

但是,在我点击按钮之后,我发现页面上的名称并没有发生改变。造成的原因可能有两个,

第一个可能是,数据改变了,但是视图没更新。第二种则是,数据本身就没改变所以不更新视图。

验证:改变数据之后,直接打印修改后的变量

从这里可以看到,我们修改的数据其实时发生了改变的,但是页面上却没有更新,这在 Vue2 中是不会存在的,因为 Vue2 中定义在 data 中的数据时响应式的,所以我们可以得出下面这个结论:那就是 直接在 setup 中声明的变量,不是响应式的。

为了解决这一个问题,Vue3 推出了 ref 函数 用来将 setup 中定义的变量转化为响应式。

Vue3 中的 ref 函数

首先,Vue3 中的 ref 是一个函数,区别于Vue2 中的ref是一个特殊属性。作用是将数据转化为响应式。在使用时,需要引入,然后将要转化的数据传入到 ref() 函数中。

<template>
  <p>{{ name }}</p>
  <button @click="changeName">改变名称</button>
</template>

<script>
import { ref } from 'vue'
export default {
  name: "App",
 
  setup() {
    let name = ref("al");

    function changeName() {
      name = '汤圆仔'
      console.log(name,'name');
    }

    return {
      changeName,
      name,
    };
  },
};
</script>

到了这一步,我们再次点击按钮 ,发现还是同样的结果,数据改变了,但是视图没刷新。不是说好的 通过 ref() 函数就能将数据转化为响应式的么,我这都转化了,为啥还是这样?

ref() 函数处理基本数据类型

 对于上面的 基本数据类型 name,我们可以在改变值之前,先看看这个 name 变量,被 ref() 函数处理成了什么样子。

 可以看到 ref()函数将基本类型数据转化为了一个 ref引用对象(RefImpl对象),同时我们展开对象查看内部属性可以发现存在以下属性。

 dep对象暂时猜测和Vue2一致,用来收集分发依赖的。四个带有_前缀的属性一般也是用来给 Vue底层源码使用的,所以这个value一看就是给我们开发者的。

而且我们还可以发现,鼠标放到value属性值的省略号上之后,提示是通过gettrt 方法获得的值,这和Vue2获取响应式数据一致。

然后打开 Prototype 属性,可以看到,针对于这个基础类型的值的 getter 和 setter方法,以及真正的value值。将方法以及初始值放在原型上是利用原型的作用,避免外层数据繁杂。然后将真正的value暴露给外层是为了方便开发者使用。

到这里我们就可以大胆的推论得出:ref() 函数在处理基础类型的值时,通过将其转化为了 RefImpl 引用实例对象后,还是通过 getter 和 setter 来实现响应式的。

得到结论之后还不够,我们要知道怎么去修改或访问通过ref()函数转化的基础数据啊。

在上面通过  name = '汤圆仔' 直接修改属性值被证明是已经行不通了的。因为这样修改之后,相当于是把这个响应式属性直接变成了一个基础类型的值,从而失去了响应式功能。

而看着 RefImpl 引用实例对象中的属性,我们能理解并使用的也就只有 value 属性了。所以当我们修改数据的时候,通过 name.value = '汤圆仔',就能在保证响应式的前提下修改数据了。

function changeName() {
  name.value = '汤圆仔'
  console.log(name,'name');
}

 

 同理,在模板中使用数据的时候,我们好像也应该通过 插值语法的形式 {{ name.value }} 去使用,但是在你真这么做之后,你会发现,页面渲染其实错误了。

  <p>姓名:{{ name.value }}</p>

这其实是因为,Vue3底层设计考虑到了这一问题,在模板中使用变量,Vue3判断当前为插值语法,且使用的是通过 ref() 函数进行转化过后的响应式数据后,会自动解包,自动读取value值,而不需要开发者手动 xxx.value 去获取属性值。所以,我们还是像以前一样,通过插值语法直接使用该属性即可。

ref() 函数处理引用类型值

 上面说的是ref()函数对于基本数据类型的值的处理。但是如果我的数据比较多,那我分别调用ref比较麻烦,所以 ref() 函数也支持传入对象形式的数据

<template>
  <p>姓名:{{ userInfo.name }}</p>
  <p>姓名:{{ userInfo.age }}</p>
  <p>姓名:{{ userInfo.work }}</p>
  <button @click="changeName">改变名称</button>
</template>

<script>
import { ref } from 'vue'
export default {
  name: "App",
 
  setup() {
    let userInfo = ref({
      name: 'al',
      age: 29,
      work:'前端'
    });

    function changeName() {
      console.log(userInfo,'userInfo');
    }

    return {
      changeName,
      userInfo,
    };
  },
};
</script>

点击按钮,控制台打印出当前通过ref()函数转化后的 userInfo 属性,我们能发现返回的还是一个RefImpl引用实例对象,而且 value还是通过 getter转化为响应式的。此时我们不点开 value的值,按照基本类型的处理方式推测一下,此时的value应该是一个对象。

于此同时我们也应该想到一个问题,那就是在Vue2 中,实现了对象的深层响应,那么在Vue3中不可能丢掉这个功能,所以我们可以推断此时 ref()函数对于引用类型的值也做了深层响应式,也应该是针对于引用类型中每个属性都应该返回一个refImpl引用对象实例,以此来保障数据完全深层响应。

那么我们可以推断出,当我们在改变unseInfo内部 name 属性的时候,我们也应该通过  name.value  去修改,也就是说当我们需要修改对象内部属性时,我们需要这样做:先通过 userInfo.value 拿到转化为响应式的 userInfo 对象,然后修改name时,也需要拿到 name 的value去修改

function changeName() {
  console.log(userInfo);
  userInfo.value.name.value = '汤圆仔'
}

 但是这时候我们发现页面报错了,且提示信息为:不能在一个字符串 al 中读取 value 属性。也就是说 userInfo.value.name 之后是取不到 value属性的。这么搞就有点混乱了啊,那我到底是加还是不加呢?

为了解决这个问题,我们看看 userInfo.value 到底返回的是个啥玩意。点开 value属性之后我们发现 value 属性并不是一个 refImpl引用实例对象,而是一个 Proxy 代理对象。而且这个代理对象上的每个属性只有键值对对应,并没有所谓的 value 属性,所以这个时候我们就需要明白一个问题:Vue3 对于基础类型和引用类型转化为响应式,用的是不同的底层逻辑

针对于基本类型的数据,Vue3走的还是和Vue2一样的 defineProperty 的getter、setter的数据劫持的方式实现的响应式。

而针对于引用类型的数据,Vue3 走的则是通过Proxy代理的方式实现的响应式( 下一节仔细讲讲怎么通过Proxy实现引用类型的响应式转化 )

搞明白了上面这个value属性值的问题,当我们需要改变引用类型中的数据时,我们就可以这样做

function changeName() {
  userInfo.value.name = '汤圆仔'
}

ref() 函数响应式原理 

ref() 函数能接收基本数据类型,也能接收引用类型的数据,所以Vue3推荐使用 ref() 函数来实现响应式,但是使用上我们一般还是使用 reactive() 函数较多,后续会讲到。

而且我们需要注意的是,ref()函数与 reactive()函数对于响应式的转化其实时不同的

  1. 在调用 ref() 函数时,会先调用 createRef() 函数。
    export function ref(value) {
      return createRef(value, false);
    }
  2. 然后在 createRef() 函数中 判断当前接收的参数是否为 ref 对象,如果是,则直接返回该响应式数据,避免重复转化,如果不是,则调用 new RefImpl() 构造函数生成 RefImpl 引用对象
    // 接收两个参数,
    // 第一个参数 rawValue 是需要转化为 ref 的原始值,
    // 第二个参数 shallow 是一个布尔值,标识是否是浅层响应,如果为 true,则只处理表面层次的响应式,而不会递归处理嵌套对象
    
    function createRef(rawValue, shallow) {
      // 判断需要转化的数据是否已经是 ref 对象,如果是,则直接返回该数据,避免重复转化
      if (isRef(rawValue)) {
        return rawValue;
      }
    
      // 如果不是 ref 对象,则调用 RefImpl 构造函数生成新的 RefImpl 引用对象
      // 同时传递 rawValue 和 shallow 来初始化响应式数据以及确定相应深度
      return new RefImpl(rawValue, shallow);
    }
  3. RefImpl 类:是 ref 对象的实际实现。主要包括

    1. 存储原始值以及响应值:_rawValue 存储原始值,value存储响应值

    2. 响应式处理:通过 shallow 来决定是否进行深层响应式数据处理

    3. 依赖收集与分发:在 get 函数中通过 trackRefValue 函数来收集依赖。在set函数中通过 triggerRefValue 函数通知依赖更新。

    4. RefImpl 类解析

      class RefImpl {
        private _value: any;    // 用来存储响应值
        private _rawValue: any;    // 用来存储原始值
        public dep?: Dep = undefined;    // 用来收集分发依赖
        public readonly __v_isRef = true;    //是否只读,暂不考虑
      
        // 接收 new RefImpl() 传递过来的 rawValue 和 shallow  
        constructor(value, public readonly __v_isShallow: boolean) {
          // 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应
          this._rawValue = __v_isShallow ? value : toRaw(value);
      
          // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
          this._value = __v_isShallow ? value : reactive(value);
        }
      
        get value() {
          // 收集依赖
          trackRefValue(this);
      
          // 返回响应式数据
          return this._value;
        }
      
        set value(newVal) {
      
          // 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖
          if (hasChanged(toRaw(newVal), this._rawValue)) {
      
            //判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应
            this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal);
      
            // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
            this._value = this.__v_isShallow ? newVal : reactive(newVal);
      
            // 分发依赖,通知更新
            triggerRefValue(this);
          }
        }
      }
    5. trackRefValue() 函数:用来收集依赖

      // 接收参数 ref ,也就是当前 refImpl 引用实例对象
      function trackRefValue(ref) {
      
        // 判断当前是否处于依赖收集状态,在 Vue2.x 中,相当于 window.target 。一般用来判断当前是否有活跃的响应式副作用正在运行
        if (isTracking()) {
      
          // ref.dep 是 RefImpl 实例对象上的一个属性,相当于 Vue2.x中的 Dep 类,用来收集或分发依赖
      
          // 判断 ref.dep 是否存在。若存在则直接使用,若不存在,则通过 createDep 函数创建一个新的依赖集合并赋值给 ref.dep ,然后使用
      
          // 将当前活跃的副作用(effect)添加到 ref.dep 中,以便在将来 ref 值变化时能够触发这些副作用。
          trackEffects(ref.dep || (ref.dep = createDep()));
        }
      }
    6. triggerRefValue()函数:用来分发依赖

      function triggerRefValue(ref) {
        // ref.dep:依赖集合。如果存在依赖集合,则继续进行触发操作。
        if (ref.dep) {
          
          // 遍历并执行 ref.dep 中的所有副作用(effect),以响应 ref 值的变化。这个函数会通知所有依赖于 ref 值的副作用重新运行。类似于 Vue2.x中的 nofiny() 
          triggerEffects(ref.dep);
        }
      }
      
    7. 如果需要深层响应( 也就是说,ref()函数接收的参数是一个引用对象),那就需要依赖 reactive() 函数来实现,reactive() 函数的具体原理我们在下一节会详细讲到。

总结

ref的作用:定义一个响应式的数据

ref的语法:let xxx = ref('initvalue')

  1. 创建一个 包含响应式数据的引用对象
  2. 在js中操作数据时,需要使用 xxx.value 来修改
  3. 在模板中使用数据时,不需要通过 .value来读取,因为 Vue底层会自动解包

ref的参数:可以是基本类型,也可以是引用类型,但是对于这两种数据,响应式处理是完全不同的两套逻辑

  1. 基本类型数据:依然是通过 Object.defineProperty() 中的 get 与 set 进行数据劫持完成响应式
  2. 引用类型数据:如果需要深层响应,Vue3 内部求助了一个新函数 -- reactive 函数,通过ES6自带的 Proxy 方法实现了响应式,并通过 Reflect 操作源对象内部的数据( 参考下一节 -- reactive 函数实现引用类型响应式 )
  • 25
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值