手把手带你实现一个自己的简易版 Vue3(四)
👉 项目 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);
}
此时去跑我们的测试用例:可以看到,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);
}
此时去跑一下我们的测试用例:可以看到,打印正确。
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>
可以看到打印出正确的结果:
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>
此时便不会报错了。
自此,我们关于 ref
、toRef
与 toRefs
的基本实现便已经结束了,到这里的源码请看提交记录: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>
可以看到结果符合预期:
说明我们的前两个特性已经实现了,还差最后一个赋新值时表现出来的特性(响应式数据改变了,但是没有重新被使用,依旧不会触发里面的方法,使用到则触发)。
现在的情况是执行一次 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