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

VSCode 架構分析:依賴注入和組件

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

1. 前言

這一節主要介紹 VSCode 的依賴注入架構以及組件實現。

2. 依賴注入

2.1 什么是依賴注入

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

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

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

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

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

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

控制反轉(英語:Inversion of Control,縮寫為IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI)

采用依賴注入技術之后,ServiceA 的代碼只需要定義一個 private 的 ServiceB 對象,不需要直接 new 來獲得這個對象,而是通過相關的容器控制程序來將 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 一般是業務模塊,它作為最上層的業務模塊,一般不會被其他模塊依賴,在 VSCode 里面一個 Contribution 就對應一個模塊,Contribution 內部還會包含 UI 模塊、Model 模塊等。

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

2.4 Registry

Registry 一般是業務模塊的集合,隨著項目越來越復雜,Contribution 也會越來越多。

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

2.5 Service

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

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

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

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

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

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

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

2.5.1 interface

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

以負責用戶賬號的 AccountService 為例,如果一個產品支持谷歌登錄、Github 登錄等等,這些登錄的實現并不一樣。

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

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

2.5.2 createDecorator

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

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

為什么我們不需要將 testService 實例化后傳給 test2Service 呢?他們是怎么建立關聯關系的呢?帶著疑問看一下 createDecorator 的實現。

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 主要就是創建了一個裝飾器,這個裝飾器會調用 setServiceDependency,將 serviceId 設置到被裝飾類的 DI_DEPENDENCIES 屬性上面。

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

2.5.3 InstantiationService

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

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

第一種比較容易理解,就是實例化的時候將它依賴的 Service 實例自動傳入。

那么先來分析第二種方式,在建立了依賴關系之后,究竟 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 做了一次關聯,后續可以通過 ITestService 查詢到 TestService 實例。

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

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

2.5.4 invokeFunction

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

如果已經實例化過,就直接返回實例,否則就會創建一個實例。

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 會根據 serviceId 來獲取到對應的 Service 類,如果在當前 instantiationService 的 _services 上找不到,那么就從他的 parent 上繼續查找。

這里拋出一個問題,instantiationService 的 parent 是什么呢?一般來說還是一個 instantiationService,項目中可以不只有一個容器服務,容器服務內部還可以再創建容器服務。

以飛書文檔為例,在全局創建 instantiationService,用于承載日志服務、上報服務等等。

在 instantiationService 下面還可以再創建一個 instantiationService,用于存放草稿相關的服務。

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

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

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

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

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

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

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

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

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

遍歷葉子節點,從葉子節點開始調用 _createServiceInstanceWithOwner 進行實例化,因為葉子節點一定是不會再依賴其他 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,只有觸發了 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。

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 參數不會被剔除,就會要求我們手動傳入。

如果我們想將某個 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 調用。

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 中會創建 FindWidget、FindReplaceState、FindModel 等實例,作為 View 層和 Model 層的橋梁,

class FindController {
constructor() {
    // 持有 editor 引用
    this._editor = editor;
    // 實例化狀態
    this._state = this._register(new FindReplaceState());
    // 初始化查詢狀態
    this.loadQueryState();
    // 監聽狀態變更
    this._register(this._state.onFindReplaceStateChange((e) =>this._onStateChanged(e)));
    // 創建 Model
    this._model = new FindModelBoundToEditorModel(this._editor, this._state);
    // 創建 widget
    this._widget = this._register(new FindWidget(this._editor, this, this._state));
    // 監聽 editor 內容變更
    this._register(this._editor.onDidChangeModel(() => {}));
  }
}

3.2 Model 和 State

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

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

3.3 Widget

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

在 vs/base/browser/ui 目錄下面都是 VSCode 的一些基礎組件,每個組件包括了一個 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 組件的基類,它會監聽所有的 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 來實現的,組件通信就下面兩種:

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

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

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

圖片

3.5 總結

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

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

我們用 jQuery 開發項目的時候,很容易出現一個 DOM 節點被到處綁事件,最后事件滿天飛,調試起來很困難的情況。

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

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

責任編輯:武曉燕 來源: 前端小館
相關推薦

2022-04-30 08:50:11

控制反轉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

2016-11-25 13:26:50

Flume架構源碼

2016-11-29 09:38:06

Flume架構核心組件

2019-09-18 18:12:57

前端javascriptvue.js

2015-09-02 11:22:36

JavaScript實現思路

2009-07-28 15:03:02

依賴性注入

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依賴注

2021-02-28 20:41:18

Vue注入Angular

2023-06-27 08:58:13

quarkusBean

2014-07-08 14:05:48

DaggerAndroid依賴

2018-09-26 11:02:46

微服務架構組件

2021-07-25 21:13:50

框架Angular開發

2016-03-21 17:08:54

Java Spring注解區別
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 最新中文字幕 | 国产成人av在线播放 | 国产在线不卡 | av国产精品| 日韩精品一区二区三区中文在线 | 天天草av| 国产成人99久久亚洲综合精品 | av在线播放网站 | 亚洲一区亚洲二区 | 伊人网在线看 | 日本午夜免费福利视频 | 久久爱黑人激情av摘花 | 久久这里只有精品首页 | 欧美xxxx黑人又粗又长 | 在线视频中文字幕 | 美国av毛片 | 91久久伊人 | 久久aⅴ乱码一区二区三区 亚洲国产成人精品久久久国产成人一区 | 午夜精品久久久久久不卡欧美一级 | 色www精品视频在线观看 | 久久国产成人精品国产成人亚洲 | 亚洲成人国产综合 | 国产亚洲精品精品国产亚洲综合 | 一区二区三区av | 欧美日韩一 | 男女一区二区三区 | 欧美综合国产精品久久丁香 | 四虎影院在线观看av | 国产精品精品视频一区二区三区 | 一区二区三区欧美 | 九九伊人sl水蜜桃色推荐 | av入口| 日韩午夜在线观看 | 午夜视频一区二区三区 | 成人在线视频一区二区三区 | 一级毛片高清 | 最新中文字幕久久 | 久久国产精品视频 | 中文字幕在线网 | 欧美一区二区在线观看 | 亚洲成av人片在线观看无码 |