Angular 4 Forward Reference

forwardRef 的作用

Angular 通过引入 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 代码时,会有两个步骤,首先会先进行编译,然后才开始执行。编译阶段,变量声明和函数声明会自动提升,而函数表达式不会自动提升。了解完这些后,问题原因一下子明朗了。

那么要解决上面的问题,最简单的处理方式是交换类定义的顺序。除此之外,我们还可以使用 Angular 提供的 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 原理分析

// packages/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 的依赖注入系统调用,当解析 Provider 和创建对应依赖对象的时候,会自动调用该函数。

// packages/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);
  }
}

我有话说

为什么 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 不会自动提升,主要还是为了解决继承父类时,父类不可用的问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,631评论 18 399
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,715评论 0 9
  • //Clojure入门教程: Clojure – Functional Programming for the J...
    葡萄喃喃呓语阅读 3,665评论 0 7
  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,558评论 33 466
  • 本文参加【六专题】八月征文《月圆之夜》征文通知链接 造一座房,住一生梦,如圆月般迷人而梦幻。 2017年8月31日...
    夏日晓兮阅读 1,097评论 37 23