[翻译]Angular的'forwardRef'是什么以及为什么我们需要它

1,793 阅读6分钟

Angular的'forwardRef'是什么和为什么我们需要它

原文来自: AngularInDepth - Medium

几乎每一篇我读过的使用forwardRef的文章都在不需要它的时候使用了它。阅读本文来学习如何合适的使用它来减少不必要的代码复杂度。

它是什么

让我们看看官方文档对于forwardRef的描述:

Allows to refer to references which are not yet defined.

For instance, forwardRef is used when the token which we need to refer to for the purposes of DI is declared, but not yet defined. It is also used when the token which we use when creating a query is not yet defined.

这个定义谈到了对于一个类的引用,并且用一个令牌引用一个类来举例。在Angular中我们像这样定义一个依赖:

const dependency = {
    provide: SomeTokenClass,
    useClass: SomeProviderClass
};

这个例子里有一个令牌被指定给provide以及一个指向useClass,从这个定理里面我们像这样可以给令牌使用forwardRef

const dependency = {
    provide: forwardRef(()=>{ SomeTokenClass }),
    useClass: SomeProviderClass
};

我们在这里同样也有一个对someProviderClass类的引用在useClass中。我们能用给这个类提供商使用同样的方法吗?文档中没有提到,但是我们知道useClass代表了一个对类的引用同时我们也学到了forwardRef可以被应用给一个引用,所以这个答案是可以,我们能够引用这个方法给类提供商:

const dependency = {
    provide: forwardRef(()=>{ SomeTokenClass }),
    useClass: forwardRef(()=>{ SomeProviderClass })
};

但是,他也只能被应用于一个对于类的引用,比如useClass或者useExisting

const dependency = {
    provide: forwardRef(()=>{ SomeTokenClass }),
    useExisting: forwardRef(()=>{ SomeOtherClassToken })
};

同样的,如果你用@Inject装饰器来注入令牌,你也可以使用forwardRef来引用这个方法:

export class ADirective {
    constructor(@Inject(forwardRef(() => Token)) service) {
    	// your code here
    }

应用实例

Angular的文档中有这么一个例子:

class Door {
    lock: Lock;

    // Door attempts to inject Lock, 
    // despite it not being defined yet.
    // forwardRef makes this possible.
    constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { 
        this.lock = lock; 
    }
}

// Only at this point Lock is defined.
class Lock {}

不过对我来说这个样例有点不太自然,尽管讲清楚了要点但是通过它还是很难理解在实际的应用里我们需要在什么时候使用forwardRef。我可以通过把Lock类放到Door类前来定义,这样就在不用使用forwardRef的同时也可以解决问题。Angular在源码中提供了一个更加实际的样例。

你可能已经知道Angular表单有ngModelformControl指令用于表单的input元素。每个control都定义了一个类提供商允许访问指令的实例通过一个公有的令牌NgModel。所以,如果你想要在你的自定义指令中访问相应input元素的表单指令,你可以这么做:

@Directive({
    selector: '[mycustom]'
})
export class MyCustom {
    constructor(@Inject(NgControl) directive) {
...
<input type="text" ngModel mycustom>

为了去启用,ngModel以及formControl指令都定义了一个formControlBinding类提供商并且在@Directive装饰器中注册了这个提供商:

// formControl指令:
export const formControlBinding: any = {
  provide: NgControl,
  useExisting: FormControlDirective
};
@Directive({
  selector: '[formControl]',
  providers: [formControlBinding],
  ...
})

export class FormControlDirective { ... }
// ngModel指令
export const formControlBinding: any = {
  provide: NgControl,
  useExisting: NgModel
};
@Directive({
// 译者注:我们可以发现如果有formControl指令的话,ngModel指令不会被启用。
  selector: '[ngModel]:not([formControlName]):not([formControl])', 
  providers: [formControlBinding],
  ...
})
export class NgModel { ... }

这个实现有趣的地方的在于formControlBinding被定义在了指令类的装饰器之外。所以当JS运行环境解释这行定义formControlBinding对象的代码时,这两个指令还没有被定义,如果我们这个时候打印这个类提供商我们会看到:

Object {useExisting: undefined, token: function}

在这里useExisting指向了undefined所以Angular没法去解析其他token,所以Angular在这里使用了forwardRef

export const formControlBinding: any = {
  provide: NgControl,
  useExisting: forwardRef(() => FormControlDirective)
};

export class FormControlDirective { ... }
...
export const formControlBinding: any = {
  provide: NgControl,
  useExisting: forwardRef(() => NgModel)
};

export class NgModel{ ... }

但是假设我们定义formControlBinding在指令的装饰器中,我们是否可以不使用forwardRef呢?答案也是可以的,所有的装饰器都在类被定义之后才会应用到类中。但是使用内联的类提供商会让我们没法导出并复用这个类提供商。

forwardRef如何工作

现在可能有一个问题浮现在你的脑海中,forwardRef是怎么工作的?这个实际上和JavaScript中闭包的工作原理有关。当你尝试在一个闭包方法内捕获一个变量时,他捕获的不是这个变量的值而时这个变量的引用。

let a;
function enclose() {
    console.log(a);
}

enclose(); // undefined

a = 5;
enclose(); // 5

你会发现尽管变量aenclose方法被创建时是undefined,但是他获取的是a的引用,所以当值被更新到5的时候该方法也能正确的log出正确的值。 而forwardRef就是一个方法在一个闭包中去获取一个类的引用,所以类在方法被执行之前就被定义了,Angular的编译器使用了resolveForwardRef方法来执行这个方法,解包闭包以在运行时获取真正的令牌或者类提供商。

export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
  (<any>forwardRefFn).__forward_ref__ = forwardRef;
  (<any>forwardRefFn).toString = function() { return stringify(this()); };
  return (<Type<any>><any>forwardRefFn);
}

export function resolveForwardRef(type: any): any {
  if (typeof type === 'function' && type.hasOwnProperty('__forward_ref__') &&
      type.__forward_ref__ === forwardRef) {
    return (<ForwardRefFn>type)();
  } else {
    return type;
  }
}

译者注

翻译这篇主要是因为自己在写单元测试的时候发现一个错误ContentChildren没法通过Type来获得,提示的信息是Query is not defined,查询下来是我需要在ContentChildren中也需要加上forwardRef

	// not work
    @ContentChildren(StepComponent) public steps: QueryList<StepComponent>;
    // work good
    @ContentChildren(forwardRef(() => StepComponent)) public steps: QueryList<StepComponent>;

以前我一直认为forwardRef只适用于依赖注入时候的引用,实际上在所有发生循环引用的地方都适用这个方法,这一点实际上是被很多示例给误导了,网络上的大部分文章,也包括这篇文章,都只有依赖注入的样例,官方只在API文档中有寥寥一句提到了这一处应用场景:

"It is also used when the token which we use when creating a query is not yet defined"

而在官方示例中,forwardRef也只是在依赖注入实战中被提到,可以说让很多的理解都停留在了依赖注入上。而实际上forwardRef的真正含义则是在使用时才去解析Type,可以说是一个延迟加载,旨在循环引用的情况下能够避免因为TypeScript的机制导致后声明的类是一个undefined的情况。

不过比较奇怪的是上面的代码在无论是在ng serve还是ng build都没有问题,只有在跑单元测试的时候会遇到,这一点我还没有搞清楚原因,猜想可能是编译模式的问题?