案例
通过以下这个案例来进行理解,首先引入ref
和effect
两个函数,之后声明name
响应式数据,接着又执行effect
函数,该函数传入了一个匿名函数,最后两秒后又修改name
值
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { ref, effect } = Vue
const name = ref('jc')
effect(() => {
document.querySelector('#app').innerHTML = name.value
})
setTimeout(() => {
name.value = 'cc'
}, 2000)
</script>
</body>
</html>
createRef
方法
ref
函数定义在packages/reactivity/src/ref.ts
文件下:
export function ref(value?: unknown) {
return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
ref
函数实际执行的是createRef
方法,而该方法实际是返回了一个RefImpl
构造函数的实例对象,它会接收传入的值,可能是基本类型也可能是复杂类型,通过 _rawValue
记录原始值,用于之后依赖触发时新旧值的比较
我们需关注this._value = __v_isShallow ? value : toReactive(value)
这行代码,toReactive
函数被定义在packages/reactivity/src/reactive.ts
中:
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
可以看出,如果传入的数据为对象类型则调用 reactive
方法,这块逻辑可参考reactive
的源码分析;否则就直接返回当前值,此时ref
执行完毕,当前name
被赋值为jc
:
再回过头看下RefImpl
构造函数中还有get value()
和set value()
这两个方法,举个例子:
// RefImpl 构造函数
class RefImpl {
// 实例的 getter 行为: ref.value
get value() {
return 'get value'
}
// 实例的 setter 行为: ref.value = xxx
set value(newVal) {
console.log('set value')
}
}
const newRef = new RefImpl()
console.log(newRef)
输出结果如下:
当执行newRef.value
时会触发getter
,而修改值时会触发setter
,这也是为什么我们赋值或者修改 ref
值时需要加上 .value
对于基本类型的数据 ref
是不具备数据监听的,当赋值或修改值时主动触发了 get
和 set
方法, 之后执行effect
函数,接着执行赋值行为触发get
方法
get/set
get
方法核心trackRefValue(this)
实际触发了trackRefValue
方法进行数据的依赖收集,该方法定义在packages/reactivity/src/ref.ts
文件中:
实际执行的是triggerEffects
,该方法定义在packages/reactivity/src/effect.ts
,这块逻辑同reactive
,给指定属性绑定对应的fn
,将 dep
对象与 ReactiveEffect
相关联,完成整个依赖收集的过程
之后两秒后进行修改值触发set
方法:
set
方法中triggerRefValue(this, newVal)
进行依赖触发:
triggerRefValue
方法实际执行triggerEffects
,该方法定义在packages/reactivity/src/effect.ts
文件中:
上面reactive
也分析了这块逻辑,最终执行的是每个 effect.run
方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成
总结
1) ref
函数本质上做了三件事
- 返回
RefImpl
的实例 - 对数据处理,如果当前数据为基本类型则直接返回;如果是为复杂类型则调用
reactive
返回reactive
数据 RefImpl
提供get value
和set value
方法,这就是为什么设置ref
值时,需要带上.value
2) ref
基本类型的数据不具备数据监听,赋值或修改值都是主动触发 get
和 set
方法
3)为什么ref
类型数据,必须要通过.value
访问值呢?
- 因为
ref
需要处理基本数据类型的响应性,但是对于基本类型数据而言,它无法通过proxy
建立代理 - 而
vue
通过get value()
和set value()
定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和依赖触发, 所以我们必须通过.value
来保证响应性