<<往期回顾>>
手写vue3源码——创建项目
手写vue3源码——reactive, effect ,scheduler, stop
手写vue3源码——readonly, isReactive,isReadonly, shallowReadonly
本期主要实现的api有,ref, isRef, unRef, proxyRefs, computed,本次所有的源码请查看
在代码中,ref
这个api也是用的很频繁的,所以今天咋们就一起来实现下
ref处理的数据有两种, 原始值类型和引用值类型,不管是
get
原始值还是引用类型的值都需要使用.value
的形式来获取,set
的时候也是同用的需要使用.value
来进行操作
既然都需要使用 .value,是不是意味着,传入的数据都会被一个对象所包裹,基于这个特点,咋们是否可以使用class 里面有get, set 方法呢?class 本身是一个实例对象,刚好里面的get,set 可以对属性进行拦截存取行为, 详情请查看es6阮一峰class
处理普通值的时候,ref在使用get的时候,需要使用 .value 来获取值
test('ref 处理普通值 get', () => {
const aRef = ref(1)
// ref 会有一个value属性
expect(aRef.value).toBe(1)
aRef.value = 2;
expect(aRef.value).toBe(2)
})
复制代码
/**
* 把数据变成一个ref
* @param val
* @returns
*/
export function ref(val) {
return new Ref(val)
}
class Ref{
private _value: any;
constructor(value) {
this._value = value
}
get value(){
return this._value
}
set value(val){
this._value = val
}
}
复制代码
这样写的话,ref处理普通值的场景就ok了,上面的测试用例也是可以通过的
给ref进行一个包装,调用的
.value
其实是调用Ref class
的一个get方法
,有没有发现ref的value是这么来的
我们知道,ref也是可以处理对象的,处理对象的时候,调用的是 reactive
方法来进行包装
这里为啥要调用reactive来包装对象呢?
ref绑定的数据是双向数据绑定的,需要对 对象内部的属性进行劫持,就是说对象里面的内容发生变化,要能够接收到通知,然后进行数据更新操作
根据ref处理对象,咋们可以写出测试用例
test('ref 处理对象', () => {
const aRef = ref({ a: 1, b: 2 })
// ref 会有一个value属性
expect(aRef.value.a).toBe(1)
expect(isReactive(aRef.value)).toBe(true)
// update
aRef.value.b = 4;
expect(aRef.value.b).toBe(4)
})
复制代码
修改之前的代码,在这里对于构造函数的数据做一个是否是对象的判断和set值的时候,也需要对set的值做判断
constructor(value) {
// 判断value是否是对象,对象直接调用reactive
this._value = isObj(value) ? reactive(value) : value
}
// 省略其他
set value(val){
this._value = isObj(val) ? reactive(val) : val
}
复制代码
这样的话,咋们的测试用例是可以通过的,完了么,no, ref也需要和reactive一样,在get数据的时候进行track, 修改数据的时候进行 trigger
这里咋们先看vue给出的一个官方的测试用例,通过测试用例来分析功能
test('ref 把数据变成响应式', () => {
const a = ref(1);
let dummy;
let calls = 0;
// 依赖收集
effect(() => {
calls++;
dummy = a.value;
});
expect(calls).toBe(1);
expect(dummy).toBe(1);
// update
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
// same value should not trigger
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
})
复制代码
通过上面的测试用例,咋们可以分析出以下需求:
既然要进行依赖收集,那在我们ref
中怎么来进行依赖收集呢?
毫无疑问的是 —— 收集依赖肯定是在get value
函数中进行的,而触发依赖是在 set 函数中进行,但是需要与effect进行联动,就会用到effect里面的activeEffect 和 shouldTrack 等,这里需要注意.
控制新旧值的变化也是在set中完成的
// 在 effect模块中咋们可以抽离出以下几个函数来空供我们ref模块使用
/**
* 收集effect
* @param deps
* @returns
*/
export function trackEffect(deps) {
// 存在的话,不需要反复收集
if (deps.has(activeEffect)) return
// 收集依赖
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
/**
* 是否可以进行依赖收集
* @returns
*/
export function tracking() {
// 可以进行track和activeEffect 有值
return shouldTrack && activeEffect
}
/**
* 遍历触发依赖
* @param deps
*/
export function triggerEffect(deps) {
deps.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
})
}
// 来改造咋们的class
class Ref {
private _value: any;
// 收集的ref依赖
private deps
// 原始值,由于对象会被转成proxy,所以咋们需要保存一个原始的值,用于控制值是否改变
private _rawVal: any;
constructor(value) {
// 判断value是否是对象,对象直接调用reactive
this._value = isObj(value) ? reactive(value) : value
this._rawVal = value
this.deps = new Set()
}
get value() {
// 进行依赖收集
if (tracking()) {
trackEffect(this.deps)
}
return this._val
}
set value(val) {
// 数据没有发生改变,不需要重新trigger
if (!hasChanged(val, this._rawVal)) return
this._rawVal = val;
this._value = isObj(val) ? reactive(val) : val
triggerEffect(this.deps)
}
}
复制代码
通过这里咋们可以分析出,ref的响应式其实是通过包装了一层实例对象,通过劫持实例对象的 get 和 set 方法来做到的,而为啥需要使用value呢?因为get和set的是方法,value可以被咋们改成任何的名称
isRef
是用于判断一个对象是否被 Ref 实例对象所包裹
对于这个api,咋们怎么实现呢?有了isReactive
和isReadonly
的基础,我相信你不难想到,也是同样的配方,熟悉的味道。
test('isRef', () => {
const a = ref(1);
expect(isRef(a)).toBe(true);
const b = 1;
expect(isRef(b)).toBe(false);
const c = reactive({ a: 1 })
expect(isRef(c)).toBe(false);
})
复制代码
/**
* 判断传入的数据是否是ref
* @param val
* @returns
*/
export function isRef(val) {
return !!val._v__is_ref
}
// 接下来在我们的class中加一个_v__is_ref属性,并且设置为true即可
public _v__is_ref = true
复制代码
unRef
api是用于 当你.value 用的太繁琐的时候,不知道你后面的值到底有没有 .value,说白了就是说你不想写.value 就用这个api来进行包裹一下就行
/**
* 把ref数据变成origin数据
* @param val
* @returns
*/
export function unRef(val) {
return isRef(val) ? val.value : val
}
复制代码
proxyRefs
用的人估计不是很多,但是用了 setup的人都知道, setup返回的数据,不管有没有.value 当你在模板中使用的时候,都可以省略.value,setup返回的结果就调用了这个api
test('proxyRefs', () => {
const user = {
age: ref(10),
name: "twinkle",
};
const proxyUser = proxyRefs(user);
// 不改变原数据结构
expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("twinkle");
// set 赋值普通值
proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
// set 赋值ref
proxyUser.age = ref(10);
expect(proxyUser.age).toBe(10);
expect(user.age.value).toBe(10);
})
复制代码
通过上面的测试用例,咋们可以分析出以下需求:
proxyRefs
可以对数据进行get和set,并且还要做对应的处理实现功能
/**
*
* @param obj
* @returns
*/
export function proxyRefs(obj) {
return new Proxy(obj, {
get(target, key) {
const val = Reflect.get(target, key)
// 返回的结果进行判断
return isRef(val) ? val.value : val
},
set(target, key, val) {
// set -> target[key] is ref && val not is ref
if (isRef(target[key]) && !isRef(val)) {
return target[key].value = val
} else {
return Reflect.set(target, key, val)
}
}
})
}
复制代码
computed
这个api大家基本上都会使用,传入一个fn或者是自定义get,set, 返回一个对象,并且需要使用 .value 来调用里面的内容,看到.value是不是感觉和ref是一样的结构,来一个class给它包装以下即可。
先来简单测试用例,自己也可以动手敲一敲哦~~~✌✌✌
test('测试computed函数结果', () => {
const a = computed(() => 1)
expect(a.value).toBe(1)
})
复制代码
要实现上面内容是不是和ref是一样的,只不过传的内容不一样而已,这里就省略了哈,
来一个复杂一点点的测试用例
it('computed', () => {
const value = reactive({})
const getter = jest.fn(() => value.foo)
const cValue = computed(getter)
// lazy
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(undefined)
expect(getter).toHaveBeenCalledTimes(1)
// should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
// should not compute until needed
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)
// now it should compute
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(2)
// // should not compute again
cValue.value
expect(getter).toHaveBeenCalledTimes(2)
})
复制代码
根据上面的测试用例,咋们可以分析以下需求:
综上所述, computed会对fn进行缓存,只有内容变化,且调用了computed的返回值的.value才会去执行fn
对应的解决措施
export function computed(fn) {
return new ComputedRefImpl(fn)
}
class ComputedRefImpl{
// 传入的fn
private getter: any
private readonly setter: any
private _value: any
// 是否可以执行
private _dirty = true
// 收集getter的依赖
private deps;
// 当前的effect
private effect
constructor(getter) {
this.getter = getter
this.deps = new Set()
this.effect = new EffectReactive(this.getter, () => {
if (!this._dirty) {
this._dirty = true
// 触发依赖
triggerEffect(this.deps)
}
})
}
get value() {
// 收集依赖
if (tracking()) {
trackEffect(this.deps)
}
// 用于缓存执行结果
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
// 返回结果
return this._value
}
}
复制代码
这里咋们会发现computed设计的非常巧妙,如下图
这里来分析一下:
ComputedRefImpl
的时候就完成, deps, effect, getter
的初始化,但是对于EffectReactive而言的话,完成了 fn, scheduler
的初始化activeEffect
是否存在,存在的话对收集依赖get .value
是否第一次调用,第一次的话则进行effect
中调用run方法,否则返回历史run方法值的结果,然后把dirty设置为false作为缓存fn执行的结果activeEffect
赋值为this
, 执行fn
函数,最后反正fn
函数的结果fn
内部使用到的响应式数据进行track
effect
中是否存在 scheduler
函数,存在的话则执行该函数scheduler
函数中,会把dirty
重置为true
,标志着computed
内容fn
数据有变化,需要重新执行fn
,并且会把在get value
中收集到的依赖进行trigger
get value
,更新数据computed 其实可以存入一个对象,对象中可以自己定义get,set,自己可以实现下,有兴趣的同学可以查看源码,源码中实现了get和set哦~~~