vue3的ref之复杂数据类型
源码阅读与调试
ref的测试实例:
ref.html
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
console.log(obj)
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
首先是ref函数:
ts
export function ref(value?: unknown) {
return createRef(value, false)
}
接着进入createRef:
ts
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
第2行isRef
判断是否是ref;
ts
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
接着我们看class RefImpl
;
ts
class RefImpl<T> {
private _value: T
private _rawValue: T
// dep是一个Set类型,它用来保存多个effect
public dep?: Dep = undefined
// 用来判断是否是ref数据的标志
public readonly __v_isRef = true
// value是{name: "张三"}对象
// __v_isShallow表示是否是浅层的
constructor(value: T, public readonly __v_isShallow: boolean) {
// 此时的rawValue还是value
this._rawValue = __v_isShallow ? value : toRaw(value)
// _value是value执行reactive,ref的响应性由reactive实现的;
this._value = __v_isShallow ? value : toReactive(value)
}
// 省略...
}
constructor中_value是一个Proxy;
ts
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
// 如果value是对象时,执行reactive方法;
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value
接着往下执行,到了effect;effect方法在上篇文章详细讲到,这里就不再赘述了;
现在我们写一个测试实例去分析class RefImpl
的get
和set
方法;
js
<script>
class RefImpl {
// 实例的getter行为
get value() {
console.log('get value')
return 'get Value'
}
// 实例的setter行为
set value(val) {
console.log('set value')
}
}
const ref = new RefImpl()
console.log(ref)
</script>
因为实例中effect中会触发getter,所以来看class RefImpl
的get方法:
js
class RefImpl<T> {
// 省略...
get value() {
trackRefValue(this)
// 返回_value,它是一个Proxy
return this._value
}
}
接下来看一下trackRefValue
方法;
js
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
// 上篇文章详细介绍过trackEffects方法
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
接下来实例中setTimeout中会再一次触发Getter方法,这次的activeEffect是undefined,所以不会再次触发依赖收集的行为;接着setTimeout还会触发一次Getter方法; 总结:
- 对于ref函数,会返回RefImpl类型的实例;
- 在该实例中,根据传入的数据类型进行分开处理; .复杂数据类型:转化成reactive,返回Proxy;简单数据类型:不做处理;
- 无论我们执行obj.value.name,还是obj.value.name = XXX本质上都是触发了get value;
- 之所以会进行响应性是因为obj.value是一个reactive函数生成的Proxy;
实现vue3的ref之复杂数据类型
新建packages/reactivity/src/ref.ts;
ts
import { toReactive } from "./reactive";
import { activeEffect, trackEffects, triggerEffects } from "./effect";
import { createDep, Dep } from "./dep";
export interface Ref<T = any> {
value: T;
}
export function isRef(r: any) {
return !!(r && r.__v_isRef === true);
}
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);
}
class RefImpl<T> {
private _value;
public dep?: Dep = undefined;
public readonly __v_isRef = true;
constructor(value: T, public readonly __v_isShallow: boolean) {
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
}
}
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep);
}
}
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
在reactive.ts中增加:
ts
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value as object) : value;
在packages/shared/index.ts中增加;
ts
export const isObject = (val: unknown) =>
val !== null && typeof val === "object";
记得在两个地方导出ref方法;
接下来是我们的测试实例:
js
<script>
const { ref, effect } = Vue;
const obj = ref({
name: "张三",
});
effect(() => {
document.querySelector("#app").innerText = obj.value.name;
});
setTimeout(() => {
obj.value.name = "李四";
}, 1000);
</script>
浏览器中可以看到张三
成功变成了李四
,我们的ref已经完全实现了
vue3的ref之基本数据类型
源码阅读与调试
首先我们创建一个实例:
js
<script>
const { ref, effect } = Vue
const obj = ref('张三')
console.log(obj)
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
此时先执行ref方法;然后执行createRef方法;
ts
// 这里的rawValue是"张三"
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
然后进入到class RefImpl中;
ts
class RefImpl<T> {
// 略...
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 这里value是基本数据类型,和上面的复杂类型不同,它不执行toReactive方法;
this._value = __v_isShallow ? value : toReactive(value)
}
}
然后执行effect方法;这里的流程和之前是一样的;接着执行setTimeout的回调方法触发setter;这里和复杂数据最大的一点不同就是在settimeout中,复杂数据类型执行的是Getter,而基本数据类型执行的是Setter
js
class RefImpl<T> {
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// newVal是"李四",_rawValue是"张三",hasChanged方法来判断两个值是否相等;
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// __v_isShallow是false,因此执行toReactive
// this._value的值变成了"李四"
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
接着进行triggerRefValue方法:
ts
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
// triggerEffects方法之前都实现过;
triggerEffects(ref.dep)
}
}
}
思考问题:为什ref类型的数据,必须通过.value访问值呢?
总结:
ref的简单数据类型响应性不具备数据监听 的概念,即本身并不是响应性的 ;只是因为vue通过set value()的语法,把函数调用变成了属性调用的形式,让我们主动调用该函数,来完成类似于响应性的结果;
实现vue3的ref之基本数据类型
在pacakges/reactive/src/ref.ts中修改;
ts
class RefImpl<T> {
set value(newVal) {
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this);
}
}
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep);
}
}
写一个测试实例:
js
<script>
const { ref, effect } = Vue;
const obj = ref("张三");
effect(() => {
document.querySelector("#app").innerText = obj.value;
});
setTimeout(() => {
obj.value = "李四";
}, 2000);
</script>
此时我们的ref就实现了!