【框架实现】实现vue3的ref

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 RefImplgetset方法;

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方法; 总结:

  1. 对于ref函数,会返回RefImpl类型的实例;
  2. 在该实例中,根据传入的数据类型进行分开处理; .复杂数据类型:转化成reactive,返回Proxy;简单数据类型:不做处理;
  3. 无论我们执行obj.value.name还是obj.value.name = XXX本质上都是触发了get value
  4. 之所以会进行响应性是因为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就实现了!

相关推荐
dr李四维3 分钟前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~24 分钟前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ27 分钟前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z33 分钟前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
星星会笑滴36 分钟前
vue+node+Express+xlsx+emements-plus实现导入excel,并且将数据保存到数据库
vue.js·excel·express
彭世瑜1 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4041 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish1 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five1 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue