首页 > 编程语言 >Vue3源码解析--收集的依赖是什么?怎么收集的?什么时候收集的?

Vue3源码解析--收集的依赖是什么?怎么收集的?什么时候收集的?

时间:2024-05-26 17:21:59浏览次数:23  
标签:依赖 收集 -- effect value dep 源码 ref

从Vue开始较大范围在前端应用开始,关于Vue一些基础知识的讨论和面试问题就在开发圈子里基本上就跟前几年的股票和基金一样,楼下摆摊卖酱香饼的阿姨都能说上几句那种。找过前端开发工作或者正在找开发工作的前端都知道,面试官基本上都有那么几个常问的问题,而网上呢也有那么一套可以用来背诵的“八股文”,自己懂多少没有关系,应付面试官还是够的,可以算是屡试不爽吧。

背诵面试八股文无可厚非的,可以说是每一个找工作的人都干过和必须干的事情,因为我们都要工作,都要恰饭。只有恰上饭,才能去谈些伟大的理想。背“八股文”本是一种捷径,尤其是本身对一门技术不是特别了解的开发者,就是那种刚刚能使用它那种。

在众多关于Vue的面试“八股文”中,今天讲的是其中最常问的一个--Vue中的依赖收集。本文也将从代码层面,讲清楚关于依赖收集的几个问题。

  • 收集的依赖是什么?(what)
  • 怎么收集的依赖? (how)
  • 什么时候收集? (when)

至于为什么要收集依赖(why),现在就可以先告诉答案。收集依赖,其核心作用是在数据发生变化的时候可以做出相应的动作,比如刷新视图,为了执行这一动作,我们就得知道是谁在什么时候发生了变化,所以我们要收集依赖。

下面我们结合代码,尽可能通俗的讲解关于上述的三个问题:

在搞清楚依赖收集之前,先把源码中几个概念性的东西说明一下,建议下载Vue3源码进行对照着看:

  • Dep: 本质上是一个Map实例,同时在map实例上绑定一个celanup函数和一个computed属性。

  • ReactiveEffect: 相当于2.x版本中的Watcher类, 里头有一个deps数组,用来存dep, 每个实例里面都有一个track_id用来标识唯一性。

  • effect函数: 里头实例化一个ReactiveEffect对象,同时绑定一些options, 返回值是一个runner,实际上是对ReactiveEffect对象行为的一种业务封装。

下面以一行简单的代码开始关于依赖收集的探索。

const num = ref(1);
// packages/reactivity/src/ref.ts
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value, false)
}

ref函数主要是对createRef做了一个函数包装,主要内容看到createRef函数。

// packages/reactivity/src/ref.ts
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

createRef函数在这里对原始数据rawValue做了一个判断,如果数据本身就是响应式数据了,就直接返回它本身,如果不是,就返回一个实例化的RefImpl对象。

// packages/reactivity/src/ref.ts
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) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, DirtyLevels.Dirty, newVal)
    }
  }
}

重点来了,RefImple类里头,才是真正包含了从原始数据变成响应式数据,以及收集依赖的逻辑。在一个refImpl实例中,里面有一个dep对象,初始值是undefined, 这个dep会这trackRefValue函数执行的过程中被赋值。

下面代码从17-21(get value())行,就是依赖收集的过程:当一个ref型响应式数据通过.value访问时,会触发RefImpl实例中的getter。它会首先执行一个trackValue函数,然后再返回_value值,所以接下来重点看关注trackValue函数,所以依赖是在数据被访问的时候触发的

// packages/reactivity/src/ref.ts
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      )),
      __DEV__
        ? {
            target: ref,
            type: TrackOpTypes.GET,
            key: 'value',
          }
        : void 0,
    )
  }
}u

trackRefValue函数中有两个变量,shouldTrack和activeEffect,暂时我们不去理会它们,只要知道shouldTrack是一个布尔值,activeEffect是一个RectiveEffect实例。

在shouldTrack值为true且activeEffect有值的情况下,首先会将ref转成原始值,然后再执行trackEffect函数。

在执行trackEffect函数的中,第一个是activeEffect, 在任意时刻它在全局是具有唯一性的;第二个是ref.dep, 其中给ref.dep的赋值函数createDep返回一个Dep实例,前面说过的,本质是个map; 第三个函数是个对象,是关于开发环境下debug的一些配置。

在这里,我们可以看到,之前说个的ref实例中原来是undefined的ref.dep赋值,就在此处。

// packages/reactivity/src/effect.ts
export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  if (dep.get(effect) !== effect._trackId) {
    dep.set(effect, effect._trackId)
    const oldDep = effect.deps[effect._depsLength]
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      effect.deps[effect._depsLength++] = dep
    } else {
      effect._depsLength++
    }
    if (__DEV__) {
      effect.onTrack?.(extend({ effect }, debuggerEventExtraInfo!))
    }
  }
}

trackEffect函数绝对是依赖收集重头戏中的重头戏。

首先上来就是一个判断,dep, 也就是ref中的dep,本质是个map,判断里面是否存在对应的effect, 如果没有,就执行接下来的操作。

dep将effect也就是activeEffect作为键,其_trackId作为值添加到dep,所以我们说的收集的依赖指的就是effect对象。同时我们得到了一个关于dep和effect之间的第一关系,即一个dep可以对应多个effect

接着,将effects实例中deps数组中最后一个值取出来与当前的dep值进行比对,看是否是同一个值如果不是同一个值,而且oldDep是有值的,那么就执行cleanupDepEffect操作。如果oldDep为空值,就跳过这一步,直接往effect.deps中添加dep。因此,我们在这里得到了关于dep和effect第二个结论,一个effect可以对应多个dep

代码还有一部分,接着往下看,在oldDep不等于当前dep的时候,直接对effec_depsLength进行加操作,也就是说,effect.deps值没有变,但是_depsLength值却超出了deps数组边界的情况,这也就是为什么上面要判断oldDep是否存在的原因。

由上面上面两个结论我们可以得出,一个dep中可以对应多个effect, 一个effect也可以对应多个dep, 因此dep和effect的关系是多对多的关系。

总结

  • 收集的依赖是什么?(what)

我们常说的收集的依赖是effect对象

  • 怎么收集的依赖? (how)

判断当前数据dep中有没有activeEffct, 没有就加进去。把大象关进冰箱里要几步!!!

  • 什么时候收集? (when)

在数据被访问时,触发getter,进行依赖收集

标签:依赖,收集,--,effect,value,dep,源码,ref
From: https://www.cnblogs.com/jvxiao/p/18213813

相关文章

  • FWT & FMT
    CF662C首先有一个\(\mathcalO(2^nm)\)的做法。枚举每一行是否反转的状态\(s\),记\(g_i=\min(\operatorname{popcount}(i),n-\operatorname{popcount}(i))\),\(t_i\)表示第\(i\)列的状态。则答案为\(\min_s{\sum_{i=1}^mg_{s\operatorname{xor}t_i}}\)。发现这个东西......
  • 链栈
    链栈linkStack.h文件#pragmaoncetypedefstructNode{ intdata; structNode*next;}node,*pnode;classLinkStack{private: Node*top;public: LinkStack(); ~LinkStack(); voidpush(intx); intpop(); intgetTop(); boolisEmpty(); voiddisplay......
  • 关于BSP
    在嵌入式系统中,BSP(BoardSupportPackage)通常被称为板级支持包或板级支持软件,它是一组针对特定硬件平台的软件支持包。BSP在嵌入式系统中扮演着连接硬件和操作系统的桥梁角色,为开发人员提供了一个统一的接口层,简化了硬件和软件之间的交互。以下是关于BSP的详细解释:1.**定义与功......
  • SAP S4HANA2023 PCE系统上的VKM3?
    SAPS4HANA 2023PCE系统上的VKM3?  在SAPS4HANA2023PCE系统上,试图执行事务代码VKM3,为某个销售订单释放客户信用冻结,系统报错:无法执行事务VKM3(SAPNote2476734),     详细报错信息: 无法执行事务VKM3(SAPNote2476734)消息编号00977诊断系统配置(黑......
  • pytorch环境配置
    1.查看已有的虚拟环境condaenvlist2.创建虚拟环境的名字和python版本condacreate-nnupackpython=3.83.维基百科查看显卡算力以及对应的cuda版本4.查看自己的cuda版本5.找到pytorch历史版本安装代码https://pytorch.org/get-started/previous-versions/6.激活......
  • 代码随想录算法训练营第第18天 | 513.找树左下角的值 、112. 路径总和 、106.从中
    找树左下角的值本地递归偏难,反而迭代简单属于模板题,两种方法掌握一下题目链接/文章讲解/视频讲解:https://programmercarl.com/0513.找树左下角的值.html/***Definitionforabinarytreenode.*functionTreeNode(val,left,right){*this.val=(val===undef......
  • pinia的使用
     搭建pinia环境pinia:集中式状态管理工具,用于各组件之间共享数据(多个组件会用到的数据才考虑放到pinia中)在vue2中使用的是vuex1.终端输入:npmipinia2.在vue组件中出现pinia用pinia存储+读取数据要想好什么组件的什么数据要放入pinia,就是某个组件你希望哪些数据可以和......
  • 【wiki知识库】01.wiki知识库前后端项目搭建(SpringBoot+Vue3)
      ......
  • java —— 连接 MySQL 操作
    MySQL是独立于java之外的数据库,二者之间建立连接需要提前引入mysql-connector-java的jar包。一、引入方法:①在项目中新建一个Folder(即文件夹),该文件夹通常命名为lib,意思是存放项目所依赖的第三方库或外部的jar文件。②将 mysql-connector-java的jar包复制进......
  • java —— 封装、继承、接口和多态
    一、封装封装是将数据和操作这些数据的方法整合成一个类。在这个类中,用private修饰符将某些数据隐藏起来,只通过特定的方法实现这些数据的访问和修改,以此实现数据的完整和安全性。封装的步骤:二、继承 继承是指把子类共有的某些属性或方法抽离出来,编写为父类,这样多个子类......