首页 > 编程语言 >3. Vue3源码解析之 ref

3. Vue3源码解析之 ref

时间:2024-01-16 16:11:20浏览次数:49  
标签:__ dep effect value newVal 源码 Vue3 ref

前言

我们知道 Vue3 中声明响应式是通过 reactiveref 这两个函数,上篇我们分析了 reactive 的实现原理,接下来我们再来看下 ref 是如何实现的。

案例

首先引入 refeffect 两个函数,之后声明 name 响应式数据,接着又执行 effect 函数,该函数传入了一个匿名函数,最后两秒后又修改 name 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { ref, effect } = Vue;

      const name = ref("jc");

      effect(() => {
        document.querySelector("#app").innerHTML = name.value;
      });

      setTimeout(() => {
        name.value = "cc";
      }, 2000);
    </script>
  </body>
</html>

ref 实现

ref 函数定义在 packages/reactivity/src/ref.ts 文件下:

export function ref(value?: unknown) {
  return createRef(value, false);
}

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue;
  }
  return new RefImpl(rawValue, shallow);
}

ref 函数实际执行的是 createRef 方法,而该方法实际是返回了一个 RefImpl 构造函数的实例对象:

class RefImpl<T> {
  private _value: T;
  private _rawValue: T;

  public dep?: Dep = undefined;
  public readonly __v_isRef = true;

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 记录原始值
    this._rawValue = __v_isShallow ? value : toRaw(value);
    // 操作值
    this._value = __v_isShallow ? value : toReactive(value);
  }

  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);
    }
  }
}

RefImpl 构造函数会接收传入的值,可能是基本类型也可能是复杂类型,通过 _rawValue 记录原始值,用于之后依赖触发时新旧值的比较,我们需关注 this._value = __v_isShallow ? value : toReactive(value)toReactive 函数被定义在 packages/reactivity/src/reactive.ts 中:

export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value;

可以看出,如果传入的数据为对象类型则调用 reactive 方法,这块逻辑可参考 Vue3 源码解析之 reactive ,否则就直接返回当前值,此时 ref 执行完毕,当前 name 被赋值为 jc

ref.png

我们回过来再看下 RefImpl 构造函数中还有 get value()set value() 这两个方法,那它们具体有什么用?举个例子:

// RefImpl 构造函数
class RefImpl {
  // 实例的 getter 行为: ref.value
  get value() {
    return "get value";
  }
  // 实例的 setter 行为: ref.value = xxx
  set value(newVal) {
    console.log("set value");
  }
}
const newRef = new RefImpl();
console.log(newRef);

看下输出结果:

newRef.png

当我们执行 newRef.value 时会触发 getter,而修改值时会触发 setter这也是为什么我们赋值或者修改 ref 值时,需要加上 .value

另外我们还需知道,对于基本类型的数据 ref 是不具备数据监听的,当赋值或修改值时主动触发了 getset 方法。

之后执行 effect 函数(该原理可查看上篇),传入一个匿名函数,接着执行赋值行为触发 get 方法:

get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
}

get 方法核心 trackRefValue(this) 实际触发了 trackRefValue 方法进行数据的依赖收集,该方法定义在 packages/reactivity/src/effect.ts 文件中:

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref);
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: "value",
      });
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()));
    }
  }
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false;
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit; // set newly tracked
      shouldTrack = !wasTracked(dep);
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!);
  }

  if (shouldTrack) {
    dep.add(activeEffect!);
    activeEffect!.deps.push(dep);
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!,
      });
    }
  }
}

这块逻辑同 reactive,给指定属性绑定对应的 fn,目的是 dep 对象与 ReactiveEffect 相关联,完成整个依赖收集的过程。之后两秒后进行修改值触发 set 方法:

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)
    }
}

set 方法中 triggerRefValue(this, newVal) 进行依赖触发:

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref);
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: "value",
        newValue: newVal,
      });
    } else {
      triggerEffects(ref.dep);
    }
  }
}

triggerRefValue 方法实际执行了 triggerEffects,该方法定义在packages/reactivity/src/effect.ts 文件中:

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep];
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo);
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
    }
    if (effect.scheduler) {
      effect.scheduler();
    } else {
      effect.run();
    }
  }
}

上篇 reactive 我们也分析了这块逻辑,最终执行的是每个 effect.run 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成。

ref-set.png

总结

  1. ref 函数本质上做了三件事:一是返回 RefImpl 的实例;二是对数据处理,如果当前数据为基本类型,则直接返回;如果为复杂类型,则调用 reactive 返回 reactive 数据;三是 RefImpl 提供 get valueset value 方法,这就是为什么设置 ref 值时,需要带上 .value
  2. ref 基本类型的数据不具备数据监听,赋值或修改值都是主动触发 getset 方法。
  3. 为什么 ref 类型数据,必须要通过 .value 访问值呢?
    a. 因为 ref 需要处理基本数据类型的响应性,但是对于基本类型数据而言,它无法通过 proxy 建立代理
    b. 而 vue 通过 get value()set value() 定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和依赖触发
    c. 所以我们必须通过 .value 来保证响应性。

Vue3 源码实现

vue-next-mini

标签:__,dep,effect,value,newVal,源码,Vue3,ref
From: https://www.cnblogs.com/wp-leonard/p/17967906

相关文章

  • 初始化一个vite+vue3的前端项目要做的额外的事儿
    添加.editorconfig文件#http://editorconfig.orgroot=true[*]charset=utf-8indent_style=spaceindent_size=4end_of_line=lfinsert_final_newline=truetrim_trailing_whitespace=true[*.md]insert_final_newline=falsetrim_trailing_whitespace......
  • YOLOv8原理与源码解析(视频教程)
    课程链接:https://edu.51cto.com/course/35522.html【为什么要学习这门课】Linux创始人LinusTorvalds有一句名言:Talkischeap.Showmethecode.冗谈不够,放码过来!代码阅读是从基础到提高的必由之路。YOLOv8基于先前YOLO版本的成功,引入了新功能和改进,进一步提升性能和灵活性。......
  • JMeter 源码解读 - HashTree
    背景:在JMeter中,HashTree是一种用于组织和管理测试计划元素的数据结构。它是一个基于LinkedHashMap的特殊实现,提供了一种层次结构的方式来存储和表示测试计划的各个组件。HashTree的特点如下:层次结构:HashTree使用树状结构来组织测试计划元素。每个节点都可以包含子节点......
  • HashMap源码随笔
    源码第一块:概述:Map接口的基于哈希表的实现。此实现提供所有可选的映射操作,并允许null值和null键。(HashMap类大致等同于Hashtable,只不过它是不同步的,并且允许null。此类不保证地图的顺序;特别是,它不保证订单会随着时间的推移保持不变。此实现为基本操作(get和put)提供恒......
  • SecureCRT & SecureFX 9.5 for macOS, Linux, Windows
    SecureCRT&SecureFX9.5formacOS,Linux,Windows-跨平台的多协议终端仿真和文件传输请访问原文链接:SecureCRT&SecureFX9.5formacOS,Linux,Windows,查看最新版。原创作品,转载请保留出处。作者主页:sysin.orgSecureCRT客户端运行于Windows、Mac和Linux,将坚如磐......
  • PriorityQueue源码阅读
    目录简介模型代码分析成员变量方法总结参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,......
  • 学习spring源码(一)
    学习文档来自小傅哥,详情可以去原文章了解,这边只是简单记录一下学习体会《Spring手撸专栏》第3章:初显身手,运用设计模式,实现Bean的定义、注册、获取工程结构:类似是这样,我这边稍微有点区别,仅做参考small-spring-step-02└──src├──main│└──java......
  • vue3使用 vant ui 3 如何获取组件 popup dom的高度?
    我目前使用的是vant-ui 3.1.2popup弹出层组件,我想要获取弹出层的高度来计算一些东西,但是使用常规定义refdom的方式总是无法获取,最终找到方案如下:vant-ui官方文档:https://vant-contrib.gitee.io/vant/v3/#/zh-CN/popup<template><van-popupv-model:show="show......
  • PDF.js实现按需分片加载pdf文件-包含前后端开发源码和详细开发教程
    PDF.js实现按需分片加载pdf文件-包含前后端开发源码和详细开发教程:https://blog.csdn.net/qq_29864051/article/details/130742657?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170529842016800186594900%2522%252C%2522scm%2522%253A%252220140713.130102334..%252......
  • 【Vue2+3入门到实战】(21)认识Vue3、使用create-vue搭建Vue3项目、熟悉项目和关键文件
    目录一、认识Vue31.Vue2选项式APIvsVue3组合式API2.Vue3的优势二、使用create-vue搭建Vue3项目1.认识create-vue2.使用create-vue创建项目三、熟悉项目和关键文件四、总结一、认识Vue31.Vue2选项式APIvsVue3组合式API<script>exportdefault{data(){r......