Vue3响应式
- vue3的响应式使用的是reactivity中的reactive与effect来实现
- 该响应式应用广泛
需要用到一个html来展示
<body>
<script src="../../../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<div id="app"></div>
<script>
const App = document.querySelector('#app');
const {
effect,
reactive
} = VueReactivity;
const obj = {
name: 'cc',
age: 0,
address: {
code: 1
}
}
const state = reactive(obj);
effect(() => {
App.innerHTML = `
姓名:${state.name}<br />
年龄:${state.age}<br />
邮政编码:${state.address.code}
`
});
setTimeout(() => {
state.name = '崔';
}, 1000)
</script>
</body>
reactive
- 通过proxy来代理,返回一个proxy对象
- 接收一个非null的object
import { isObject } from '@vue/shared';
export function reactive(target) {
if (!isObject(target)) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, reactive) {
return target[key];
},
set(target, key, value, reactive) {
target[key] = value;
return true;
}
});
return proxy;
}
问题1
- 第一版,是存在一些问题
- 比如使用属性访问器来拿到值的话,获取的是源对象,且get只能监听到当前访问值,无法监听到访问器函数内部依赖的值
...
get getName() {
console.log(this === obj);
return this.name;
}
...
state.getName;
- 解决办法,使用es6的Reflect来处理get和set
const proxy = new Proxy(target, {
get(target, key, reactive) {
return Reflect.get(target, key, reactive)
},
set(target, key, value, reactive) {
return Reflect.set(target, key, value, reactive)
}
})
问题2
- 使用官网代码可得到以下结果:
- 也就是说reactive方法会走缓存,类似require
- 如果传入的值相同,返回原有数据
- 如果传入的是返回的proxy,那么依然返回原有的proxy
- 我们的代码暂时不具备这种功能
const state1 = reactive(obj);
const obj1 = reactive(state);
const address = reactive(obj.address);
console.log(state === state1, '两次代理obj是否相等');
console.log(state === obj1, '等不等于state它自身再代理');
console.log(state === address, 'state等不等于内部object再代理');
- 实现步骤:
- 通过WeakMap来存储this指向
- 在代码中添加缓存判断条件
const reactiveMap = new WeakMap();
const enum ReactiveFlags {
IS_REACTIVE = '__v_isRective'
};
export function reactive(target) {
...
if (reactiveMap.get(target)) {
return reactiveMap.get(target);
}
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, reactive) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
return Reflect.get(target, key, reactive)
}
...
})
reactiveMap.set(target, proxy);
return proxy;
}
- 感觉核心逻辑没有动,那么我们把它抽离出来
- 新建文件baseHandler.ts
- 原文件导入使用
export const enum ReactiveFlags {
IS_REACTIVE = '__v_isRective'
};
export const mutableHandlers = {
get(target, key, reactive) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
return Reflect.get(target, key, reactive)
},
set(target, key, value, reactive) {
return Reflect.set(target, key, value, reactive)
}
}
effect
- 接收一个函数
- 首次执行一次,后续数据改变,函数执行
- 我们需要做一下优化,不能说每一个key改变,所有的effect都重新执行
- 那么我们需要做一个依赖收集,也就是将函数按照使用的key进行分类
- effect会嵌套使用
export let activeEffect = undefined;
class ReactiveEffect {
public active = true
public parent = null
public deps = [];
constructor(public fn) {
}
run() {
if (!this.active) {
return this.fn();
}
try {
this.parent = activeEffect;
activeEffect = this;
return this.fn();
} finally {
activeEffect = this.parent;
}
}
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
- 依赖收集我们需要考虑一些情况,比如说,一个key对应多个fn,一个fn对应多个key,也就是所谓的多对多关系
- 使用如下结构:
WeakMap{this:Map{name:Set[]}}
track
const targetMap = new WeakMap();
export function track(target, type, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map());
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, dep = new Set())
}
let shouldTrack = !dep.has(activeEffect);
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
}
}
trigger
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(effect => {
if(effect!==activeEffect)effect.run();
})
}
cleanupEffect
- 清除存储的effect
- 为什么要清除,看下面demo
effect(() => {
console.log('render');
App.innerHTML = state.flag ? state.name : state.age;
});
state.getName;
setTimeout(() => {
state.flag = false;
}, 1000);
setTimeout(()=>{
state.name = '更新';
},2000)
function cleanupEffect(effect) {
const { deps } = effect;
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
class ReactiveEffect {
...
run() {
...
try {
...
cleanupEffect(this);
return this.fn();
}
....
}
}
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let effects = depsMap.get(key);
if (effects) {
effects = new Set(effects);
effects.forEach(effect => {
if (effect !== activeEffect) effect.run();
})
}
}
stop
- 失活,执行以后,后续更新不会再执行当前effect
let render = effect(() => {
console.log('render');
App.innerHTML = state.flag ? state.name : state.age;
});
render.effect.stop();
setTimeout(() => {
state.flag = false;
setTimeout(()=>{
state.age++;
},1000)
},1000)
- 我们失活的时候,需要将active变为false,而且要清除effect
- effect要有返回值,render实际上还是effect
- 实现:
class ReactiveEffect {
...
stop() {
if (this.active) {
this.active = false;
cleanupEffect(this);
}
}
...
}
export function effect(fn) {
const _effect = new ReactiveEffect(fn);
_effect.run();
let render = _effect.run.bind(_effect);
render.effect = _effect;
return render;
};
effect的第二个参数-调度器
- 第二个参数是一个object
- 里面可以传一个scheduler函数,我们可以在这个函数内决定什么时候更新
let render = effect(() => {
console.log('render');
App.innerHTML = state.age;
}, {
scheduler() {
if(state.age===5){
render();
}
console.log('run')
}
});
state.age = 2;
state.age = 3;
state.age = 4;
state.age = 5;
class ReactiveEffect {
...
constructor(public fn, public scheduler) { }
...
}
export function effect(fn, options: any = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
...
};
export function trigger(target, type, key, value, oldValue) {
...
if (effects) {
effects = new Set(effects);
effects.forEach(effect => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
})
}
}
computed
- 计算属性
- 可传递function与object
- function只能获取
- object内置get与set
- 如果依赖没改变,function与get函数不会多次执行,会走缓存(性能优化点)
- computed执行会返回一个object,里面内置一个value,我们可以对value进行获取与修改
- computed不会立即触发,而是在调用value时触发
const { computed } = VueReactivity;
const obj = {
name: '崔',
age: 1
}
const state = reactive(obj);
let fullName = computed(() => {
console.log('执行计算');
return state.name + state.age;
});
console.log(fullName.value);
console.log(fullName.value);
console.log(fullName.value);
state.name = '崔';
console.log(fullName.value);
console.log(fullName.value);
- 当在effect中使用时候,计算属性依赖的值改变,那么会重新执行effect,然后执行计算
let fullName = computed(() => {
console.log('执行计算');
return state.name + state.age;
});
effect(() => {
console.log('runder')
App.innerHTML = fullName.value;
});
setTimeout(() => {
state.age = 2;
})
let fullName = computed({
get() {
return state.name + state.age;
},
set(value) {
state.name = value;
}
});
effect(() => {
console.log('runder')
App.innerHTML = fullName.value;
});
setTimeout(() => {
fullName.value = '崔崔崔'
})
- 可以看到规律,调用才会执行,如果在effect中,那么会执行effect,然后effect会调用计算函数,依赖值没变,不重新计算
export const isFunction = (value) => {
return typeof value === 'function'
}
export function track(target, type, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, depsMap = new Map());
}
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, dep = new Set())
}
trackEffect(dep);
}
export function trackEffect(deps) {
let shouldTrack = !deps.has(activeEffect);
if (shouldTrack) {
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
}
export function trigger(target, type, key, value, oldValue) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
let effects = depsMap.get(key);
triggerEffect(effects);
}
export function triggerEffect(effects) {
if (effects) {
effects = new Set(effects);
effects.forEach(effect => {
if (effect !== activeEffect) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
})
}
}
import { isFunction } from '@vue/shared';
import { activeEffect, ReactiveEffect, trackEffect, triggerEffect } from './effect'
export function computed(getterOrOptions) {
let isGetter = isFunction(getterOrOptions)
let getter;
let setter;
let fn = () => console.warn('没传递set,不能修改');
if (isGetter) {
getter = getterOrOptions;
setter = fn;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set || fn;
}
return new ComputedRefImpl(getter, setter);
}
class ComputedRefImpl {
private _value;
private _dirty = true;
public effect;
public deps;
constructor(getter, public setter) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerEffect(this.deps);
}
});
}
get value() {
if (activeEffect) {
trackEffect(this.deps || (this.deps = new Set()))
}
if (this._dirty) {
this._dirty = false;
this._value = this.effect.run();
}
return this._value;
}
set value(value) {
this.setter(value);
}
}
ref
const {
ref,
effect
} = Vue;
const flag = ref(true);
effect(() => {
console.log(flag.value ? 'true' : 'false');
});
setTimeout(() => {
flag.value = !flag.value;
})
实现
import { isObject } from "@vue/shared";
import { trackEffect, triggerEffect } from "./effect";
import { reactive } from "./reactive";
function toReactive(value) {
return isObject(value) ? reactive(value) : value;
}
class RefImpl {
public dep = new Set;
public _value;
public __v_isRef = true;
constructor(public rawValue) {
this._value = toReactive(rawValue);
}
get value() {
trackEffect(this.dep);
return this._value;
}
set value(newValue) {
if (newValue !== this.rawValue) {
this._value = toReactive(newValue);
this.rawValue = newValue;
triggerEffect(this.dep);
}
}
}
export function ref(value) {
return new RefImpl(value);
}
toRefs
const {
ref,
effect,
reactive,
toRefs
} = Vue;
const state = reactive({
name: 'cc',
age: 1
});
effect(() => {
console.log(`姓名:${state.name},年龄:${state.age}`);
})
setTimeout(() => {
state.name = 'aa';
})
const state = reactive({
name: 'cc',
age: 1
});
const {
name,
age
} = toRefs(state);
effect(() => {
console.log(`姓名:${name.value},年龄:${age.value}`);
})
setTimeout(() => {
state.name = 'aa';
})
实现
class ObjectRefImpl {
constructor(public object, public key) {
}
get value() {
return this.object[this.key]
}
set value(newValue) {
this.object[this.key] = newValue;
}
}
function toRef(object, key) {
return new ObjectRefImpl(object, key);
}
export function toRefs(object) {
const result = isArray(object) ? new Array(object.length) : {};
for (let key in object) {
result[key] = toRef(object, key);
}
return result;
}
proxyRefs
- 反向代理,实现不使用.value访问
- 体验
- 下面的例子中还是将proxyRefs返回的对象结构的,但实际在vue中,我们是可以省略with(school)的
const {
ref,
effect,
reactive,
toRefs,
proxyRefs
} = Vue;
const state = reactive({
name: 'cc',
age: 1
});
const school = proxyRefs(state);
effect(() => {
with(school){
console.log(this);
console.log(`姓名:${name},年龄:${age}`);
}
})
setTimeout(() => {
state.name = 'aa';
})
实现
export function proxyRefs(object) {
return new Proxy(object, {
get(target, key, recevier) {
let r = Reflect.get(target, key, recevier);
return r.__v_isRef ? r.value : r;
},
set(target, key, value, recevier) {
let oldValue = target[key];
if (oldValue['__v_isRef']) {
oldValue.value = value;
return true;
} else {
Reflect.set(target, key, value, recevier);
}
}
})
}