【Vue3源码】第五章 ref的原理 实现ref

news2024/9/27 7:19:02

【Vue3源码】第五章 ref的原理 实现ref

上一章节我们实现了reactive 和 readonly 嵌套对象转换功能,以及shallowReadonly 和isProxy几个简单的API。

这一章我们开始实现 ref 及其它配套的isRef、unRef 和 proxyRefs

1、实现ref

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

ref 对象是可更改的,也就是说你可以为 .value 赋予新的值。它也是响应式的,即所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

单元测试代码

首先还是先看单元测试,我们测试代码去测试ref的功能是否齐全

在tests文件夹下新建一个ref.spec.ts文件

import { effect } from "../effect";
import { ref } from "../ref";
describe("ref", () => {
  it("should be reactive", () => {
    const a = ref(1);
    let dummy;
    let calls = 0;
    effect(() => {
      calls++;
      dummy = a.value;
    });
    expect(calls).toBe(1);
    expect(dummy).toBe(1);
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
    // same value should not trigger
    a.value = 2;
    expect(calls).toBe(2);
    expect(dummy).toBe(2);
  });

  it("should make nested properties reactive", () => {
    const a = ref({
      count: 1,
    });
    let dummy;
    effect(() => {
      dummy = a.value.count;
    });
    expect(dummy).toBe(1);
    a.value.count = 2;
    expect(dummy).toBe(2);
  });
});

实现代码

ref其实就是为了给基础类型绑定响应式而创建的(如果是引用类型它会被转为reactive),它的原理和vue2的Object.defineProperty()类似。

同理的ref内也有和Proxy类似的get和set捕获器,不过ref主要是通过类实现的,所以ref的get和set叫类修饰符更贴切一点。他们运行的逻辑也很相似,都是在get时去收集依赖,set时触发依赖。

那么ref和reative原理最大区别是什么呢?

我们先看effect中的track依赖收集函数和trigger依赖触发函数:

在这里插入图片描述

是的不管track还是trigger两个函数他们都接收target和key两个参数,而ref我在上文说了它为了给基础类型绑定响应式而创建的。所以操作target和key的操作我们就可以省略了。

优化:

我们的ref在get时直接添加dep即可

//依赖收集
export function track(target, key) {
  if (activeEffect && shouldTrack) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }

    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Set();
      depsMap.set(key, dep);
    }
    trackEffects(dep)
  }
}

export function trackEffects(dep) {
  if(dep.has(activeEffect)) return
  dep.add(activeEffect);
  //浅拷贝反向收集到dep
  activeEffect.deps.push(dep);
}

我们的trigger,也直接操作dep即可

//依赖触发
export function trigger(target, key) {
  let depsMap = targetMap.get(target);
  //用stop时所有的dep都被删了
  let dep = depsMap.get(key);
  triggerEffects(dep)
}
export function triggerEffects(dep) {
  for (let effect of dep) {
    // 当触发set时,如果有scheduler就执行scheduler
    if (effect.scheduler) {
      effect.scheduler();
      // 没有就触发ReactiveEffect实例的run方法
    } else {
      effect.run();
    }
  }
}

做完上面的操作我们已经抽离出了

  • 一个直接收集dep的trackEffects函数
  • 一个直接触发dep中依赖的triggerEffects函数

现在我们就可以封装ref函数了~

在reactivity文件夹下

新建ref.ts文件

import { hasChanged, isObject } from "../shared"
import { trackEffects, triggerEffects, activeEffect, shouldTrack } from "./effect"
import { reactive } from "./reactive"


class RefImpl {
  private _value: any //劫持的数据
  public dep //收集dep
  private _rawValue:any //劫持的数据备份
  constructor(value) {
    // 初始化 value
    this._rawValue = value
    // 如果是个对象就用reative包裹
    this._value = convert(value) 
    // 初始化空的dep
    this.dep = new Set()
  }
  get value() {
    // 收集activeEffect实例
    trackRefValue(this)
    // 返回当前劫持的数据
    return this._value
  }
  set value(newValue) {
    // 要先修改value值再触发依赖
    // 如果修改过的值和初始化的值不相同
    if (hasChanged(newValue,this._rawValue)) {
      // 给初始化的值赋给成新的值
      this._rawValue = newValue
      // 如果是reactive对象也替换新值
      this._value = convert(newValue) 
      // 去触发依赖
      triggerEffects(this.dep)
    }
  }
}

function convert(value) {
  return isObject(value) ? reactive(value) : value
}

function trackRefValue(ref) {
  if (activeEffect && shouldTrack) {
    trackEffects(ref.dep)
  }
}


export function ref(value) {
  return new RefImpl(value)
}

shared文件夹下

index.ts文件

export const hasChanged = (val,newVal) => {
  return !Object.is(val,newVal)
}

2、实现isRef和unRef

isRef()

检查某个值是否为 ref。

unref()

如果参数是 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

先看单测

it("isRef", () => {
    const a = ref(1);
    const user = reactive({
      age: 1,
    });
    expect(isRef(a)).toBe(true);
    expect(isRef(1)).toBe(false);
    expect(isRef(user)).toBe(false);
  });

  it.skip("unRef", () => {
    const a = ref(1);
    expect(unRef(a)).toBe(1);
    expect(unRef(1)).toBe(1);
  });

代码实现

实现起来非常的简单,我们怎么检查某个值是否为 ref呢?只要在RefImpl添加一个公共属性__v_isRef = true

这样我们就可以通过访问实例中ref.__v_isRef是否存在来鉴定某个值是否为 ref~

有了isRef,我们就可以实现unRef了,这是 val = isRef(val) ? val.value : val 计算的一个语法糖。

import { hasChanged, isObject } from "../shared"
import { trackEffects, triggerEffects, activeEffect, shouldTrack } from "./effect"
import { reactive } from "./reactive"


class RefImpl {
  private _value: any
  public dep
  private _rawValue:any
  //新增
  public __v_isRef =true
  constructor(value) {
    this._rawValue = value
    this._value = convert(value) 
    this.dep = new Set()
  }
  get value() {
    trackRefValue(this)
    return this._value
  }
  set value(newValue) {
    // 要先修改value值再触发依赖
    if (hasChanged(newValue,this._rawValue)) {
      this._rawValue = newValue
      this._value = convert(newValue) 
      triggerEffects(this.dep)
    }
  }
}




export function ref(value) {
  return new RefImpl(value)
}

export function isRef(ref) {
  return !! ref.__v_isRef
}

export function unRef(ref) {
  return isRef(ref) ? ref.value : ref
}

3、实现proxyRefs

看见proxyRefs你可能会觉得奇怪Vue3文档中并没有这个API,proxyRefs的功能主要是实现了不需要.value去访问ref对象。

先看单测

it("proxyRefs", () => {
    const user = {
      age: ref(10),
      name: "xiaohong",
    };
    const proxyUser = proxyRefs(user);
    expect(user.age.value).toBe(10);
    expect(proxyUser.age).toBe(10);
    expect(proxyUser.name).toBe("xiaohong");

    (proxyUser as any).age = 20;
    expect(proxyUser.age).toBe(20);
    expect(user.age.value).toBe(20);

    proxyUser.age = ref(20);
    expect(proxyUser.age).toBe(20);
    expect(user.age.value).toBe(20);
  });

实现代码

proxyRefs有什么用呢?

例如我们在template中访问ref对象时其实是不用.value去访问的。因为proxyRefs帮我们在get时做了处理让他返回成普通值,set时也做了处理让它返回普通值即可。

export function proxyRefs(objectWithRefs) {
  return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs,{
    get(target,key) {
      // get 如果是ref类型那么就返回.value的值
      // 如果是普通的值直接返回
      return unRef(Reflect.get(target,key))
    },
    set(target,key,value) {
      // 判断旧值是不是ref,新值是ref还是普通类型
      if(isRef(target[key]) && !isRef(value)) {
        // 普通类型就替换成普通类型
        return target[key].value = value
      }else {
        // 是ref就返回.value的值
        return Reflect.set(target,key,value)
      }
    }
  })
}

下期分享:实现computed

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/367040.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3款实用又强的软件,值得收藏,不妨试试

1、白描 白描是一款高效准确的OCR文字识别、翻译与文件扫描软件,文字识别、表格识别转Excel、识别后翻译、文件扫描等功能,都非常方便,免费使用无任何广告。白描可以自动识别文档边界,生成清晰的扫描件,高效批量处理文…

Java8 Stream流Collectors.toMap当key重复时报异常(IllegalStateException)

一、问题 在使用Collectors.toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper)&#xff08;两个参数的&#xff09;时&#xff0c;如果 key 有重复&#xff0c;则会报异常&#xff08;IllegalStateException…

工赋开发者社区 | (案例)中译语通:差别化纺纱柔性智慧工厂

中译语通&#xff1a;差别化纺纱柔性智慧工厂01应用成效中译语通科技股份有限公司是一家大数据和人工智能高科技公司。在机器翻译、跨语言大数据、产业链科技、科研数据分析、数字城市和工业互联网等领域拥有自主研发的先进系统平台&#xff0c;能够为全球企业级用户提供全方位…

[oeasy]python0091_仙童公司_八叛逆_intel_8080_altair8800_牛郎星

编码进化 个人电脑 计算机 通过电话网络 进行连接 极客 利用技术 做一些有趣的尝试 极客文化 是 认真研究技术的 文化 计算机 不再是 高校和研究机构高墙里面的 神秘事物而是 生活中常见的 家用电器 ibm 蓝色巨人脚步沉重 dec 小型机不断蚕食低端市场甚至组成网络干掉大型机…

【仔细理解】计算机视觉基础1——特征提取之Harris角点

Harris角点是图像特征提取中最基本的方法&#xff0c;本篇内容将详细分析Harris角点的定义、计算方法、特点。 一、Harris角点定义 在图像中&#xff0c;若以正方形的小像素窗口为基本单位&#xff0c;按照上图可以将它们划分三种类型如下&#xff1a; 平坦区域&#xff1a;在任…

【C++】Visual Studio C++使用配置Json库文件(老爷式教学)

在visual studio中使用C调用Json的三方库有很多种办法&#xff0c;这里简述一种比较方便的方法。绝对好用&#xff0c;不好用你砍我。 文章目录在visual studio中使用C调用Json的三方库有很多种办法&#xff0c;这里简述一种比较方便的方法。绝对好用&#xff0c;不好用你砍我。…

百度前端一面高频react面试题指南

React 高阶组件、Render props、hooks 有什么区别&#xff0c;为什么要不断迭代 这三者是目前react解决代码复用的主要方式&#xff1a; 高阶组件&#xff08;HOC&#xff09;是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分&#xff0c;它是一…

Python 零基础入门必看,这些知识点你都掌握了吗?

导读 Python 作为当今最受欢迎的编程语言之一&#xff0c;几乎各个领域都会涉及到&#xff0c;所以学习 Python 自然刻不容缓&#xff01;作为一个没有接触过 Python 的小白&#xff0c;一开始要想的不是如何使用以及各种高深莫测的玩法&#xff0c;从最基础的了解以及构建环境…

node笔记

一、FS模块 fs.readFile查询文件 fs.writeFile修改文件 __dirname表示当前文件所处的目录&#xff08;双下划线&#xff09; 二、path模块 什么是path 路径模块 path模块是Node.,js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性&#xff0c;用来满足用户…

Spring 之bean的生命周期

文章目录IOCBean的生命周期运行结果实例演示实体类实例化前后置代码初始化的前后置代码application.xml总结今天我们来聊一下Spring Bean的生命周期&#xff0c;这是一个非常重要的问题&#xff0c;Spring Bean的生命周期也是比较复杂的。IOC IOC&#xff0c;控制反转概念需要…

华为OD机试题,用 Java 解【合规数组】问题

最近更新的博客 华为OD机试 - 猴子爬山 | 机试题算法思路 【2023】华为OD机试 - 分糖果(Java) | 机试题算法思路 【2023】华为OD机试 - 非严格递增连续数字序列 | 机试题算法思路 【2023】华为OD机试 - 消消乐游戏(Java) | 机试题算法思路 【2023】华为OD机试 - 组成最大数…

JAVA基础常见面试题

1.Java接口和抽象类的区别&#xff1f; 接口 接口中不能定义构造器 方法全部都是抽象方法&#xff0c;JDK8提供方法默认实现 接口中的成员都是public的 接口中定义的成员变量实际上都是常量 一个类可以实现多个接口 抽象类 抽象类中可以定义构造器 可以有抽象方法和具体…

字节序

字节序 字节序&#xff1a;字节在内存中存储的顺序。 小端字节序&#xff1a;数据的高位字节存储在内存的高位地址&#xff0c;低位字节存储在内存的低位地址 大端字节序&#xff1a;数据的低位字节存储在内存的高位地址&#xff0c;高位字节存储在内存的低位地址 bit ( 比特…

设计模式第八讲:观察者模式和中介者模式详解

一. 观察者模式1. 背景在现实世界中&#xff0c;许多对象并不是独立存在的&#xff0c;其中一个对象的行为发生改变可能会导致一个或者多个其他对象的行为也发生改变。例如&#xff0c;某种商品的物价上涨时会导致部分商家高兴&#xff0c;而消费者伤心&#xff1b;还有&#x…

内网部署api接口文档服务器端口如何让外网访问?

计算机业内人士对于swagger并不陌生&#xff0c; 不少人选择用swagger做为API接口文档管理。Swagger 是一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新文件的方法&#x…

Apache Hadoop生态部署-3台设置的免密登录,xsync分发脚本,jpsall脚本

目录 查看服务架构图-服务分布、版本信息 集群服务器间的免密登录 jpsall集群jps查看脚本 xsync集群分发脚本 查看服务架构图-服务分布、版本信息 系统环境&#xff1a;centos7 Java环境&#xff1a;Java8 集群服务器间的免密登录 作用&#xff1a;这里配置的是root用户…

联想昭阳E5-ITL电脑开机后绿屏怎么U盘重装系统?

联想昭阳E5-ITL电脑开机后绿屏怎么U盘重装系统&#xff1f;有用户电脑正常开机之后&#xff0c;出现了屏幕变成绿屏&#xff0c;无法进行操作的情况。这个问题是系统出现了问题&#xff0c;那么如何去进行问题的解决呢&#xff1f;接下来我们一起来分享看看如何使用U盘重装电脑…

数据库系统概论——绪论

1、绪论 1.1、数据库系统概述 数据库系统的构成示意图 1.1.1、数据库系统基本概念 基本概念&#xff1a;数据、数据库、数据库管理系统和数据库系统 1&#xff09;数据&#xff08;data&#xff09; 定义&#xff1a;描述事物的符号记录称为数据数据是数据库中存储的基本对象…

Latex三线表画法合集

Latex常用表格画法合集 前言 当我们使用Latex写论文的时候&#xff0c;三线表是展示实验结果的常用方法&#xff0c;但网上的方法杂七杂八&#xff0c;找到自己想要的表格需要花费一番功夫。本篇文章旨在记录论文常用表格的画法&#xff0c;方便论文的书写。 导入的包&#x…

11.7 指针和多维数组

C语言学习栏目目录 目录 1 指向多维数组的指针 2 指针的兼容性 3 函数和多维数组 指针和多维数组有什么关系&#xff1f;为什么要了解它们的关系&#xff1f;处理多维数组的函数要用到指针&#xff0c;所以在使用这种函数之前&#xff0c;先要更深入地学习指针。至于第 1 个…