从Vue3源码深入理解ref响应式

  在Vue3中,reactive和ref两个响应式API相信大家用的都比较熟练了,但是在使用时还有众多的响应式API函数,本文就从源码角度,来看下不同API及其实现方式,相信看完本文之后会加深对Vue3响应式的理解。

  我们在看源码的时候,由于众多的函数和类,经常会陷入迷茫的状态,不知道这个类或者类下面某个属性是做什么用的;而且随着现在vue3彻底拥抱Typescript,类型的使用也加大了我们读源码时的难度,因为我们还需要对ts的用法有所了解;因此笔者有以下几点小小的建议:

  1. 剔除无关代码,比如无关的类型系统,注重核心逻辑、注重核心逻辑、注重核心逻辑,重要的事情说三遍。
  2. 不要纠结于一两个函数或者类的功能,非核心的代码能跳过就跳过吧。
  3. 看注释,很多注释也会写明函数的作用,帮助我们理解。
  4. 看名称,名称可以告诉我们这个函数的作用,加上合理的猜测,去揣摩作者的意图。
  5. 最后实在看不出来也猜不到,还有众多强大AI问答工具,用好AI工具。
  6. 梳理好主要流程,有时一个参数会流转经过很多的函数,我们必须梳理好每个函数的参数,可以重点关注函数的入参及出参。
  7. 我们看一个函数的时候经常需要跳转到多个不同的函数,在编辑器上将代码分成多个屏查看,一个屏注重主线逻辑,其他可以看工具函数。
  8. 结合平时的使用场景看代码,对理解函数的作用会有很大的帮助。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  因此我们会看到下面代码,很多地方笔者都标注了省略部分代码,只贴了重点的代码出来;带着下面的疑问我们来看源码:

  1. 编译和运行有什么区别?
  2. reactive和ref有什么关系和区别?
  3. 响应收集是怎么做的?
  4. 谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本文应该是全网最深入的对ref源码解读,文章内容较长,觉得有所收获的记得点赞关注收藏,一键三连。

项目介绍

  首先我们要去学习源码,就要对整个项目有一个基本了解,了解每个模块、每个目录的作用,这样对我们后期阅读源码也会有很大的帮助;我们将项目从Github上克隆下来:

1
git clone https://github.com/vuejs/core.git vue3-core

如果因为网络问题克隆不下来,直接跳转文末获取Vue3的源码;笔者克隆下来时,当前版本号为3.5.12

  我们看到整个项目的目录还是非常多的,又有编译时,又有运行时的目录,主要的代码都存放在packages目录,因此我们主要看下它下面的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
changelogs/ # 项目的变更日志
script/ # 存放构建脚本和其他与开发流程相关的脚本。
packages/ # 存放了Vue3核心的各种包和模块
├── compiler-core
├── compiler-dom
├── compiler-sfc
├── compiler-ssr
├── dts-built-test
├── dts-test
├── reactivity
├── runtime-core
├── runtime-dom
├── runtime-test
├── server-renderer
├── sfc-playground
├── shared
├── template-explorer
├── vue
└── vue-compat

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  上面目录,我们将其归类一下,主要分为下面几个部分;首先就是编译器相关的目录,主要是以下几个目录:

  • compiler-core:编译核心,包括模板解析、AST(抽象语法树)构建等。这是Vue模板编译的基础部分,不依赖于特定的平台。
  • compiler-dom:编译器针对浏览器环境的代码,主要处理模板编译成渲染函数的过程。这是Vue在浏览器环境下进行模板编译的关键部分。
  • compiler-ssr:编译器针对服务端渲染环境的代码,使得Vue能够在服务端进行模板编译
  • compiler-sfc:用于解析.vue单文件组件的编译器模块,它负责将.vue文件编译成JavaScript模块,以便在Vue项目中使用。

  其次是响应式模块相关的目录,也就是本文重点讲解的目录:

  • reactivity:响应式系统的核心代码,这是Vue3实现数据响应式的基础部分,它使得Vue能够追踪数据的变化并自动更新DOM。

  还有是运行时模块,包括如下目录:

    谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  • runtime-core:与平台无关的运行时核心代码,包括虚拟DOM、组件实例化等。这是Vue运行时系统的核心部分,它负责处理组件的创建、更新和销毁等生命周期事件。
  • runtime-dom:运行时针对浏览器环境的代码,包括节点操作、事件处理等。这部分代码使得Vue能够在浏览器环境下进行DOM操作,从而实现对用户界面的更新。
  • runtime-test:用于测试的运行时代码。这部分代码主要用于对Vue的运行时系统进行测试,以确保其正确性和稳定性。

  最后是一些其他功能模块,包括以下目录:

  • server-renderer:Vue3单文件组件playground,用于测试和演示Vue单文件组件的功能。
  • shared:包含了一些公共的工具函数和类型定义,这些函数和类型定义被其他模块广泛使用。

  我们发现,对目录的功能进行划分后,其实功能就没有那么复杂了;回到上面的第一个问题,编译和运行有什么区别?编译模块compiler通常用来将我们代码中写的Vue模板编译成浏览器可以执行渲染函数,一般是在开发环境和构建工具(Webpack、Vite等)中运行。

  而运行时模块runtime是可以直接加载(渲染函数)并执行的,像我们常见的组件的实例化、渲染、更新和销毁等生命周期事件都是在运行时去执行的。

Ref源码部分

  下面我们就来看Ref部分的源码,打开reactivity/src/index.ts,我们看到从各个响应式的子模块中导出了不少函数和type:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export {
// ...
} from './ref'

export {
// ...
} from './reactive'

export {
// ...
} from './watch'

export {
// ...
} from './computed'

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们看到很多模块名称都是我们熟悉的功能,比如watch和computed;我们打开本节要介绍的reactivity/src/ref.ts,我们先来看下里面最重要的两个函数:ref和shallowRef。

1
2
3
4
5
6
7
8
9
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}

export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
return createRef(value, true)
}

  我们发现ref和shallowRef都接收一个任意值any做参数,然后返回Ref和ShallowRef的对象,两者本质上没什么差别,我们看下两者的ts类型定义:

1
2
3
4
5
6
7
8
9
10
11
declare const RefSymbol: unique symbol
export interface Ref<T = any, S = T> {
get value(): T
set value(_: S)
[RefSymbol]: true
}

declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any, S = T> = Ref<T, S> & {
[ShallowRefMarker]?: true
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们看到Ref上只有getter和setter两个方法,而ShallowRef本质上也是从Ref合成而来,只是将ShallowRefMarker标识为true。然后ref和shallowRef都调用了createRef函数,我们找到createRef函数的实现:

1
2
3
4
5
6
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}

  这里先对传入值进行isRef的判断,判断对象是否是ref对象,判断逻辑其实也非常简单,就是通过对象上是否有一个__v_isRef的标识判断,因为后面ref对象处理后都会打上这个标识:

1
2
3
4
5
6
7
8
export enum ReactiveFlags {
// ...其他标识
IS_REF = '__v_isRef',
}

export function isRef(r: any): r is Ref {
return r ? r[ReactiveFlags.IS_REF] === true : false
}

  回到createRef函数中,我们看到实例化了RefImpl类并且返回,这个类的主要作用就是用来创建Ref对象;它的第一个参数就是传入的初始值,第二个参数表明是否是对象是否是浅层响应shallow;如果我们将ref创建的对象打印出来,就会发现本质上其实都是从RefImpl实例化出来,这也符合我们源码里面的逻辑。

1
2
const num: Ref<number> = ref(2);
console.log(num, 'num1');

ref对象

核心类RefImpl

  __v_isRef的标识我们上面在isRef函数中也说了,只要是ref创建出来的对象都会有;我们接着看ref核心类RefImpl的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RefImpl<T = any> {
_value: T
private _rawValue: T

dep: Dep = new Dep()

public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false

constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
// ...
}
set value(newValue) {
// ...
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

带有下划线的属性一般是类的私有属性,不会对外暴露,比如_value和_rawValue,在ref对象上就不能被访问。

  在RefImpl构造函数中,两个只读的标识符ReactiveFlags.IS_REFReactiveFlags.IS_SHALLOW其实是两个字符串变量的标识符,用来判断对象是否是Ref和是否是ShallowRef,这里不再赘述;dep对象是用来实现依赖收集的,我们下面会说到。

  这里的_value存储的其实就是我们ref对象的真实值(响应化之后的拦截对象Proxy Object),我们下面通过getter和setter访问和修改时,实际上就是操作_value中的值,它值的变化追踪是整个响应式的核心;而_rawValue中存储的是真实值(没有响应化的),在修改value的时候用来进行新老值判断。

  在_value值初始化的时候,我们看到如果是非浅层Shallow,还调用了toReactive,这个函数其实是我们以后会说的reactive.ts文件中暴露出来的,如果传入的初始值是对象,还会对其进行更深层次响应化的处理:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
// packages/reactivity/src/ref.ts
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value

  我们看到toReactive中如果传入的value是对象等引用类型,会调用reactive进行Proxy拦截;而如果我们传入一个数字或者一个字符串等基本类型时,返回本身。

  因此我们可以简单的总结一下,如果ref接收到一个基本类型,_value中存储的也是原始值,本质上劫持的是_value的getter和setter函数来实现响应式的;而如果ref函数接收到引用类型,_value中存储的是转化后的Proxy Object代理对象,通过代理对象的属性来实现的响应式。

getter和setter函数

  我们接着通过一个简略版的代码,来看下getter和setter中对_value值时做了哪些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RefImpl<T = any> {
// 其他属性
get value() {
// 省略了开发调试环境下的代码
this.dep.track()
return this._value
}
set value(newValue) {
// 省略其他代码
this._value = useDirectValue ? newValue : toReactive(newValue)
this.dep.trigger()
}
}

  首先我们要理解getter和setter函数是在什么时候调用的,就能理解它里面的操作逻辑了;当我们读取ref对象的.value属性时,getter函数起作用,比如我们在页面上使用插值模板{{ num }}时(本质调用了num.value),或者watch(num)监听时,都会触发getter函数;而我们在修改值num.value = 3时就会触发setter函数。

当然还会有其他的触发逻辑,这里是列举了最常见的一些情况。

  因此上面的简略版的代码就很好理解了,也印证了我们上面的说法,即:_value存储的是真实值(响应化之后的),getter和setter操作的是_value中的值。

  那么这里的this.dep也很好理解了,我们可以简单的将其理解为内部存储了一个数组,当getter在不同地方访问或者显示value的时候,就将它添加到数组中;而当我们去修改了value值时,就触发trigger函数,将数组中存储的修改的页面地方,全部通知更新。

  我们看到上面浏览器里直接打印出来的ref对象num,由于没有触发getter,因此它的dep显示undefined,我们把它放到template中和watch里面去:

1
2
3
4
5
6
7
8
9
10
<template>
<div>{{ num }}</div>
</template>
<script lang="ts" setup>
const num: Ref<number> = ref(2);
watch(num, (val) => {
// ...
})
console.log(num, 'num');
</script>

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

ref对象中的dep属性

  我们看到上面打印出来的dep属性中的map就有两个收集到的依赖了,而不是之前的undefined了。我们继续来看完整的setter函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RefImpl<T = any> {
set value(newValue) {
const oldValue = this._rawValue
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||
isReadonly(newValue)
newValue = useDirectValue ? newValue : toRaw(newValue)
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
// 省略了开发环境的代码
this.dep.trigger()
}
}
}

  这里将_rawValue暂存到oldValue变量,这里useDirectValue我们可以把它简单理解为newValue,然后判断传入newValue和oldValue是否有变化,其实本质上也就是判断传入的新值和_rawValue是否有变化,如果变化了,_rawValue和_value都改为新的值,同时触发dep.trigger。

  那这里可能还有小伙伴会提出这样的疑问,为什么不直接将newValue和_value中的值进行对比,而要通过_rawValue?因为结合这里的setter函数,我们回想一下构造函数中就能明白了,_rawValue存储的是数据的原始值,而_value存储的是响应化之后的数据,而_rawValue存储的原始值就是用来进行对比操作的;如果我们直接将newValue和_value对比,那么hasChanged永远都会返回true。

  我们将ref函数的基本逻辑介绍完后,再把其他几个ref相关的函数顺便也来看一下。

unref

  首先就是unref,用来获取ref对象中的value值:

1
2
3
export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
return isRef(ref) ? ref.value : ref
}

  我们发现,其实unref这个函数本质上就是一个语法糖,只是帮我们拿到ref对象的value属性;对于基本类型,我们知道value中存储的就是值本身;而对于数组和对象等引用类型,value中存的则是Proxy Object拦截对象。

  因此如果我们还想继续获取引用类型的真实值,还需要通过toRaw进行一下转换:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const num = ref(10);

// 10
console.log(unref(num));

const obj = ref({
a: 1,
});

// Proxy(Object)
console.log(unref(obj));

// {a:1}
console.log(toRaw(unref(obj)));

// {a:1},效果和unref是一样的
console.log(toRaw(obj.value));

toRefs

  ref中还有一个常用的函数就是toRefs,复制reactive中的所有属性并转成ref对象,我们先来看下它的用法:

1
2
3
4
5
const state = reactive({
foo: 1,
bar: 2,
});
const { foo, bar } = toRefs(state)

  下面就来看下它的源码逻辑:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
export function toRefs<T extends object>(object: T): ToRefs<T> {
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = propertyToRef(object, key)
}
return ret
}

  这里先对传入的object进行是否数组的判断,如果是数组,创建相同长度的数组;否则,创建一个空对象,最后返回这个对象。

  这里的object其实是我们传入的reactive对象,但是我们发现这里object还可以传入数组,因此,我们可以写出下面的代码:

1
2
3
4
5
6
7
const list = reactive([{ a: 11 }, { a: 22 }]);

const res = toRefs(list);

setTimeout(() => {
res[0].value.a = 111;
}, 800);

一般没有人这么干。

  回到toRefs,我们发现对object调用了propertyToRef函数,这里的key就是object上每个属性的名称;我们继续看propertyToRef做了什么:

1
2
3
4
5
6
7
8
9
10
function propertyToRef(
source: Record<string, any>,
key: string,
defaultValue?: unknown,
) {
const val = source[key]
return isRef(val)
? val
: (new ObjectRefImpl(source, key, defaultValue) as any)
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

Record类型

  我们看到propertyToRef中传入的参数source是一个Record类型,这里简单介绍一下这个类型;Record是ts内置的一个高级类型,可以让我们来定义具有特定键和相应值类型的对象类型;它的语法也很简单,后面是一对尖括号,尖括号中两个分别表示定义对象的键的类型和值的类型:

1
2
3
4
5
6
7
8
type MyRecord = Record<string, number>;

const obj: MyRecord = {
aa: 1,
bb: 2
};

console.log(obj.aa)

  这边我们定义了一个MyRecord类型,具有字符串和数字的对象类型;相比于object类型,我们直接访问object上面的属性会报错:

1
2
3
4
5
6
7
const obj1: object = {
aa: 1,
bb: 2
};

// Error:类型“object”上不存在属性“aa”。
console.log(obj1.aa)

  我们还可以使用联合类型来限制对象的键,限制为特定的字符串:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
type Names = 'alice' | 'bob' | 'charlie';

const obj: Record<Names, number> = {
alice: 1,
bob: 2,
charlie: 3,
};

  但是这种写法要求我们将所有的键都写完全,不然会提示报错;可以结合Partial,将所有的键变为可选的:

1
2
3
const obj: Partial<Record<Names, number>> = {
alice: 1,
};

  这样我们就不需要把所有的属性都写全了。除了将值定义为基本类型,我们还可以定义为对象;比如定义一个学生的字典对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Sex {
Male,
Female,
}

interface Student {
id: number;
name: string;
sex: Sex;
}

const map: Record<string, Student> = {
chen: {
id: 0,
name: 'Chen',
sex: Sex.Male,
},
li: {
id: 0,
name: 'Li',
sex: Sex.Female,
},
};

  总结一下,Record类型是ts中一个强大并且灵活的工具,可以很方便的让我们定义带有特定键值对的对象。

ObjectRefImpl类

  回到上面propertyToRef函数中,我们看到又封装了一个新的类ObjectRefImpl,它的源码如下:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly [ReactiveFlags.IS_REF] = true
public _value: T[K] = undefined!
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K],
) {}
get value() {
const val = this._object[this._key]
return (this._value = val === undefined ? this._defaultValue! : val)
}
set value(newVal) {
this._object[this._key] = newVal
}
// 省略部分代码
}

  这里constructor的缩略写法写法有些小伙伴可能会看不懂了,我们稍微解释一下;constructor传入了带有修饰符的参数,这样ts就会自动帮我们在类里声明实例上的属性,不用我们在下面去吭哧吭哧地一个一个显示地写出来了;因此其实上面的constructor写法就相当于如下:

1
2
3
4
5
6
7
8
9
10
11
12
class ObjectRefImpl<T extends object, K extends keyof T> {
private readonly _object: T;
private readonly _key: K;
private readonly _defaultValue?: T[K];

constructor(_object: T, _key: K, _defaultValue?: T[K]) {
this._object = _object;
this._key = _key;
this._defaultValue = _defaultValue;
}
// 其他代码
}

因此缩略写法中constructor中的修饰符不能省略掉,具体的用法可以参考实例属性的简写形式

  回到ObjectRefImpl类中,它里面的属性其实很简单,一个_object,存储的是我们传进入的reactive对象;_key是reactive对象上需要映射的一个键名;_defaultValue是默认的值;我们发现ObjectRefImpl这个类做的事情其实非常简单,就是把reactive对象的取值和赋值过程进行了一下封装。

toRef

  最后一个常用的函数就是toRef,我们主要用它来复制reactive里面的单个属性然后转成ref对象,既保留了响应式,也保留了对原有reactive的引用;我们先看下它的常见用法:

1
2
3
4
5
const state = reactive({
foo: 1,
bar: 2,
});
const fooRef = toRef(state, 'foo');

  看到这里很多小伙伴可能也想到了,上面toRefs源码中不是有propertyToRef函数么,只要传入封装一下不就完事了。

  但是实际上这个只是它的其中一个用法,toRef支持传入多种类型的数据;下面我们看下它里面的主要实现源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function toRef(
source: Record<string, any> | MaybeRef,
key?: string,
defaultValue?: unknown,
): Ref {
if (isRef(source)) {
return source
} else if (isFunction(source)) {
return new GetterRefImpl(source) as any
} else if (isObject(source) && arguments.length > 1) {
return propertyToRef(source, key!, defaultValue)
} else {
return ref(source)
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这里分为多种情况,首先第一种情况,如果传入的参数本来就是ref对象,则返回自己,这里不在赘述了;第二种情况,isFunction检查参数是否是一个函数,因此我们可以在toRef中传入一个函数,比如像下面这种写法:

1
2
3
4
5
6
7
8
9
10
const state = reactive({
foo: 1,
bar: 2,
});

const barRef: Readonly<Ref<number>> = toRef(() => state.bar);

setTimeout(() => {
state.bar = 4;
}, 1000);

  这里由于传入的是函数,返回的是一个只读的Ref,我们不能去修改它的value值;回到toRef函数中,我们看到又将source(函数)传入了GetterRefImpl类,我们看它的实现逻辑:

1
2
3
4
5
6
7
8
9
class GetterRefImpl<T> {
// 省略几个标识属性
public _value: T = undefined!

constructor(private readonly _getter: () => T) {}
get value() {
return (this._value = this._getter())
}
}

  这里constructor的缩略写法和上面一样,就不再赘述了。这里的_getter参数本质上就是一个函数,然后返回泛型T,因此它的类型其实就是() => T;然后GetterRefImpl类的getter函数就调用这个函数并返回结果。

  toRef函数的第三种情况,如果source是对象,并且还有其他参数,调用propertyToRef函数,就是我们最常见的用法了。

  最后一种情况如果是其他类型,直接返回一个ref对象:

1
2
3
toRef(1)

toRef('2')

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  因此我们用toRef传入基本类型,其实和直接调用ref函数的效果是一样的。

一些细节问题

  上面我们只注重ref函数整体的逻辑,忽略了一些细节问题,本小结我们就来看下细节问题;比如我们上面提到,带有下划线的属性一般是类的私有属性,不会对外暴露;但是我们看到_value定义的时候没有加private,而_rawValue加了private:

1
2
3
4
class RefImpl<T = any> {
_value: T
private _rawValue: T
}

  因此其实_value属性默认就是public的,这就和我们的认知不符了,但是我们在ref对象上确实也访问不到_value。当事实和我们的认知有所冲突时,肯定是哪里出了问题,这里就需要耐心以及合理的猜测假设了。

  我们继续看ref的类型定义,上面确实没有_value,因此这也就是我们在编辑器里面访问_value属性会报错的根本原因所在。

1
2
3
4
5
interface Ref<T = any, S = T> {
get value(): T
set value(_: S)
[RefSymbol]: true
}

  那么既然外面访问不到_value,那么笔者就大胆的猜测,是不是需要在源码内部去访问呢?经过全局查找,总算在笔者忽略的一个函数中看到了_value的身影:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function triggerRef(ref: Ref): void {
if ((ref as unknown as RefImpl).dep) {
if (__DEV__) {
;(ref as unknown as RefImpl).dep.trigger({
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
// 在这里访问了_value属性
newValue: (ref as unknown as RefImpl)._value,
})
} else {
;(ref as unknown as RefImpl).dep.trigger()
}
}
}

  我们看到在newValue中用到了RefImpl类的_value,这就解释了上面为什么_value不是定义成了private。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

总结

  这里我们对ref函数源码做个简单的总结,本文从ref和shallowRef函数入手,找到了它们共同的封装函数createRef,然后找到了ref的核心类RefImpl,ref函数传入的值实际上都存储在它的_value属性中,基本类型的响应式是通过_value的getter和setter函数,而复杂类型则是继续使用reactive.ts中的toReactive转化成代理对象,实现响应式的。

  然后继续对ref相关的几个函数unref、toRefs和toRef进行了说明,同时通过源码我们学到了constructor中参数的缩略写法,还有Record类型定义的对象类型,这些小技巧都可以极大的方便我们后续项目中ts的使用。

  本来笔者是打算把reactive的源码一起写的,但是随着ref中的类越看越多,本文的内容也越来越多,因此笔者将reactive源码的解析后面会单独一篇文章,希望大家多多点赞收藏,更新会更快哦。

获取Vue3源码敬请关注公众号【前端壹读】,后台回复关键词【Vue3源码】即可获取。


谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。