【源码&库】 Vue3 的依赖收集,这里的依赖指代的是什么?

2,096 阅读10分钟

Vue的响应式大家都知道,依赖收集和依赖派发这两个词汇也是经常听到的,但是这里的依赖指的是什么呢?

根据我上上篇的分析,依赖就是Vue中的effect,也就是Vue中的副作用函数,这一篇也是上上篇的一个补充,这次我们来详细分析一下Vue中的effect是如何实现的,以及effect的第二个参数还有调度器scheduler的作用。

effect

Vue中的effect是一个函数,它接受一个函数作为参数,这个函数就是我们的副作用函数;

effect函数会在执行的时候,会执行我们传入的函数,并且会将这个函数保存到一个全局的effect函数的数组中,这样我们就可以在需要的时候,调用这个数组中的函数,从而达到我们的副作用函数的执行。

先简单的看一下effect函数的实现:

/**
 * effect 函数
 * @param fn 副作用函数
 * @param options 配置项
 * @return {any} 返回一个执行副作用函数的函数
 */
function effect(fn, options) {
    // 如果传入的函数是一个 effect 函数,那么就直接取出它的 fn
    if (fn.effect) {
        fn = fn.effect.fn;
    }
    
    // 创建一个响应式副作用函数
    const _effect = new ReactiveEffect(fn);
    
    // 如果用户传入了配置项
    if (options) {
        
        // 合并配置项
        extend(_effect, options);
        
        // 记录 effect 函数的作用域
        if (options.scope)
            recordEffectScope(_effect, options.scope);
    }
    
    // 如果用户没有传入 lazy 配置项,那么就立即执行一次 effect 函数
    if (!options || !options.lazy) {
        _effect.run();
    }
    
    // 返回一个执行 effect 函数的函数
    const runner = _effect.run.bind(_effect);
    
    // 将 effect 函数保存到 runner.effect 中
    runner.effect = _effect;
    
    // 返回 runner
    return runner;
}

通过上面代码,不去看实现的细节,我们可以知道的是:

  1. effect函数接受一个函数作为参数,这个函数就是我们的副作用函数;
  2. effect函数还有第二个参数,这个参数是一个配置项,根据仅有的代码可以知道,这个配置项有两个属性,一个是lazy,一个是scope
  3. effect函数会返回一个执行副作用函数的函数;

也就是说一个最简单的effect函数的实现只需要做上面这三件事情就可以了,简单实现如下:

function effect(fn, options) {
    // 创建一个 runner 函数,用来执行副作用函数
    function runner() {
        fn();
    }
    
    // 是否立即执行
    if (!options || !options.lazy) {
        fn();
    }
    
    // 返回 runner
    return runner;
}

在这个最简单的实现中,我们发现其中的核心就是一个runner函数,而在源码中,runner函数是通过ReactiveEffect类来实现的;

同时ReactiveEffect还充当了effect函数的默认配置项的一个角色,runner函数只是ReactiveEffect的其中一个方法,所以effect的核心就是ReactiveEffect类;

ReactiveEffect

ReactiveEffect类的实现如下:

class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.active = true;
        this.deps = [];
        this.parent = undefined;
    }
    run() {
        // ...
    }
    stop() {
        // ...
    }
}

ReactiveEffect类的实现在之前已经分析过了,这一章会分析的是细节操作,如果想了解ReactiveEffect具体做了什么可以看:【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析

通过上面的代码,我们可以得知ReactiveEffect类最后实例化出来之后的结果如下:

// 创建一个响应式副作用函数
const _effect = new ReactiveEffect(fn);

_effect = {
    fn: fn,
    scheduler: null,
    active: true,
    deps: [],
    parent: undefined,
    run() {},
    stop() {}
}

这些属性我们其实很难猜到他们的具体作用,这个时候就需要去查看ts版的源码了,ts版的源码如下:

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []
  parent: ReactiveEffect | undefined = undefined

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: boolean

  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
  }

  run() {
      // ...
  }

  stop() {
      // ...
  }
}

这样看肯定还是不怎么得劲,我再来帮大家整理一下:

interface ReactiveEffect<T = any> {

    // 副作用函数
    fn: () => T;
    
    // 调度器
    scheduler: EffectScheduler | null;
    
    // 当前 副作用函数 是否处于活动状态
    active: boolean;
    
    // 当前 副作用函数 的所有依赖
    deps: Dep[];
    
    // 当前 副作用函数 的父级 副作用函数
    parent: ReactiveEffect | undefined;
    
    // 计算属性,可以在创建副作用函数之后再赋值
    computed?: ComputedRefImpl<T>;
    
    // 是否允许递归
    allowRecurse?: boolean;
    
    // 是否延迟停止,这是个私有属性
    deferStop?: boolean;
    
    // 停止时的回调函数
    onStop?: () => void;
    
    // 只有开发环境才会有的属性
    onTrack?: (event: DebuggerEvent) => void;
    onTrigger?: (event: DebuggerEvent) => void
    
    // 执行副作用函数
    run(): void;
    
    // 停止副作用函数
    stop(): void;
}

这里的最需要关心的其实是scheduler调度器,其他的属性几乎都不受用户控制,它们都是在运行时,因为处理各种问题而产生的;

例如active属性是在当前副作用函数执行的时候会被设置为true,在执行完毕之后会被设置为false,因为可能会出现嵌套副作用函数的情况,副作用函数可能会相互影响,就需要通过active属性来判断当前副作用函数是否处于活动状态;

例如deps属性是在当前副作用函数执行的时候会被设置为当前副作用函数所依赖的所有属性,这样在下一次执行副作用函数的时候,就可以通过deps属性来判断当前副作用函数是否需要重新执行;

还有其他的一些属性,大家感兴趣可以自己去深挖一下,这里就不一一分析了,而我们这次主要分析的是scheduler调度器;

scheduler

scheduler调度器是什么?可以看到ts源码会对应这个类型,来看看:

export type EffectScheduler = (...args: any[]) => any

这个类型的定义很简单,就是一个函数,这个函数可以接收任意数量的参数,返回值是任意类型的值;

它的目的是可以让我们自定义副作用函数的执行方式,通常情况下我们在使用副作用函数的时候是会直接执行的,但是有时候我们可能需要自定义副作用函数的执行方式;

大家可以尝试如下代码:

import { reactive, effect } from 'vue';

const state = reactive({
    count: 0
});

effect(() => {
    console.log(state.count);
}, {
    scheduler: () => {
        console.log('scheduler');
    },
});

state.count++;

image.png

可以看到,我们在创建副作用函数的时候,通过scheduler属性来自定义了副作用函数的执行方式,这个时候并不会直接执行副作用函数,而是会通过scheduler属性来执行副作用函数;

scheduler属性是我们自定义的,所以是需要我们手动来执行副作用函数的,这个时候我们可以通过scheduler属性来实现一些自定义的功能,例如:

import { reactive, effect } from 'vue';

const state = reactive({
    count: 0
});

const runner = effect(() => {
    console.log(state.count);
}, {
    scheduler: () => {
        console.log('scheduler');
        if (state.count % 2 === 0) {
            runner();
        }
    },
});

state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;

image.png

还记得effect会返回一个runner函数吗?这个runner函数就是用来执行副作用函数的,这里通过scheduler属性来实现了一个自定义的功能,就是当state.count是偶数的时候,才会再次执行副作用函数;

这个玩意儿可以用来做什么呢?例如我们可以通过scheduler属性来实现一个防抖的功能,例如:

import { reactive, effect } from 'vue';

const state = reactive({
    count: 0
});

let timer = null;
const runner = effect(() => {
    console.log(state.count);
}, {
    scheduler: () => {
        console.log("scheduler");
        clearTimeout(timer);
        timer = setTimeout(() => {
            runner();
        }, 300);
    },
});

state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;
state.count++;

image.png

可以看到这里只有两次打印state.count的值,所以方案完全可行,当然你也可以做一些其他的功能;

这里就再提一嘴watch方法,通过讲解scheduler属性,是不是发现和watch方法的行为很相似呢?我们用scheduler属性来实现一个简单的watch方法:

import {reactive, effect} from 'vue';

const state = reactive({
    count: 0
});


function watch(getter, cb, options = {}) {
    let oldValue = getter();
    const runner = effect(() => {
        const newValue = getter();
        if (oldValue !== newValue) {
            cb(newValue, oldValue);
            oldValue = newValue;
        } else if (options.immediate === true) {
            cb(newValue, undefined);
        }
    }, {
        scheduler() {
            runner();
        }
    });
}

watch(
    () => state.count,
    (newValue, oldValue) => {
        console.log(newValue, oldValue);
    }
)

watch(
    () => state.count,
    (newValue, oldValue) => {
        console.log(newValue, oldValue);
    },
    {
        immediate: true
    }
)

state.count++;

这个结果大家自行尝试一下,这里就不贴图了;

lazy

lazy属性是用来控制副作用函数是否在创建的时候就执行一次的,如果设置为true的话,那么副作用函数就不会在创建的时候就执行一次,而是需要手动执行一次;

例如我们上面的示例,每次runner函数都会至少执行两次,第一次是在创建副作用函数的时候,后面就都是响应式对象的值发生变化的时候;

如果我们设置lazy属性为true的话,那么副作用函数就不会在创建的时候就执行一次,而是需要手动执行一次,例如:

import { reactive, effect } from 'vue';

const state = reactive({
    count: 0
});

const runner = effect(() => {
    console.log(state.count);
}, {
    lazy: true,
});

state.count++;
state.count++;
state.count++;

上面这样是不会有任何打印的,因为副作用函数没有执行,没有执行就不会收集依赖,所以也就不会有任何打印;

需要注意的是要在响应式对象发生变化之前手动执行一次runner函数,否则就不会有任何打印;

我们手动执行一次runner函数,就可以看到打印了三次state.count的值,这就是lazy属性的作用,也是lazy属性使用的一个细节;

其他杂项

effect方法还有一些其他的属性,例如onTrackonTriggeronStop等,这些属性都是用来做一些额外的事情的,例如:

import { reactive, effect } from 'vue';

const state = reactive({
    count: 0
});

const runner = effect(
    () => {
        console.log(state.count);
    },
    {
        onTrack(event) {
            console.log("onTrack", event);
        },
        onTrigger(event) {
            console.log("onTrigger", event);
        },
        onStop() {
            console.log("onStop");
        }
    }
);

state.count++;
state.count++;
state.count++;

runner.effect.stop();

state.count++;
state.count++;

image.png

这里的onTrackonTrigger都是在开发环境下才会有的,主要用来调试当前的副作用函数有多少依赖,以及依赖发生了什么变化;

看名字就知道,onTrack是在收集依赖的时候触发的,onTrigger是在依赖发生变化的时候触发的;

onStop是在副作用函数被停止的时候触发的,例如上面的示例,我们手动调用了runner.effect.stop()方法,那么就会触发onStop方法;

并且调用runner.effect.stop()方法之后,再次修改state.count的值,也不会有任何打印,因为副作用函数已经被停止了;

总结

effect方法是Vue3中用来创建副作用函数的方法,通过effect方法可以创建一个副作用函数,副作用函数可以收集依赖,当依赖发生变化的时候,就会重新执行副作用函数;

effect方法还有一些其他的属性,例如lazyonTrackonTriggeronStop等,这些属性都是用来做一些额外的事情的;

当我们了解了effect方法之后,后面会发现Vue3中的watchcomputedwatchEffect等方法都是基于effect方法实现的;

这为我们后续了解Vue3的源码打下了基础,effect方法还有很多细节,但是已经不在使用上体现了,而是在依赖收集和依赖触发的时候体现;

下一篇文章我们会来看看Vue3中是如何收集依赖的,以及依赖是如何触发的,同这篇文章一样,也是对上上篇文章的一个补充;

宣传

大家好,这里是田八的【源码&库】系列,Vue3的源码阅读计划,Vue3的源码阅读计划不出意外每周一更,欢迎大家关注。

如果想一起交流的话,可以点击这里一起共同交流成长

系列章节:

本文正在参加「金石计划」