Vue3源码学习

45 阅读7分钟

1reactive方法可将一个普通对象变为一个代理对象,如果再次将一个代理对象传入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() {

  }

}

2effect嵌套调用时,全局的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;

    }

  }

}

3reactive返回的代理对象属性,和effect的关系是什么?

答:多对多

        const state = reactive({ name: 'jw', age: 30 })

        effect(() => { // 使用类似 react-hook

            app.innerHTML = state.name + state.age

        });

上述代码中的effect对应name age两个属性

name age两个属性可能会对应多个effect

4vue3WeakMap的数据结构是什么样的?

{

  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方法

6ReactiveEffect中的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();

});

Effectdeps是什么属性?有什么作用?

deps是一个二维数组结构,数组的第一维映射代理对象的属性,第二维是这个属性关联的effect

8effect中的回调每次执行触发对象的get钩子,进而触发track时,是否会重新收集依赖?

会,effect中的回调通过effect的run方法来执行,run方法里面会调用cleanupEffect,该effect里涉及到的对象属性所关联的effect中如果有当前正在执行的这个effect,则会将其从WeakMap里清掉

清除的原因为每次effect执行时,里面的属性值都有可能和之前的不一样,因此收集本次effect执行时用到的属性关联的effect即可

9、依赖是否需要清理?在什么时机清理?怎么清理?

需要清理,在effect的run方法执行时会先清理之前effect回调里的对象属性所关联的effect,再执行effect的回调

10trigger中从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);

});

ReactiveEffectstop方法是什么?有什么作用?

在effect的回调中,可能会多次改变代理对象上属性的值,如果没有做仁和处理,会导致effect多次重复渲染,降低性能,因此需要stop方法做节流

ReactiveEffectrun方法中,会判断如果是失活态(activefalse),也会调用this.fn执行一下,为什么这么做?

因为调用effect之后,可以返回run方法,用户得到这个run方法后可以自己调用执行,这种执行的场景有可能是调用过effect.stop方法的,但这种情况也需要effect回调执行生效,只是不再收集依赖了,所以才有这个逻辑

ReactiveEffectoptions.schedular参数用在什么地方?

用在watch的实现中,ReactiveEffect是对变化发生时做什么事情的一个抽象,watch的设计思路是当自己定义的变化发生时,做自己定义的一些事情,这个和组件属性或状态发生变化时,更新组件是一类行为,所以抽象成了ReactiveEffect

在多级对象将其变成响应式的逻辑中,Vue3Vue2有什么区别?

Vue2是在初始化的过程中,递归遍历对象的每一个属性,为其增加get/set方法,而Vue3是在effect执行时,收集依赖的过程中,get钩子里取到对象类型时,再将其变成代理对象,相当于延迟了添加代理的时间

watchwatchEffect的区别是什么?

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,以将其标识为已改动过