You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functionsetServiceDependency(id: ServiceIdentifier<any>,ctor: any,index: number): void{if(ctor[DI_TARGET]===ctor){ctor[DI_DEPENDENCIES].push({ id, index });}else{ctor[DI_DEPENDENCIES]=[{ id, index }];ctor[DI_TARGET]=ctor;}}functioncreateDecorator<T>(serviceId: string): ServiceIdentifier<T>{if(serviceIds.has(serviceId)){returnserviceIds.get(serviceId)!;}constid=function(target: any,key: string,index: number): any{if(arguments.length!==3){thrownewError('@IServiceName-decorator can only be used to decorate a parameter');}setServiceDependency(id,target,index);}asany;id.toString=()=>serviceId;serviceIds.set(serviceId,id);returnid;}
InstantiationService 在实例化的时候,将传入 services 挂载到 this 上,并且会建立 IInstantiationService 到自身实例的关系。
2.5.4 invokeFunction
Service 只有在被访问的时候才会实例化,也就是在 invokeFunction 的 accessors.get 的时候开始实例化。
如果已经实例化过,就直接返回实例,否则就会创建一个实例。
classInstantiationService{constructor(services: ServiceCollection=newServiceCollection(),parent?: InstantiationService,){this._services=services;}invokeFunction(fn){constaccessor: ServicesAccessor={const _trace =Trace.traceInvocation(this._enableTracing,fn);let_done=false;try{constaccessor: ServicesAccessor={get: <T>(id: ServiceIdentifier<T>)=>{if(_done){thrownewError('service accessor is only valid during the invocation of its target method');}constresult=this._getOrCreateServiceInstance(id,_trace);if(!result){this._handleError({errorType: InstantiationErrorType.UnknownDependency,issuer: 'service-accessor',dependencyId: `${id}`,message: `[invokeFunction] unknown service '${id}'`,});}returnresult;},};returnfn(accessor, ...args);}finally{_done=true;_trace.stop();}};returnfn(accessor, ...args);}}
for(constdependencyofgetServiceDependencies(item.desc.ctor)){constinstanceOrDesc=this._getServiceInstanceOrDescriptor(dependency.id);if(instanceOrDescinstanceofSyncDescriptor){constd={id: dependency.id,desc: instanceOrDesc,_trace: item._trace.branch(dependency.id,true),};// 当依赖没有初始化为实例,仍然是描述符式,添加到临时依赖图// 创建从依赖 service 到当前 service 的一条边graph.insertEdge(item,d);stack.push(d);}}
1. 前言
上一节介绍了 VSCode 启动流程,这一节主要介绍 VSCode 的依赖注入架构以及组件实现。
2. 依赖注入
2.1 什么是依赖注入
这部分主要讲解 VSCode DI 的实现,在开始之前,需要介绍一下什么是依赖注入。
前面讲到,VSCode 里面有很多服务,这些服务是以 class 的形式声明的。那服务之间也可能会互相调用,比如我有个 EditorService,他是负责编辑器的服务类,需要调用 FileService,用来做文件的存取。
如果服务类比较多,就会出现 A 依赖 B,B 依赖 C,C 依赖 D 和 E 等情况,我们就需要先将依赖的服务类实例化,当做参数传给依赖方。
随着项目越来越复杂,Service 和 Manager 类也会越来越多,手动管理这些模块之间的依赖和实例化顺序心智负担会变得很重。
为了解决对象间耦合度过高的问题,软件专家 Michael Mattson提出了 IOC 理论,用来实现对象之间的“解耦”。
控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)
采用依赖注入技术之后,ServiceA 的代码只需要定义一个 private 的 ServiceB 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 ServiceB 对象在外部 new 出来并注入到 ServiceA 类里的引用中。
2.2 概念介绍
在 VSCode 里面存在很多概念,Registry、Service、Contribution、Model 等等,下面会进行一一介绍。
2.3 Contribution
Contribution 一般是业务模块,它作为最上层的业务模块,一般不会被其他模块依赖,在 VSCode 里面一个 Contribution 就对应一个模块,Contribution 内部还会包含 UI 模块、Model 模块等。
举个例子,我们在编辑器里面常用的查找替换,它就是一个 Contribution。
2.4 Registry
Registry 一般是业务模块的集合,随着项目越来越复杂,Contribution 也会越来越多。
比如左侧菜单包括 Explore、Search、debug、Settings 等等,这里的每个模块都是一个 Contribution,Registry 就是将这些 Contribution 归类的一个集合。
2.5 Service
Service 一般是基础服务,提供一系列的基础能力,可以被多个 Contribution 共享。
一句话:Service 用于解决某个领域下的问题。
举几个例子:
我们写一个 Service 的时候,需要写哪些东西呢?下面是一个 Service 的例子:
2.5.1 interface
为什么要实现一个接口呢?我们希望 Service 之间可以不互相依赖具体的实现,不产生任何耦合,Service 应该只依赖其接口,做到面向接口编程。
以负责用户账号的 AccountService 为例,如果一个产品支持谷歌登录、Github 登录等等,这些登录的实现并不一样。
对于依赖用户登录信息的组件来说,应该依赖的是什么呢?GoogleAccountService?GithubAccoutService?我不想关心到底是什么账号,可能只是想调用 hasLogin 判断是否登录,我要依赖的应该只是 interface,不需要关心到底是什么账号体系。
在 VSCode 里面也有类似的例子,在 Electron 和 Web 环境注册的 Service 实现可能不一样,但 interface 是一样的。
2.5.2 createDecorator
我们先思考一个问题,createDecorator 做了哪些事情?用法是什么呢?假设有个 Test2Service 依赖了 TestService。
为什么我们不需要将 testService 实例化后传给 test2Service 呢?他们是怎么建立关联关系的呢?带着疑问看一下 createDecorator 的实现。
createDecorator 主要就是创建了一个装饰器,这个装饰器会调用 setServiceDependency,将 serviceId 设置到被装饰类的 DI_DEPENDENCIES 属性上面。
这样上面的例子中,我们就可以通过 @ITestService 建立 ITestService 和 Test2Service 的关联关系,指定 Test2Service 依赖了 ITestService。
2.5.3 InstantiationService
VSCode 里面 Service 有两种方式可以访问到:
第一种比较容易理解,就是实例化的时候将它依赖的 Service 实例自动传入。
那么先来分析第二种方式,在建立了依赖关系之后,究竟 Service 是怎么实例化,并且将依赖项自动传入的?我们来初始化一下 Service:
对于 ServiceCollection,可以简单理解为使用一个 Map 将 ITestService 和 TestService 做了一次关联,后续可以通过 ITestService 查询到 TestService 实例。
最终将存有关联信息的这个 Map 传给了 InstantiationService,这个 InstantiationService 是负责实例化的容器 Service,它提供了 invokeFunction 和 createChild、createInstance 方法。
InstantiationService 在实例化的时候,将传入 services 挂载到 this 上,并且会建立 IInstantiationService 到自身实例的关系。
2.5.4 invokeFunction
Service 只有在被访问的时候才会实例化,也就是在 invokeFunction 的 accessors.get 的时候开始实例化。
如果已经实例化过,就直接返回实例,否则就会创建一个实例。
PS:在 invokeFunction 中如果存在异步,那就需要在异步之后新开一个 invokeFunction 来访问 Service,不然访问就会报错。
_getOrCreateServiceInstance 会根据 serviceId 来获取到对应的 Service 类,如果在当前 instantiationService 的 _services 上找不到,那么就从他的 parent 上继续查找。
这里抛出一个问题,instantiationService 的 parent 是什么呢?一般来说还是一个 instantiationService,项目中可以不只有一个容器服务,容器服务内部还可以再创建容器服务。
以飞书文档为例,在全局创建 instantiationService,用于承载日志服务、上报服务等等。
在 instantiationService 下面还可以再创建一个 instantiationService,用于存放草稿相关的服务。
上一篇也有讲过容器化的概念。比如飞书文档中从文档 A 需要无刷新切换到文档 B。对于日志服务、配置服务这类基础服务是不需要销毁的,可以继续复用。
但是原本在文档 A 里面初始化的模块、快捷键、绑定的事件都需要销毁,在文档 B 中重新创建。
如果代码实现的没有那么安全,很容易就有一些模块的副作用没有被清理干净,就会影响到文档 B 的正常使用。
所以如果是通过 editorContainerService 来查找 environmentService,直接找不到,它就会从 parent 上面找。
如果从 _services 找到了,还需要判断是不是一个 SyncDescriptor,如果不是 SyncDescriptor,说明已经被实例化过了,就直接返回。如果是,那就走实例化的逻辑。
实例化的过程在 _createAndCacheServiceInstance 中,他会先创建一个依赖图,将当前的 serviceId 和 syncDescriptor 信息当做图的一个节点存入。
接着会从 graph 里面获取叶子节点,如果没有叶子节点,但 graph 又不为空,说明发生了循环依赖,会抛出错误。
遍历叶子节点,从叶子节点开始调用 _createServiceInstanceWithOwner 进行实例化,因为叶子节点一定是不会再依赖其他 Service 的。
如果注册的时候传入 supportsDelayedInstantiation,就会进行延迟初始化,延迟初始化会返回一个 Proxy,只有触发了 get,才会对 Service 进行实例化,可以减轻首屏的负担。
如果没有延迟初始化,就会调用 _createInstance 进行创建。实例化的时候会将通过 new SyncDescriptor 创建的参数带进去。
如果不是叶子节点,那就会将依赖的 Service 实例 + SyncDescriptor 的参数一起传进去。
至此,Service 的实例化就完成了。
2.5.5 createInstance
除了 Service,VSCode 里面还存在很多业务模块,为了方便理解,我们可以统一称之为 Manager。这些 Manager 有的是用 createInstance 实例化,有的是用 new 实例化。
用 createInstance 实例化的类拥有 DI 的能力,也可以通过依赖注入的方式获取依赖。和上述的 Service 创建最终走了相同的流程,这里不过多阐述。
还有个问题,我们在写 Service 的时候为什么要写一个 _serviceBrand 呢?这个到底有什么用?那你会不会好奇,为什么我们使用 DI 注入构造参数,TS 却不会报错呢?
看一下 createInstance 方法的签名就理解了,GetLeadingNonServiceArgs 会从构造函数参数类型里面剔除带 _serviceBrand 的参数,所以我们在 createInstance 的时候可以不传依赖的 Service。
如果不写 _serviceBrand, 那这个 Service 参数不会被剔除,就会要求我们手动传入。
如果我们想将某个 Service 当做参数传下去,因为 TS 会剔除这个参数,createInstance 反而会提示你少了一个参数报错。
3. 组件化
Vscode 没有使用 React/Vue 技术栈来编写 UI,而是选择使用纯原生来编写,那么他的 UI 是怎么渲染出来的呢?组件是怎么通信的呢?
与大多数以 React 作为 View 层,Redux/Mobx 处理数据和状态的形式不一样,VSCode 组件也都是 class 的形式。就以我们最熟悉的编辑器内 FindReplace 模块展开说说组件化是如何实现的。
3.1 Controller
VSCode 的复杂 UI 模块是 MVC 的形式来组织,划分成 Controller、View、Model 三层。
查找替换功能的入口在 FindController 里面,VSCode 里面的 UI 模块设计是以 Controller 为入口,创建对应的 Model 层和 View 层,其中 Model 层就是管理数据和状态的。
FindController 被当做 contribution 通过 registerEditorContribution 挂载到编辑器实例上面。
同时,VSCode 会将用户的操作作为 Action 注册到 EditorContributionRegistry,将快捷键作为 EditorCommand 也注册到 EditorContributionRegistry,Controller 也提供了一系列 public 方法供给 Action 和 Command 调用。
在 FindController 中会创建 FindWidget、FindReplaceState、FindModel 等实例,作为 View 层和 Model 层的桥梁,
3.2 Model 和 State
FindReplaceState 负责维护 searchString、replaceString、isRegex、matchesCount 等查找状态和匹配结果,它本身没有什么业务逻辑,可以理解为纯粹的 Store,而且 State 这一层不是必要的。
Model 层包含了 State,主要是做查找替换的业务逻辑,他会监听 State 的状态变更,从 Editor 进行搜索,将结果更新到 FindReplaceState。
在 Controller 上持有 Editor 实例, 它可以监听到 onDidChangeModel(编辑器内容变化),触发 Model 的搜索,更新搜索结果。
3.3 Widget
在开始之前,我们先看一个 VSCode 里面最简单的 Toggle 组件实现。
在 vs/base/browser/ui 目录下面都是 VSCode 的一些基础组件,每个组件包括了一个 JS 文件和一个 CSS 文件。
可以看到,Toggle 组件继承了 Widget 类,Widget 类是所有 UI 组件的基类,它会监听所有的 DOM 的事件,将其通过事件分发出去。
Toggle 支持传入 options 作为初始值,内部创建了 DOM 节点,所有的 UI 更新都是直接操作 DOM,并且将 get/set 方法暴露出去,这样调用方式也很简单,不再需要通过更新 state 来间接更新 UI。
通过这种对属性精细化的控制,可以将渲染性能优化到极致,这种做法 Canvas/WebGL 渲染层也可以参考。
接着说 FindWidget,它也继承了 Widget 类,初始化的时候内部会构建 DOM,其中查找输入框和替换输入框都是通过 Widget 来创建的,所以 Widget 具有组合的能力。
FindWidget 也监听了 State 的状态变更事件,在状态变更之后,就会根据变更原因来更新对应的 Widget 的 UI。比如 Command + D 引起搜索值变化了,就需要调用 findInputWidget.setValue 来更新搜索框的 UI。
3.4 组件通信
从上面可以看到每个 Widget 的职责都比较清晰,除了维护自身的功能,它还将细粒度的 get/set 方法暴露出去,方便外部更新。
对于复杂组件通信的情况,一般是通过事件 + set 来实现的,组件通信就下面两种:
比如查找替换这个组件,我们修改了搜索值,右侧的匹配结果就会更新,主要步骤可以简化为:
3.5 总结
为什么在 React/Vue 出现之前,大家都觉得原生JS、jQuery 这种开发模式不适合大型项目呢?为什么在 VSCode 上又可以呢?
原因是 jQuery 时期几乎没有模块化和组件化的概念,即使可以用 AMD/CMD 来做模块化、jQuery 插件来做组件化,但 jQuery 的组件化的不够彻底,上手成本也高一些。
我们用 jQuery 开发项目的时候,很容易出现一个 DOM 节点被到处绑事件,最后事件满天飞,调试起来很困难的情况。
如果使用模板引擎,更新效率比较低,DOM 重绘开销大,远远比不上 React/Vue
但在 VSCode 里面,每个组件只暴露自己的 getter/setter,内部变更通过事件通知,组件之间通信都是用事件的形式,组件和模块的划分也非常清晰。
通过对 DOM 属性细粒度更新,VSCode 性能也是比 React/Vue 更高的。
The text was updated successfully, but these errors were encountered: