Angular 2 Forward Reference

semlinker發表於2019-02-16

Angular 2 通過引入 forwardRef 讓我們可以在使用構造注入時,使用尚未定義的依賴物件型別。下面我們先看一下如果沒有使用 forwardRef ,在開發中可能會遇到的問題:

@Injectable()
class Socket {
  constructor(private buffer: Buffer) { }
}

console.log(Buffer); // undefined

@Injectable()
class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

console.log(Buffer); // [Function: Buffer]

若執行上面的例子,將會丟擲以下異常:

Error: Cannot resolve all parameters for Socket(undefined).
Make sure they all have valid type or annotations

為什麼會出現這個問題 ?在探究產生問題的具體原因時,我們要先明白一點。不管我們是使用開發語言是 ES6、ES7 還是 TypeScript,最終我們都得轉換成 ES5 的程式碼。然而在 ES5 中是沒有 Class ,只有 Function 物件。這樣一來,我們的解決問題的思路就是先看一下 Socket 類轉換後的 ES5 程式碼:

var Buffer = (function () {
    function Buffer(size) {
        this.size = size;
    }
    return Buffer;
}());

我們發現 Buffer 類最終轉成 ES5 中的函式表示式。我們也知道,JavaScript VM 在執行 JS 程式碼時,會有兩個步驟,首先會先進行編譯,然後才開始執行。編譯階段,變數宣告和函式宣告會自動提升,而函式表示式不會自動提升。瞭解完這些後,問題原因一下子明朗了。

那麼要解決上面的問題,最簡單的處理方式是交換類定義的順序。除此之外,我們還可以使用 Angular2 提供的 forward reference 特性來解決問題,具體如下:

import { forwardRef } from`@angular2/core`;

@Injectable()
class Socket {
  constructor(@Inject(forwardRef(() => Buffer)) 
      private buffer) { }
}

class Buffer {
  constructor(@Inject(BUFFER_SIZE) private size: Number) { }
}

問題來了,出現上面的問題,我互動個順序不就完了,為什麼還要如此大費周章 ?話雖如此,但這樣增加了開發者的負擔,要時刻警惕類定義的順序,特別當一個 ts 檔案內包含多個內部類的時候。所以更好地方式還是通過 forwardRef 來解決問題,下面我們就來進一步揭開 forwardRef 的神祕面紗。

forwardRef 原理分析

// @angular/core/src/di/forward_ref.ts

/**
 * Allows to refer to references which are not yet defined.
 */
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
  // forwardRefFn: () => Buffer
  (<any>forwardRefFn).__forward_ref__ = forwardRef;
  (<any>forwardRefFn).toString = function() { return stringify(this()); };
  return (<Type<any>><any>forwardRefFn);
}

/**
 * Lazily retrieves the reference value from a forwardRef.
 */
export function resolveForwardRef(type: any): any {
  if (typeof type === `function` && type.hasOwnProperty(`__forward_ref__`) &&
      type.__forward_ref__ === forwardRef) {
    return (<ForwardRefFn>type)(); // Call forwardRefFn get Buffer 
  } else {
    return type;
  }
}

通過原始碼可以看出,當呼叫 forwardRef 方法時,我們只是在 forwardRefFn 函式物件上,增加了一個私有屬性__forward_ref__,同時覆寫了函式物件的 toString 方法。在上面程式碼中,我們還發現了resolveForwardRef 函式,通過函式名和註釋資訊,我們很清楚地瞭解到,該函式是用來解析通過 forwardRef 包裝過的引用值。

那麼 resolveForwardRef 這個函式是由誰負責呼叫,又是什麼時候呼叫呢 ?其實 resolveForwardRef 這個函式由 Angular 2 的依賴注入系統呼叫,當解析 Provider 和建立依賴物件的時候,會自動呼叫該函式。

// @angular/core/src/di/reflective_provider.ts

/**
 * 解析Provider
 */
function resolveReflectiveFactory(provider: NormalizedProvider): ResolvedReflectiveFactory {
  let factoryFn: Function;
  let resolvedDeps: ReflectiveDependency[];
  ...
  if (provider.useClass) {
    const useClass = resolveForwardRef(provider.useClass);
    factoryFn = reflector.factory(useClass);
    resolvedDeps = _dependenciesFor(useClass);
  }
}

/***************************************************************************************/

/**
 * 構造依賴物件
 */
export function constructDependencies(
    typeOrFunc: any, dependencies: any[]): ReflectiveDependency[] {
  if (!dependencies) {
    return _dependenciesFor(typeOrFunc);
  } else {
    const params: any[][] = dependencies.map(t => [t]);
    return dependencies.map(t => _extractToken(typeOrFunc, t, params));
  }
}

/**
 * 抽取Token
 */
function _extractToken(
  typeOrFunc: any, metadata: any[] | any, params: any[][]): ReflectiveDependency {
    
  token = resolveForwardRef(token);
  if (token != null) {
    return _createDependency(token, optional, visibility);
  } else {
    throw noAnnotationError(typeOrFunc, params);
  }
}

我有話說

1.為什麼 JavaScript 直譯器不自動提升 class ?

因為當 class 使用 extends 關鍵字實現繼承的時候,我們不能確保所繼承父類的有效性,那麼就可能導致一些無法預知的行為。

class Dog extends Animal {}

function Animal {
  this.move = function() {
    alert(defaultMove);
  }
}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

以上程式碼能夠正常的輸出 moving,因為 JavaScript 直譯器把會把程式碼轉化為:

let defaultMove,dog;

function Animal {
  this.move = function() {
    alert(defaultMove);
  }
}

class Dog extends Animal { }

defaultMove = "moving";

dog = new Dog();
dog.move();

然而,當我們把 Animal 轉化為函式表示式,而不是函式宣告的時候:

class Dog extends Animal {}

let Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

此時以上程式碼將會轉化為:

let Animal, defaultMove, dog;

class Dog extends Animal { }

Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

defaultMove = "moving";

dog = new Dog();
dog.move();

當 class Dog extends Animal 被解釋執行的時候,此時 Animal 的值是 undefined,這樣就會丟擲異常。我們可以簡單地通過調整 Animal 函式表示式的位置,來解決上述問題。

let Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

class Dog extends Animal{

}

let defaultMove = "moving";

let dog = new Dog();
dog.move();

假設 class 也會自動提升的話,上面的程式碼將被轉化為以下程式碼:

let Animal, defaultMove, dog;

class Dog extends Animal{ }

Animal = function () {
  this.move = function () {
    alert(defaultMove);
  }
}

defaultMove = "moving";

dog = new Dog();
dog.move();

此時 Dog 被提升了,當直譯器執行 extends Animal 語句的時候,此時的 Animal 仍然是 undefined,同樣會丟擲異常。所以 ES6 中的 Class 不會自動提升,主要還是為了解決繼承父類時,父類不可用的問題。

相關文章