【框架实现】实现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就实现了!

相关推荐
乔峰不是张无忌33013 分钟前
【HTML】动态闪烁圣诞树+雪花+音效
前端·javascript·html·圣诞树
鸿蒙自习室20 分钟前
鸿蒙UI开发——组件滤镜效果
开发语言·前端·javascript
m0_7482507427 分钟前
高性能Web网关:OpenResty 基础讲解
前端·openresty
前端没钱1 小时前
从 Vue 迈向 React:平滑过渡与关键注意点全解析
前端·vue.js·react.js
NoneCoder1 小时前
CSS系列(29)-- Scroll Snap详解
前端·css
无言非影1 小时前
vtie项目中使用到了TailwindCSS,如何打包成一个单独的CSS文件(优化、压缩)
前端·css
我曾经是个程序员1 小时前
鸿蒙学习记录
开发语言·前端·javascript
顽疲2 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
羊小猪~~2 小时前
前端入门之VUE--ajax、vuex、router,最后的前端总结
前端·javascript·css·vue.js·vscode·ajax·html5
摸鱼了2 小时前
🚀 从零开始搭建 Vue 3+Vite+TypeScript+Pinia+Vue Router+SCSS+StyleLint+CommitLint+...项目
前端·vue.js