成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

VSCode 架構(gòu)分析:依賴(lài)注入和組件

開(kāi)發(fā) 前端
為什么在 React/Vue 出現(xiàn)之前,大家都覺(jué)得原生JS、jQuery 這種開(kāi)發(fā)模式不適合大型項(xiàng)目呢?為什么在 VSCode 上又可以呢?

1. 前言

這一節(jié)主要介紹 VSCode 的依賴(lài)注入架構(gòu)以及組件實(shí)現(xiàn)。

2. 依賴(lài)注入

2.1 什么是依賴(lài)注入

這部分主要講解 VSCode DI 的實(shí)現(xiàn),在開(kāi)始之前,需要介紹一下什么是依賴(lài)注入。

前面講到,VSCode 里面有很多服務(wù),這些服務(wù)是以 class 的形式聲明的。那服務(wù)之間也可能會(huì)互相調(diào)用,比如我有個(gè) EditorService,他是負(fù)責(zé)編輯器的服務(wù)類(lèi),需要調(diào)用 FileService,用來(lái)做文件的存取。

如果服務(wù)類(lèi)比較多,就會(huì)出現(xiàn) A 依賴(lài) B,B 依賴(lài) C,C 依賴(lài) D 和 E 等情況,我們就需要先將依賴(lài)的服務(wù)類(lèi)實(shí)例化,當(dāng)做參數(shù)傳給依賴(lài)方。

class ServiceA {
constructor(serviceB: ServiceB) {}
}

class Service B {
constructor(serviceC: ServiceC) {}
}

class ServiceC {
constructor(serviceD: ServiceD, serviceE: ServiceE) {}
}

const serviceD = new ServiceD();
const serviceE = new ServiceE();
const serviceC = new ServiceC(serviceD, serviceE);
const serviceB = new ServiceB(serviceC);

隨著項(xiàng)目越來(lái)越復(fù)雜,Service 和 Manager 類(lèi)也會(huì)越來(lái)越多,手動(dòng)管理這些模塊之間的依賴(lài)和實(shí)例化順序心智負(fù)擔(dān)會(huì)變得很重。

為了解決對(duì)象間耦合度過(guò)高的問(wèn)題,軟件專(zhuān)家 Michael Mattson提出了 IOC 理論,用來(lái)實(shí)現(xiàn)對(duì)象之間的“解耦”。

控制反轉(zhuǎn)(英語(yǔ):Inversion of Control,縮寫(xiě)為IoC),是面向?qū)ο缶幊讨械囊环N設(shè)計(jì)原則,可以用來(lái)減低計(jì)算機(jī)代碼之間的耦合度。其中最常見(jiàn)的方式叫做依賴(lài)注入(Dependency Injection,簡(jiǎn)稱(chēng)DI)

采用依賴(lài)注入技術(shù)之后,ServiceA 的代碼只需要定義一個(gè) private 的 ServiceB 對(duì)象,不需要直接 new 來(lái)獲得這個(gè)對(duì)象,而是通過(guò)相關(guān)的容器控制程序來(lái)將 ServiceB 對(duì)象在外部 new 出來(lái)并注入到 ServiceA 類(lèi)里的引用中。

class ServiceA {
  constructor(@IServiceB private _serviceB: ServiceB) {}
}

class Service B {
  constructor(@IServiceC serviceC: ServiceC) {}
}

2.2 概念介紹

在 VSCode 里面存在很多概念,Registry、Service、Contribution、Model 等等,下面會(huì)進(jìn)行一一介紹。

2.3 Contribution

Contribution 一般是業(yè)務(wù)模塊,它作為最上層的業(yè)務(wù)模塊,一般不會(huì)被其他模塊依賴(lài),在 VSCode 里面一個(gè) Contribution 就對(duì)應(yīng)一個(gè)模塊,Contribution 內(nèi)部還會(huì)包含 UI 模塊、Model 模塊等。

舉個(gè)例子,我們?cè)诰庉嬈骼锩娉S玫牟檎姨鎿Q,它就是一個(gè) Contribution。

2.4 Registry

Registry 一般是業(yè)務(wù)模塊的集合,隨著項(xiàng)目越來(lái)越復(fù)雜,Contribution 也會(huì)越來(lái)越多。

比如左側(cè)菜單包括 Explore、Search、debug、Settings 等等,這里的每個(gè)模塊都是一個(gè) Contribution,Registry 就是將這些 Contribution 歸類(lèi)的一個(gè)集合。

2.5 Service

Service 一般是基礎(chǔ)服務(wù),提供一系列的基礎(chǔ)能力,可以被多個(gè) Contribution 共享。

一句話:Service 用于解決某個(gè)領(lǐng)域下的問(wèn)題。 舉幾個(gè)例子:

  • ReportService,上報(bào)時(shí)都用它,其他的不用操心。
  • StorageService,存儲(chǔ)時(shí)都用它,其他的不用操心。
  • AccountService,負(fù)責(zé)賬號(hào)等狀態(tài)維護(hù),有需要都找它。

我們寫(xiě)一個(gè) Service 的時(shí)候,需要寫(xiě)哪些東西呢?下面是一個(gè) Service 的例子:

// 先實(shí)現(xiàn)一個(gè)接口
interface ITestService {
    readonly _serviceBrand: undefined;
    test: () =>void;
}

// 再創(chuàng)建一個(gè) service id
const ITestService = createDecorator<ITestService>('test-service');

// 再創(chuàng)建 Service
class TestService implements ITestService {
    public readonly _serviceBrand: undefined;
    
    test() {
        // ...
    }
}

2.5.1 interface

為什么要實(shí)現(xiàn)一個(gè)接口呢?我們希望 Service 之間可以不互相依賴(lài)具體的實(shí)現(xiàn),不產(chǎn)生任何耦合,Service 應(yīng)該只依賴(lài)其接口,做到面向接口編程。

以負(fù)責(zé)用戶賬號(hào)的 AccountService 為例,如果一個(gè)產(chǎn)品支持谷歌登錄、Github 登錄等等,這些登錄的實(shí)現(xiàn)并不一樣。

對(duì)于依賴(lài)用戶登錄信息的組件來(lái)說(shuō),應(yīng)該依賴(lài)的是什么呢?GoogleAccountService?GithubAccoutService?我不想關(guān)心到底是什么賬號(hào),可能只是想調(diào)用 hasLogin 判斷是否登錄,我要依賴(lài)的應(yīng)該只是 interface,不需要關(guān)心到底是什么賬號(hào)體系。

在 VSCode 里面也有類(lèi)似的例子,在 Electron 和 Web 環(huán)境注冊(cè)的 Service 實(shí)現(xiàn)可能不一樣,但 interface 是一樣的。

2.5.2 createDecorator

我們先思考一個(gè)問(wèn)題,createDecorator 做了哪些事情?用法是什么呢?假設(shè)有個(gè) Test2Service 依賴(lài)了 TestService。

class Test2Service {
    constructor(
        @ITestService private readonly _testService: ITestService, 
    ) {
    }
}

為什么我們不需要將 testService 實(shí)例化后傳給 test2Service 呢?他們是怎么建立關(guān)聯(lián)關(guān)系的呢?帶著疑問(wèn)看一下 createDecorator 的實(shí)現(xiàn)。

function setServiceDependency(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;
  }
}

function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
if (serviceIds.has(serviceId)) {
    return serviceIds.get(serviceId)!;
  }

const id = 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);
return id;
}

createDecorator 主要就是創(chuàng)建了一個(gè)裝飾器,這個(gè)裝飾器會(huì)調(diào)用 setServiceDependency,將 serviceId 設(shè)置到被裝飾類(lèi)的 DI_DEPENDENCIES 屬性上面。

這樣上面的例子中,我們就可以通過(guò) @ITestService 建立 ITestService 和 Test2Service 的關(guān)聯(lián)關(guān)系,指定 Test2Service 依賴(lài)了 ITestService。

2.5.3 InstantiationService

VSCode 里面 Service 有兩種方式可以訪問(wèn)到:

  1. 通過(guò) DI 的方式,在構(gòu)造函數(shù)里面可以引入
  2. 通過(guò) instantiationService.invokeFunction 的形式拿到 accessors 進(jìn)行訪問(wèn)

第一種比較容易理解,就是實(shí)例化的時(shí)候?qū)⑺蕾?lài)的 Service 實(shí)例自動(dòng)傳入。

那么先來(lái)分析第二種方式,在建立了依賴(lài)關(guān)系之后,究竟 Service 是怎么實(shí)例化,并且將依賴(lài)項(xiàng)自動(dòng)傳入的?我們來(lái)初始化一下 Service:

const services = new ServiceCollection();

// 注冊(cè) Service
services.set(ITestService, TestService);
services.set(ITest2Service, new SyncDescriptor(Test2Service, []));

// 實(shí)例化容器 Service
const instantiationService = new InstantiationService(services);

// 獲取 testService 實(shí)例
const testService = instantiationService.invokeFunction(accessors => accessors.get(ITestService));

// 實(shí)例化一個(gè) testManager
const testManager = instantiationService.createInstance(TestManager);

對(duì)于 ServiceCollection,可以簡(jiǎn)單理解為使用一個(gè) Map 將 ITestService 和 TestService 做了一次關(guān)聯(lián),后續(xù)可以通過(guò) ITestService 查詢(xún)到 TestService 實(shí)例。

最終將存有關(guān)聯(lián)信息的這個(gè) Map 傳給了 InstantiationService,這個(gè) InstantiationService 是負(fù)責(zé)實(shí)例化的容器 Service,它提供了 invokeFunction 和 createChild、createInstance 方法。

InstantiationService 在實(shí)例化的時(shí)候,將傳入 services 掛載到 this 上,并且會(huì)建立 IInstantiationService 到自身實(shí)例的關(guān)系。

2.5.4 invokeFunction

Service 只有在被訪問(wèn)的時(shí)候才會(huì)實(shí)例化,也就是在 invokeFunction 的 accessors.get 的時(shí)候開(kāi)始實(shí)例化。

如果已經(jīng)實(shí)例化過(guò),就直接返回實(shí)例,否則就會(huì)創(chuàng)建一個(gè)實(shí)例。

class InstantiationService {
constructor(
    services: ServiceCollection = new ServiceCollection(),
    parent?: InstantiationService,
  ) {
      this._services = services;
  }
  invokeFunction(fn) {
    const accessor: ServicesAccessor = {
      const _trace = Trace.traceInvocation(this._enableTracing, fn);
      let _done = false;
      try {
        const accessor: ServicesAccessor = {
          get: <T>(id: ServiceIdentifier<T>) => {
            if (_done) {
              thrownewError('service accessor is only valid during the invocation of its target method');
            }

            const result = this._getOrCreateServiceInstance(id, _trace);
            if (!result) {
              this._handleError({
                errorType: InstantiationErrorType.UnknownDependency,
                issuer: 'service-accessor',
                dependencyId: `${id}`,
                message: `[invokeFunction] unknown service '${id}'`,
              });
            }
            return result;
          },
        };
        return fn(accessor, ...args);
      } finally {
        _done = true;
        _trace.stop();
      }
    };
    return fn(accessor, ...args);
  }
}

PS:在 invokeFunction 中如果存在異步,那就需要在異步之后新開(kāi)一個(gè) invokeFunction 來(lái)訪問(wèn) Service,不然訪問(wèn)就會(huì)報(bào)錯(cuò)。

_getOrCreateServiceInstance 會(huì)根據(jù) serviceId 來(lái)獲取到對(duì)應(yīng)的 Service 類(lèi),如果在當(dāng)前 instantiationService 的 _services 上找不到,那么就從他的 parent 上繼續(xù)查找。

這里拋出一個(gè)問(wèn)題,instantiationService 的 parent 是什么呢?一般來(lái)說(shuō)還是一個(gè) instantiationService,項(xiàng)目中可以不只有一個(gè)容器服務(wù),容器服務(wù)內(nèi)部還可以再創(chuàng)建容器服務(wù)。

以飛書(shū)文檔為例,在全局創(chuàng)建 instantiationService,用于承載日志服務(wù)、上報(bào)服務(wù)等等。

在 instantiationService 下面還可以再創(chuàng)建一個(gè) instantiationService,用于存放草稿相關(guān)的服務(wù)。

比如飛書(shū)文檔中從文檔 A 需要無(wú)刷新切換到文檔 B。對(duì)于日志服務(wù)、配置服務(wù)這類(lèi)基礎(chǔ)服務(wù)是不需要銷(xiāo)毀的,可以繼續(xù)復(fù)用。

但是原本在文檔 A 里面初始化的模塊、快捷鍵、綁定的事件都需要銷(xiāo)毀,在文檔 B 中重新創(chuàng)建。

如果代碼實(shí)現(xiàn)的沒(méi)有那么安全,很容易就有一些模塊的副作用沒(méi)有被清理干凈,就會(huì)影響到文檔 B 的正常使用。

// 創(chuàng)建一個(gè)服務(wù)集合
const collection = new ServiceCollection();
// 注冊(cè)服務(wù)進(jìn)去
this._registerCommonService(ctx, collection);
// 基于全局容器服務(wù)創(chuàng)建一個(gè)屬于編輯器的容器服務(wù),將 collection 里面的 service 都注冊(cè)進(jìn)去
this._editorContainerService = this._containerService.createChild(collection);

所以如果是通過(guò) editorContainerService 來(lái)查找 environmentService,直接找不到,它就會(huì)從 parent 上面找。

如果從 _services 找到了,還需要判斷是不是一個(gè) SyncDescriptor,如果不是 SyncDescriptor,說(shuō)明已經(jīng)被實(shí)例化過(guò)了,就直接返回。如果是,那就走實(shí)例化的邏輯。

實(shí)例化的過(guò)程在 _createAndCacheServiceInstance 中,他會(huì)先創(chuàng)建一個(gè)依賴(lài)圖,將當(dāng)前的 serviceId 和 syncDescriptor 信息當(dāng)做圖的一個(gè)節(jié)點(diǎn)存入。

for (const dependency of getServiceDependencies(item.desc.ctor)) {
const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
if (instanceOrDesc instanceof SyncDescriptor) {
    const d = {
      id: dependency.id,
      desc: instanceOrDesc,
      _trace: item._trace.branch(dependency.id, true),
    };
    // 當(dāng)依賴(lài)沒(méi)有初始化為實(shí)例,仍然是描述符式,添加到臨時(shí)依賴(lài)圖
    // 創(chuàng)建從依賴(lài) service 到當(dāng)前 service 的一條邊
    graph.insertEdge(item, d);
    stack.push(d);
  }
}

接著會(huì)從 graph 里面獲取葉子節(jié)點(diǎn),如果沒(méi)有葉子節(jié)點(diǎn),但 graph 又不為空,說(shuō)明發(fā)生了循環(huán)依賴(lài),會(huì)拋出錯(cuò)誤。

遍歷葉子節(jié)點(diǎn),從葉子節(jié)點(diǎn)開(kāi)始調(diào)用 _createServiceInstanceWithOwner 進(jìn)行實(shí)例化,因?yàn)槿~子節(jié)點(diǎn)一定是不會(huì)再依賴(lài)其他 Service 的。

class Service4 {
    constructor(
        @IService1 private readonly _service1: IService1, 
        @IService2 private readonly _service1: IService2, 
    ) {}
}

class Service5 {
    constructor(
        @IService3 private readonly _service3: IService3, 
        @IService2 private readonly _service1: IService2, 
    ) {}
}

class Service6 {
    constructor(
        @IService4 private readonly _service4: IService4, 
        @IService5 private readonly _service5: IService5, 
    ) {}
}

圖片圖片

如果注冊(cè)的時(shí)候傳入 supportsDelayedInstantiation,就會(huì)進(jìn)行延遲初始化,延遲初始化會(huì)返回一個(gè) Proxy,只有觸發(fā)了 get,才會(huì)對(duì) Service 進(jìn)行實(shí)例化,可以減輕首屏的負(fù)擔(dān)。

如果沒(méi)有延遲初始化,就會(huì)調(diào)用 _createInstance 進(jìn)行創(chuàng)建。實(shí)例化的時(shí)候會(huì)將通過(guò) new SyncDescriptor 創(chuàng)建的參數(shù)帶進(jìn)去。

如果不是葉子節(jié)點(diǎn),那就會(huì)將依賴(lài)的 Service 實(shí)例 + SyncDescriptor 的參數(shù)一起傳進(jìn)去。

至此,Service 的實(shí)例化就完成了。

2.5.5 createInstance

除了 Service,VSCode 里面還存在很多業(yè)務(wù)模塊,為了方便理解,我們可以統(tǒng)一稱(chēng)之為 Manager。這些 Manager 有的是用 createInstance 實(shí)例化,有的是用 new 實(shí)例化。

用 createInstance 實(shí)例化的類(lèi)擁有 DI 的能力,也可以通過(guò)依賴(lài)注入的方式獲取依賴(lài)。和上述的 Service 創(chuàng)建最終走了相同的流程,這里不過(guò)多闡述。

還有個(gè)問(wèn)題,我們?cè)趯?xiě) Service 的時(shí)候?yàn)槭裁匆獙?xiě)一個(gè) _serviceBrand 呢?這個(gè)到底有什么用?那你會(huì)不會(huì)好奇,為什么我們使用 DI 注入構(gòu)造參數(shù),TS 卻不會(huì)報(bào)錯(cuò)呢?

看一下 createInstance 方法的簽名就理解了,GetLeadingNonServiceArgs 會(huì)從構(gòu)造函數(shù)參數(shù)類(lèi)型里面剔除帶 _serviceBrand 的參數(shù),所以我們?cè)?createInstance 的時(shí)候可以不傳依賴(lài)的 Service。

export type BrandedService = { _serviceBrand: undefined };
export type GetLeadingNonServiceArgs<TArgs extends any[]> =
  TArgs extends [] ? []
  : TArgs extends [...infer TFirst, BrandedService] ? GetLeadingNonServiceArgs<TFirst>
  : TArgs;
  
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;

如果不寫(xiě) _serviceBrand, 那這個(gè) Service 參數(shù)不會(huì)被剔除,就會(huì)要求我們手動(dòng)傳入。

如果我們想將某個(gè) Service 當(dāng)做參數(shù)傳下去,因?yàn)?TS 會(huì)剔除這個(gè)參數(shù),createInstance 反而會(huì)提示你少了一個(gè)參數(shù)報(bào)錯(cuò)。

3. 組件化

Vscode 沒(méi)有使用 React/Vue 技術(shù)棧來(lái)編寫(xiě) UI,而是選擇使用純?cè)鷣?lái)編寫(xiě),那么他的 UI 是怎么渲染出來(lái)的呢?組件是怎么通信的呢?

與大多數(shù)以 React 作為 View 層,Redux/Mobx 處理數(shù)據(jù)和狀態(tài)的形式不一樣,VSCode 組件也都是 class 的形式。就以我們最熟悉的編輯器內(nèi) FindReplace 模塊展開(kāi)說(shuō)說(shuō)組件化是如何實(shí)現(xiàn)的。

3.1 Controller

VSCode 的復(fù)雜 UI 模塊是 MVC 的形式來(lái)組織,劃分成 Controller、View、Model 三層。

查找替換功能的入口在 FindController 里面,VSCode 里面的 UI 模塊設(shè)計(jì)是以 Controller 為入口,創(chuàng)建對(duì)應(yīng)的 Model 層和 View 層,其中 Model 層就是管理數(shù)據(jù)和狀態(tài)的。

FindController 被當(dāng)做 contribution 通過(guò) registerEditorContribution 掛載到編輯器實(shí)例上面。

同時(shí),VSCode 會(huì)將用戶的操作作為 Action 注冊(cè)到 EditorContributionRegistry,將快捷鍵作為 EditorCommand 也注冊(cè)到 EditorContributionRegistry,Controller 也提供了一系列 public 方法供給 Action 和 Command 調(diào)用。

registerEditorContribution(CommonFindController.ID, FindController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState`
registerEditorAction(StartFindWithArgsAction);
const FindCommand = EditorCommand.bindToContribution<CommonFindController>(CommonFindController.get);

registerEditorCommand(new FindCommand({
  id: FIND_IDS.CloseFindWidgetCommand,
  precondition: CONTEXT_FIND_WIDGET_VISIBLE,
  handler: x => x.closeFindWidget(),
  kbOpts: {
    weight: KeybindingWeight.EditorContrib + 5,
    kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
    primary: KeyCode.Escape,
    secondary: [KeyMod.Shift | KeyCode.Escape]
  }
}));

在 FindController 中會(huì)創(chuàng)建 FindWidget、FindReplaceState、FindModel 等實(shí)例,作為 View 層和 Model 層的橋梁,

class FindController {
constructor() {
    // 持有 editor 引用
    this._editor = editor;
    // 實(shí)例化狀態(tài)
    this._state = this._register(new FindReplaceState());
    // 初始化查詢(xún)狀態(tài)
    this.loadQueryState();
    // 監(jiān)聽(tīng)狀態(tài)變更
    this._register(this._state.onFindReplaceStateChange((e) =>this._onStateChanged(e)));
    // 創(chuàng)建 Model
    this._model = new FindModelBoundToEditorModel(this._editor, this._state);
    // 創(chuàng)建 widget
    this._widget = this._register(new FindWidget(this._editor, this, this._state));
    // 監(jiān)聽(tīng) editor 內(nèi)容變更
    this._register(this._editor.onDidChangeModel(() => {}));
  }
}

3.2 Model 和 State

FindReplaceState 負(fù)責(zé)維護(hù) searchString、replaceString、isRegex、matchesCount 等查找狀態(tài)和匹配結(jié)果,它本身沒(méi)有什么業(yè)務(wù)邏輯,可以理解為純粹的 Store,而且 State 這一層不是必要的。

Model 層包含了 State,主要是做查找替換的業(yè)務(wù)邏輯,他會(huì)監(jiān)聽(tīng) State 的狀態(tài)變更,從 Editor 進(jìn)行搜索,將結(jié)果更新到 FindReplaceState。

class FindController {
    constructor() {
    this._editor = editor;
    this._findWidgetVisible = CONTEXT_FIND_WIDGET_VISIBLE.bindTo(contextKeyService);
    this._contextKeyService = contextKeyService;
    this._storageService = storageService;
    this._clipboardService = clipboardService;
    this._notificationService = notificationService;
    this._hoverService = hoverService;

    this._updateHistoryDelayer = new Delayer<void>(500);
    this._state = this._register(new FindReplaceState());
    this.loadQueryState();
    this._register(this._state.onFindReplaceStateChange((e) =>this._onStateChanged(e)));

    this._model = null;

    this._register(this._editor.onDidChangeModel(() => {
    }
}

在 Controller 上持有 Editor 實(shí)例, 它可以監(jiān)聽(tīng)到 onDidChangeModel(編輯器內(nèi)容變化),觸發(fā) Model 的搜索,更新搜索結(jié)果。

3.3 Widget

在開(kāi)始之前,我們先看一個(gè) VSCode 里面最簡(jiǎn)單的 Toggle 組件實(shí)現(xiàn)。

在 vs/base/browser/ui 目錄下面都是 VSCode 的一些基礎(chǔ)組件,每個(gè)組件包括了一個(gè) JS 文件和一個(gè) CSS 文件。

export class Toggle extends Widget {

private readonly _onChange = this._register(new Emitter<boolean>());
  readonly onChange: Event<boolean/* via keyboard */> = this._onChange.event;

private readonly _onKeyDown = this._register(new Emitter<IKeyboardEvent>());
  readonly onKeyDown: Event<IKeyboardEvent> = this._onKeyDown.event;

private readonly _opts: IToggleOpts;
private _icon: ThemeIcon | undefined;
  readonly domNode: HTMLElement;

private _checked: boolean;
private _hover: IManagedHover;

constructor(opts: IToggleOpts) {
    super();

    this._opts = opts;
    this._checked = this._opts.isChecked;

    const classes = ['monaco-custom-toggle'];
    if (this._opts.icon) {
      this._icon = this._opts.icon;
      classes.push(...ThemeIcon.asClassNameArray(this._icon));
    }
    if (this._opts.actionClassName) {
      classes.push(...this._opts.actionClassName.split(' '));
    }
    if (this._checked) {
      classes.push('checked');
    }

    this.domNode = document.createElement('div');
    this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(opts.hoverDelegate ?? getDefaultHoverDelegate('mouse'), this.domNode, this._opts.title));
    this.domNode.classList.add(...classes);
    if (!this._opts.notFocusable) {
      this.domNode.tabIndex = 0;
    }
    this.domNode.setAttribute('role', 'checkbox');
    this.domNode.setAttribute('aria-checked', String(this._checked));
    this.domNode.setAttribute('aria-label', this._opts.title);

    this.applyStyles();

    this.onclick(this.domNode, (ev) => {
      if (this.enabled) {
        this.checked = !this._checked;
        this._onChange.fire(false);
        ev.preventDefault();
      }
    });

    this._register(this.ignoreGesture(this.domNode));

    this.onkeydown(this.domNode, (keyboardEvent) => {
      if (keyboardEvent.keyCode === KeyCode.Space || keyboardEvent.keyCode === KeyCode.Enter) {
        this.checked = !this._checked;
        this._onChange.fire(true);
        keyboardEvent.preventDefault();
        keyboardEvent.stopPropagation();
        return;
      }

      this._onKeyDown.fire(keyboardEvent);
    });
  }

get enabled(): boolean {
    returnthis.domNode.getAttribute('aria-disabled') !== 'true';
  }

  focus(): void {
    this.domNode.focus();
  }

get checked(): boolean {
    returnthis._checked;
  }

set checked(newIsChecked: boolean) {
    this._checked = newIsChecked;

    this.domNode.setAttribute('aria-checked', String(this._checked));
    this.domNode.classList.toggle('checked', this._checked);

    this.applyStyles();
  }

  setIcon(icon: ThemeIcon | undefined): void {
    if (this._icon) {
      this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));
    }
    this._icon = icon;
    if (this._icon) {
      this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));
    }
  }

  width(): number {
    return2/*margin left*/ + 2/*border*/ + 2/*padding*/ + 16/* icon width */;
  }

protected applyStyles(): void {
    if (this.domNode) {
      this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';
      this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';
      this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';
    }
  }

  enable(): void {
    this.domNode.setAttribute('aria-disabled', String(false));
  }

  disable(): void {
    this.domNode.setAttribute('aria-disabled', String(true));
  }

  setTitle(newTitle: string): void {
    this._hover.update(newTitle);
    this.domNode.setAttribute('aria-label', newTitle);
  }

set visible(visible: boolean) {
    this.domNode.style.display = visible ? '' : 'none';
  }

get visible() {
    returnthis.domNode.style.display !== 'none';
  }
}

可以看到,Toggle 組件繼承了 Widget 類(lèi),Widget 類(lèi)是所有 UI 組件的基類(lèi),它會(huì)監(jiān)聽(tīng)所有的 DOM 的事件,將其通過(guò)事件分發(fā)出去。

Toggle 支持傳入 options 作為初始值,內(nèi)部創(chuàng)建了 DOM 節(jié)點(diǎn),所有的 UI 更新都是直接操作 DOM,并且將 get/set 方法暴露出去,這樣調(diào)用方式也很簡(jiǎn)單,不再需要通過(guò)更新 state 來(lái)間接更新 UI。

通過(guò)這種對(duì)屬性精細(xì)化的控制,可以將渲染性能優(yōu)化到極致,這種做法 Canvas/WebGL 渲染層也可以參考。

接著說(shuō) FindWidget,它也繼承了 Widget 類(lèi),初始化的時(shí)候內(nèi)部會(huì)構(gòu)建 DOM,其中查找輸入框和替換輸入框都是通過(guò) Widget 來(lái)創(chuàng)建的,所以 Widget 具有組合的能力。

FindWidget 也監(jiān)聽(tīng)了 State 的狀態(tài)變更事件,在狀態(tài)變更之后,就會(huì)根據(jù)變更原因來(lái)更新對(duì)應(yīng)的 Widget 的 UI。比如 Command + D 引起搜索值變化了,就需要調(diào)用 findInputWidget.setValue 來(lái)更新搜索框的 UI。

3.4 組件通信

從上面可以看到每個(gè) Widget 的職責(zé)都比較清晰,除了維護(hù)自身的功能,它還將細(xì)粒度的 get/set 方法暴露出去,方便外部更新。

對(duì)于復(fù)雜組件通信的情況,一般是通過(guò)事件 + set 來(lái)實(shí)現(xiàn)的,組件通信就下面兩種:

  1. 父子組件通信:父組件持有子組件,可以直接調(diào)子組件的 set 方法更新子組件。子組件內(nèi)部變更也可以通過(guò)拋事件通知父組件更新。
  2. 兄弟組件通信:一般需要有個(gè)父組件或者 Controller 來(lái)持有兩個(gè)組件,組件 A 內(nèi)部變化的時(shí)候拋事件出去,父組件監(jiān)聽(tīng)到之后,直接調(diào)用組件 B 的 set 方法來(lái)更新。

比如查找替換這個(gè)組件,我們修改了搜索值,右側(cè)的匹配結(jié)果就會(huì)更新,主要步驟可以簡(jiǎn)化為:

  1. 用戶輸入修改 findInputWidget 的值,findInputWidget 發(fā)送 onDidChange 通知出去,findWidget 更新 findState。
  2. Model 監(jiān)聽(tīng)到 state 變更之后重新搜索,搜索之后再更新 findState 的匹配結(jié)果。
  3. findWidget 監(jiān)聽(tīng)到狀態(tài)變更之后,主動(dòng)調(diào)用 matchesCount 去更新 DOM。

圖片

3.5 總結(jié)

為什么在 React/Vue 出現(xiàn)之前,大家都覺(jué)得原生JS、jQuery 這種開(kāi)發(fā)模式不適合大型項(xiàng)目呢?為什么在 VSCode 上又可以呢?

原因是 jQuery 時(shí)期幾乎沒(méi)有模塊化和組件化的概念,即使可以用 AMD/CMD 來(lái)做模塊化、jQuery 插件來(lái)做組件化,但 jQuery 的組件化的不夠徹底,上手成本也高一些。

我們用 jQuery 開(kāi)發(fā)項(xiàng)目的時(shí)候,很容易出現(xiàn)一個(gè) DOM 節(jié)點(diǎn)被到處綁事件,最后事件滿天飛,調(diào)試起來(lái)很困難的情況。

如果使用模板引擎,更新效率比較低,DOM 重繪開(kāi)銷(xiāo)大,遠(yuǎn)遠(yuǎn)比不上 React/Vue 但在 VSCode 里面,每個(gè)組件只暴露自己的 getter/setter,內(nèi)部變更通過(guò)事件通知,組件之間通信都是用事件的形式,組件和模塊的劃分也非常清晰。

通過(guò)對(duì) DOM 屬性細(xì)粒度更新,VSCode 性能也是比 React/Vue 更高的。

責(zé)任編輯:武曉燕 來(lái)源: 前端小館
相關(guān)推薦

2022-04-30 08:50:11

控制反轉(zhuǎn)Spring依賴(lài)注入

2011-05-31 10:00:21

Android Spring 依賴(lài)注入

2023-07-11 09:14:12

Beanquarkus

2017-08-16 16:00:05

PHPcontainer依賴(lài)注入

2022-12-29 08:54:53

依賴(lài)注入JavaScript

2016-11-25 13:26:50

Flume架構(gòu)源碼

2016-11-29 09:38:06

Flume架構(gòu)核心組件

2019-09-18 18:12:57

前端javascriptvue.js

2015-09-02 11:22:36

JavaScript實(shí)現(xiàn)思路

2009-07-28 15:03:02

依賴(lài)性注入

2024-12-30 12:00:00

.NET Core依賴(lài)注入屬性注入

2024-04-01 00:02:56

Go語(yǔ)言代碼

2024-05-27 00:13:27

Go語(yǔ)言框架

2022-04-11 09:02:18

Swift依賴(lài)注

2014-07-08 14:05:48

DaggerAndroid依賴(lài)

2021-02-28 20:41:18

Vue注入Angular

2023-06-27 08:58:13

quarkusBean

2018-09-26 11:02:46

微服務(wù)架構(gòu)組件

2025-04-10 08:10:00

Web 框架FastAPIPython

2021-07-25 21:13:50

框架Angular開(kāi)發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 一区二区三区在线免费观看 | 人碰人操 | 黄色片a级 | 国产精品国产a级 | 99re在线视频免费观看 | 欧美中文字幕在线 | 国产免费一区二区 | 黄在线免费观看 | 日本免费一区二区三区四区 | 岛国av在线免费观看 | 精品小视频 | 一二三在线视频 | 久久精品一区二区三区四区 | 一级毛片在线看 | 国产精品久久国产精品 | 久久久久久久久久久久久久久久久久久久 | 亚洲欧洲日韩精品 中文字幕 | 99精品网站 | 精品欧美乱码久久久久久 | 日韩精品在线播放 | 又黄又色 | 精品久久香蕉国产线看观看亚洲 | 日韩精品在线一区 | 亚洲精品68久久久一区 | 久久精品综合 | 日本一二三区电影 | 欧美成人激情视频 | av在线黄 | 亚洲97| 亚洲一区二区三区免费在线观看 | 成年网站在线观看 | 国产一区二区精 | 久久最新精品视频 | 香蕉久久a毛片 | 欧美日韩国产综合在线 | 午夜爽爽爽男女免费观看 | 久久久亚洲精品视频 | www.97zyz.com| 国产精品视频一区二区三区, | 国产精品视频在线播放 | 成人欧美一区二区三区黑人孕妇 |