標籤:

【ngMiracle】Write Your Own Structural Directive

貌似是知乎專欄中第一篇 Tutorial 向的文章,由於寫一篇合格的 Tutorial 的成本比較高,所以平時基本不太願意寫(O_O)。BTW,在博客重新上線之前,可能主要都靠知乎專欄來作為發布平台。順便試水一下雙語版本,英文渣求輕噴(T^T)。

It seems to be the first article of tutorial on the Zhihu column, since it would need a lot effort to write a qualified tutorial and Im quite lazy, so its very hard to find a tutorial in my blog. By the way, the main publication place for my article may still be the Zhihu column until my personal blog being re-opened. And also experiment on bilingualism, feel free to raise complain about it.

Directive 一直是 Angular 中的一個重要組成部分,職能為擴展 HTML 語義。在 Angular 2 中,Directive 被明確劃分成三種類型:Component,Attribute Directive 以及 Structural Directive(其中 Angular 1.x 也有這個劃分,只是官方文檔中沒有明確提及)。其中,Structural Directive 雖然數量最少,卻有著極高的重要性。

Directive plays an important part in both Angular 1.x and Angular 2, since it could extend the semantics of HTML language. In Angular 2, it could be categorized to 3 types: Component, Attribute Directive and Structural Directive (Actually, it could also be applied to Angular 1.x, but it was not mentioned in official documentation).

Angular 2 中,在 common 模塊里內置了 3 個 Structural Directive:NgIf,NgFor 和 NgSwitch。和 Angular 1.x 有很大不同的是,Angular 2 提供了通用的星號 DSL (類似於 *prop="something")來實現自定義表達式語法,而 Angular 1.x 只能自己靠正則匹配。其中,在 Structural Directives 和 Template Syntax 部分對這些 Directives 做了一些簡單的介紹。但由於實在介紹的過於簡單,既沒有解釋 DSL 的詳細規則,也沒有說明其運作原理,更沒有指出應該如何自己實現一個 Structural Directive。(附註@2016/09/16,現在有了,不過還是過於簡單)

In Angular 2, three Structural Directives are provided in built-in common module. Unlike Angular 1.x, there is a universal asterisk DSL (looks like *prop="something") provided by Angular template compiler to implement user-defined expression syntax in Angular 2, rather than manually write regex to match them. In the documentation, there is some demonstration about these Structural Directives: Structural Directives andTemplate Syntax, but it only reaches the surface, there is no illustration for the complete DSL rules, nor the mechanism for how the built-in Structural Directive implemented, nor how to write an own Structural Directive. (Note at 2016/09/16, there is one for now, but still to simple to learn)

本文中,將會解決所有這些問題,最後我們將自行實現一個 for 循環的 Structural Directive。不過並不是像 Angular 內置的 NgFor 那樣造出 `for (let item of items) { }` 的語法,我們造的是另一個更古老的 for 循環:`for (let i = 0; i < length; i++) { }` ,效果類似於:

<element *mrcFor="let i from 0 to 10 step 1">{{ i + 1 }}</element>n

In this article, all of these problems will be solved, and in the end we will write our own Structural for the for loop. But not the `for (let item of items) { }` loop the NgFor did, but a more traditional one: `for (let i = 0; i < length; i++) { }`. Which would finally look like:

<element *mrcFor="let i from 0 to 10 step 1">{{ i + 1 }}</element>n

DSL 語法規則 / Whats the rules of DSL

為了解釋語法規則,我們得從 DSL 的翻譯過程入手。星號 DSL 會被翻譯兩次,其中的第一次非常特別極其簡單,是個人都能看懂,也就是:

<element *prop="whatever here is">Content</element>nn<!-- 變成 -->nn<element template="prop whatever here is">Content</element>n

To illustrate the the syntax rules, we starts from the translation for DSL. There are two translation for the asterisk DSL, with the first one extremely simple that even a monkey could understand, that is:

<element *prop="whatever here is">Content</element>nn<!-- Becomes -->nn<element template="prop whatever here is">Content</element>n

接下來才是重要的部分,也就是第二次翻譯,在第一次翻譯後,才是 Angular 的解析器真正工作的開始(第一次翻譯連正則都不用靠字元串拼接都能完成)。簡單的說,template 屬性的值必須是一系列分號分隔的語句,每個語句可以是: 1)賦值,2)冒號分隔的鍵值對。類似於:

<element template="key1: expression1; let var1 = expression2; key2: expression3; key3: expression4; let var2 = expression5;">n

What after the first translation is the important part, namely where Angulars parser really starts (for the first translation, there is even no need for regex, string concatenation is enough). In short, the value of the template attribute must be a sequence of semicolon-delimited statement, each statement must be either 1) assignment or 2) colon-delimited key-value pair. Which can be illustrate as:

<element template="key1: expression1; let var1 = expression2; key2: expression3; key3: expression4; let var2 = expression5;">n

由於第一次翻譯的緣故,第一個語句必然是鍵值對,也就是星號 DSL 的第一項內容必然是表達式,後面的語句可以自己任意排列。但是,稍微有點常識的人都能夠看出,我們天天在用的 NgFor 顯然不滿足這個規則,腫么會是這個規則呢?我們的 NgFor 第一次翻譯後明明是:

<element template="ngFor let hero of heroes trackBy trackByHeroes">n

Due to the first translation, the first statement will always be key-value pair, namely the first item of the asterisk DSL must be a statement, with latter statements being arranged in any order. But, we would have a question about that: The NgFor we uses every day does not match these rule, the result of first translation for NgFor is:

<element template="ngFor let hero of heroes trackBy trackByHeroes">n

這裡並沒有那些分隔符號的痕迹,不是么?是,也不是。事實上,和很多現代語言一樣,當你不主動提供分隔符時,Angular 的模版解析器會自動嘗試添加分隔符,也就是幫你斷句。此外,由於這個 DSL 幾乎完全沒有任何的二義性,因此添加或不添加(或部分添加部分不添加)分隔符幾乎永遠都能得到相同的結果。因此,上面的代碼完全等價於:

<element template="ngFor let hero; of: heroes; trackBy: trackByHeroes">n

There is no sign of those separators, isnt it? Yes, and no. Actually, like many modern language, when you not write separators manually, the Angular template parser would try to add it automatically according to its understanding. As there is almost no ambiguity for the DSL, it (almost) always have the same result whether you add the separators or not (or partially). So, the code above would be equal to:

<element template="ngFor let hero; of: heroes; trackBy: trackByHeroes">n

這樣看著和我們上面的規則接近了很多,但還是有些不同,也就是:1)第一個 key(也就是 ngFor)並沒有對應任何的 value,2)賦值部分沒有賦任何值。實際上,這裡僅僅是一丟丟的簡簡單單的語法糖的語法糖,也就是規則的附加規則,即:1)如果後一語句是賦值,則當前的鍵值對可以只有鍵沒有值,2)如果賦值只有變數聲明部分沒有賦值部分,相當於賦上 $implicit 這個變數的值。另外,不知道是 Bug 還是 Feature,第一條額外規則所述的情況中鍵值對和賦值之間不能手動加分號,當然,對於從星號 DSL 翻譯過來的情況,第一個鍵值對和第一個賦值(如果是賦值的話)之間本來就不會有分號。因此,上面的代碼又可以繼續等價為:

<element template="ngFor let hero = $implicit; of: heroes; trackBy: trackByHeroes">n

Its much closer to the rules, but therere still some difference: 1) The first key (namely ngFor) does not have a corresponding value, 2) The assignment didnt assign anything. Actually, these are just some syntactic sugar for syntactic sugar, namely some additional rules for the rules, to be: 1) If the next statement is an assignment, the current key-value pair could have no value part, 2) if the assignment didnt assign anything, it would be assigned with the variable called $implicit. Additionally, discard its a bug or feature, for the first additional rule, there can be no manually added semicolon between the key-value pair and the assignment. Apparently, for the result of the first translation, there will always be no semicolon between the first key-value pair and the first assignment (if it is). Then, the code above could also be equal to:

<element template="ngFor let hero = $implicit; of: heroes; trackBy: trackByHeroes"> n

接著,現在開始就可以真的進行第二次翻譯了。又一次簡單的說,1)對於鍵值對,第一個鍵值對的鍵會被當作基,添加到其後所有鍵值對的鍵之前,然後變成屬性綁定(如果鍵值對沒有值之後也就是一個沒有值的屬性);2)對於賦值,會被翻譯成一個 `let-` 開頭的字面值屬性綁定。至此,我們就能夠很清晰的理解第二次翻譯的結果了,也就是:

<template ngFor let-hero="$implicit" [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">n <element></element>n</template>n

Next, we can actually start our second translation. In short again: 1) for key-value pair, the key of first key-value pair would be regard as base, and keys of every latter key-value pairs would be prepended with it, then becomes property binding (if there is no value in key-value pair, there is no value for property binding either); 2) for assignment, it would become a literally property binding starts with `let-`. Until now, we could clearly understand the result of the second translation:

<template ngFor let-hero="$implicit" [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">n <element></element>n</template>n

友情提示,在 Angular 2 中,有方括弧(或 bind- 前綴)的屬性和沒有方括弧(或 bind- 前綴)的屬性都是屬性綁定。對於有方括弧(或 bind- 前綴)的屬性而言,例如 `[prop]="1 + 1"`,其數據綁定的內容是 `1 + 1` 的求值結果,也就是 2 這個數值(number);而對於沒有方括弧(或 bind- 前綴)的屬性而言,例如 `prop="1 + 1"`,其數據綁定的內容是 `1 + 1` 這個字面值本身,也就是 "1 + 1" 這個字元串(如果有插值的話綁定的是插值後的結果)。一些文章或視頻中為了和事件綁定押韻而把屬性綁定說成是有方括弧(或 bind- 前綴)的屬性是錯誤的,希望大家不要繼續傳播這種錯誤說法。

To be honest, in Angular 2, both attributes with or without brackets (or bind- prefix) are property binding. For the condition with brackets (or bind- prefix), like `[prop]="1 + 1"`, the data of the binding is the result of evaluation of expression `1 + 1`, or number 2. For the condition without brackets (or bind- prefix), the data of the binding is the literal of `1 + 1`, or the string "1 + 1" (if there is interpolation, it will be the result after interpolation). Its wrong for regarding property binding as the attributes with the brackets (or bind- prefix) for the symmetric of event binding in some article and videos.

忘了說,`$implicit` 其實只是語義上的表現,並不直接存在於翻譯結果中,所以直接的翻譯結果是:

<template ngFor let-hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">n <element></element>n</template>n

Something to notice is that the `$implicit` part is only representation in semantics, not directly lies in the translation result, so the direct translation result is:

<template ngFor let-hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">n <element></element>n</template>n

綜上,這就(應該)是星號 DSL 的規則了,因此我們現在也終於能理解 NgFor 的翻譯過程了。

Above all, these (should) is/be the rules of the asterisk DSL, and finally we can understand how is NgFor being translated.

現在,如果我們願意,我們可以用任何喜歡的方式來寫 NgFor:

<element *ngFor="let hero of heroes trackBy trackByHeroes"></element>n<element *ngFor="let hero trackBy trackByHeroes of heroes"></element>n<element *ngFor="let hero ;of heroes; trackBy trackByHeroes"></element>n<element *ngFor="let hero ;of: heroes; trackBy: trackByHeroes"></element>n<element *ngFor="let hero = $implicit; of: heroes; trackBy: trackByHeroes"></element>n

Now, we can use any way to write NgFor as we like:

<element *ngFor="let hero of heroes trackBy trackByHeroes"></element>n<element *ngFor="let hero trackBy trackByHeroes of heroes"></element>n<element *ngFor="let hero ;of heroes; trackBy trackByHeroes"></element>n<element *ngFor="let hero ;of: heroes; trackBy: trackByHeroes"></element>n<element *ngFor="let hero = $implicit; of: heroes; trackBy: trackByHeroes"></element>n

NgFor 的內部實現 / How NgFor works internally

從上一小節中我們知道,星號語法糖會被編譯為普通的屬性綁定。也就是說,Structural Directive 在形式上並不會和 Attribute Directive 有任何不同,只是它在內部所做的事情和 Attribute Directive 所做的事情的側重點有些差異。

From the last section we can infer that asterisk DSL would be translated to normal property bindings. In other words, there is no difference between Structural Directive and Attribute Directive in their formats, but there do have some difference in what theyre going to do internally.

與其他 Directives 一樣(對於 Entry Component 而言可能不是必須的),NgFor 需要有一個 selector 來指定其應用的範圍。從上面的翻譯結果我們知道,NgFor 需要作用在一個有 ngFor 屬性和 ngForOf 屬性的(template)元素上,因此對應的選擇器就是 [ngFor][ngForOf]。在 CSS 中,屬性選擇器使用方括弧的語法,並且多個選擇器的組合仍然為選擇器。於是我們得到 NgFor 的骨架:

@Directive({n selector: [ngFor][ngForOf]n})nclass NgFor {n // 類名叫什麼並不重要n}n

Same as many other Directives (may not fit Entry Component), NgFor needs a selector to specify the circumstances it should be applied to. According to the translation result, NgFor should be applied to a/an (template) element with ngFor and ngForOf attributes, so its selector should be [ngFor][ngForOf]. In CSS, an attribute selector is surrounded by brackets, and composition of several selectors is still a selector. Now we have the skeleton of NgFor:

@Directive({n selector: [ngFor][ngForOf]n})nclass NgFor {n // The name of class is not importantn}n

Angular 2 中,所有的 Directive(當然也包括 Component)都是 ES Class(當然實際上也可以用 Angular 2 的 ES5 API 來模擬 class 的方式),通過 Decorator 來實現額外的配置內容。

In Angular 2, every Directive (includes Component) is an ES Class (Actually it could also be an emulated class powered by Angular 2 ES5 API), with configuration provided in decorator.

從上面我們已經知道,NgFor 會有兩個 @Input() 屬性(即屬性綁定):ngForOf 和 ngForTrackBy。這裡我們會發現一個問題,NgFor 的選擇器和其屬性綁定並不對應,這是正常的情況。對於選擇器而言,我們需要應用在一個所有場景里都能滿足的選擇器上,而 trackBy 是可選的部分,所以並不會出現在 Directive 的 selector 中,否則我們不寫 trackBy 的時候它就不是 NgFor 了;對於屬性綁定而言,我們只需要有值的屬性,ngFor 屬性雖然始終存在但是並木有任何內容,所以每次去拿到一個 undefined 也沒有任何意義,而輸入屬性需要考慮所有的可選內容,比如這裡的 trackBy。(實際上可能還有另外的屬性綁定,但此處不作深入討論)

As far as we know, NgFor has two @Input() properties (property binding): ngForOf and ngForTrackBy. But as we can see, theyre not matched with the selector of NgFor, thats all right. For the selector, we need to use a selector which can fulfill every case of usage, so it would not include the optional attribute such as what generated by trackBy part, if not, the NgFor would not work if we use it without a trackBy part; for the property binding, we only need the properties which has value, as the ngFor attribute always has no value in it, so it is useless to get value from it, and now we need to consider the optional attributes, such as the trackBy here. (Actually there may still be other property binding, but not discussed here)

所以現在我們可以增加屬性綁定的聲明:

@Directive({n selector: [ngFor][ngForOf]n})nclass NgFor {n @Input() ngForOf: anyn @Input() ngForTrackBy: TrackByFnn}nn// interface TrackByFn { (index: number, item: any): any }n

這裡我們增加一個 ngForOf 屬性,為 any 類型(其實這裡本應該是 Iterable 類型,即實現了 Symbol.iterator 屬性的對象);另一個是 ngForTrackBy,是一個滿足特定簽名的函數類型(接收當前的 index 和集合中的當前 item 作為參數,返回任意值作為唯一性標識),這和我們在 Angular 1.x 中的用法稍有不同,我們無法繼續使用內聯表達式來作為標識,必須抽取成一個類方法。

Now we can write the declaration of property bindings:

@Directive({n selector: [ngFor][ngForOf]n})nclass NgFor {n @Input() ngForOf: anyn @Input() ngForTrackBy: TrackByFnn}nn// interface TrackByFn { (index: number, item: any): any }n

We wrote an ngForOf property, which has type of any (theoretically it should be of type Iterable, who implements the Symbol.iterator property); Another one is ngForTrackBy, being a function that implements a specific signature (with the current index and current item in collection as parameters, any value to be a unique identifier as the return value), its quite different to what we have done in Angular 1.x, we can not use inline expression now.

有了參數,接下來我們就可以來進行操作了。這裡我們需要做的事情很簡單,就是把 Template 變成一個 View,然後放進 ViewContainer 里。

With parameters, we can do some real operations based on it. The first thing we need to do is very simple, just transform the Template to a View, and attach it to ViewContainer.

聽上去可能有些陌生,Template 是什麼呢?在 Component 中,我們會提供一個 template(或 templateUrl)屬性,會自動進行 Template -> View 的轉換,因為這裡要做的操作非常簡單,一個 Component 有且只有一個 Template,並且也只會對應的一個 View。而在 Structural Directive 中,就未必能有這麼簡單了,比如這裡我們需要根據循環項把同一個 Template 變成多個 View。

If may not sound familiar, what is Template? In Component, we will provide a template (or templateUrl) property, and Component will covert the Template to a View automatically, as its very simple, a Component would have only one Template, and result in a corresponding View. But here in Structural Directive, it may be more complex, for example, now we need to convert the template to many separate Views based on the loop.

我們都知道 Angular 1.x 中有一個 $compile,其工作過程是:

compile + scopentemplate(raw/element) ---------> linkFn ---------> instance elementn

類似的,在 Angular 2 中,也有一個模版(Template)到視圖(View)的過程:

For component template:nn compilentemplate(raw) ---------> view factory -----------> viewnnnFor template tag template:nn compile + contextntemplate(raw) ---------> template(compiled) -----------> viewn

看到這裡,讀者可能會有若干的疑問:

  1. 為什麼 Angular 2.0 中生成的結果是一個 View,這個抽象實體的存在目的是什麼?
  2. 為什麼 Angular 2.0 中的 Template 在 Compile 之後還是 Template,那麼 Template 到底指什麼?
  3. 為什麼 Component 的 Template 就不需要 Context 就能實例化,而其他的 Template 就需要 Context 才能實例化呢?
  4. Angular 2.0 中的 Context 和 Angular 1.x 中的 Scope 是什麼關係?是不是像 Filter -> Pipe 那樣的簡單改名?

接下來,我們就會對這些問題一一解答。

As we all know, there is a service called $compile in Angular 1.x, with the procedure:

compile + scopentemplate(raw/element) ---------> linkFn ---------> instance elementn

Similiar in Angular 2.0, there is a procedure in which Template being generated to View:

For component template:nn compilentemplate(raw) ---------> view factory -----------> viewnnnFor template tag template:nn compile + contextntemplate(raw) ---------> template(compiled) -----------> viewn

Now, readers may have several questions here:

  1. Why is Angular 2.0 generating a View, whats the purpose for this entity?
  2. Why is Angular 2.0 still generates a Template after compile a Template, whats the term Template means?
  3. Why is the Template in
  4. How is the Context in Angular 2.0 relates to Scope in Angular 1.x? Is that some rename like Filter -> Pipe?

Next, well answer all these questions.

眾所周知,Angular 2.0 的一個重大改進就是能夠跨平台,因此,不再像 Angular 1.x 中一樣使用基於 DOM 的模版,也不能像伺服器端模版那樣直接使用基於字元串的模版。為此,必須引入一個中間抽象(但並沒有使用 Virtual-DOM)。所以,Angular 2.0 中不再是直接生成 DOM 元素的實例,而是一個更為高層的抽象視圖的實例,這裡叫做 View。

其實目前已有在 Web 平台中直接生成 DOM 元素的改進計劃 Perf: Direct Dom Rendering · Issue #11394 · angular/angular · GitHub,但這個屬於性能上的透明優化,並不會影響到這裡的概念解釋。

It is widely know that Angular 2.0 has a great improvement for being platform-independent, so it does not use a DOM-based template like in Angular 1.x, nor a string-based template like in server-side application. So there must be an intermediary abstraction (but not a Virtual-DOM). Then, DOM element is not directly generated in Angular 2.0, but a high-level abstracted view instance, just called a View.

Actually there is an improvement plan for directly generating DOM elementPerf: Direct Dom Rendering · Issue #11394 · angular/angular · GitHub, but its a performance improvement invisible to user, and will not affect the notions described here.

另外,由於不是基於 DOM 的模版,並不像 Angular 1.x 那樣先將 Template 丟給瀏覽器渲染然後再對 DOM 對象進行操作,而是自行對原始字元串進行解析,然後根據分析結果直接操縱 DOM API 來按照需要構建元素,整個過程中都沒有瀏覽器對 HTML 字元串的渲染行為發生。當然,由於 Compiler 的功能過於強大,其本身的文件大小和工作時間都是不小的成本。

Besides, since there is no DOM-based template, the procedure in Angular 1.x that templates are being processed by Browser at first and then being manipulated via DOM object is never going to happen in Angular 2.0. Instead, Angular would parse the raw template by itself, and use DOM API to create element according to the result directly. The Browser is never going to see the template string in the whole procedure. Then, with the Compiler being too powerful, its file size and process duration would become a cost.

不過另一方面,由於 Angular 2.0 中引入了一套通用的模版語法(對 HTML 的擴展),而不再像 Angular 1.x 中那樣由每個 Directive 自行定義其 Compile 函數和自行正則匹配複雜表達式,因此,Angular 2.0 的模版編譯並不需要發生在運行時,可以直接在編譯時完成模版的編譯(類似於 jsx 的構建方式,但稍略複雜)。所以我們在運行時永遠拿到的都是編譯後的模版,而不會用到原始的字元串模版。

一些 Demo 中可能會使用在運行時編譯模版的方式,但這只是為了方便演示,實際生產環境為了性能基本並不會這麼做。另外,一些極其特殊的項目可能也會需要對組件的動態模版來實時構建,這裡會需要用到 Compiler API,但在一般項目中基本不會遇到,這裡不做過多討論。

另外,不知道有沒有人會誤以為 Virtual-DOM 能比 DOM 快的,實際上 Virtual-DOM 顯然會比真實 DOM 操作要慢,其他的中間過程也是一樣(例如 Angular 編譯後的模版)。Virtual-DOM 是為了更簡單的操作(可以直接整體重新渲染)和跨平台,降低了維護成本,所謂的高性能只是在保持這個前提下性能仍不會差很多。Virtual-DOM 本身是降低性能的地方。

In the other hand, with unified template syntax (extensions to HTML) in Angular 2.0, there is no need for Directives to define there own compile function, nor to match the expression manually be regex.Then, the compilation for Angular 2.0 template does not need to be in the runtime, it could be used in the design time (like what to do with jsx, but a little more complex). So what we get in the runtime is always the compiled template, rather than the original raw string template.

In some demos there may be template compilation in runtime, its only for demonstration, and not fit for production due to its bad performance. In addition, some special project may do need to compile the component with dynamic generated template, it would needs Compiler API, but thats not a common case, would not be discussed here.

In Addition, Im not sure whether someone would think that Virtual-DOM is faster than DOM manipulation, actually Virtual-DOM is surely slower than the directly DOM manipulation, as well as any other intermediary procedure (eg. Angular Compiled Template). Virtual is designed for simpler manipulation (could directly re-render the whole application) and platform-independent, which decrease the cost of management, and the so-called high-performance is that it would not slow too much with these goal. Virtual-DOM itself is where against the high-performance.

然後,我們還有一個新的實體叫 Context,但嚴格地說,它並不是一個實體。就像很多庫中(比如 lodash)會有最後一個叫做 context 的參數一樣,僅僅是一個環境綁定,沒有任何固定的類型也沒有任何職能所在,除了作為 this 指針(變數)的指向。或者說,這裡的 Context 概念上就是一個純粹的 ViewModel。反觀 Scope,除了作為 ViewModel 外,還承擔了 Watcher、Evaluator、ChangeDetector、Differ、EventEmitter、EventListener 的作用,嚴重破壞了單一職責的基本要求。所以,Context 完全不是 Scope,但反過來,Scope 確實是 Context。不過,由於 Context 本身只是一個抽象職責,所以很多時候並不是一個獨立的實體。比如在組件模版中,Context 就是組件實例自身;而在 NgFor 的模版中,Context 就是一個 NgForRow 的實例。

Next, we still have one new entity called Context. However, it is not an entity at all. Just like in many library (eg. lodash), there will be a last parameter called context, its only a environment binding, with no fixed type nor role, except for being what this stand for. Namely, The Context is a pure ViewModel. Back to the Scope, apart from a ViewModel, it is also a Watcher, a Evaluator, a ChangeDetector, a Differ, a EventEmitter, a EventListener etc., which heavily break the rule of single responsibility. Therefore, Context is not a Scope at all, but conversely , Scope is indeed a Context. However, since Context itself is just a abstract role, it may not be a real independent entity in real cases. In Component template, the Context is just the Component instance; in template of NgFor, the Context is the instance of NgForRow class.

在瀏覽器中,我們可以在控制台內使用:

ng.probe($0 /* a DOM element reference */)n

得到的 DebugElement 來快速查看某個位置對應的 Context 信息。(如下圖所示)

In browser console, we could use:

ng.probe($0 /* a DOM element reference */)n

To get the DebugElement and check the Context information. (Shown below)

回到正題,由於有依賴注入,我們這裡可以很方便地獲取我們所需的服務:

//...nclass NgFor {n //...n constructor(n private _viewContainer: ViewContainerRef,n private _template: TemplateRef<NgForRow>,n private _differs: IterableDiffers, n private _cdr: ChangeDetectorRefn ) { }n //...n}nnclass NgForRow {n constructor(public $implicit: any, public index: number, public count: number) { }nn get first(): boolean { return this.index === 0; }n get last(): boolean { return this.index === this.count - 1; }n get even(): boolean { return this.index % 2 === 0; }n get odd(): boolean { return !this.even; }n}n

首先解釋下這裡用到的 TypeScript 語法。TypeScript 中,通過在構造函數的參數前加上 public/private 修飾符,可以將其自動綁定為實例的屬性。即:

class SomeClass {n constructor(private name: Type) { }n}n

等價於:

class SomeClass {n private name: Typen constructor(name: Type) { this.name = name }n}n

另外,這裡還用到了泛型,這也是面向對象語言中很常用的特性。其中,TemplateRef 是一個開類型,可接受一個類型參數;這裡的 NgForRow 其實就是這裡的 Context 的類型,在這裡作為 TemplateRef 的類型參數,之後即可通過 TemplateRef<NgForRow> 這樣得到對應的閉類型。在類型定義中,我們可以充分利用泛型語法,來構造通用的 API:

function identity<T>(self: T): T { return self }nconst sth = identity(5)n

例如上面的示例中,我們定義了一個沒什麼用的函數,永遠返回參數本身。但通過泛型,我們可以讓返回值的類型永遠等同於參數的類型,而不需要對所有的類型都定義一個函數,或者使用 any 造成無法進行類型檢查。

此外,class、getter/setter 等都是 JavaScript 的基本概念,這裡不多介紹。

English version is coming soon~

這裡我們需要 4 個依賴,分別是 ViewContainer、Template、Differ 和 ChangeDetector,其中,ViewContainer、Template 和 ChangeDetector 是直接通過 Ref 的方式獲得,而 Differ 是通過 Factory 的方式來主動創建的。

Angular 2.0 默認採用了臟檢測作為變化檢測的策略。臟檢測是一種廣泛使用的變化檢測方式,通過事後比較的方式來尋找變化部分,Angular 1.x、Angular 2.0 和 React 等主流庫/框架都使用了臟檢測的方式來檢測變化。(雖然 React 自己很少這麼說)

因此,為了能夠檢測變化,我們需要回答兩個問題:什麼時候進行檢測?怎樣進行檢測?Angular 1.x 採用了較為落後的方式,在每一次可能引起變化的事件中都進行全量的比較。而在 Angular 2.0 和 React 中採用了一定的優化方案,基於組件化和單項數據流的特性,我們可以很大程度上縮小需要檢測的範圍,提高性能。

而在 Angular 2.0 中,我們可以通過 ChangeDetectorRef 類型來實現對 ChangeDetection 的過程的控制,雖然在一般的程序中可能不大會用得到。實際上,這裡的 ChangeDetectorRef 僅僅是一個 Role,並不是真的有這個 ChangeDetectorRef 類型單獨的實例,由於 TypeScript 中 interface 是運行時不可見的而 class 又是 structural 的,這麼做並沒有什麼實現上的問題。由於 Angular 2.0 的應用是一個組件樹,我們可以動態調整當前組件子樹的狀態,從其類型介面就可以了解:

class ChangeDetectorRef {n markForCheck() : voidn detach() : voidn detectChanges() : voidn checkNoChanges() : voidn reattach() : voidn}n

另外還有一個 Differ 類型,相比之下,Differ 並不是 Angular 2.0 的什麼重要組成部分,只是一<del>個</del>系列工具庫,用來對不同類型的對象進行比較得出變化情況。實際上,如果我們願意的話,我們也可以完全不使用臟檢測,自行定義別的檢測方式,不過鑒於 Angular 2.0 本身的優化情況,使用臟檢測並沒有什麼性能問題。

對於 IterableDiffer(同樣適用於其他的 Differ 類型),這裡涉及到了幾個類型分別是:IterableDiffer、IterableDiffers、IterableDifferFactory 和 DefaultIterableDiffer。其中 IterableDiffer 是一個介面,DefaultIterableDiffer 是 IterableDiffer 的一個實現類,IterableDifferFactory 是 IterableDiffer 對應工廠類型的介面,IterableDiffers 是 IterableDiffer 的工具類。當然,事實上也是有 DefaultIterableDifferFactory 這個類型的,只是並沒有歸入文檔中,因此不做介紹。其中,IterableDifferFactory 有一個 supports 方法,用來確定是否支持某個具體的 Iterable 對象(有很多類型都實現了 Iterable,比如常見的數組,不常見的生成器函數的返回值等)。為此,IterableDiffers 也提供了一個 find 方法,用來自動查找可用於這個對象的 IterableDifferFactory,類似於:

this._differ = this._differs.find(value).create(this._cdr, this.ngForTrackBy);n

當然,作為工廠類型,有一個 create 方法是無序過多介紹的,其接受兩個參數,分別是 ChangeDetectorRef 和 TrackBy 函數,之後返回一個 IterableDiffer 實例。當然,因為這裡的 find 是可能會失敗的,因為可能支持某個對象實例的 Differ 並不存在。換句話說,我們也可以提供自定義的 IterableDifferFactory 來擴展 NgFor 能夠支持的類型範圍(通過 IterableDiffers.extend ),這也是單一職責或者說低耦合性帶來的一個巨大便利。

Angular 2.0 也和其他組件化的實現一樣,具有生命周期(Life-Cycle Hook)這個概念。這裡我們需要用到兩個 Hooks:OnChanges 和 DoCheck。

OnChanges 正如其名,就在每次發生變化時調用。然而,我們乾的事情並不是每次變化都起作用,而是只需要第一次執行就能完成:

//...nclass NgFor implements OnChanges {n //...nn private _differ: IterableDiffer = null;nn ngOnChanges(changes: SimpleChanges): void {n if (ngForOf in changes) {n const value = changes[ngForOf].currentValue;n if (!this._differ && value) {n try {n this._differ = this._differs.find(value).create(this._cdr, this.ngForTrackBy);n } catch (e) { throw new Error(`...`); }n }n }n }n}n

這裡又接觸到了一個新的類型,SimpleChanges,就是一個簡單的當字典用的對象,每個 key 是屬性名稱(實際上是 input 的名稱),value 是一個 SimpleChange 類型的對象。那麼 SimpleChange 又是什麼呢?看一看結構就很清楚了:

class SimpleChange {n constructor(previousValue: any, currentValue: any)n previousValue : anyn currentValue : anyn isFirstChange() : booleann}n

這個我們在熟悉不過了,和 Scope.$watch 的回調參數非常相似,這裡我們可以得到每個變化了的屬性的舊值和新值,以及一個能確定是否為首次變化的函數。

不過上面並不是重點,只是為了讓我們能夠繼續讀代碼。仔細進行分支檢查我們可以發現,真正的內容只可能會被執行一次,而且所謂的真正的內容唯一的作用就是初始化 Differ,之後的變動也不會造成任何影響。那麼,為什麼一定要這麼做呢?而不是在 OnInit 的時候就把 Differ 給初始化好呢?

這裡就有一個邏輯問題,上面我們已經知道,具體使用的 Differ 是需要根據對應 Iterable 的實際類型來確定的,而 ngForOf 卻不一定是在組件初始化時就已經存在的(比如使用 async 這個 Pipe),所以在組件初始化的時候我們還未必能確定 ngForOf 這個 Iterable 的類型,因此也就無法獲取對應類型的 IterableDiffer。(不過話說真的有必要搞得這麼複雜么?)所以說,雖然這裡是 OnChanges 這個介面,但只是進行初始化這個操作,理解了這點,我們就能夠繼續往下了。

當然,我們也能夠發現這裡用到了兩個假設:

  1. ngForOf 的類型是不會發生變化的。即不會現在循環一個 Array,過一會又變成一個 Set,當然實現上可能並不會發生問題,但原理上並不應當出現這種情況;
  2. ngForTrackBy 始終不會發生變化。即不能在初始化以後修改 ngForTrackBy 的綁定,顯然沒有什麼不對。

除了 OnChanges 這個介面外,我們還實現了另一個介面:DoChecks。並且這裡是真真正正的來 DoCheck,並不像上面的 OnChanges 那樣掛羊頭賣狗肉,而是真正的實現了檢測變化的功能:

//...nclass NgFor implements OnChanges, OnChanges {n //...nn ngDoCheck() {n if (this._differ) {n const changes = this._differ.diff(this.ngForOf);n if (changes) this._applyChanges(changes);n }n }n}n

這裡的功能很簡單,如果有 Differ 的情況下(上面說到由於 ngForOf 指不定什麼時候才有因此 Differ 也是指不定什麼時候才有的),會調用 Differ 的 diff 方法來檢測變化,如果有變化則進行後續操作。這裡略有一點 Tricky,甚至可以說可能有潛在錯誤隱患,直接假定了當前的 Differ 就是 DefaultIterableDiffer(實際上上面也說道用戶可以提供另外的 DifferFactory),從而可以寫出下面的變化應用代碼:

private _applyChanges(changes: DefaultIterableDiffer) {n const insertTuples: RecordViewTuple[] = [];n changes.forEachOperation(n (item: CollectionChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => {n if (item.previousIndex == null) {n const view = this._viewContainer.createEmbeddedView(n this._template, new NgForRow(null, null, null), currentIndex);n const tuple = new RecordViewTuple(item, view);n insertTuples.push(tuple);n } else if (currentIndex == null) {n this._viewContainer.remove(adjustedPreviousIndex);n } else {n const view = this._viewContainer.get(adjustedPreviousIndex);n this._viewContainer.move(view, currentIndex);n const tuple = new RecordViewTuple(item, <EmbeddedViewRef<NgForRow>>view);n insertTuples.push(tuple);n }n });nn //...n}n

這裡用到了 DefaultIterableDiffer 的一個重要特性,其 diff 方法中,如果沒有變化內容,則返回 null,如果有變化內容,則返回其自身,因此這裡的 changes 參數仍然是一個 DefaultIterableDiffer。之後的代碼就很簡單,對於每一個元素變化,如果是新增的元素,就在自己的 ViewContainer 中為其新建一個 View,其中,Template 就是拿到的 TemplateRef,Context 為新建的一個 NgForRow 類型的實例,Index 為當前 Iterable 元素的 Index,並將其單獨添加到一個數組中以供某後續操作(其實就是設一下 $implicit 的值);如果是被刪除的元素,則直接將其 View 從 ViewContainer 中刪除即可;如果是內容有變化的元素(可能是順序變化也可能是元素本身變化),則將其 View 在 ViewContainer 的位置也進行對應調整,也同樣將其加入到某待後續操作的數組中。

private _applyChanges(changes: DefaultIterableDiffer) {n //...n for (let i = 0; i < insertTuples.length; i++) {n this._perViewChange(insertTuples[i].view, insertTuples[i].record);n }n //...n}nnprivate _perViewChange(view: EmbeddedViewRef<NgForRow>, record: CollectionChangeRecord) {n view.context.$implicit = record.item;n}n

緊接著就是剛剛我們提到的某後續操作,真的就只是設置一下 Context 的 $implicit 屬性。如果不記得這個是什麼了的,可以往上翻翻看,其實就是 「let item of items」 中 item 的值。

至此,我們可以知道,NgFor 沒有使用 DOM 操作,仍然保持著 Angular 2 的平台無關性。同時,也並沒有使用什麼黑魔法,只是用到了一些平常不太會用到的 API。所以,NgFor 並沒有任何的實現上的特殊性,我們完全可以自行實現一個 NgFor 或者類似功能的 Structural Directive。

我們自己的結構型指令 / Our Own Structural Directive

// 此坑待填: 上面扯了那麼多這裡應該主要直接上代碼就好了

// TODO: Thanks to the knowledges above it would be easy to just write the code here


推薦閱讀:

使用UpgradeAdapter將angular 1 升級到 2 的開發體驗如何?

TAG:Angular? |