前言
Vue3已经出来好几年,之前了解过之后又忘记,今天来简单的了解一下reactive、effect、ref、toRef、toRefs、computed 使用和实现。
Vue3的reactive、effect、ref、toRef、toRefs、computed 实现
reactive实现
基本使用
const { reactive, effect } = VueReactive;
// 数据响应式
const state = reactive({name:"张三"});
// 在方法中获取值
effect(()=>{
app.innerHTML = state.name;
})
// 赋值,effect再次执行,页面动态改变
state.name = '李四';
响应式实现思路
- proxy实现数据的响应式,get中执行track收集,set中触发trigger更新
- effect方法传入函数,函数被立即执行,里面访问数据时,触发get中的track,将数据和effect关联
- 该数据数据被设置时,触发set中的trigger,取出数据中关联的effect,再次执行
proxy代理
const isObject = t => t && typeof t === 'object';
function reactive(target) {
return new Proxy(target, {
get(target,key,receiver) {
// 获取值
const res = Reflect.get(target, key, receiver);
// 收集effect
track(target, key);
// 如果是对象,递归执行
if(isObject(res)) {
return reactive(res);
}
return res;
},
set(target,key,value,receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if(value !== oldValue) {
// 触发effect执行
trigger(target,key)
}
return result;
}
})
}
effect方法
- effect方法,传入的函数,在执行中触发get
function effect(fn) {
const _effect = createEffect(fn);
_effect()
return _effect
}
let activeEffect;
// 栈结构存储,防止嵌套的effect
const effectStack = [];
function createEffect(fn) {
const effect = function() {
try {
effectStack.push(effect);
activeEffect = effect;
return fn()
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
}
return effect;
}
track方法
- track方法,在proxy的get中执行,收集effect,让它与被访问的数据产生关联
const targetMap = new WeakMap();
function track(target, key) {
let depMaps = targetMap.get(target);
if(!depMaps) {
targetMap.set(target, (depMaps = new Map())
}
let deps = depMaps.get(key);
if(!deps) {
depMaps.set(key, (deps = new Set()))
}
if(!deps.has(activeEffect)) deps.add(activeEffect);
}
trigger方法
- trigger方法,在proxy的set中执行,查看该属性是否存有effect方法,存有方法说明属性之前被get访问过,再次执行effect方法
function trigger(target, key) {
const depMaps = targetMap.get(target);
if(!depMaps) return
const effectSet = new Set();
const add = (effects) => {
if(effects) {
effects.forEach(e => {
effectSet.add(e);
});
}
}
add(depMaps.get(key))
effectSet.forEach(e => e())
}
ref实现
- 原因:因为proxy只能传对象,所以对于初始值的响应式来说,提供了ref的api
- 思路:通过class的get和set中收集和触发effect方法,实现使用的vue2的劫持
使用
const { ref, effect } = VueReactive;
// 数据响应式
const age = ref(12);
// 在方法中获取值
effect(()=>{
app.innerHTML = age.value;
})
// 赋值,effect再次执行,页面动态改变
age.value = 30;
实现
function ref(rawValue) {
return new RefImp(rawValue);
}
class RefImp {
constructor(rawValue) {
this._value = rawValue
this.rawValue = rawValue
}
get value() {
// 收集effect
track(this, 'value')
return this._value;
}
set value(newValue) {
if (this._value !== newValue) {
this._value = newValue
this.rawValue = newValue
// 触发effect执行
trigger(this, 'value', newValue)
}
}
}
toRef和toRefs
toRef和toRefs作用是为了减少state中的访问,将数据解构出来,直接赋值和访问,响应式还是依赖的reactive
使用
const { reactive, effect, toRef, toRefs } = VueReactive;
// 数据响应式
const state = reactive({name:"张三", age: 20});
// name通过toRef处理,可以直接使用
const name = toRef(state, 'name');
// toRef处理整个state
// const { name, age } = toRefs(state);
// 在方法中获取值
effect(()=>{
app.innerHTML = name.value;
})
// 赋值,effect再次执行,页面动态改变
name.value = '李四';
实现
- 通过拦截set和get实现
class ObjectRefIml {
constructor(target, key) {
this.target = target
this.key = key
}
get value() {
return this.target[this.key]
}
set value(newValue) {
this.target[this.key] = newValue
}
}
function toRef(target, key) {
return new ObjectRefIml(target, key);
}
function toRefs(target) {
let ret = Array.isArray(target) ? new Array(target.length) : {};
for (let key in target) {
ret[key] = toRef(target, key)
}
return ret;
}
计算属性
使用
const { ref, computed } = VueReactive;
// 数据响应式
const age = ref(10);
// 创建计算属性,不会立马执行
const myAge = computed(()=>{
return age.value + 8
})
// 读取时,执行computed传入的函数
console.log(myAge.value)
// 重复读取,去缓存里取值
console.log(myAge.value)
setTimeout(()=>{
age.value = 20;
// 访问时,重新执行获取值
console.log(myAge.value)
})
实现
- 将传入的getter函数做为effect,访问变量时才执行effect,同时收集依赖(getter函数中使用到的响应式数据)
- 数据变化时,不执行effect方法,但是执行传入sch方法,将_dirty值改为true,下次访问计算属性,重新执行getter取新值
function computed(getterOrOptions) {
// 取出参数中的getter函数
let getter, setter;
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions;
setter = () => {
console.warn('computed value must be readonly')
}
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
// 创建计算属性的实例
return new ComputedRefImp(getter, setter);
}
class ComputedRefImp {
constructor(getter, setter) {
this._dirty = true;
this._value = undefined;
this.setter = setter
// 将getter传入effect中,lazy为true时,不会立马执行effect
this.effect = effect(getter, { lazy: true, sch:()=>{
// 在触发trigger时,不会执行effect方法,执行sch方法
if(!this._dirty) this._dirty = true;
}})
}
get value() {
// 第一次执行effect,取到新值,后面不再重复执行
if(this._dirty) {
this._value = this.effect();
this._dirty = false;
}
return this._value
}
set value(newValue) {
this.setter(newValue)
}
}
// 数据变化时,trigger方法执行时,不执行effect方法,执行传入的sch
function trigger(target, key) {
// ...
effectSet.forEach(e => {
// 存在sch方法,说明是计算属性的effect,只需将sch执行
if(e.options.sch) {
e.options.sch(e);
} else {
e();
}
})
}
最后
这里只是简单的介绍Vue3响应式相关的几个api,很多特殊场景都未考虑进去,想要深入了解原理,还是需要自己深入源码。总体来说Vue3使用了新的proxy属性,简化了整个响应式流程;相比于Vue2没有一开始对数据做响应式处理,访问时才会代理,减少内存消耗。
原文链接:https://juejin.cn/post/7258233838371061821 作者:竹业