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

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

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中是如何收集依赖的,以及依赖是如何触发的,同这篇文章一样,也是对上上篇文章的一个补充;



目录
相关文章
|
10天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
131 11
|
5月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
745 5
|
2月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
255 1
|
2月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
147 0
|
3月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
98 0
|
4月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件的实现代码:支持自定义表情库、快捷键发送和输入框联动的聊天表情解决方案
本文详细介绍了在 Vue 项目中实现一个功能完善、交互友好的表情包输入组件的方法,并提供了具体的应用实例。组件设计包含表情分类展示、响应式布局、与输入框的交互及样式定制等功能。通过核心技术实现,如将表情插入输入框光标位置和点击外部关闭选择器,确保用户体验流畅。同时探讨了性能优化策略,如懒加载和虚拟滚动,以及扩展性方案,如自定义主题和国际化支持。最终,展示了如何在聊天界面中集成该组件,为用户提供丰富的表情输入体验。
325 8
|
5月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
414 17
|
6月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点
|
6月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
417 6
|
5月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1507 0

热门文章

最新文章