vue3与vue2的区别对比
- Vue3 响应式是独立的包,叫reactive,基于Proxy实现; Vue2 响应式基于Object.defineProperty;
- Vue3中Effect的与Vue2中的Watcher的概念一致。
用例1:
import { effect, reactive } from "@hpstream/reactivity";
const state = reactive({ name: "jw", age: 30 });
effect(() => {
var app = document.querySelector("#app");
app.innerHTML = state.name + "今年" + state.age + "岁了";
// console.log(state);
});
setTimeout(() => {
state.age++;
}, 1000);
需要实现功能如下:
- 实现effect函数
- 实现reactive函数
- 当状态改变是重新执行effect函数
实现effect;
function effect(fn){
fn();
}
但是如果这么实现的话,我们只能初次渲染,当状态改变是我们无法在进行effect调用了。 于是:
var activeEffect = undefined;
class ReactEffect(){
public deps = [];
constructor(public fn) {};
run(){
try {
activeEffect = this;
return this.fn();
} catch (error) {
} finally {
activeEffect = undefined;
}
}
}
function effect(fn){
const _effect = new ReactiveEffect(fn);
_effect.run(); // 默认执行一次;
}
我们创建一个 ReactEffect,方便做收集用;
实现reactive
const isObject = (value) => {
return typeof value === "object" && value !== null;
};
function reactive(target){
// 如果targer 不是对象的话,我们就直接返回就行了;
if(!isObject(target)){
return targer;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
if (isObject(res)) {
// 如果是对象,则进行深度响应式
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
let oldvalue = target[key];
let result = Reflect.set(target, key, value, receiver);
return result;
},
});
return proxy;
}
上面是reactive实现的主要代码,但是没有实现与effect进行关联的逻辑,也就是没有实现依赖收集,和修改更新的逻辑。
实现依赖收集:
const proxy = new Proxy(target, {
get(target, key, receiver) {
let res = Reflect.get(target, key, receiver);
track(target, "get", key); // 收集依赖的函数 track;
if (isObject(res)) {
// 如果是对象,则进行深度响应式
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
let oldvalue = target[key];
let result = Reflect.set(target, key, value, receiver);
if (oldvalue !== value) {
// 通知更新页面函数:trigger
trigger(target, "set", key, value);
}
return result;
},
});
实现track:
const targetMap = new WeakMap(); // 生成弱引用,来标记是否响应式
function track(target, type, key){
let depsMap = targetMap.get(target)
if(!depsMap){
targetMap.set(target,depsMap = new Map())
}
let deps = depsMap.get(key);
if(!deps){
deps.set(key,deps = new Set())
}
if (!activeEffect) return;
var sholdEffect = deps.has(activeEffect);// 防止重复添加依赖
if(!sholdEffct){
deps.add(sholdEffect); //收集依赖
activeEffect.deps.push(dep); // 反向收集依赖
}
}
实现trigger:
function trigger(target, type, key, value){
const depsMap = targetMap.get(target);
if (!depsMap) return; // 触发的值不在模板中使用
let deps = depsMap.get(key); // 找到了属性对应的effect
// deps 里面装的都是effect;
deps && deps.forEach(effect=>{
effect.run()
})
}
上面的代码以最简单的方式现实了依赖收集的实现逻辑,简单的说get的实现收集effect, set 去通知收集的effect实现更新逻辑;
问题
但是上面的代码很脆弱,稍微不注意就出问题了;
问题1:
import { effect, reactive } from "@hpstream/reactivity";
const state = reactive({ name: "jw", age: 30,address:'北京' });
effect(() => {
state.name = 'jw1'
effect(() => {
state.age = 31;
});
state.address = '上海'; // 无法进行依赖收集,因为activeEffect为undefined
});
问题2:
import { effect, reactive } from "@hpstream/reactivity";
const state = reactive({ name: "jw", age: 30,address:'北京' });
effect(() => {
// 由于在effect中触发了;更新逻辑,又会触发更新逻辑,执行当前函数,导致死循环
state.age = Math.random()
app.innerHTML = state.name + "今年" + state.age + "岁了";
});
问题3:
const state = reactive({ flag: true, name: 'jw', age: 30 })
effect(() => { // 副作用函数 (effect执行渲染了页面)
console.log('render')
document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
state.flag = false;
setTimeout(() => {
// 由于name已经没有使用了,所以应该不更新的,但是还是进行了更新逻辑;
console.log('修改name,原则上不更新')
state.name = 'zf'
}, 1000);
}, 1000)
问题1:产生的原因
由于嵌套使用了effect导致;导致内部effect退出时,activeEffect 丢失,无法找到父effect;
var activeEffect = undefined;
class ReactEffect(){
constructor(public fn) {};
run(){
try {
activeEffect = this;
return this.fn();
} catch (error) {
} finally {
// 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
activeEffect = undefined;
}
}
}
function effect(fn){
const _effect = new ReactiveEffect(fn);
_effect.run(); // 默认执行一次;
}
// 采用tree的逻辑解决,定义一个父节点
class ReactEffect(){
public parent = null;
constructor(public fn) {};
run(){
try {
// 通过递归收集的方式,保证effect 永远是当前的effect;
this.parent = activeEffect;
activeEffect = this;
return this.fn();
} catch (error) {
} finally {
// 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
activeEffect = this.parent;
}
}
}
问题2:产生原因
由于在effect中触发了;修改了响应的值,所以响应的值又会去通知更新,因此导致了死循环
function trigger(target, type, key, value){
const depsMap = targetMap.get(target);
if (!depsMap) return; // 触发的值不在模板中使用
let deps = depsMap.get(key); // 找到了属性对应的effect
// deps 里面装的都是effect;
deps && deps.forEach(effect=>{
// 如果activeEffect 与更新的effect 是同一个,那么就不用在更新了。
if(effect !== activeEffect) effect.run()
})
}
问题3:产生原因
这是应为我们在name已经收集了依赖,所以他能继续响应式,我们在每次触发更新的时候,删除依赖,然后重新才比较合适。
class ReactEffect(){
constructor(public fn) {};
run(){
try {
activeEffect = this;
cleanupEffect(this);
return this.fn();
} catch (error) {
} finally {
// 直接定义成了undefined,导致嵌套,退出的时候,找不到外层的effect了;
activeEffect = undefined;
}
}
}
// 清楚依赖
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect; // deps 里面装的是name对应的effect, age对应的effect
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect); // 解除effect,重新依赖收集
}
effect.deps.length = 0;
}
但是我们这样子处理会导致页面死循环,需要在分发的那个处理下;
function trigger(target, type: string, key, oldvalue) {
const depsMap = targetMap.get(target);
if (!depsMap) return; // 触发的值不在模板中使用
let effects = depsMap.get(key); // 找到了属性对应的effect
// 永远在执行之前 先拷贝一份来执行, 不要关联引用
effects = [...effects]; 生成一个新 set;
if (effects) {
// triggerEffects(effects);
effects.forEach((effect) => {
if (effect !== activeEffect) effect.run(); // 防止循环
});
}
}
关于为啥会产生死循环,是一个set遍历导致的结果;所以需要复制出来一个新的,这里由于代码太过分散了不好理解,我重新模拟了一个案例:
let age, effect;
class Effect {
constructor(public fn) {}
deps = [];
run() {
const { deps } = this; // deps 里面装的是name对应的effect, age对应的effect
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect); // 解除effect,重新依赖收集
}
this.deps.length = 0;
this.fn();
}
}
function fn() {
effect.deps.push(age);
age.add(effect);
// this.fn();
}
age = new Set();
effect = new Effect(fn);
effect.run();
// 循环一直不结束
age.forEach((effect) => {
effect.run(); // 由于run函数删除了age,又重新添加了age所以导致循环不会停止
});
上面的代码手动模拟依赖收集的问题,也会触发循环。
我们复制一份就不会产生死循环了。
// 解决办法
var age1 = new Set(age);
age1.forEach((effect) => {
// 导致循环停不下来
effect.run(); // 由于run函数删除了age,又重新添加了age所以导致循环不会停止
});
这是我对于reactive与effect的理解,git 仓库如下:
https://github.com/hpstream/vue-source
感兴趣的可以关注下,后面会继续更新