likes
comments
collection
share

手把手带你实现一个自己的简易版 Vue3(四)

作者站长头像
站长
· 阅读数 25

👉 项目 Github 地址:github.com/XC0703/VueS…

(希望各位看官给本菜鸡的项目点个 star,不胜感激。)

5、ref、toRef 与 toRefs

5-1 ref

5-1-1 ref 的基本使用

在前面,我们借助 Proxy 代理实现了对象、数组类型的响应式(reactive),而基本数据类型的响应式则要借助 ref 实现。ref 就是将普通的数据类型实现代理,其原理基于 Object.defineProperty 实现(Proxy 是代理对象的)。

<!-- weak-vue\packages\examples\3.ref.html -->
<script>
  // ref就是将普通的数据类型实现代理,其原理基于Object.defineProperty实现。
  let { reactive, effect, ref } = VueReactivity;
  let name = ref("张三"); // 返回一个实例对象,添加一个value属性,这个属性就是该普通数据的值
  effect(() => {
    app.innerHTML = name.value; // 显示'张三'
  });
</script>

5-1-2 ref 实例对象的包装

ref 本质是一个方法,将我们需要代理的基本数据包装成一个可以访问 value 属性的实例对象。因此第一步是包装对象。新建一个 weak-vue\packages\reactivity\src\ref.ts 文件:

// weak-vue\packages\reactivity\src\ref.ts
// 普通ref代理
export function ref(target) {
  return createRef(target);
}

// 如果target是一个对象,则浅层代理
export function shallowRef(target) {
  return createRef(target, true);
}

// 创建ref类
class RefImpl {
  // 给实例添加一些公共属性(实例对象都有的,相当于this.XXX = XXX)
  public __v_isRef = true; // 用来表示target是通过ref实现代理的
  public _value; // 值的声明
  constructor(public rawValue, public shallow) {
    // 参数前面添加public标识相当于在构造函数调用了this.target = target,this.shallow = shallow
    this._value = rawValue; // 用户传入的值赋给_value
  }

  // 借助类的属性访问器实现value属性的访问以及更改
  get value() {
    return this._value;
  }
  set value(newValue) {
    this._value = newValue;
  }
}

// 创建ref实例对象(rawValue表示传入的目标值)
function createRef(rawValue, shallow = false) {
  return new RefImpl(rawValue, shallow);
}

此时去跑我们的测试用例:手把手带你实现一个自己的简易版 Vue3(四)可以看到,ref 实例对象基本实现。

5-1-3 ref 的响应式实现

因此在上面我们已经实现将基本数据包装成一个具有 value 属性的示例对象了,所以响应式的实现直接借助我们前面使用过的两个方法:收集依赖(Track)和触发更新(trigger)即可:

// weak-vue\packages\reactivity\src\ref.ts
  // 响应式的实现需要借助两个方法:收集依赖(Track)和触发更新(trigger)。
  // 借助类的属性访问器实现value属性的访问以及更改
  get value() {
    Track(this, TrackOpType.GET, "value"); // get的时候实现依赖收集
    return this._value;
  }
  set value(newValue) {
    // 如果值已变,则赋新值并触发更新
    if (hasChange(newValue, this._value)) {
      this._value = newValue;
      this.rawValue = newValue;
      trigger(this, TriggerOpType.SET, "value", newValue);
    }
  }

此时便可以变成响应式了,去跑一下我们新的测试用例:

<!-- weak-vue\packages\examples\3.ref.html -->
<script>
  let { reactive, effect, ref } = VueReactivity;
  let name = ref("张三"); // 返回一个实例对象,添加一个value属性,这个属性就是该普通数据的值
  console.log(name);
  effect(() => {
    app.innerHTML = name.value; // 显示'张三'
  });

  setTimeout(() => {
    name.value = "李四"; // 一秒后显示'李四'
  }, 1000);
</script>

可以看到显示结果是会更改的,说明响应式有效。

5-2 toRef

5-2-1 toRef 的使用

toRef 就是将对象的某个属性的值变成 ref 对象。

<!-- weak-vue\packages\examples\5.toRefs.html -->
<script>
  // toRef就是将对象的某个属性的值变成ref对象。
  let { reactive, effect, toRef } = VueReactivity;
  let state = { age: 10 };
  let myAge = toRef(state, "age");
  console.log(myAge.value);
</script>

5-2-2 toRef 实例对象的包装

像前面一样,我们需要将值包装成一个对象,并且可以拿到正确的值。

// weak-vue\packages\reactivity\src\ref.ts
class ObjectRefImlp {
  public __v_isRef = true; // 用来表示target是通过ref实现代理的
  constructor(public target, public key) {}

  // 获取值
  get value() {
    return this.target[this.key];
  }
  // 设置值
  set value(newValue) {
    this.target[this.key] = newValue;
  }
}

// 创建toRef对象
export function toRef(target, key) {
  return new ObjectRefImlp(target, key);
}

此时去跑一下我们的测试用例:手把手带你实现一个自己的简易版 Vue3(四)可以看到,打印正确。

5-2-3 toRef 的响应式实现

官网对 toRef 的作用解释为:

基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

可以看到,如果我们源对象是响应式的(经过 reactive 代理),此时的 ref 对象才是响应式的,否则不是。像下面这样:

<!-- weak-vue\packages\examples\5.toRefs.html -->
<script>
  // toRef就是将对象的某个属性的值变成ref对象。
  let { reactive, effect, toRef } = VueReactivity;
  let state = { age: 10 }; // 非响应的普通对象,如果是reactive( { age: 10 } )则是响应式的。
  let myAge = toRef(state, "age");
  console.log(myAge);
  effect(() => {
    app.innerHTML = myAge.value;
  });

  setTimeout(() => {
    myAge.value = 50; // 显示不变,因为此时不是响应式的
  }, 1000);
</script>

5-3 toRefs

5-3-1 toRefs 的基本原理

toRefs 是我们日常开发中用得比较多的一个 api。官方对 toRefs 的作用解释为:

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

所以 toRefs 的实现原理是基于 toRef 的,只不过多了一层属性的遍历:

// weak-vue\packages\reactivity\src\ref.ts
// 实现toRefs
export function toRefs(target) {
  // 判断是否为数组
  let ret = isArray(target) ? new Array(target.length) : {};
  // 遍历target对象的每个属性key
  for (const key in target) {
    ret[key] = toRef(target, key); // 每个属性都有自己的toRef实例对象
  }

  return ret;
}

此时去跑一下我们的测试用例:

<!-- weak-vue\packages\examples\5.toRefs.html -->
<script>
  // toRefs就是将对象的所有属性的值变成ref
  let { reactive, effect, toRefs } = VueReactivity;
  let state = { name: "张三", age: 10 };
  let { name, age } = toRefs(state);
  console.log(name, age);
</script>

可以看到打印出正确的结果:手把手带你实现一个自己的简易版 Vue3(四)

5-3-2 toRefs 在实际开发中的使用

上面提到,toRefs 是我们日常开发中用得比较多的一个 api。那具体怎么使用,下面请先看一个常见场景:

<!-- weak-vue\packages\examples\5.toRefs.html -->
<div id="app">{{state.name}}</div>
<script>
  // toRefs就是将对象的所有属性的值变成ref
  let { reactive, effect, toRefs, createApp } = Vue;
  let APP = {
    // Vue3组件的入口函数
    setup() {
      let state = reactive({ name: "张三", age: 10 });
      return { state };
    },
  };
  createApp(App).mount("#app");
</script>

此时确实实现了 state.name 的响应式,但是我们如果不想通过 state.name 这种方式访问 name,而是通过解构响应式对象来直接访问:

<!-- weak-vue\packages\examples\5.toRefs.html -->
<div id="app">{{name}}</div>
<script>
  // toRefs就是将对象的所有属性的值变成ref
  let { reactive, effect, toRefs, createApp } = Vue;
  let APP = {
    // Vue3组件的入口函数
    setup() {
      let { name, age } = reactive({ name: "张三", age: 10 });
      return { name };
    },
  };
  createApp(App).mount("#app");
</script>

此时会报错 undefined,无法访问。此时便可通过 toRefs 解决:

<!-- weak-vue\packages\examples\5.toRefs.html -->
<div id="app">{{name}}</div>
<script>
  // toRefs就是将对象的所有属性的值变成ref
  let { reactive, effect, toRefs, createApp } = Vue;
  let APP = {
    // Vue3组件的入口函数
    setup() {
      let state = reactive({ name: "张三", age: 10 });
      // 返回解构的所有属性的ref示例对象
      return { ...toRefs(state) };
    },
  };
  createApp(App).mount("#app");
</script>

此时便不会报错了。


自此,我们关于 reftoReftoRefs 的基本实现便已经结束了,到这里的源码请看提交记录:5、ref、toRef 与 toRefs

6、computed 计算属性

6-1 computed 的基本使用和特性

computed 为计算属性,主要用于对 Vue 实例的数据进行动态计算,且具有缓存机制,只有在相关依赖发生改变时才会重新计算。这种特性使得计算属性非常适合用于处理模板中的逻辑。

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

computed 的基本特性如下:

<!-- weak-vue\packages\examples\6.computed.html -->
<div id="app">{{age}}</div>
<script>
  // 特性1:computed计算属性,如果没有被使用,则不会触发里面的方法(懒执行)
  let { reactive, effect, ref, computed } = Vue;
  let age = ref(10);
  let myAge = computed(() => {
    console.log("666");
    return age.value + 20;
  });

  // 特性2:两次使用myAge.value,但也只会打印一次666,因为计算的数据没有被改变,存在缓存机制
  // 打印结果:连续打印两次30,但只打印一次666
  console.log(myAge.value);
  console.log(myAge.value);

  // 特性3:此时响应式数据虽然改变了,但是没有重新被使用,依旧不会触发里面的方法
  setTimeout(() => {
    age.value = 20;
    // console.log(myAge.value); // 再次使用到了,打印出40和6666
  }, 1000);
</script>

6-2 computed 的基本实现

6-2-1 获取 computed 的值

我们主要根据上面我们提到的基本使用和特性来实现 computed。首先我们实现一个 computed 函数,接收传过来的参数(可能是函数(此时只能读不能写),也可能是对象({get{}、set{}})),并返回一个 ref 实例对象。

// weak-vue\packages\reactivity\src\computed.ts
export function computed(getterOptions) {
  // 注意,传过来的可能是函数(此时只能读不能写),也可能是对象({get{}、set{}})
  let getter;
  let setter;
  if (isFunction(getterOptions)) {
    getter = getterOptions;
    setter = () => {
      console.warn("computed value must be readonly");
    };
  } else {
    getter = getterOptions.get;
    setter = getter.set;
  }

  return new ComputedRefImpl(getter, setter);
}

6-2-2 实现 computed

此时再去实现我们的 ComputedRefImpl 类,需要借助我们前面实现过的 effect 方法实现(传入 fn 和对应的 effect 配置,每个 fn 都有自己的 effect 高阶函数,可以进行依赖收集与触发更新),传入的 getterOptions 也要有自己的 effect 高阶属性,用于控制 getterOptions 执行与否。

这里由于需要实现不使用时不会触发 computed 里面的方法(懒执行),也就是不能让 effect 高阶函数默认去执行因此需要配置 lazy 属性。同时借助_dirty 属性控制使得获取时才去执行:

// weak-vue\packages\reactivity\src\computed.ts
class ComputedRefImpl {
  public _dirty = true; // 控制使得获取时才去执行
  public _value; // 计算属性的值
  public effect; // 每个传入的getterOptions对应的effect高阶函数
  constructor(getter, public setter) {
    this.effect = effect(getter, {
      lazy: true, // 实现特性1
    });
  }

  // 获取值的时候触发依赖(实现特性1)
  get value() {
    if (this._dirty) {
      this._value = this.effect(); // 此时里面的方法执行,this._value的值就是getterOptions返回return的结果,因此需要this.effect()返回的结果是就是用户传入的fn执行返回的结果(weak-vue\packages\reactivity\src\effect.ts里面改为return fn())
      this._dirty = false; // 这个是为了实现缓存机制,再去获取值的时候,直接返回旧的value即可(实现特性2)
    }
    return this._value;
  }

  set value(newValue) {
    this.setter(newValue);
  }
}

此时去执行我们的测试用例:

<!-- weak-vue\packages\examples\6.computed.html -->
<script>
  // 特性1:computed计算属性,如果没有被使用,则不会触发里面的方法(懒执行)
  let { reactive, effect, ref, computed } = VueReactivity;
  let age = ref(10);
  let myAge = computed(() => {
    console.log("computed里面的方法被触发了!!!");
    return age.value + 20;
  });

  // 特性2:两次使用myAge.value,但也只会打印一次666,因为计算的数据没有被改变,存在缓存机制
  // 打印结果:连续打印两次30,但只打印一次666
  console.log(myAge.value);
  console.log("computed里面的方法不会被触发了!!!这是旧的结果!!!");
  console.log(myAge.value);

  //   // 特性3:此时响应式数据虽然改变了,但是没有重新被使用,依旧不会触发里面的方法
  //   setTimeout(() => {
  //     age.value = 20;
  //     // console.log(myAge.value); // 再次使用到了,打印出40和6666
  //   }, 1000);
</script>

可以看到结果符合预期: 手把手带你实现一个自己的简易版 Vue3(四)

说明我们的前两个特性已经实现了,还差最后一个赋新值时表现出来的特性(响应式数据改变了,但是没有重新被使用,依旧不会触发里面的方法,使用到则触发)。

现在的情况是执行一次 computed 里面的方法之后,this._dirty 变成已经改为 false 了,如果需要重新执行,则需要将该状态变量改为 true

由于 computed 计算属性是 readonly 的,因此不能在 set value(){}里面进行相关操作,而是在 effect 里面进行操作。用一个 sch 函数使得 this._dirty = true,然后 effectSet 触发更新时执行(可以回去重新梳理一下 weak-vue\packages\reactivity\src\effect.ts的逻辑)。

// weak-vue\packages\reactivity\src\computed.ts
  constructor(getter, setter) {
    this.effect = effect(getter, {
      lazy: true, // 实现特性1
      sch: () => {
        // 实现特性3,修改数据时使得有机会被重新执行
        if (!this._dirty) {
          this._dirty = true;
        }
      },
    });
  }
// weak-vue\packages\reactivity\src\computed.ts
effectSet.forEach((effect: any) => {
  if (effect.options.sch) {
    effect.options.sch(effect); // 用于实现computed计算属性的特性3,触发更新时使得this._dirty = true,以便执行computed里面的方法
  } else {
    effect();
  }
});

自此,我们关于 computed 的基本实现就结束了,到这里的源码请看提交记录:6、computed 计算属性

转载自:https://juejin.cn/post/7354650329471205417
评论
请登录