自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

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

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

1. 前言

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

2. 依賴注入

2.1 什么是依賴注入

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

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

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

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);

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

為了解決對象間耦合度過高的問題,軟件專家 Michael Mattson提出了 IOC 理論,用來實現(xiàn)對象之間的“解耦”。

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

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

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

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

2.2 概念介紹

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

2.3 Contribution

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

舉個例子,我們在編輯器里面常用的查找替換,它就是一個 Contribution。

2.4 Registry

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

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

2.5 Service

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

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

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

我們寫一個 Service 的時候,需要寫哪些東西呢?下面是一個 Service 的例子:

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

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

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

2.5.1 interface

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

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

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

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

2.5.2 createDecorator

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

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

為什么我們不需要將 testService 實例化后傳給 test2Service 呢?他們是怎么建立關(guān)聯(lián)關(guān)系的呢?帶著疑問看一下 createDecorator 的實現(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)建了一個裝飾器,這個裝飾器會調(diào)用 setServiceDependency,將 serviceId 設(shè)置到被裝飾類的 DI_DEPENDENCIES 屬性上面。

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

2.5.3 InstantiationService

VSCode 里面 Service 有兩種方式可以訪問到:

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

第一種比較容易理解,就是實例化的時候?qū)⑺蕾嚨?Service 實例自動傳入。

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

const services = new ServiceCollection();

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

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

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

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

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

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

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

2.5.4 invokeFunction

Service 只有在被訪問的時候才會實例化,也就是在 invokeFunction 的 accessors.get 的時候開始實例化。

如果已經(jīng)實例化過,就直接返回實例,否則就會創(chuàng)建一個實例。

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 中如果存在異步,那就需要在異步之后新開一個 invokeFunction 來訪問 Service,不然訪問就會報錯。

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

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

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

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

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

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

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

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

所以如果是通過 editorContainerService 來查找 environmentService,直接找不到,它就會從 parent 上面找。

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

實例化的過程在 _createAndCacheServiceInstance 中,他會先創(chuàng)建一個依賴圖,將當前的 serviceId 和 syncDescriptor 信息當做圖的一個節(jié)點存入。

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),
    };
    // 當依賴沒有初始化為實例,仍然是描述符式,添加到臨時依賴圖
    // 創(chuàng)建從依賴 service 到當前 service 的一條邊
    graph.insertEdge(item, d);
    stack.push(d);
  }
}

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

遍歷葉子節(jié)點,從葉子節(jié)點開始調(diào)用 _createServiceInstanceWithOwner 進行實例化,因為葉子節(jié)點一定是不會再依賴其他 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, 
    ) {}
}

圖片圖片

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

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

如果不是葉子節(jié)點,那就會將依賴的 Service 實例 + SyncDescriptor 的參數(shù)一起傳進去。

至此,Service 的實例化就完成了。

2.5.5 createInstance

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

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

還有個問題,我們在寫 Service 的時候為什么要寫一個 _serviceBrand 呢?這個到底有什么用?那你會不會好奇,為什么我們使用 DI 注入構(gòu)造參數(shù),TS 卻不會報錯呢?

看一下 createInstance 方法的簽名就理解了,GetLeadingNonServiceArgs 會從構(gòu)造函數(shù)參數(shù)類型里面剔除帶 _serviceBrand 的參數(shù),所以我們在 createInstance 的時候可以不傳依賴的 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;

如果不寫 _serviceBrand, 那這個 Service 參數(shù)不會被剔除,就會要求我們手動傳入。

如果我們想將某個 Service 當做參數(shù)傳下去,因為 TS 會剔除這個參數(shù),createInstance 反而會提示你少了一個參數(shù)報錯。

3. 組件化

Vscode 沒有使用 React/Vue 技術(shù)棧來編寫 UI,而是選擇使用純原生來編寫,那么他的 UI 是怎么渲染出來的呢?組件是怎么通信的呢?

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

3.1 Controller

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

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

FindController 被當做 contribution 通過 registerEditorContribution 掛載到編輯器實例上面。

同時,VSCode 會將用戶的操作作為 Action 注冊到 EditorContributionRegistry,將快捷鍵作為 EditorCommand 也注冊到 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 中會創(chuàng)建 FindWidget、FindReplaceState、FindModel 等實例,作為 View 層和 Model 層的橋梁,

class FindController {
constructor() {
    // 持有 editor 引用
    this._editor = editor;
    // 實例化狀態(tài)
    this._state = this._register(new FindReplaceState());
    // 初始化查詢狀態(tài)
    this.loadQueryState();
    // 監(jiān)聽狀態(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)聽 editor 內(nèi)容變更
    this._register(this._editor.onDidChangeModel(() => {}));
  }
}

3.2 Model 和 State

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

Model 層包含了 State,主要是做查找替換的業(yè)務(wù)邏輯,他會監(jiān)聽 State 的狀態(tài)變更,從 Editor 進行搜索,將結(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 實例, 它可以監(jiān)聽到 onDidChangeModel(編輯器內(nèi)容變化),觸發(fā) Model 的搜索,更新搜索結(jié)果。

3.3 Widget

在開始之前,我們先看一個 VSCode 里面最簡單的 Toggle 組件實現(xiàn)。

在 vs/base/browser/ui 目錄下面都是 VSCode 的一些基礎(chǔ)組件,每個組件包括了一個 JS 文件和一個 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 類,Widget 類是所有 UI 組件的基類,它會監(jiān)聽所有的 DOM 的事件,將其通過事件分發(fā)出去。

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

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

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

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

3.4 組件通信

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

對于復(fù)雜組件通信的情況,一般是通過事件 + set 來實現(xiàn)的,組件通信就下面兩種:

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

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

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

圖片

3.5 總結(jié)

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

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

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

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

通過對 DOM 屬性細粒度更新,VSCode 性能也是比 React/Vue 更高的。

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

2022-04-30 08:50:11

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

2011-05-31 10:00:21

Android Spring 依賴注入

2023-07-11 09:14:12

Beanquarkus

2017-08-16 16:00:05

PHPcontainer依賴注入

2022-12-29 08:54:53

依賴注入JavaScript

2009-07-28 15:03:02

依賴性注入

2019-09-18 18:12:57

前端javascriptvue.js

2015-09-02 11:22:36

JavaScript實現(xiàn)思路

2016-11-29 09:38:06

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

2016-11-25 13:26:50

Flume架構(gòu)源碼

2024-12-30 12:00:00

.NET Core依賴注入屬性注入

2024-04-01 00:02:56

Go語言代碼

2024-05-27 00:13:27

Go語言框架

2022-04-11 09:02:18

Swift依賴注

2014-07-08 14:05:48

DaggerAndroid依賴

2023-06-27 08:58:13

quarkusBean

2021-02-28 20:41:18

Vue注入Angular

2009-05-21 16:41:22

GuiceJava依賴注入

2018-03-12 10:02:30

PHP依賴注入

2016-03-21 17:08:54

Java Spring注解區(qū)別
點贊
收藏

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