ref原理

72 阅读4分钟

什么是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有个依赖收集与触发依赖过程,只是与之前有一些差别。

  1. 收集依赖不需要额外使用Map容器收集,因为没有key所以直接存储到ref内部就行
  2. 设置数据直接拿到depsSet集合直接循环调用即可
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,那就没必要进行依赖收集,直接做特殊处理,如果普通修改,则按照普通方式处理