-
Notifications
You must be signed in to change notification settings - Fork 4
Description
VS Code: IoC Container and Dependency Injection
The brief introduction of VS Code IoC container
- The source code is at
src/vs/platform/instantiation/common
, includes 506 lines code - Implements using TypeScript
Decorator
andReflect.construct
- Implements
Dependency Injection
,constructor injection
method. - Implements an simple
Dependency Lookup
interface calledinvokeFunction
.
The brief introduction of usage
-
Conventionally, the dependent object is called a
service
-
In the IoC container, all services are
singletons
. -
A service definition is two parts:
- the interface of a service (a practice of Dependence Inversion Principle - DIP)
- a service identifier, which is a
decorator
-
Code example
// 1. Define the service interface export interface IDBService { select(id: string): Promise<any[]> } // 2. Create the decorator import { createDecorator } from 'vs/platform/instantiation/common/instantiation' export const IDBService = createDecorator<IDBService>('dbService') // Declaration Merging
// 3. Implement the service export class MysqlService implements IDBService { select(id: string): Promise<any[]> { console.log('mysql sql:', id) return Promise.resolve([]) } } // 4. Use the service in client export class BookService { constructor( private name: string, @IDBService private dbService: IDBService ) { console.log('dbService:', dbService) } getBook() { console.log('name:', this.name) return this.dbService.select('select * from book') } } // 5. Init the IoC container import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService' import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection' const services = new ServiceCollection() // key is decorator services.set(IDBService, new MysqlService()) // InstantiationService is the core of the IoC container const instantiationService = new InstantiationService(services) // 6. Create instance using instantiationService instantiationService.createInstance(BookService, 'hello book').getBook()
Implement from scratch
The overall process
-
Use
decorator
as serviceidentifier
, not the service itself. -
Mark injection using
decorator
export class BookService { constructor( private name: string, @IDBService private dbService: IDBService, @ICacheService cacheService: ICacheService ) { console.log('dbService:', dbService) } }
-
Collect dependencies of service to be a array like form, such as
[decorator1, decorator2, …]
-
Centrally store decorator service pairs, it’s a map like form, such as
{ decorator1: service1, decorator2: service2, …}
. The value stored is either a instance of Service or Service itself (actually a wrapper of Service class which will be instantiated when needed). The store is calledserviceCollection
here. -
The main process of
createInstance(Client)
is:- Get all the dependencies of Client. A array like of decorators,
[decorator1, decorator2, …]
- Get all the instances of the dependencies from
serviceCollection
using decorators as key. If the value is a instance, return it. If the value is a wrapper, then create an instance using the wrapper and replaced the wrapper with the created instance. - Create a instance of the Client using
Reflect.construct
and all dependent instances.Reflect.construct(Client, [param1, param2, ..., instance1, instance2, ...])
- Get all the dependencies of Client. A array like of decorators,
Collect dependencies of service using decorator
-
As the identifier, the decorator type is:
// vs/platform/instantiation/common/instantiation.ts export interface ServiceIdentifier<T> { (...args: any[]): void; type: T; }
-
The Parameter Decorator is applied to the function for a class constructor or method declaration, it will be called at runtime with three arguments:
- Either the
constructor function
of the class for a static member, or the prototype of the class for an instance member. - The name of the member.
- The
ordinal index
of the parameter in the function’s parameter list.
- Either the
-
In the decorator, we can add an array property to the constructor function with a special name for storing all the dependencies, here is ‘$di$dependencies’ . Then put the current
decorator
andindex
in the array. When getting the dependencies of a service, just return the constructor’s $di$dependencies property. To avoid redefining the decorator, we use a map to store the decorator which key is a string id passed to the createDecorator as parameter.-
Core code
// vs/platform/instantiation/common/instantiation.ts export namespace _util { export const serviceIds = new Map<string, ServiceIdentifier<any>>(); export const DI_TARGET = '$di$target'; export const DI_DEPENDENCIES = '$di$dependencies'; export function getServiceDependencies(ctor: any): { id: ServiceIdentifier<any>; index: number }[] { return ctor[DI_DEPENDENCIES] || []; } } export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> { if (_util.serviceIds.has(serviceId)) { return _util.serviceIds.get(serviceId)!; } const id = <any>function (target: Function, key: string, index: number): any { if (arguments.length !== 3) { throw new Error('@IServiceName-decorator can only be used to decorate a parameter'); } storeServiceDependency(id, target, index); }; id.toString = () => serviceId; _util.serviceIds.set(serviceId, id); return id; } function storeServiceDependency(id: Function, target: Function, index: number): void { if ((target as any)[_util.DI_TARGET] === target) { (target as any)[_util.DI_DEPENDENCIES].push({ id, index }); } else { (target as any)[_util.DI_DEPENDENCIES] = [{ id, index }]; (target as any)[_util.DI_TARGET] = target; } }
Notice that a
toString
method which returns theserviceId
is added to the decorator:id.toString = () => serviceId
. This method is used as a hash function in graph structure implementation that will be detailed later. -
Example code
import { IDBService } from './service/db' import { ICacheService } from './service/cache' class BookService { constructor( private name: string, @IDBService dbService: IDBService, @ICacheService cacheService: ICacheService ) { } } // result BookService.$di$dependencies = [{ id: IDBService, index: 1}, { id: ICacheService, index: 2}]
-
Implement the centrally store ServiceCollection
-
As mentioned before, we use a map to centrally store decorator service pairs.
private _entries = new Map<ServiceIdentifier<any>, any>();
. The value stored is either a instance of the service or a wrapper of the service, the wrapper is used to store the service constructor and other information like static parameters.-
The wrapper definition
// vs/platform/instantiation/common/descriptors.ts export class SyncDescriptor<T> { readonly ctor: any; readonly staticArguments: any[]; // 非依赖注入的参数 // 是否延迟实例化 // 为true时, 被注入时不进行实例化,而是返回一个Proxy 等使用时再实例化 readonly supportsDelayedInstantiation: boolean; constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) { this.ctor = ctor; this.staticArguments = staticArguments; this.supportsDelayedInstantiation = supportsDelayedInstantiation; } }
-
-
As a store,
ServiceCollection
includes three methods:get
,set
,has
-
Core code
// vs/platform/instantiation/common/serviceCollection.ts import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { SyncDescriptor } from './descriptors'; export class ServiceCollection { private _entries = new Map<ServiceIdentifier<any>, any>(); constructor(...entries: [ServiceIdentifier<any>, any][]) { for (const [id, service] of entries) { this.set(id, service); } } set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> { const result = this._entries.get(id); this._entries.set(id, instanceOrDescriptor); return result; } has(id: ServiceIdentifier<any>): boolean { return this._entries.has(id); } get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> { return this._entries.get(id); } }
-
Implement the core of the IoC container InstantiationService
I will remove some unimportant code from source code in this part.
- The
InstantiationService
is the core of the IoC container.-
A
InstantiationService
instance is associated with aServiceCollection
instance. -
The main function is
createInstance
-
It implements a method called
invokeFunction
to provideDependency Lookup
-
The IoC containers are layered, the
InstantiationService
implements a method callcreateChild
to create a childInstantiationService
.- The child
InstantiationService
has a reference to the parentInstantiationService
, but the parentInstantiationService
has no reference to the childInstantiationService
. - So when the service is not present in the child
InstantiationService
, the childInstantiationService
will recursively look up from the ancestorInstantiationService
- The child
-
The interface of
InstantiationService
// vs/platform/instantiation/common/instantiation.ts export interface IInstantiationService { createInstance<T>(descriptor: descriptors.SyncDescriptor0<T>): T; createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R; invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R; createChild(services: ServiceCollection): IInstantiationService; }
-
The Constructor
-
The constructor accepts a
ServiceCollection
instance and an optional parentInstantiationService
instance. -
The
InstantiationService
is also a service, sothis
will be stored in theServiceCollection
instance passed to the constructor. -
The core code
// vs/platform/instantiation/common/instantiation.ts export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
// vs/platform/instantiation/common/instantiationService.ts import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation' export class InstantiationService implements IInstantiationService { constructor( private readonly _services: ServiceCollection = new ServiceCollection(), private readonly _parent?: InstantiationService ) { this._services.set(IInstantiationService, this); }
The createChild
Method
Create a new InstantiationService
with this
as parent, and return it.
-
Code
// vs/platform/instantiation/common/instantiationService.ts createChild(services: ServiceCollection): IInstantiationService { return new InstantiationService(services, this); }
The createInstance
Method
The createInstance
Method is the core of the IoC container. Its main function is to instantiate the services.
instantiationService.createInstance(BookService, 'hello booke').getBook()
If there is only one layer of dependency, like the previous example: BookService
dependents IDBService
and ICacheService
, IDBService
and ICacheService
have no dependency. The instantiation process is simple and clear:
-
Get the dependence list from
Constructor[$di$dependencies]
.BookService[$di$dependencies]
-
Get all the dependency services from
serviceCollection
and instantiate them if they have not been instantiated. Assume serviceCollection stores theSyncDescriptor
of them.// 1. Get the DBService Descriptor and instantiate DBService. const dbServiceDescriptor = this._services.get(IDBService) const dbService = Reflect.construct(dbServiceDescriptor.ctor, dbServiceDescriptor.staticArguments) // 2. Get the CacheService Descriptor and instantiate CacheService const cacheServiceDescriptor = this._services.get(ICacheService) const cacheService = Reflect.construct(cacheServiceDescriptor.ctor, cacheServiceDescriptor.staticArguments)
-
Instantiate the
BookService
usingReflect.construct
.Reflect.construct(BookService, ['hello booke', dbService, cacheService])
If there are multiple layers, perform this process recursively starting from leaf services (A leaf service means a service has no dependency ). So, the brief common algorithm includes two steps:
-
Construct the dependency graph.
Suppose we want to instantiate the
client
which depends on 1. We get the dependency graph as the picture showed, 1 depends on 2 and 3, 2 depends on 4 and 6, 3 depends on 4 and 5, 4 depends on 5 and 6, 5 depends 6 and 7, 6 and 7 have no dependence. Client is not in the graph. -
Perform instantiation recursively starting from leaf services.
- 6 and 7 are leaf services, instantiate them and remove them from the graph.
- Now, 5 is leaf service, instantiate it and remove it from the graph.
- Now, 4 is leaf service, instantiate it and remove it from the graph.
- Now, 2 and 3 are leaf services, instantiate them and remove them from the graph.
- Now, 1 is leaf service, instantiate it and remove it from the graph.
- Now, the graph is empty, then we instantiate the
client
.
Now let’s look at the algorithm implementation in detail.
Implement the Graph
structure
-
Using a
Orthogonal linked list
like structure to storeGraph
:-
Every
Node
has two map properties, one is used to store the nodes that depend on the current node calledincoming
, the other is used to store the nodes that the current node depends on calledoutgoing
. That meansincoming
andoutgoing
store theedges
.// vs/platform/instantiation/common/graph.ts export class Node<T> { readonly incoming = new Map<string, Node<T>>(); readonly outgoing = new Map<string, Node<T>>(); constructor( readonly key: string, readonly data: T ) { } }
-
The
Graph
class uses a map to stores all the nodes.
// vs/platform/instantiation/common/graph.ts export class Graph<T> { private readonly _nodes = new Map<string, Node<T>>(); constructor(private readonly _hashFn: (element: T) => string) { // empty } }
The constructor accepts a hash function which is used to generate a string as the map key . As mentioned before, the IoC container uses
decorator.toString
as the hash function.// vs/platform/instantiation/common/instantiationService.ts const graph = new Graph<Triple>(data => data.id.toString());
-
-
The
Graph
class provides alookupOrInsertNode
method: returns the node if it already exists, otherwise create the node and add it to the map first.// vs/platform/instantiation/common/graph.ts lookupOrInsertNode(data: T): Node<T> { const key = this._hashFn(data); let node = this._nodes.get(key); if (!node) { node = new Node(key, data); this._nodes.set(key, node); } return node; }
-
The
Graph
class provides ainsertEdge
method for inserting an edge. Lookup or create thefrom
and theto
Node first, then insert them is each other’soutgoing
andincoming
.// vs/platform/instantiation/common/graph.ts insertEdge(from: T, to: T): void { const fromNode = this.lookupOrInsertNode(from); const toNode = this.lookupOrInsertNode(to); fromNode.outgoing.set(toNode.key, toNode); toNode.incoming.set(fromNode.key, fromNode); }
-
The
Graph
class provides aremoveNode
method for removing one node from the graph.// vs/platform/instantiation/common/graph.ts removeNode(data: T): void { const key = this._hashFn(data); this._nodes.delete(key); for (const node of this._nodes.values()) { node.outgoing.delete(key); node.incoming.delete(key); } }
-
The Graph class provides a
roots()
method returns all the leaf nodes. A leaf node is a node who’soutgoing
size is 0, that means it has no dependence. I think the method name is inverse, but we still think them as leaf nodes in this post.// vs/platform/instantiation/common/graph.ts roots(): Node<T>[] { const ret: Node<T>[] = []; for (const node of this._nodes.values()) { if (node.outgoing.size === 0) { ret.push(node); } } return ret; }
-
The Graph class provides a
findCycleSlow
to find cycle in the graph usingDFS
. This method is slow, so it is only used to display the cycle information when there is a cycle. The IoC container uses other lightweight ways to detect cycle based on some assumptions.// vs/platform/instantiation/common/graph.ts findCycleSlow() { for (const [id, node] of this._nodes) { const seen = new Set<string>([id]); const res = this._findCycle(node, seen); if (res) { return res; } } return undefined; } private _findCycle(node: Node<T>, seen: Set<string>): string | undefined { for (const [id, outgoing] of node.outgoing) { if (seen.has(id)) { return [...seen, id].join(' -> '); } seen.add(id); const value = this._findCycle(outgoing, seen); if (value) { return value; } seen.delete(id); } return undefined; }
Implement the createInstance
algorithm
The createInstance is a override method.
// vs/platform/instantiation/common/instantiationService.ts
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any {
let result: any;
if (ctorOrDescriptor instanceof SyncDescriptor) {
result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest));
} else {
result = this._createInstance(ctorOrDescriptor, rest);
}
return result;
}
It accepts a constructor
or a SyncDescriptor
.
// vs/platform/instantiation/common/instantiationService.ts
private _createInstance<T>(ctor: any, args: any[] = []): T {
// 1. get all dependence identifiers from ctor['$di$dependencies']
const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
const serviceArgs: any[] = [];
// 2. get all dependence instances
for (const dependency of serviceDependencies) {
const service = this._getOrCreateServiceInstance(dependency.id);
if (!service) {
this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);
}
serviceArgs.push(service);
}
const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;
// 3. Correction parameter sequence
if (args.length !== firstServiceArgPos) {
console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);
const delta = firstServiceArgPos - args.length;
if (delta > 0) {
args = args.concat(new Array(delta));
} else {
args = args.slice(0, firstServiceArgPos);
}
}
// 4. now create the instance
return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));
}
- The main logic of
_createInstance
:- Get all dependence identifiers from
constructor[’$di$dependencies’]
- Get all dependence instances, if the dependence is already instantiated return it, otherwise instantiate it first.
this._getOrCreateServiceInstance(dependency.id)
- Now, we have all the dependence instances and other arguments, correct the arguments order.
- Instantiate the
ctor
using theReflect.construct
API.
- Get all dependence identifiers from
- The logic of
_getOrCreateServiceInstance
is very simple:- Look up the
value
recursively fromserviceCollection
store using decorator as key. - If the
value
is aSyncDescriptor
then instantiate and return it, otherwise the value is an instance already, just return it.
- Look up the
// vs/platform/instantiation/common/instantiationService.ts
protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>): T {
const thing = this._getServiceInstanceOrDescriptor(id);
if (thing instanceof SyncDescriptor) {
return this._safeCreateAndCacheServiceInstance(id, thing);
} else {
return thing;
}
}
private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
const instanceOrDesc = this._services.get(id);
if (!instanceOrDesc && this._parent) {
return this._parent._getServiceInstanceOrDescriptor(id);
} else {
return instanceOrDesc;
}
}
_safeCreateAndCacheServiceInstance
saves the instantiating service in a Set
, if there is duplicated instantiating service, there is a cycle.
// vs/platform/instantiation/common/instantiationService.ts
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();
private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
if (this._activeInstantiations.has(id)) {
throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);
}
this._activeInstantiations.add(id);
try {
return this._createAndCacheServiceInstance(id, desc, _trace);
} finally {
this._activeInstantiations.delete(id);
}
}
_createAndCacheServiceInstance
is the main instance algorithm.
// vs/platform/instantiation/common/instantiationService.ts
private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>): T {
type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>;};
const graph = new Graph<Triple>(data => data.id.toString());
let cycleCount = 0;
const stack = [{ id, desc }];
while (stack.length) {
const item = stack.pop()!;
graph.lookupOrInsertNode(item);
// a weak but working heuristic for cycle checks
if (cycleCount++ > 1000) {
throw new CyclicDependencyError(graph);
}
// check all dependencies for existence and if they need to be created first
for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {
const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
if (!instanceOrDesc) {
this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);
}
if (instanceOrDesc instanceof SyncDescriptor) {
const d = { id: dependency.id, desc: instanceOrDesc };
graph.insertEdge(item, d);
stack.push(d);
}
}
}
while (true) {
const roots = graph.roots();
// if there is no more roots but still
// nodes in the graph we have a cycle
if (roots.length === 0) {
if (!graph.isEmpty()) {
throw new CyclicDependencyError(graph);
}
break;
}
for (const { data } of roots) {
// Repeat the check for this still being a service sync descriptor. That's because
// instantiating a dependency might have side-effect and recursively trigger instantiation
// so that some dependencies are now fullfilled already.
const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
if (instanceOrDesc instanceof SyncDescriptor) {
// create instance and overwrite the service collections
const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation);
this._setServiceInstance(data.id, instance);
}
graph.removeNode(data);
}
}
return <T>this._getServiceInstanceOrDescriptor(id);
}
private _setServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
if (this._services.get(id) instanceof SyncDescriptor) {
this._services.set(id, instance);
} else if (this._parent) {
this._parent._setServiceInstance(id, instance);
} else {
throw new Error('illegalState - setting UNKNOWN service instance');
}
}
There are two while loop.
- The fist
while
loop is used to build the Graph using stack structure, the algorithm is routine and simple.- Put the start element in the stack
- Pop one element named
item
from the stack, lookup or insert it into the graph usinggraph.lookupOrInsertNode(item)
. - Get all the dependence of
item.desc.ctor
- Traverse the dependencies, if the dependence is a
SyncDescriptor
push it into the stack and add edges in graph. - Repeat step 2 from step 4.
- There is a assumption: If there are still elements in the stack after 1000 runs, we assume that there are cyclic dependencies.
- The second
while
loop is used to instance all the services from leaf services- Get all the leaf nodes using
const roots = graph.roots()
- Traverse the
roots
, instantiates all the leaf services, puts the instances in theserviceCollection
, removes the node from the graph. - Repeat step 1 and 2.
- There is a cyclic check: If there is no leaf node but the graph is not empty, so there are cyclic dependencies.
- Get all the leaf nodes using
If supportsDelayedInstantiation
is false
then create the instance, this._createInstance
is called here and all the dependencies are instantiated already.
If supportsDelayedInstantiation
is true
then return a Proxy
defines the get
, set
and getPrototypeOf
traps, the service will be instantiated when actually used.
private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean): T {
if (this._services.get(id) instanceof SyncDescriptor) {
return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation);
} else if (this._parent) {
return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation);
} else {
throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
}
}
private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean): T {
if (!supportsDelayedInstantiation) {
// eager instantiation
return this._createInstance(ctor, args);
} else {
const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);
child._globalGraphImplicitDependency = String(id);
// Return a proxy object that's backed by an idle value. That
// strategy is to instantiate services in our idle time or when actually
// needed but not when injected into a consumer
// return "empty events" when the service isn't instantiated yet
const earlyListeners = new Map<string, LinkedList<Parameters<Event<any>>>>();
const idle = new IdleValue<any>(() => {
const result = child._createInstance<T>(ctor, args);
// early listeners that we kept are now being subscribed to
// the real service
for (const [key, values] of earlyListeners) {
const candidate = <Event<any>>(<any>result)[key];
if (typeof candidate === 'function') {
for (const listener of values) {
candidate.apply(result, listener);
}
}
}
earlyListeners.clear();
return result;
});
return <T>new Proxy(Object.create(null), {
get(target: any, key: PropertyKey): any {
if (!idle.isInitialized) {
// looks like an event
if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {
let list = earlyListeners.get(key);
if (!list) {
list = new LinkedList();
earlyListeners.set(key, list);
}
const event: Event<any> = (callback, thisArg, disposables) => {
const rm = list!.push([callback, thisArg, disposables]);
return toDisposable(rm);
};
return event;
}
}
// value already exists
if (key in target) {
return target[key];
}
// create value
const obj = idle.value;
let prop = obj[key];
if (typeof prop !== 'function') {
return prop;
}
prop = prop.bind(obj);
target[key] = prop;
return prop;
},
set(_target: T, p: PropertyKey, value: any): boolean {
idle.value[p] = value;
return true;
},
getPrototypeOf(_target: T) {
return ctor.prototype;
}
});
}
}
The invokeFunction
Method
The IoC container provides a simple Dependency Lookup like behavior, the invokeFunction
accepts a function
, the function
will be called immediately with an accessor
as the first argument, you can use the accessor
to lookup services.
// vs/platform/instantiation/common/instantiationService.ts
invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
let _done = false;
try {
const accessor: ServicesAccessor = {
get: <T>(id: ServiceIdentifier<T>) => {
if (_done) {
throw illegalState('service accessor is only valid during the invocation of its target method');
}
const result = this._getOrCreateServiceInstance(id);
if (!result) {
throw new Error(`[invokeFunction] unknown service '${id}'`);
}
return result;
}
};
return fn(accessor, ...args);
} finally {
_done = true;
}
}
instantiationService.invokeFunction((accessor) => {
const dbService = accessor.get(IDBService)
const cacheService = accessor.get(ICacheService)
dbService.select(1)
cacheService.get(1)
})
Implement the Singleton Service Register
The IoC container provides a additional singleton service register of the whole app.
// vs/platform/instantiation/common/extensions.ts
import { SyncDescriptor } from './descriptors';
import { BrandedService, ServiceIdentifier } from './instantiation';
const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];
export const enum InstantiationType {
/**
* Instantiate this service as soon as a consumer depdends on it. _Note_ that this
* is more costly as some upfront work is done that is likely not needed
*/
Eager = 0,
/**
* Instantiate this service as soon as a consumer uses it. This is the _better_
* way of registering a service.
*/
Delayed = 1
}
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation: InstantiationType): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, descriptor: SyncDescriptor<any>): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean | InstantiationType): void {
if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], Boolean(supportsDelayedInstantiation));
}
_registry.push([id, ctorOrDescriptor]);
}
export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {
return _registry;
}
The registerSingleton()
method stores the service of SyncDescriptor
and decorator in the array, every element is a SyncDescriptor
and decorator pair, the getSingletonServiceDescriptors()
returns the array. The array is usually used to create ServiceCollection
Example usage code:
registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed);
registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService, InstantiationType.Delayed);
registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService, InstantiationType.Delayed);
// vs/workbench/api/common/extensionHostMain.ts
const services = new ServiceCollection(...getSingletonServiceDescriptors());