从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,进行依赖收集

Vue3源码解析--收集的依赖是什么?怎么收集的?什么时候收集的?的更多相关文章

  1. 【spring-boot 源码解析】spring-boot 依赖管理梳理图

    在文章 [spring-boot 源码解析]spring-boot 依赖管理 中,我梳理了 spring-boot-build.spring-boot-parent.spring-boot-depen ...

  2. .NET Core实战项目之CMS 第三章 入门篇-源码解析配置文件及依赖注入

    作者:依乐祝 原文链接:https://www.cnblogs.com/yilezhu/p/9998021.html 写在前面 上篇文章我给大家讲解了ASP.NET Core的概念及为什么使用它,接着 ...

  3. 【spring-boot 源码解析】spring-boot 依赖管理

    关键词:spring-boot 依赖管理.spring-boot-dependencies.spring-boot-parent 问题 maven 工程,依赖管理是非常基本又非常重要的功能,现在的工程 ...

  4. Vue3源码解析(computed-计算属性)

    作者:秦志英 前言 上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的. 在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供 ...

  5. seajs1.3.0源码解析之module依赖有序加载

    /** * The core of loader */ ;(function(seajs, util, config) { // 模块缓存 var cachedModules = {} // 接口修改 ...

  6. Spring源码解析-基于注解依赖注入

    在spring2.5版本提供了注解的依赖注入功能,可以减少对xml配置. 主要使用的是 AnnotationConfigApplicationContext: 一个注解配置上下文 AutowiredA ...

  7. 【源码解析】凭什么?spring boot 一个 jar 就能开发 web 项目

    问题 为什么开发web项目,spring-boot-starter-web 一个jar就搞定了?这个jar做了什么? 通过 spring-boot 工程可以看到所有开箱即用的的引导模块 spring- ...

  8. vue3源码node的问题

    下载vue3源码后,下载依赖时,node的版本需要在10.0.0以上,并且不同的vue3里面的插件的配置对版本依赖还不同,14.0.0以上的版本基本都不支持win7了, win7系统可以安装12.0. ...

  9. Maven 依赖调解源码解析(三):传递依赖,路径最近者优先

    本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第三篇,主要介绍依赖调解的第一条原则:传递依赖,路径最近者优先.本篇内容较多,也是开始源码分析的第一篇,请务必仔细阅读,否则后 ...

  10. 简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析

    简单理解 OAuth 2.0 及资料收集,IdentityServer4 部分源码解析 虽然经常用 OAuth 2.0,但是原理却不曾了解,印象里觉得很简单,请求跳来跳去,今天看完相关介绍,就来捋一捋 ...

随机推荐

  1. Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!

    Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来! 1. 优势介绍 Obsidian 是一款强大的本地知识管理软件,它像一个积木盒,让你用 Markdown 笔记 ...

  2. AI回答:一个简洁的php中间件类

    <?php class MiddlewareStack { private $middlewares = []; private $request; private $response; /** ...

  3. k8s:The connection to the server localhost:8080 was refused - did you specify the right host or port?

    前言 k8s 集群 node节点报错:The connection to the server localhost:8080 was refused - did you specify the rig ...

  4. Python设置递归最大深度

    博客地址:https://www.cnblogs.com/zylyehuo/ import sys sys.setrecursionlimit(100000) # 设置最大递归深度,默认是3000

  5. PLSQL自动登录,记住用户名密码&日常使用技巧

    配置启动时的登录用户名和密码 这是个有争议的功能,因为记住密码会给带来数据安全的问题. 但假如是开发用的库,密码甚至可以和用户名相同,每次输入密码实在没什么意义,可以考虑让PLSQL Develope ...

  6. 项目实战 TS

    项目实战 TS 通用技巧 新手先 any 再填坑,老手先定义数据结构写逻辑 遇到新场景,没把握快速,先用 any 再填坑,填坑的过程也是 TS 技能满满提升的过程. TS 发现潜在问题 1)复杂逻辑, ...

  7. ZKmall模版商城前后端分离秒级响应架构深度解析

    在当今的电商领域,用户体验和响应速度已成为决定平台竞争力的关键因素.ZKmall模版商城,作为一款高性能的电商平台解决方案,通过采用前后端分离架构,实现了秒级响应,为用户带来了极致的购物体验.本文将深 ...

  8. nodejs实现命令行工具

    为什么使用nodejs实现命令行工具 Node.js是一个基于事件驱动I/O的JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好. 众所周知 ...

  9. Spring 整合 Junit

    一.导入jar包 二.使用@RunWith 注解替换原有运行器 [main()] /** * * @Company http://www.ithiema.com * @Version 1.0 */ @ ...

  10. MySQL 中 DATETIME 和 TIMESTAMP 类型的区别是什么?

    在MySQL中,DATETIME和TIMESTAMP都是用于存储日期和时间的类型,但它们有一些关键的区别: 1. 存储方式和范围 DATETIME: 存储的日期和时间值是以"年-月-日 时: ...