iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0
Modern Web

Angular 22 Signal 進化論系列 第 15 篇

Day 15:搞懂 Injection Context,從 inject()、runInInjectionContext() 到 injectAsync()

  • 分享至 

  • xImage
  •  

前面提到 Injection Context 時,可能會讓人聯想到先前介紹過的 Reactive Context。我一開始也有點搞混,因此花了一些時間重新理解 Angular 裡這些不同的執行環境。

平常使用 Angular 提供的 API 時,我們不一定會注意它們需要在哪個 Context 中執行。直到遇到 inject()、runInInjectionContext() 這類 API,才會發現執行環境會直接影響函式能不能正常使用。

之前同事也曾經問過我,runInInjectionContext() 到底是拿來做什麼的。這篇就從這個問題開始,整理 Injection Context 的用途,以及它和 Reactive Context 的差別。

Angular DI 系統

Angular 本身建立在一套 DI(Dependency Injection,依賴注入)系統上。

假設 A class 需要使用 B class,最直接的做法是在 A 裡自行建立 B 的實例。

class A {
  private b = new B();
}

這樣做代表 A 必須知道 B 要怎麼建立,兩個 class 之間的耦合也會比較高。

另一種方式,是直接把 B 的實例傳進來。

class A {
  constructor(private b: B) {}
}

這時 A 只需要知道自己依賴 B,不需要負責它的建立方式。

當應用程式裡的依賴越來越多,就需要由 DI 系統統一管理,例如依賴如何建立、哪些實例可以共用,以及它們應該存在多久。

在 Angular 中,這些依賴會交由 Injector 管理。當 Component 或 Service 需要某個依賴時,Angular 會透過 Injector 找到對應的 Provider,再取得或建立需要的實例。

Injector 跟 inject()

Injector 和 inject() 名稱很像,但代表不同的概念。

Injector 是 DI 系統中負責管理與提供依賴的角色。它會根據註冊的 Provider,決定依賴要如何取得,以及對應實例的作用範圍與生命週期。

像常見的 providedIn: 'root',就是把 Service 提供在應用程式的 root Injector 中。其他地方需要這個 Service 時,Angular 就能透過對應的 Provider 取得或建立實例。

inject() 則是一個函式,用來向目前的 Injector 取得依賴。

private readonly userService = inject(UserService);

執行 inject(UserService) 時,Angular 會根據目前的 Injection Context 找到可用的 Injector,再透過對應的 Provider 取得或建立 UserService 實例。

不過 inject() 不能在任何地方直接呼叫。它必須在 Injection Context 中執行,Angular 才知道目前要使用哪一個 Injector。

Angular DI:依賴是怎麼取得的?

哪些地方本身就有 Injection Context

Injection Context 可以理解成「Angular 知道目前要使用哪個 Injector」的執行環境。

當 Angular 正在建立由 DI 系統管理的實例,例如 Component、Directive、Service 或 Pipe,就會提供這個執行環境,因此可以直接使用 inject()。

例如:

@Injectable({
  providedIn: 'root',
})
export class UserService {
  private readonly http = inject(HttpClient);
}

這裡的 inject(HttpClient) 可以正常執行,是因為欄位初始化發生在 Angular 建立 UserService 的過程中。

Component 也是一樣:

@Component({
  selector: 'app-user',
  template: `...`,
})
export class UserComponent {
  private readonly userService = inject(UserService);
}

constructor 執行期間也位在 Injection Context 中:

export class UserComponent {
  constructor() {
    const userService = inject(UserService);
  }
}

Provider 的 factory 同樣是在 Angular 的 DI 流程中執行,因此也能使用 inject()。

{
  provide: API_URL,
  useFactory: () => {
    const config = inject(ConfigService);

    return config.apiUrl;
  },
}

這些情況的共同點,是 inject() 執行時,Angular 正處於可用的 DI 執行環境中。

哪些地方沒有 Injection Context

離開 DI 建立流程後,就不一定還存在 Injection Context。

例如把 inject() 放在一般 method 中:

export class UserComponent {
  loadUser() {
    const userService = inject(UserService);
  }
}

loadUser() 可能是在使用者點擊按鈕後才被呼叫:

<button (click)="loadUser()">
  Load User
</button>

這時 Component 已經建立完成,loadUser() 只是一般的 method 呼叫,不能再依賴建立 Component 時的 Injection Context。

callback 也會遇到相同情況:

setTimeout(() => {
  const userService = inject(UserService);
});

等到 callback 真正執行時,也已經離開原本的 DI 流程。

因此 Injection Context 不會跟著 class 或實例持續存在。即使這個 class 是由 Angular 建立,也不代表裡面的所有 method 或 callback 都能直接呼叫 inject()。

runInInjectionContext() 是拿來做什麼

如果目前不在 Injection Context 中,但需要執行會使用 inject() 的程式碼,就可以使用 runInInjectionContext()。

可以先在有 Injection Context 的地方取得目前的 Injector,例如 Component 的欄位初始化階段:

@Component({
  selector: 'app-user',
  template: `
    <button (click)="loadUser()">Load User</button>
  `,
})
export class UserComponent {
  private readonly injector = inject(Injector);

  loadUser() {
    runInInjectionContext(this.injector, () => {
      const userService = inject(UserService);

      userService.load();
    });
  }
}

inject(Injector) 執行在 Component 的欄位初始化階段,因此可以取得目前的 Injector。

等到之後執行 loadUser() 時,雖然已經離開原本的 Injection Context,仍然可以把先前取得的 Injector 傳給 runInInjectionContext(),讓 callback 內的 inject(UserService) 正常解析依賴。

runInInjectionContext() 不會建立新的 DI 系統,而是讓 callback 暫時在指定的 Injection Context 中執行。

不過這個 Context 只存在於同步執行期間。

runInInjectionContext(this.injector, async () => {
  const userService = inject(UserService);

  await fetch('/api/users');

  // await 之後不能再依賴原本的 Injection Context
});

經過 await 後,後續程式碼已經離開原本的同步執行區段。如果後面還需要使用某個 Service,可以先在 await 前取得實例:

runInInjectionContext(this.injector, async () => {
  const userService = inject(UserService);

  await fetch('/api/users');

  userService.refresh();
});

這裡 await 之後使用的是前面已經取得的 userService,而不是再次呼叫 inject()。

Injection Context 只在同步執行期間有效

runInInjectionContext() 跟 injectAsync() 的差別

前面提到,runInInjectionContext() 提供的 Injection Context 只存在於同步執行期間。這時可能會想到前一篇介紹過的 injectAsync():既然 Injection Context 不能跨過 await,為什麼 injectAsync() 可以等到之後才取得 Service?

兩者處理的是不同問題。

runInInjectionContext() 是指定一個 Injector,讓 callback 在執行期間可以使用 inject()。

runInInjectionContext(this.injector, () => {
  const userService = inject(UserService);
});

injectAsync() 的做法不同。它通常會在 Component 或 Service 建立時執行,而這個階段本來就位在 Injection Context 中,因此可以先取得目前的 Injector。

private readonly userService = injectAsync(
  () =>
    import('./user.service')
      .then(m => m.UserService)
);

之後即使從一般 method 中呼叫:

async loadUser() {
  const userService = await this.userService();

  userService.load();
}

這時已經不一定還有 Injection Context,但 injectAsync() 不需要重新尋找 Injector,因為建立時就已經先取得了。

真正呼叫 this.userService() 後,才會執行 loader:

() =>
  import('./user.service')
    .then(m => m.UserService)

載入完成後取得 UserService 這個 Provider Token,再透過先前取得的 Injector 解析對應實例。

因此:

const userService = await this.userService();

這裡的 await 是在等待 Service 完成載入與 DI 解析,不是把原本的 Injection Context 延續到非同步流程之後。

兩個 API 可以這樣區分:

  • runInInjectionContext():指定 Injector,讓同步 callback 可以使用 inject()。
  • injectAsync():建立時取得 Injector,真正使用時再非同步載入並解析依賴。

所以 injectAsync() 不是非同步版的 runInInjectionContext()。兩者都會使用 Injector,但使用方式與時機不同。

runInInjectionContext() vs injectAsync()

前一篇提到的 prefetch 也不會改變這件事。

private readonly userService = injectAsync(
  () =>
    import('./user.service')
      .then(m => m.UserService),
  {
    prefetch: onIdle,
  }
);

prefetch: onIdle 只影響 loader 什麼時候開始載入。瀏覽器進入 Idle 後可以先載入對應的程式碼,而 injectAsync() 使用哪個 Injector,仍然是在建立時決定。

toSignal() 為什麼也需要 Injection Context

Injection Context 的限制不只會出現在 inject()。

前面介紹 Signal 時常用到的 toSignal(),預設也需要在 Injection Context 中執行。

例如放在 Component 的欄位初始化階段:

@Component({
  selector: 'app-user',
  template: `
    @if (user(); as user) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UserComponent {
  private readonly userService = inject(UserService);

  readonly user = toSignal(
    this.userService.getUser()
  );
}

這裡的 toSignal() 會在 Angular 建立 UserComponent 的過程中執行,因此當下本來就位在 Injection Context。

toSignal() 會訂閱 Observable,因此也需要處理訂閱的生命週期。它會透過 Injector 取得 DestroyRef,並把清理邏輯註冊到對應的生命週期上。

如果是在沒有 Injection Context 的地方呼叫 toSignal(),可以自行提供 Injector:

@Component({
  selector: 'app-user',
  template: `...`,
})
export class UserComponent {
  private readonly injector = inject(Injector);
  private readonly userService = inject(UserService);

  createUserSignal() {
    return toSignal(
      this.userService.getUser(),
      {
        injector: this.injector,
      }
    );
  }
}

這時 toSignal() 會使用傳入的 Injector 取得 DestroyRef,不需要依賴呼叫當下的 Injection Context。

清理時機會和傳入的 Injector 對應生命週期有關。假設使用的是 Component 所在的 Injector,Component 被銷毀時,toSignal() 建立的訂閱也會一起清理;如果使用更高層級的 Injector,對應的生命週期也會更長。

因此 toSignal() 並不是自己知道它屬於哪個 Component,而是透過 Injector 取得對應的 DestroyRef,再由 DestroyRef 處理生命週期的清理。

Injection Context 跟 Reactive Context 的差別

Reactive Context 前面已經介紹過,這裡簡單和 Injection Context 做區分:

  • Injection Context 關心的是「現在要從哪個 Injector 取得依賴」,主要和 DI、inject()、生命週期有關。
  • Reactive Context 關心的是「現在讀取了哪些 Signal」,主要用來建立並追蹤 Signal 之間的依賴關係。

Injection Context vs Reactive Context

本日結語

runInInjectionContext() 的作用,是在 callback 執行期間暫時指定目前要使用的 Injector。callback 結束後,Angular 會還原原本的狀態,因此這個 Injection Context 只存在於同步執行期間。

遇到 await 後,原本的同步執行區段已經結束,後續程式碼不能再依賴同一個 Injection Context 呼叫 inject()。如果後面還需要使用某個依賴,可以先取得實例,或保留 Injector 再交給需要它的 API。

Reactive Context 也有類似的概念。computed() 或 effect() 執行時,Angular 會記錄目前是哪個 Consumer 正在讀取 Signal,用來建立依賴關係。Injection Context 處理的是依賴從哪個 Injector 取得,Reactive Context 處理的則是 Signal 之間的依賴追蹤。

實務上可以把依賴盡量放在建立階段取得,後續直接使用已經取得的實例或 Injector。需要在其他執行時機建立 Injection Context 時,再使用 runInInjectionContext() 或 API 提供的 injector 選項。

資料來源


上一篇
Day 14:Lazy Loading 不只一種,從 Router Preloading、@defer 到 injectAsync()
系列文
Angular 22 Signal 進化論 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言