从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. 【计算力学】CST单元格式推导

    CST单元格式推导

  2. 自动化-Yaml文件写入函数封装

    1.文件布局 打开文件修改读取方式为w dump函数写入文件 写入中文 使用allow_unicode=True class ReadConfiYaml: def __init__(self,yaml ...

  3. 网络虚拟存储 Iscsi实现方案

    网络虚拟存储,通过Iscsi协议可以实现本地挂载磁盘,扩展本机设备存储. Iscsi有以下实现方案,简单介绍下: 1.Powershell 脚本 PowerShell 提供了一些 cmdlet,比如  ...

  4. Error: Address already in use

    端口被某个进程占用 使用命令 lsof -i:端口号 然后看到进程号,直接杀掉进程就好 kill -9 进程号

  5. 云服务器下如何部署Flask项目详细操作步骤

    参考网上各种方案,再结合之前学过的Django部署方案,最后确定Flask总体部署是基于:centos7+nginx+uwsgi+python3+Flask之上做的. 本地windows开发测试好了我 ...

  6. 批量操作DB

    批量操作DB 记录一次批量操作数据库,sqlServer服务器参数过多问题. 1.案例引入 对接另一个批发系统B,B需要把订单那些数据弄到系统A中.就是一个批量插入,批量更新的问题.系统保密需要,本文 ...

  7. MaxKB+Ollama 离线部署

    主题:在 Centos7 环境部署 MaxKB 以及 Ollama 实现基于离线大模型的的小助手调用. 选择离线部署的原因:原计划是打算直接使用 1Panel 进行 MaxKB 和 Ollama 一键 ...

  8. WEBGL 笔记

    目录 前言 h2 { text-align: center } 前言 WebGL 是一个在浏览器里使用的高效渲染二维和三维图形的 javascript API,于 2006 年起源,该技术基于 Ope ...

  9. MYSQL数据库 MariaDB断电恢复总结

    背景:本次是机房异常断电,导致数据库文件损坏.在数据库自启动之后频繁宕机,在多次尝试以后,总结了一下几种方法,及供参考. 1.mariadb服务器断电重启之后Missing MLOG_CHECKPOI ...

  10. 配置springmvc的springmvc.xml

    <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.spr ...