第五章:响应系统 - ref 的响应性
01:前言
在上一章中我们完成了 reactive
函数,同时也知道了 reactive
函数的局限性,知道了只靠 reactive
函数,vue
是没有办法构建出完善的响应式系统的。
所以我们还需要另外一个函数 ref
。
本章我们将致力于解决以下三个问题:
ref
函数是如何进行实现的呢?ref
可以构建简单数据类型的响应性吗?- 为什么
ref
类型的数据,必须要通过.value
访问值呢?
接下来,准备好了吗?我们开始吧
02:源码阅读:ref 复杂数据类型的响应性
和学习 reactive
的时候一样,我们首先先来看一下 ref
函数下,vue 3
源码的执行过程。
- 创建测试实例
packages/vue/examples/imooc/ref.html
<body>
<div id="app"></div>
</body>
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000);
</script>
- 通过
Live Server
运行测试实例 ref
的代码位于packages/reactivity/src/ref.ts
之下,我们可以在这里打下断点
ref 函数
ref
函数中,直接触发createRef
函数在
createRef
中,进行了判断如果当前已经是一个ref
类型数据则直接返回,否则 返回RefImpl
类型的实例那么这个
RefImpl
是什么呢?RefImpl
是同样位于packages/reactivity/src/ref.ts
之下的一个类该类的构造函数中,执行了一个
toReactive
的方法,传入了value
并把返回值赋值给了this._value
,那么我们来看看toReactive
的作用:toReactive
方法把数据分成了两种类型:- 复杂数据类型:调用了
reactive
函数,即把value
变为响应性的。 - 简单数据类型:直接把
value
原样返回
- 复杂数据类型:调用了
该类提供了一个分别被
get
和set
标记的函数value
- 当执行
xxx.value
时,会触发get
标记 - 当执行
xxx.value = xxx
时,会触发set
标记
- 当执行
至此 ref 函数执行完成。
由以上逻辑可知:
- 对于
ref
而言,主要生成了RefImpl
的实例 - 在构造函数中对传入的数据进行了处理:
- 复杂数据类型:转为响应性的
proxy
实例 - 简单数据类型:不去处理
- 复杂数据类型:转为响应性的
RefImpl
分别提供了get value
、set value
以此来完成对getter
和setter
的监听,注意这里并没有使用proxy
effect 函数
当 ref
函数执行完成之后,测试实例开始执行 effect
函数。
effect
函数我们之前跟踪过它的执行流程,我们知道整个 effect
主要做了3 件事情:
生成
ReactiveEffect
实例触发
fn
方法,从而激活getter
建立了
targetMap
和activeEffect
之间的联系dep.add(activeEffect)
activeEffect.deps.push(dep)
通过以上可知,effect 中会触发 fn 函数,也就是说会执行 obj.value.name ,那么根据 get value 机制,此时会触发 RefImpl 的 get value 方法。
所以我们可以在 117
行增加断点,等代码进入 get value
get value()
在
get value
中会触发trackRefValue
方法触发
trackEffects
函数,并且在此时为ref
新增了一个dep
属性:jstrackEffects(ref.dep || (ref.dep = createDep())...}
而
trackEffects
其实我们是有过了解的,我们知道trackEffects
主要的作用就是:收集所有的依赖
至此
get value
执行完成
由以上逻辑可知:
整个 get value
的处理逻辑还是比较简单的,主要还是通过之前的 trackEffects
属性来收集依赖。
再次触发 get value()
最后就是在两秒之后,修改数据源了:
obj.value.name = '李四'
但是这里有一个很关键的问题,需要大家进行思考,那就是:此时会触发 get value
还是 set value
?
我们知道以上代码可以被拆解为:
const value = obj.value
value.name = '李四'
那么通过以上代码我们清晰可知,其实触发的应该是 get value
函数。
在 get value
函数中:
再次执行
trackRefValue
函数:- 但是此时
activeEffect
为undefined
,所以不会执行后续逻辑
- 但是此时
返回
this._value
:- 通过 构造函数,我们可知,此时的
this._value
是经过toReactive
函数过滤之后的数据,在当前实例中为proxy
实例。
- 通过 构造函数,我们可知,此时的
get value
执行完成
由以上逻辑可知:
const value
是proxy
类型的实例,即:代理对象,被代理对象为{name: '张三'}
- 执行
value.name = '李四'
,本质上是触发了proxy
的setter
- 根据
reactive
的执行逻辑可知,此时会触发trigger
触发依赖。 - 至此,修改视图
总结:
由以上逻辑可知:
- 对于
ref
函数,会返回RefImpl
类型的实例 - 在该实例中,会根据传入的数据类型进行分开处理
- 复杂数据类型:转化为
reactive
返回的proxy
实例 - 简单数据类型:不做处理
- 复杂数据类型:转化为
- 无论我们执行
obj.value.name
还是obj.value.name = xxx
本质上都是触发了get value
- 之所以会进行 响应性 是因为
obj.value
是一个reactive
函数生成的proxy
03:框架实现:ref 函数 - 构建复杂数据类型的响应性
在上一小节中,我们已经查看了 vue 3
中 ref
函数针对复杂数据类型的响应性处理代码逻辑,那么这一小节,我们就可以实现一下对应的代码。
- 创建
packages/reactivity/src/ref.ts
模块:
import { createDep, Dep } from './dep'
import { activeEffect, trackEffects } from './effect'
import { toReactive } from './reactive'
export interface Ref<T = any> {
value: T
}
/**
* ref 函数
* @param value unknown
*/
export function ref(value?: unknown) {
return createRef(value, false)
}
/**
* 创建 RefImpl 实例
* @param rawValue 原始数据
* @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
* @returns
*/
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
// 是否为 ref 类型数据的标记
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 如果 __v_isShallow 为 true,则 value 不会被转化为 reactive 数据,即如果当前 value 为复杂数据类型,则会失去响应性。对应官方文档 shallowRef :https://cn.vuejs.org/api/reactivity-advanced.html#shallowref
this._value = __v_isShallow ? value : toReactive(value)
}
/**
* get语法将对象属性绑定到查询该属性时将被调用的函数。
* 即:xxx.value 时触发该函数
*/
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {}
}
/**
* 为 ref 的 value 进行依赖收集工作
*/
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
/**
* 指定数据是否为 RefImpl 类型
*/
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
- 在
packages/reactivity/src/reactive.ts
中,新增toReactive
方法:
/**
* 将指定数据变为 reactive 数据
*/
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value
- 在
packages/shared/src/index.ts
中,新增isObject
方法:
/**
* 判断是否为一个对象
*/
export const isObject = (val: unknown) =>
val !== null && typeof val === 'object'
- 在
packages/reactivity/src/index.ts
中,导出ref
函数:
...
export { ref } from './ref'
- 在
packages/vue/src/index.ts
中,导出ref
函数::
export { reactive, effect, ref } from '@vue/reactivity'
至此,ref
函数构建完成。
我们可以增加测试案例 packages/vue/examples/reactivity/ref.html
中:
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000);
</script>
可以发现代码测试成功。
04:总结:ref 复杂数据类型的响应性
根据以上代码实现我们知道,针对于 ref
的复杂数据类型而言,它的响应性本身,其实是 利用 reactive
函数 进行的实现,即:
const obj = ref({
name: '张三'
})
const obj = reactive({
name: '张三'
})
本质上的实现方案其实是完全相同的,都是利用 reactive
函数,返回一个 proxy
实例,监听 proxy
的 getter
和 setter
进行的依赖收集和依赖触发。
但是它们之间也存在一些不同的地方,比如:
ref
:ref
的返回值是一个RefImpl
类型的实例对象- 想要访问
ref
的真实数据,需要通过.value
来触发get value
函数,得到被toReactive
标记之后的this._value
数据,即:proxy
实例
reactive
:reactive
会直接返回一个proxy
的实例对象,不需要通过.value
属性得到
同时我们也知道,对于 reactive
而言,它是不具备 简单数据类型 的响应性呢,但是 ref
是具备的。
那么 ref
是如何处理 简单数据类型 的响应性的呢?我们继续来往下看~~~
05:源码阅读:ref 简单数据类型的响应性
这一小节,我们将来看一下:ref
针对简单数据类型的响应性。
和之前一样我们需要先创建对应的测试案例:
- 我们创建如下测试案例 创建测试实例
packages/vue/examples/imooc/ref-shallow.html
:
const { ref, effect } = Vue
const obj = ref('张三')
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000);
该测试案例,利用 ref
创建了一个 简单数据类型 的响应性。那么我们接下来就借助该案例,来看下 vue3
对简单数据类型的响应性的处理。
ref 函数
整个 ref
初始化的流程与 02
小节完全相同,但是有一个不同的地方,需要 特别注意:因为当前不是复杂数据类型,所以在 toReactive
函数中,不会通过 reactive
函数处理 value
。所以 this._value
不是 一个 proxy
。即:无法监听 setter
和 getter
。
effect 函数
整个 effect
函数的流程与 02
小节完全相同。
get value()
整个 effect
函数中引起的 get value()
的流程与 02
小节完全相同。
大不同:set value()
延迟两秒钟,我们将要执行 obj.value = '李四'
的逻辑。我们知道在复杂数据类型下,这样的操作(obj.value.name = '李四'
),其实是触发了 get value
行为。
但是,此时,在 简单数据类型之下,obj.value = '李四'
触发的将是 set value
形式,这里也是 ref
可以监听到简单数据类型响应性的关键。
- 跟踪代码,进入到
set value(newVal)
:- 通过
hasChanged
方法,对比数据是否发生变化 - 发生变化,则触发
triggerRefValue
,进入:- 执行
triggerEffects
触发依赖,完成响应性
- 执行
- 通过
由以上代码可知:
- 简单数据类型的响应性,不是基于
proxy
或Object.defineProperty
进行实现的,而是通过:set
语法,将对象属性绑定到查询该属性时将被调用的函数 上,使其触发xxx.value = '李四'
属性时,其实是调用了xxx.value('李四')
函数。 - 在
value
函数中,触发依赖
所以,我们可以说:对于 ref
标记的简单数据类型而言,它其实 “并不具备响应性”,所谓的响应性只不过是因为我们 主动触发了 value
方法 而已。
总结
目前我们已经明确了 ref
针对于简单数据类型的 “响应性” 的问题,那么下面我们就可以针对我们的理解,来对这一块进行对应的实现了。
06:框架实现:ref 函数 - 构建简单数据类型的响应性
- 在
packages/reactivity/src/ref.ts
中,完善set value
函数:
class RefImpl<T> {
private _value: T
private _rawValue: T
...
constructor(value: T, public readonly __v_isShallow: boolean) {
...
// 原始数据
this._rawValue = value
}
...
set value(newVal) {
/**
* newVal 为新数据
* this._rawValue 为旧数据(原始数据)
* 对比两个数据是否发生了变化
*/
if (hasChanged(newVal, this._rawValue)) {
// 更新原始数据
this._rawValue = newVal
// 更新 .value 的值
this._value = toReactive(newVal)
// 触发依赖
triggerRefValue(this)
}
}
}
/**
* 为 ref 的 value 进行触发依赖工作
*/
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
- 在
packages/shared/src/index.ts
中,新增hasChanged
方法
/**
* 对比两个数据是否发生了改变
*/
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
至此,简单数据类型的响应性处理完成。
我们可以创建对应测试实例:packages/vue/examples/reactivity/ref-shallow.html
<script>
const { ref, effect } = Vue
const obj = ref('张三')
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000);
</script>
测试成功,表示代码完成。
07:总结:ref 简单数据类型响应性
那么到这里我们实现了 ref
简单数据类型的响应性处理。
在这样的代码,我们需要知道的最重要的一点是:简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。
只是因为 vue
通过了 set value()
的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 “类似于” 响应性的结果。
08:总结
那么到这里我们就已经完成了 ref
响应性函数的构建,那么大家还记不记得开篇时所问的三个问题:
ref
函数是如何进行实现的呢?ref
可以构建简单数据类型的响应性吗?- 为什么
ref
类型的数据,必须要通过.value
访问值呢?
大家现在再次面对这三个问题,是否能够回答出来呢?
- 问题一:
ref
函数是如何进行实现的呢?ref
函数本质上是生成了一个RefImpl
类型的实例对象,通过get
和set
标记处理了value
函数
- 问题二:
ref
可以构建简单数据类型的响应性吗?- 是的。
ref
可以构建简单数据类型的响应性
- 是的。
- 问题三:为什么
ref
类型的数据,必须要通过.value
访问值呢?- 因为
ref
需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过proxy
建立代理。 - 所以
vue
通过get value()
和set value()
定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖 - 所以我们必须通过
.value
来保证响应性。
- 因为