在上一章中我们完成了 reactive
函数,同时也知道了 reactive
函数的局限性,知道了只靠 reactive
函数,vue
是没有办法构建出完善的响应式系统的。
所以我们还需要另外一个函数 ref
。
本章我们将致力于解决以下三个问题:
ref
函数是如何进实现的?ref
是如何构建简单数据类型的?ref
类型的数据,必须要通过 .value
访问?我们知道 ref
其实也是可以实现复杂类型数据的响应性的,那么它是如何实现的呢?我们从下面这段程序开始研究
html<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>
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
中
8. 在
trackRefValue
中,触发了 trackEffects
函数,并且在此时为 ref
新增了一个 dep
属性。而 trackEffects
其实我们是有过了解的,我们知道 trackEffects
主要的作用就是:收集所有的依赖
至此 get value
执行完成
obj.value.name = '李四'
,这里的步骤可以拆分成两步tsconst 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
packages/reactivity/src/ref.ts
模块:tsimport { 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
方法:ts/**
* 将指定数据变为 reactive 数据
*/
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value
packages/shared/src/index.ts
中,新增 isObject
方法:ts/**
* 判断是否为一个数组
*/
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
中:
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>
可以发现代码测试成功。
我们继续从下面的代码研究 ref
是如何实现简单数据类型的响应性的
html<script>
const { ref, effect } = Vue
const obj = ref('张三')
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
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()
的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 “类似于” 响应性的结果。
packages/reactivity/src/ref.ts
中,完善 set value
函数:tsclass 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
方法ts/**
* 对比两个数据是否发生了改变
*/
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
至此,简单数据类型的响应性处理完成。
测试
创建对应测试实例:packages/vue/examples/reactivity/ref-shallow.html
html<script>
const { ref, effect } = Vue
const obj = ref('张三')
// 调用 effect 方法
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
测试成功,表示代码完成。
我们现在来回答一下 前言中的三个问题
ref
函数是如何进实现的?
ref
函数本质上是生成了一个 RefImpl
类型的实例对象,通过 get
和 set
标记处理了 value
函数
ref
是如何构建简单数据类型的?
ref
通过 get value()
和 set value()
定义了两个属性函数,通过 主动 触发这两个函数(属性调用)的形式来进行 依赖收集 和 触发依赖
为什么 ref
类型的数据,必须要通过 .value
访问值呢?
因为 ref
需要处理简单数据类型的响应性,但是对于简单数据类型而言,它无法通过 proxy
建立代理。只能通过 get value()
和 set value()
的方式来处理对依赖的收集和触发,所以我们必须通过 .value
来保证响应性。
本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!