什么是ref
接受一个内部值,返回一个响应式的、可更改的
ref对象,此对象只有一个指向其内部值的
对于基本数据类型来说,无法使用reactive来实现响应式,因为proxy接收是一个对象,而ref既可以支持一个对象又可以支持基本数据类型,调用ref后会返回一个对象,用户访问了ref.value会进行依赖收集,修改属性后进行派发更新,所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用.
首先明确一下ref具备哪些功能具备下列两个功能
- 具备响应式
- 如果传入是一个对象则按照
reactive去处理
基本功能实现
首先因为考虑到ref传递进来不是一个对象而是一个值,那么便说明不能使用Proxy,但是可以使用ES6的class 中的对某个属性设置存值函数和取值函数 get set。
接下来看代码实现 deps表示依赖收集存储到的依赖
class RefImpl {
private _value: any;
private deps;
constructor(value) {
this._value = value;
this.deps = new Set();
}
get value(value) {
...
return value
}
set value(newValue) {
...
}
}
以上便实现了ref 功能骨架。
收集依赖与触发依赖
那么接下来呢就要进行思考,在之前实现reactive有个依赖收集与触发依赖过程,只是与之前有一些差别。
- 收集依赖不需要额外使用
Map容器收集,因为没有key所以直接存储到ref内部就行 - 设置数据直接拿到
deps的Set集合直接循环调用即可
import { hasChanged, isObject } from "../shared";
import { trackEffects, triggerEffect, isTracking } from "./effect";
class RefImpl {
private _value: any;
private deps;
constructor(value) {
this._value = value;
this.deps = new Set();
}
get value() {
if (isTracking()) {
trackEffects(this.deps);
}
return this._value;
}
set value(newValue) {
if (hasChanged(newValue, this._value)) {
this._value = newValue;
triggerEffect(this.deps);
}
}
}
调用isTracking是为了保证收集依赖时内部currentEffect必须存在才收集
调用hasChanged是为了保证只有新值与旧值不一样才会进行触发依赖操作
下面是导入effect文件的函数实现,
```typescript
export function triggerEffect(dep: Set<any>) {
for (let effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
export function trackEffects(dep: Set<any>) {
// 看看 dep 之前有没有添加过,添加过的话 那么就不添加了
if (dep.has(currentEffect)) return;
dep.add(currentEffect);
currentEffect.deps.push(dep);
}
export function isTracking() {
return shouldTrack && currentEffect !== undefined;
}
下面shared工具方法实现
export const isObject = (val) => val !== null && typeof val === "object";
export const hasChanged = (val, newValue) => {
return !Object.is(val, newValue);
};
实现ref支持对象传入
和reactive有一些差别,依赖收集与触发依赖并没有进行核心改变,只是在监听用户操作行为由Proxy改变成了ES6的取值函数与存值函数。
其实往往在开发过程中,ref是支持传入对象实现响应式,这个问题很简单 只需要稍微改变一下代码,判断如果传入对象就调用reactive进行处理和在设置数据时进行判断,在设置值得时候同样判断一下就可以了,并且多添加了一个私有成员 _rawValue,判断当前新数据与旧数据是否不一致。
class RefImpl {
private _value: any;
public _v_isRef = true;
private deps;
private _rawValue;
constructor(value) {
this._rawValue = value;
this._value = convert(value);
this.deps = new Set();
}
get value() {
if (isTracking()) {
trackEffects(this.deps);
}
return this._value;
}
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
this._value = convert(newValue);
triggerEffect(this.deps);
}
}
}
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
export function ref(value) {
return new RefImpl(value);
}
相关API
isRef实现
用来判断某个值是否为ref()创建出来的对象
实现isRef功能很简单,只需要在RefImpl类中建立一个私有属性进行判断是否存在,如果存在便是ref如果不存在那便不是
function isRef(ref) {
return !!ref["_v_isRef"];
}
unRef实现
对ref内的值进行解析,如果是ref则返回
ref.value,如果不是ref则返回传入的对象
function unRef(ref) {
return isRef(ref) ? ref.value : ref;
}
实现proxyRefs
当我们使用ref时,很多人吐槽,我们总是要加一个.value,用起来太不爽了。但我们发现,我们在模板中使用ref的时候,并不需要.value,那它是怎么实现的呢?
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
return unRef(Reflect.get(target, key));
},
set(target, key, value) {
//如果是普通修改
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
//如果value是ref则特殊处理
return Reflect.set(target, key, value);
}
},
});
}
很简单返回一个proxy对象,用户访问返回出来的对象时就使用unRef函数返回如果是ref,正好就是ref.value,如果是不是ref直接返回传入对象
如果是修改属性呢?如果ref内部设置数据会判断是都是普通修改,如果新值是一个ref也就是嵌套ref,那就没必要进行依赖收集,直接做特殊处理,如果普通修改,则按照普通方式处理