1、reactive方法可将一个普通对象变为一个代理对象,如果再次将一个代理对象传入reactive之后,如何避免再次创建代理对象?
答:reactive的参数target如果还是Proxy对象的话,target[ReactiveFlags.IS_REACTIVE]取值时就会触发target的get钩子,在get钩子里
export const enum ReactiveFlags { // 对象
IS_REACTIVE = "__v_isReactive",
}
export function reactive(target) {
if (target[ReactiveFlags.IS_REACTIVE]) {
return target;
}
const proxy = new Proxy(target, mutableHandlers);
return proxy;
}
// mutableHandlers中有针对target取ReactiveFlags.IS_REACTIVE时的处理逻辑
export const mutableHandlers = {
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
const res = Reflect.get(target, key, receiver);
track(target, key);
return res;
},
set() {
}
}
2、effect嵌套调用时,全局的activeEffect是如何指向当前正在执行的effect
答:通过parent属性,调用effect时,会执行ReactiveEffect.run方法,会将当前上下文的activeEffect记录到parent上,执行完后,再将activeEffect还原为进入该effect之前的状态,也就是进入的时候存在parent上的值
export class ReactiveEffect {
constructor(private fn) {}
parent = undefined;
deps = []; // 我依赖了哪些列表
run() {
try {
this.parent = activeEffect;
activeEffect = this;
cleanupEffect(this); // 清理了上一次的依赖收集
return this.fn(); // fn()会触发依赖收集
} finally {
activeEffect = this.parent;
this.parent = undefined;
}
}
}
3、reactive返回的代理对象属性,和effect的关系是什么?
答:多对多
const state = reactive({ name: 'jw', age: 30 })
effect(() => { // 使用类似 react-hook
app.innerHTML = state.name + state.age
});
上述代码中的effect对应name age两个属性
name age两个属性可能会对应多个effect
4、vue3中WeakMap的数据结构是什么样的?
{
target: { key: Effect[] }
}
或者
{
target: { key: Dep[] }
}
上面的Dep[] 或 Effect[],是一个Set的结构
最外层的映射是原对象(target,特指没有添加代理的那个对象)和响应式信息之间的关系
target的value,所映射的是这个target在effect中使用到的key和这个key关联的Effect
Effect又叫dependency
5、响应式的关键步骤总结:
1、effect的执行,创建对应的ReactiveEffect对象
2、执行effect的run方法
3、执行run方法的过程中,会将当前ReactiveEffect对象放到全局变量activeEffect中去
4、run方法里面接下来会继续传给effect的回调执行一次
5、回调执行过程中,会触发代理对象的get钩子,进一步触发track方法
6、track中将Effect收集进WeakMap中,让原对象和Effect形成映射
7、effect执行完毕,activeEffect恢复为执行effect之前的状态
8、代理对象上的属性重新被赋值时,触发trigger方法
9、trigger里面把和key相关的effect取出,挨个执行其run方法
6、ReactiveEffect中的deps属性是做什么用的?里面存了什么东西?
deps用来存储和这个Effect相关的其他Effect的集合,deps是一个二维数组结构,第一维映射某个原生对象,第二维映射这个原生对象中关联到的Effect,举例来说:
const state1 = reactive({ name: 'jw', age: 30 });
const state2 = reactive({ name: 'zry', age: 37 });
const state3 = reactive({ name: 'myj', age: 32 });
// effect1
effect(() => {
app1.innerHTML = state1.name + state1.age;
});
// effect2
effect(() => {
app2.innerHTML = state2.name + state2.age;
});
// effect3
effect(() => {
app3.innerHTML = state3.name + state3.age;
});
// effect4
effect(() => {
app4.innerHTML = (state1.age + state2.age + state3.age) / 3;
});
// effect1执行时,activeEffect为effect1,会执行state1的name age两个属性对应的track方法
// WeakMap的结构为:
{
[state1]: {
name: [effect1],
age: [effect1]
}
}
// effect1的deps为
[[effect1]]
// 同理,effect2 effect3执行后,WeakMap的结构为:
{
[state1]: {
name: [effect1],
age: [effect1]
},
[state2]: {
name: [effect2],
age: [effect2]
},
[state3]: {
name: [effect3],
age: [effect3]
}
}
effect2和effect3的deps为
[[effect2]] [[effect3]]
// effect4执行时,会依次触发state1 state2 state3的track方法,执行完后WeakMap的结构为:
{
[state1]: {
name: [effect1],
age: [effect1, effect4]
},
[state2]: {
name: [effect2],
age: [effect2, effect4]
},
[state3]: {
name: [effect3],
age: [effect3, effect4]
}
}
effect1 effect2 effect3的deps不变,effect4的deps为
[[effect1], [effect2], [effect3]]
7、如果在一个effect中既取了一个值,又修改了这个值,会导致什么问题,vue3中是如何避免的?
effect(() => { // 使用类似 react-hook
app.innerHTML = state.name + state.age
state.name = 'addfdfa'
});
上述代码的effect中,既触发了name属性的track方法,又触发了name属性的trigger方法,在trigger中会从WeakMap里找到name对应的effect来执行,这个effect也就是当前effect,进而导致effect循环执行,所以在trigger里面有如下判断:
effects.forEach((effect) => {
// 正在执行的effect ,不要多次执行
if (effect !== activeEffect) effect.run();
});
Effect的deps是什么属性?有什么作用?
deps是一个二维数组结构,数组的第一维映射代理对象的属性,第二维是这个属性关联的effect
8、effect中的回调每次执行触发对象的get钩子,进而触发track时,是否会重新收集依赖?
会,effect中的回调通过effect的run方法来执行,run方法里面会调用cleanupEffect,该effect里涉及到的对象属性所关联的effect中如果有当前正在执行的这个effect,则会将其从WeakMap里清掉
清除的原因为每次effect执行时,里面的属性值都有可能和之前的不一样,因此收集本次effect执行时用到的属性关联的effect即可
9、依赖是否需要清理?在什么时机清理?怎么清理?
需要清理,在effect的run方法执行时会先清理之前effect回调里的对象属性所关联的effect,再执行effect的回调
10、trigger中从WeakMap里取出属性对应的effect集合之后,为何需要浅拷贝一份?
在effect执行run方法的过程中,会先清掉WeakMap中属性关联的effect,而在effect的回调里,又可能会添加effect,这样一来forEach遍历的就是一个动态的effect列表,循环永远不会结束
Set集合执行forEach时上述场合会有问题,但数组的话,没有上面的问题,也就是说:
如下代码会陷入死循环:
let myset = new Set([1]);
myset.forEach(n => {
myset.delete(1);
myset.add(1);
});
而下面的代码则不会死循环:
let arr = new Set([1]);
arr(n => {
arr.delete(1);
arr.add(1);
});
ReactiveEffect的stop方法是什么?有什么作用?
在effect的回调中,可能会多次改变代理对象上属性的值,如果没有做仁和处理,会导致effect多次重复渲染,降低性能,因此需要stop方法做节流
在ReactiveEffect的run方法中,会判断如果是失活态(active为false),也会调用this.fn执行一下,为什么这么做?
因为调用effect之后,可以返回run方法,用户得到这个run方法后可以自己调用执行,这种执行的场景有可能是调用过effect.stop方法的,但这种情况也需要effect回调执行生效,只是不再收集依赖了,所以才有这个逻辑
ReactiveEffect的options.schedular参数用在什么地方?
用在watch的实现中,ReactiveEffect是对变化发生时做什么事情的一个抽象,watch的设计思路是当自己定义的变化发生时,做自己定义的一些事情,这个和组件属性或状态发生变化时,更新组件是一类行为,所以抽象成了ReactiveEffect
在多级对象将其变成响应式的逻辑中,Vue3和Vue2有什么区别?
Vue2是在初始化的过程中,递归遍历对象的每一个属性,为其增加get/set方法,而Vue3是在effect执行时,收集依赖的过程中,get钩子里取到对象类型时,再将其变成代理对象,相当于延迟了添加代理的时间
watch和watchEffect的区别是什么?
watchEffect就是effect,如果不需要关注新老值有什么区别,可以使用watchEffect
watch的回调中,onCleanUp是什么作用?如何实现?
let arr = []; // 用于存储上一次的清理操作
// 什么是闭包? 我定义函数的作用域和执行函数的作用域不是同一个
watch(() => state.age, async function (newVal, oldVal, onCleanup) {
// while (arr.length > 0) {
// let fn = arr.shift();
// fn();
// }
let flag = true
onCleanup(function () {
// 取消操作 1) 取消请求 2) 清理 3) 屏蔽
flag = false
})
let r = await getData(newVal);
flag && (app.innerHTML = r)
}, { flush: 'sync' })
// 模拟用户输入1234
state.age = 11; // 请求2s返回 100
state.age = 111; // 请求2s返回 200
state.age = 1111; // 请求2s返回 200
计算属性是如何利用ReactiveEffect来实现的?
计算属性通过ComputedRefImpl类来定义,计算属性的getter会作为ReactiveEffect的fn(这里的fn其实和调用effect时传入的回调类似),此外ComputedRefImpl里对于ReactiveEffect监听的值变化时回调的处理中会将dirty改为true,以将其标识为已改动过