前言
在上一章中我们完成了 reactive
函数,同时也知道了 reactive
函数的局限性,知道了只靠 reactive
函数,vue
是没有办法构建出完善的响应式系统的。
所以我们还需要另外一个函数 ref
。
本章我们将致力于解决以下三个问题:
ref
函数是如何进实现的?ref
是如何构建简单数据类型的?- 为什么
ref
类型的数据,必须要通过.value
访问?
1. ref 复杂类型数据的响应性
我们知道 ref
其实也是可以实现复杂类型数据的响应性的,那么它是如何实现的呢?我们从下面这段程序开始研究
<div id="app"></div>
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
1.1 源码阅读
- 我们直接进入源码
packages/reactivity/src/ref.ts
之下,找的 ref 函数的实现,并在这里打下断点。
- 可以看到
ref
函数中最后就是返回了一个RefImpl
对象,我们进到RefImpl
类中。
RefImpl
类的构造函数中 执行了一个toReactive
的方法,传入了value
并把返回值赋值给了this._value
,那么我们来看看toReactive
的作用
toReactive
方法把数据分成了两种类型:1. 复杂类型调用了reactive
函数,即把value
变为响应性的。2.简单数据类型:直接把 value 原样返回。- 而且,
RefImpl
类 还 提供了一个分别被get
和set
标记的函数value
。1.当执行xxx.value
时,会触发get
标记。2.当执行xxx.value = xxx
时,会触发set
标记。
至此 ref
函数执行完成。
- 接下来开始执行
effect
函数。effect
函数我们在上一张的时候跟踪过它的执行流程。我们知道整个effect
主要做了3
件事情:1.生成ReactiveEffect
实例。2.触发fn
方法,从而激活getter
。3. 建立了targetMap
和activeEffect
之间的联系。 - 通过上述可知,在执行
obj.value.name = '张三'
时,会执行RefImpl
类中的get value
方法,而get value
方法中 实际执行的是trackRefValue
,我们直接跳到trackRefValue
中
- 在
trackRefValue
中,触发了trackEffects
函数,并且在此时为ref
新增了一个dep
属性。而trackEffects
其实我们是有过了解的,我们知道trackEffects
主要的作用就是:收集所有的依赖
至此 get value
执行完成
- 接着,在两秒之后,修改数据源了:
obj.value.name = '李四'
,这里的步骤可以拆分成两步
const value = obj.value
value.name = '李四'
- 第一步
const value = obj.value
,此时还会触发一遍get value
中的trackRefValue
函数。
- 但是这次不一样了, 这次
activeEffect
为undefined
,所以不会执行后续逻辑,直接返回this._value
- 第二步
value.name = '李四'
, 因为 这里的value
是toReactive
转化而来的proxy
对象,根据reactive
的执行逻辑可知,此时会触发trigger
触发依赖。 - 至此,视图上的文字改为
李四
,程序结束
总结:
- 对于
ref
函数,会返回RefImpl
类型的实例 在该实例中,会根据传入的数据类型进行分开处理
- 复杂数据类型:转化为
reactive
返回的proxy
实例 - 简单数据类型:不做处理
- 复杂数据类型:转化为
- 无论我们执行
obj.value.name
还是obj.value.name = xxx
本质上都是触发了get value
4,之所以会进行 响应性 是因为 obj.value
是一个 reactive
函数生成的 proxy
1.2 代码实现
- 创建
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 isArray = Array.isArray
/**
* 判断是否为一个对象
*/
export const isObject = (val: unknown) =>
val !== null && typeof val === 'object'
- 在
packages/reactivity/src/index.ts
中,导出ref
函数: - 在
packages/vue/src/index.ts
中,导出ref
函数::
至此,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>
可以发现代码测试成功。
2. ref 简单数据类型的响应性
我们继续从下面的代码研究 ref
是如何实现简单数据类型的响应性的
<script>
const { ref, effect } = Vue
const obj = ref('张三')
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
2.1 源码阅读
ref 函数
整个 ref
初始化的流程和上一小节完全相同,但是有一个不同的地方,需要 特别注意:因为当前不是复杂数据类型,所以在 toReactive
函数中,不会通过 reactive
函数处理 value
。所以 this._value
不是 一个 proxy
。即:无法监听 setter
和 getter
。
effect 函数
整个 effect 函数的流程与上一小节完全相同。
get value()
整个 effect 函数中引起的 get value() 的流程与上一小节完全相同。
大不同:set value()
延迟两秒钟,我们将要执行 obj.value = '李四'
的逻辑。我们知道在复杂数据类型下,这样的操作(obj.value.name = '李四'
),其实是触发了 get value
行为。
但是,此时,在 简单数据类型之下,obj.value = '李四'
触发的将是 set value
形式,这里也是 ref
可以监听到简单数据类型响应性的关键。跟踪代码,进入到 set value(newVal)
:
由以上代码可知:
- 简单数据类型的响应性,不是基于
proxy
或Object.defineProperty
进行实现的,而是通过:set
语法,将对象属性绑定到查询该属性时将被调用的函数 上,使其触发xxx.value = '李四'
属性时,其实是调用了xxx.value('李四')
函数。 - 在
value
函数中,触发依赖
总结:
简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。
只是因为 vue
通过了 set value()
的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 “类似于” 响应性的结果。
2.2 代码实现
- 在
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>
测试成功,表示代码完成。
3. 总结
我们现在来回答一下 前言中的三个问题
ref
函数是如何进实现的?ref
函数本质上是生成了一个RefImpl
类型的实例对象,通过get
和set
标记处理了value
函数ref
是如何构建简单数据类型的?ref
通过get value()
和set value()
定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖- 为什么
ref
类型的数据,必须要通过.value
访问值呢?因为
ref
需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过proxy
建立代理。只能通过get value()
和set value()
的方式来处理对依赖的收集和触发,所以我们必须通过.value
来保证响应性。