VSCode用它管理上千个服务:依赖注入从入门到实战

VSCode用它管理上千个服务:依赖注入从入门到实战

摘要 :你的代码里到处都是 new?类之间紧密耦合难以测试?本文从最基础的依赖注入概念讲起,逐步深入到装饰器与DI容器,最后剖析VSCode如何用依赖注入系统优雅地管理整个应用架构。读完你将理解为什么依赖注入是现代应用架构的基石。

引言

在日常开发中,你可能经常写出这样的代码:

typescript 复制代码
class UserService {
    private userRepo = new UserRepo();
    private logger = new Logger();
    
    public findUser(id: string) {
        this.logger.log('查询用户');
        return this.userRepo.findById(id);
    }
}

这段代码看起来很直观,但隐藏着一个严重的问题:UserService 和它的依赖 UserRepoLogger 紧紧地绑在了一起。想要替换数据库实现?想要在测试中 Mock Logger?几乎不可能。

更糟糕的是,当你的应用规模扩大,类之间的依赖关系变成一张复杂的网,你会发现:

  • 修改一个类的构造函数,可能需要修改几十个地方
  • 单元测试变得异常困难,因为无法隔离依赖
  • 想要支持不同环境的配置,需要到处修改 new 语句

有没有更优雅的方式?有的,这就是依赖注入(Dependency Injection,DI)

VSCode 作为一个拥有数百万行代码的大型项目,正是通过依赖注入系统来管理成百上千个服务。今天,我们就从最基础的概念开始,一步步理解依赖注入的精髓,最后看看 VSCode 是如何实现工业级的 DI 系统的。

什么是依赖注入

依赖注入(Dependency Injection,DI) 是一种设计模式,核心思想非常简单:

不要在类内部创建依赖对象,而是从外部传入所需的依赖。

这个简单的改变,带来了四个重要的好处:

  • 降低耦合度:类不需要知道具体的实现,只依赖于抽象接口
  • 提高可测试性:可以轻松注入 Mock 对象进行单元测试
  • 增强灵活性:可以在运行时动态切换不同的实现
  • 符合 SOLID 原则:特别是依赖倒置原则(Dependency Inversion Principle)

让我们用一个简单的例子来理解这个转变:

传统方式(紧耦合)

typescript 复制代码
class UserService {
    // ❌ 在内部创建依赖
    private userRepo = new UserRepo();
    
    public findUser(id: string) {
        return this.userRepo.findById(id);
    }
}

在这个例子中,UserRepo 是在 UserService 内部实例化的,两者紧密耦合。如果想把 UserRepo 替换成其他实现(比如换一个数据库),你必须修改 UserService 的代码。

依赖注入方式(松耦合)

typescript 复制代码
class UserService {
    private userRepo: UserRepo; // 只声明依赖类型
    
    // ✅ 从外部传入依赖
    constructor(userRepo: UserRepo) {
        this.userRepo = userRepo;
    }
    
    public findUser(id: string) {
        return this.userRepo.findById(id);
    }
}

// 使用时在外部创建依赖并注入
const userRepo = new UserRepo();
const userService = new UserService(userRepo);

现在,UserRepoUserService 外部实例化,并通过构造函数传入。UserService 只需要知道依赖的接口,不需要关心具体实现。这样就可以轻松替换成其他实现,甚至在测试中使用 Mock 对象:

typescript 复制代码
// 生产环境使用真实实现
const userService = new UserService(new UserRepo());

// 测试环境使用 Mock
const userService = new UserService(new MockUserRepo());

这就是依赖注入的本质:控制权的反转(Inversion of Control,IoC)。依赖对象的创建权从类内部转移到了外部,类本身只负责使用这些依赖。

手动依赖注入:最基础的实践

理解了概念之后,让我们通过一个更完整的例子来看看依赖注入的实际应用。

第一步:定义接口,而非具体实现

这是依赖注入的关键原则:依赖于抽象,而非具体实现。

typescript 复制代码
// 定义日志接口
interface Logger {
    log(message: string): void;
}

// 定义数据库接口
interface DatabaseService {
    save(data: any): Promise<void>;
    findById(id: string): Promise<any>;
}

注意,这里我们只定义了接口,没有涉及任何具体实现。这样做的好处是:UserService 将只依赖这些接口,不依赖任何具体的实现类。

第二步:为不同场景提供不同实现

接口定义好之后,我们可以为不同的场景提供不同的实现:

typescript 复制代码
// 控制台日志实现
class ConsoleLogger implements Logger {
    log(message: string): void {
        console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
    }
}

// 文件日志实现
class FileLogger implements Logger {
    log(message: string): void {
        // 实际项目中这里会写入文件
        console.log(`[FILE] ${message}`);
    }
}

// MongoDB 数据库实现
class MongoDBService implements DatabaseService {
    async save(data: any): Promise<void> {
        console.log('保存到 MongoDB:', data);
    }
    
    async findById(id: string): Promise<any> {
        console.log('从 MongoDB 查询:', id);
        return { id, name: 'John' };
    }
}

// MySQL 数据库实现
class MySQLService implements DatabaseService {
    async save(data: any): Promise<void> {
        console.log('保存到 MySQL:', data);
    }
    
    async findById(id: string): Promise<any> {
        console.log('从 MySQL 查询:', id);
        return { id, name: 'Jane' };
    }
}

现在我们有了多种实现:日志可以输出到控制台或文件,数据库可以是 MongoDB 或 MySQL。关键是,这些实现都遵循同样的接口。

第三步:业务服务只依赖接口

现在来实现我们的业务服务 UserService。注意,它只依赖接口,不依赖任何具体实现:

typescript 复制代码
// 使用依赖注入的业务服务
class UserService {
    constructor(
        private logger: Logger,           // 依赖 Logger 接口
        private database: DatabaseService // 依赖 DatabaseService 接口
    ) {}

    async createUser(userData: any): Promise<void> {
        this.logger.log('开始创建用户');
        
        try {
            await this.database.save(userData);
            this.logger.log('用户创建成功');
        } catch (error) {
            this.logger.log(`用户创建失败: ${error}`);
            throw error;
        }
    }

    async getUser(id: string): Promise<any> {
        this.logger.log(`查询用户: ${id}`);
        return await this.database.findById(id);
    }
}

UserService 不关心具体用的是哪种 Logger,也不关心用的是哪种数据库。它只需要知道"能打日志"和"能存储数据"就够了。

第四步:灵活组装,随心切换

现在,我们可以根据不同的需求,灵活地组装不同的服务组合:

typescript 复制代码
// 开发环境:使用控制台日志 + MongoDB
const devLogger = new ConsoleLogger();
const devDatabase = new MongoDBService();
const devUserService = new UserService(devLogger, devDatabase);

// 生产环境:使用文件日志 + MySQL
const prodLogger = new FileLogger();
const prodDatabase = new MySQLService();
const prodUserService = new UserService(prodLogger, prodDatabase);

// 测试环境:使用 Mock 对象
const mockLogger = new MockLogger();
const mockDatabase = new MockDatabase();
const testUserService = new UserService(mockLogger, mockDatabase);

这就是依赖注入的魅力:

  1. 解耦合 - UserService 不依赖具体的日志和数据库实现,只依赖抽象接口
  2. 可扩展 - 可以轻松替换不同的 Logger(控制台/文件)和 DatabaseService(MongoDB/MySQL)实现
  3. 易测试 - 可以注入 Mock 对象进行单元测试
  4. 灵活配置 - 同一个业务逻辑可以在不同环境下使用不同的底层服务实现

但是,你可能注意到了一个问题:每次创建 UserService,我们都需要手动创建并注入所有依赖。当依赖关系变得复杂时,这会变得非常繁琐。有没有办法自动化这个过程?

自动化DI:装饰器与容器

当应用规模扩大,手动管理依赖注入会变得非常繁琐。想象一下,如果你有几十个服务,每个服务又依赖其他几个服务,依赖关系会形成一张复杂的网。

这时候,我们需要一个**DI容器(DI Container)**来自动管理这些依赖关系。

什么是DI容器?

DI 容器的核心职责是:

  1. 注册(Register):记录服务标识符和具体实现之间的映射关系
  2. 解析(Resolve):当需要某个服务时,自动创建实例并注入所有依赖
  3. 生命周期管理:决定服务是单例还是每次都创建新实例

让我们从零开始实现一个简单但完整的 DI 容器。

第一步:定义核心类型

typescript 复制代码
// 构造函数类型
type Constructor<T = any> = new (...args: any[]) => T;

// 服务标识符:可以是类本身、字符串或Symbol
type ServiceIdentifier<T = any> = Constructor<T> | string | symbol;

// 服务生命周期
enum ServiceLifetime {
    Transient = 'transient', // 每次都创建新实例
    Singleton = 'singleton'  // 单例模式
}

// 服务描述符
interface ServiceDescriptor<T = any> {
    implementation: Constructor<T>;  // 具体实现类
    lifetime: ServiceLifetime;       // 生命周期
    dependencies: ServiceIdentifier[]; // 依赖的服务列表
}

// 依赖元数据
interface DependencyMetadata {
    index: number;           // 参数位置
    identifier: ServiceIdentifier; // 服务标识符
}

第二步:实现DI容器

typescript 复制代码
class DIContainer {
    // 存储服务注册信息
    private services = new Map<ServiceIdentifier, ServiceDescriptor>();
    
    // 缓存单例实例
    private singletonInstances = new Map<ServiceIdentifier, any>();
    
    // 解析栈,用于检测循环依赖
    private resolutionStack: ServiceIdentifier[] = [];

    // 注册瞬态服务(每次创建新实例)
    register<T>(identifier: ServiceIdentifier<T>, implementation: Constructor<T>): void {
        this.registerWithLifetime(identifier, implementation, ServiceLifetime.Transient);
    }

    // 注册单例服务(全局唯一实例)
    registerSingleton<T>(identifier: ServiceIdentifier<T>, implementation: Constructor<T>): void {
        this.registerWithLifetime(identifier, implementation, ServiceLifetime.Singleton);
    }

    // 统一的注册逻辑
    private registerWithLifetime<T>(
        identifier: ServiceIdentifier<T>,
        implementation: Constructor<T>,
        lifetime: ServiceLifetime
    ): void {
        // 分析构造函数的依赖
        const dependencies = this.getDependencies(implementation);
        this.services.set(identifier, { implementation, lifetime, dependencies });
    }

    // 解析服务:获取服务实例
    resolve<T>(identifier: ServiceIdentifier<T>): T {
        // 检测循环依赖
        if (this.resolutionStack.includes(identifier)) {
            throw new Error(`循环依赖: ${[...this.resolutionStack, identifier].join(' -> ')}`);
        }

        // 获取服务描述符
        const descriptor = this.services.get(identifier);
        if (!descriptor) {
            throw new Error(`服务未注册: ${String(identifier)}`);
        }

        // 加入解析栈
        this.resolutionStack.push(identifier);

        try {
            // 单例模式:使用缓存
            if (descriptor.lifetime === ServiceLifetime.Singleton) {
                if (!this.singletonInstances.has(identifier)) {
                    this.singletonInstances.set(identifier, this.createInstance(descriptor));
                }
                return this.singletonInstances.get(identifier);
            }
            
            // 瞬态模式:每次创建新实例
            return this.createInstance(descriptor);
        } finally {
            // 移出解析栈
            this.resolutionStack.pop();
        }
    }

    // 创建实例:递归解析所有依赖
    private createInstance<T>(descriptor: ServiceDescriptor<T>): T {
        // 递归解析所有依赖
        const resolvedDependencies = descriptor.dependencies.map(dep => this.resolve(dep));
        // 使用解析后的依赖创建实例
        return new descriptor.implementation(...resolvedDependencies);
    }

    // 获取类的依赖信息(通过装饰器添加的元数据)
    private getDependencies(target: Constructor): ServiceIdentifier[] {
        const metadata: DependencyMetadata[] = (target as any).__dependencies__ || [];
        return metadata.sort((a, b) => a.index - b.index).map(meta => meta.identifier);
    }
}

这个 DI 容器实现了三个核心功能:

  1. 循环依赖检测 :通过 resolutionStack 追踪解析链,防止无限递归
  2. 单例管理:对于单例服务,只创建一次并缓存
  3. 自动依赖解析:递归解析并创建所有依赖

第三步:定义装饰器

为了简化使用,我们定义两个装饰器:

typescript 复制代码
// @Injectable 装饰器:标记类可以被注入
function Injectable<T extends Constructor>(target: T): T {
    return target;
}

// @Inject 装饰器:标记构造函数参数需要注入的服务
function Inject(identifier: ServiceIdentifier) {
    return function (target: any, propertyKey: string | symbol | undefined, parameterIndex: number) {
        // 在类的原型上记录依赖信息
        const metadata: DependencyMetadata[] = target.__dependencies__ || [];
        metadata.push({ index: parameterIndex, identifier });
        target.__dependencies__ = metadata;
    };
}

第四步:使用示例

现在,我们可以用装饰器来简化依赖注入的使用:

typescript 复制代码
// 定义唯一标识符(使用 Symbol 避免命名冲突)
const TOKENS = {
    LOGGER: Symbol('Logger'),
    DATABASE: Symbol('Database'),
    USER_SERVICE: Symbol('UserService')
};

// 定义服务
@Injectable
class Logger {
    log(message: string): void {
        console.log(`[LOG] ${message}`);
    }
}

@Injectable
class Database {
    // 使用 @Inject 指定要注入的服务
    constructor(@Inject(TOKENS.LOGGER) private logger: Logger) {}

    async save(data: any): Promise<void> {
        this.logger.log(`保存数据: ${JSON.stringify(data)}`);
    }
}

@Injectable
class UserService {
    constructor(
        @Inject(TOKENS.DATABASE) private database: Database,
        @Inject(TOKENS.LOGGER) private logger: Logger
    ) {}

    async createUser(userData: any): Promise<any> {
        this.logger.log('创建用户');
        await this.database.save(userData);
        return { id: Date.now(), ...userData };
    }
}

// 注册服务
const container = new DIContainer();
container.registerSingleton(TOKENS.LOGGER, Logger);     // Logger 是单例
container.register(TOKENS.DATABASE, Database);           // Database 每次创建新实例
container.register(TOKENS.USER_SERVICE, UserService);

// 使用服务(所有依赖自动注入)
const userService = container.resolve<UserService>(TOKENS.USER_SERVICE);
userService.createUser({ name: 'John', email: 'john@example.com' });

现在,依赖注入变得非常简洁:

  1. @Injectable 标记类
  2. @Inject(TOKENS.XXX) 标记依赖
  3. 注册到容器
  4. 容器自动解析所有依赖并创建实例

关键概念Q&A

Q:registerregisterSingleton 有什么区别?

A:生命周期不同。registerSingleton 注册的是单例,不管多少个类依赖它,使用的总是同一个实例,适合全局唯一的资源(如日志服务、配置服务)。register 注册的不是单例,每次有类依赖时,都会创建一个全新的实例。

Q:DI 注册的 class 是在什么阶段实例化的?

A:在 register 阶段,DI 容器仅仅记录了 class 和标识符之间的映射关系,以及构造函数依赖的元数据(__dependencies__),并没有实例化。只有在调用 resolve 时,才会实例化该类。如果此类依赖其他类,会深度递归解析所有依赖并逐个实例化。这是 DI 容器**延迟实例化(Lazy Instantiation)**的特性,可以提高启动性能。

VSCode的DI系统:工业级实现

前面我们学习了依赖注入的基本原理和实现。现在让我们看看,VSCode 这样一个拥有数百万行代码的大型项目,是如何应用依赖注入系统的。

VSCode DI 的使用方式

VSCode 使用了自定义的依赖注入系统,使用起来非常优雅。让我们通过一个真实的例子来看看:

第一步:创建服务标识符

typescript 复制代码
// 来源:src/vs/workbench/services/textfile/common/textfiles.ts
// 使用 createDecorator 创建服务标识符
const ITextFileService = createDecorator<ITextFileService>('textFileService');

第二步:全局注册单例

typescript 复制代码
// 来源:src/vs/workbench/services/textfile/electron-sandbox/nativeTextFileService.ts
// 注册为单例服务
registerSingleton(ITextFileService, NativeTextFileService, InstantiationType.Eager);

第三步:在其他服务中使用

typescript 复制代码
// 在任何需要文件服务的地方,直接通过装饰器注入
class XXXService {
    constructor(
        @ITextFileService private readonly _textFileService: ITextFileService 
    ) {}
    
    someMethod() {
        // 直接使用注入的服务
        this._textFileService.save(/* ... */);
    }
}

是不是非常简洁?只需要三步:

  1. createDecorator 创建服务标识符
  2. registerSingleton 全局注册
  3. 在构造函数中用装饰器注入

这背后有三个关键的实现。

关键实现一:createDecorator

createDecorator 函数用于创建服务标识符和对应的装饰器:

typescript 复制代码
// 来源:src/vs/platform/instantiation/common/instantiation.ts

export namespace _util {
    export const serviceIds = new Map<string, ServiceIdentifier<any>>();
    export const DI_TARGET = '$di$target';
    export const DI_DEPENDENCIES = '$di$dependencies';

    export function getServiceDependencies(ctor: any): { id: ServiceIdentifier<any>; index: number }[] {
        return ctor[DI_DEPENDENCIES] || [];
    }
}

export interface ServiceIdentifier<T> {
    (...args: any[]): void;
    type: T;
}

// 在类原型上存储依赖信息
function storeServiceDependency(id: Function, target: Function, index: number): void {
    if ((target as any)[_util.DI_TARGET] === target) {
        (target as any)[_util.DI_DEPENDENCIES].push({ id, index });
    } else {
        (target as any)[_util.DI_DEPENDENCIES] = [{ id, index }];
        (target as any)[_util.DI_TARGET] = target;
    }
}

// 创建服务标识符和装饰器
export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
    // 避免重复创建
    if (_util.serviceIds.has(serviceId)) {
        return _util.serviceIds.get(serviceId)!;
    }

    // 返回一个装饰器函数
    const id = <any>function (target: Function, key: string, index: number) {
        if (arguments.length !== 3) {
            throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
        }
        // 在类的原型上记录依赖关系
        storeServiceDependency(id, target, index);
    };

    id.toString = () => serviceId;

    _util.serviceIds.set(serviceId, id);
    return id;
}

createDecorator 的作用是生成一个装饰器,当装饰器被使用时,会在被装饰的类原型上挂载 DI_TARGETDI_DEPENDENCIES 属性,用于后续的依赖分析。

关键实现二:registerSingleton

registerSingleton 用于全局注册服务:

typescript 复制代码
// 来源:src/vs/platform/instantiation/common/extensions.ts

// 全局注册表
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];

// 实例化类型
export const enum InstantiationType {
    Eager = 0,    // 立即实例化
    Delayed = 1   // 延迟实例化
}

// 注册单例服务
export function registerSingleton<T, Services extends BrandedService[]>(
    id: ServiceIdentifier<T>, 
    ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, 
    supportsDelayedInstantiation?: boolean | InstantiationType
): void {
    if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
        ctorOrDescriptor = new SyncDescriptor<T>(
            ctorOrDescriptor as new (...args: any[]) => T, 
            [], 
            Boolean(supportsDelayedInstantiation)
        );
    }

    // 记录到全局注册表
    _registry.push([id, ctorOrDescriptor]);
}

// 获取所有已注册的服务
export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {
    return _registry;
}

register 阶段,服务只是被记录到全局注册表 _registry 中,并没有被实例化。实例化发生在第一次使用时。

关键实现三:InstantiationService

InstantiationService 是 VSCode DI 系统的核心,负责实际的依赖解析和实例创建:

typescript 复制代码
// 来源:src/vs/platform/instantiation/common/instantiationService.ts

export class InstantiationService implements IInstantiationService {
    constructor(
        private readonly _services: ServiceCollection = new ServiceCollection(),
    ) {
        // 把自己也注册到容器中,这样其他服务也可以依赖 InstantiationService
        this._services.set(IInstantiationService, this);
    }

    createInstance<T>(ctor: any, ...args: any[]): T {
        // 获取构造函数的依赖
        const serviceDependencies = _util.getServiceDependencies(ctor)
            .sort((a, b) => a.index - b.index);
        
        const serviceArgs: any[] = [];
        
        // 解析每个依赖
        for (const dependency of serviceDependencies) {
            const service = this._getOrCreateServiceInstance(dependency.id);
            serviceArgs.push(service);
        }

        // 使用 Reflect.construct 创建实例
        return Reflect.construct<any, T>(ctor, serviceArgs);
    }

    private _getOrCreateServiceInstance(id: ServiceIdentifier<any>): any {
        // 如果已经创建过,直接返回
        // 否则创建新实例(递归解析依赖)
        // ...
    }
}

createInstance 方法的核心逻辑:

  1. 通过 getServiceDependencies 获取要创建类的依赖列表
  2. 递归解析每个依赖,调用 _getOrCreateServiceInstance 获取依赖实例
  3. 使用 Reflect.construct 创建目标实例,把所有依赖作为构造函数参数传入

DI 系统的初始化

那么,VSCode 的 DI 系统是在哪里启动的呢?答案是在应用的入口文件:

typescript 复制代码
// 来源:src/vs/code/electron-main/main.ts

class CodeMain {
    private createServices() {
        const services = new ServiceCollection();
        
        // 手动创建一些基础服务
        const environmentMainService = new EnvironmentMainService(xxx);
        services.set(IEnvironmentMainService, environmentMainService);
        
        // ... 其他基础服务
        
        // 创建 InstantiationService
        return [new InstantiationService(services, true)];
    }
}

你可能会疑惑:为什么有些服务(如 EnvironmentMainService)还是手动 new 创建的,而不是通过 DI 系统自动创建?

这是因为:

  1. DI 系统本身无法自举:必须先有第一批手动创建的服务,才能启动 DI 系统
  2. 基础服务的特殊性:一些核心服务(如环境服务、配置服务)是 DI 系统本身的依赖,必须先手动创建
  3. 初始化顺序控制:某些服务的初始化顺序很重要,手动创建可以精确控制

等基础服务启动完成后,后续的服务就可以利用 DI 系统自动实例化了。

ServiceCollection:简单的服务容器

typescript 复制代码
// 来源:src/vs/platform/instantiation/common/serviceCollection.ts

export class ServiceCollection {
    private _entries = new Map<ServiceIdentifier<any>, any>();

    constructor(...entries: [ServiceIdentifier<any>, any][]) {
        for (const [id, service] of entries) {
            this.set(id, service);
        }
    }

    set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> {
        const result = this._entries.get(id);
        this._entries.set(id, instanceOrDescriptor);
        return result;
    }

    has(id: ServiceIdentifier<any>): boolean {
        return this._entries.has(id);
    }

    get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
        return this._entries.get(id);
    }
}

ServiceCollection 的实现非常简单,就是一个 Map,用于存储服务标识符和服务实例(或描述符)之间的映射关系。

总结

依赖注入的核心思想很简单:不要在类内部创建依赖,而是从外部传入。这个简单的改变带来了解耦、易测试、可扩展等诸多好处。

我们从三个层次理解了依赖注入:手动注入展示了基本概念,DI 容器实现了自动化管理,VSCode 则演示了工业级的应用实践。关键技术包括装饰器、服务容器、延迟实例化和循环依赖检测。

依赖注入不是银弹,但当你的代码库变得复杂,类之间的依赖关系让你焦头烂额时,它确实是让代码变得更优雅、更易维护的重要工具。

写在最后

依赖注入是我在阅读 VSCode 源码时最先注意到的设计模式之一。起初看到到处都是 @IXXXService 装饰器时,我还有些困惑。但深入理解后,我被它的优雅深深折服:整个应用的几百个服务,就像积木一样可以自由组合,每个服务只关注自己的职责,依赖关系清晰明了。

留给你的思考题

写完这篇文章,我想问问你:

  • 你的项目中是否也遇到过"到处都是 new,依赖关系一团乱麻"的问题?
  • 你是如何解决的?是引入了 DI 框架,还是有其他更好的方案?
  • 对于小型项目,你觉得依赖注入是不是"过度设计"?

交流与分享

如果你对 VSCode 的架构设计感兴趣,欢迎关注我的 VSCode 源码寻宝:那些藏在代码里的设计智慧 专栏,我会持续分享 VSCode 源码中的精彩设计。

最后,如果你在实践中遇到了问题,或者有更好的实践经验,欢迎在评论区分享。期待你的见解:

  • 你在引入 DI 时遇到过哪些坑?
  • 你的团队是如何平衡"灵活性"和"复杂度"的?
  • 有没有什么"不适合用 DI"的场景?
相关推荐
骑自行车的码农3 小时前
React 合成事件的设计原理 2
前端·react.js
JamesGosling6663 小时前
详解 Vue 3.6 Vapor Mode:从原理到问题,看透 VDOM 逐步退场的底层逻辑
前端·vue.js
一个很帅的帅哥3 小时前
Vue中的hash模式和history模式
前端·vue.js·history模式·hash模式
进阶的鱼3 小时前
React+ts+vite脚手架搭建(三)【状态管理篇】
前端·javascript·react.js
By北阳4 小时前
Less resolver error:‘~antd/es/style/themes/index.less‘ wasn‘t found.
前端·elasticsearch·less
charlie1145141914 小时前
精读C++20设计模式——结构型设计模式:外观模式
c++·学习·设计模式·c++20·外观模式
西洼工作室4 小时前
SSE与轮询技术实时对比演示
前端·javascript·css
IT_陈寒5 小时前
Vite 5.0 性能优化实战:3 个关键配置让你的构建速度提升50%
前端·人工智能·后端
excel5 小时前
Vue2 动态添加属性导致页面不更新的原因与解决方案
前端