浴火重生的Angular

RogJolley 8年前發布 | 11K 次閱讀 Web框架 angularjs

來自: https://github.com/xufei/blog/issues/9

浴火重生的Angular

Angular團隊近期公布了他們對2.0版本的一些考慮,很詳盡,很誠懇,我讀了好幾遍,覺得有必要寫點東西。

對于一個運行在瀏覽器中的JavaScript框架而言,最喜歡什么,最害怕什么?是標準的變動。那么,放眼最新的這些標準,有哪些因素會對框架產生影響呢?

  • module
  • Web Components
  • observe
  • promise

這幾點,我是按照影響程度從大到小排列的。下面逐條來說:

module

早期的JavaScript在模塊定義方面基本沒有約束,但作為框架來說,不按照某種約定的方式去寫代碼,就會導致一盤散沙,所以各家都自己搞一套,大部分還是各不兼容的,這是歷史原因造成的,不能怪罪這些框架。最近幾年,為了解決這些問題,人們又發明了AMD,CMD,以及各種配套庫和工程庫。好不容易有些框架向它們靠攏了,可是module又來了。

這真是沒完。我知道很多人是對module有意見的,我自己也覺得有些別扭,但我堅信一個道理:有一個可用但是稍微別扭的標準,比沒有標準要好得多。所以,不管怎樣,既然他來了,就得想辦法往上靠。不靠攏標準的后果是什么?非常嚴重,因為現在的Web是加速發展的,瀏覽器只要過了一個升級瓶頸,后面的發展快得出奇。不要看現在還有這么多老舊瀏覽器,很可能你睡一覺起來突然發現已經基本沒人用了,到那時候,不緊跟標準的框架就很慘了,瞬間就邊緣化了。

我們來看看Angular原先的module設計,如果是在五年前,它剛起步的時候看,可能覺得還可以,現在再看,問題就比較多了。Angular現有版本的module,其實跟我們在ES中看到的module不是一個概念,更像C#里面的namespace,它的各種controller,service,factory才是真的模塊。

我認為現有版本的這塊,有幾個不好的地方:

  • 現有的module實際上毫無意義,根本不能起約束作用。
  • 從API的角度強制區分模塊職責是沒有必要的,比如說,service和factory的區別在哪里?僅僅在于返回與封裝的方式不同,其實是可以通用的,所以只需一種工廠方法就可以了,用戶愿意返回什么就返回什么,他自己通過命名來區分這個模塊的職責。
  • 沒考慮模塊的動態加載。這是Angular目前版本最大的設計問題,對于大型應用來說,很致命,所以目前大家都是通過各種黑魔法來解決這個問題。

所以,Angular 2.0中,把這一塊徹底改變了,使用ES6的module來定義模塊,也考慮了動態加載的需求。變動很大,很多人有意見,但我是支持他們的。這件事不得不做,即使現在不做,將來也還是要做。毛主席教導我們:革命不徹底,勞動人民就要吃兩茬苦,受兩茬罪。在現在這個時代,如果還繼續用非標準的模塊API,基本等于找死,所以它需要改變。

Web Components

為什么Web Components也能帶來這么大的影響呢,因為它同樣會造成斷代升級,也就是說,你非完全跟著它的路不可,沒有選擇。

Web Components標準本身同樣是個見仁見智的話題,在本文中我不評價,它作為標準,既然來了,大家當然要往上靠。一個致力于大規模Web前端開發的現代框架,不考慮Web Components是完全不可想象的。那么,怎么去靠攏它呢?

Web Components提供了一種封裝組件的方式,對外體現為自定義標簽,對內體現為Shadow DOM,可以定義自己的屬性、事件等,這樣問題就來了。

我們看當前版本的Angular,能看到ng-click之類的擴展元素屬性,那么,他為什么要寫成ng-click?是因為這是對原生click的一層封裝和轉換,同理,如果我的原始事件不是click,是另外一個名字,你當然也得跟著加一個,不然針對這種東西的操作就玩不下去。所以說,其實它是給每個有價值的原生事件都寫了擴展。

這就有問題了,你能這么做的原因是,你預先知道有這么一些元素,這么一些事件,也知道這些元素上的這些事件是什么行為,如果全是自定義的,他不告訴你,你急死也不知道,怎么辦?所以這一塊必須重新設計。

同理,屬性也是這樣,之前像img的src,就有一個ng-src,如果沒這個,你設置在上面的表達式就會被當成真的url,先去加載一次,顯然是不對的。所以,設置在ng-src上,它等表達式解析出結果了,再把結果設置到src去。如果是用Web Components擴展的自定義組件,它不知道你有哪些屬性,就搞不下去了。

所以,Angular 2.0團隊在這一塊還很糾結,需要很多探索,很多權衡才能找到一種能接受的方式。

后來在這一塊,我跟@RubyLouvre 探討了一下,他的觀點是不要讓數據綁定接觸到Web Components,也就是說,不讓掃描進入“暗世界”,我想了想,覺得也有道理,只是這樣Web Components跟原生元素就要區別對待了。

或者對所有Web Components使用同一個殼子再次封裝?感覺還是很怪。

observe

當前版本的Angular使用臟檢測的方式來實現數據的關聯更新,這種機制有一定優點,但缺點也非常明顯。

在其他語言中要監控數據的變更,很多在語言層面上有get和set,一般都是從這個角度入手,有不少JavaScript框架也是從這個方面做下去的,Angular不是。

Angular的臟檢測很有特色,它采用的是新舊值比對的方式,也就是說,對每個可變動的模型,保存上一次的值,然后通過手動,或者是封裝事件調用檢測,一遍又一遍地刷新模型,直到穩定,或者超出容忍限度。

為什么這里面會有不穩定現象呢?我舉個簡單的例子,這是偽代碼,僅供演示:

function Entity() {
    //初始化
    this.a = 1;
    this.b = 1;
    this.c = 1;

//監控語句,偽代碼
this.b = this.a + 1;
this.c = this.b + 1;

}</pre>

這里面幾條語句不是真的賦值,是用來表示:每當a變化了,b就跟著變,然后c也跟著變,那我們在這里就要創建兩個監控,一個是對a的監控,在里面給b賦值,一個是對b的監控,在里面給c賦值。

好了,比如有人給a賦了個新值,我們一個臟檢測循環下來,b增加了1,c也跟著增加了,好像沒什么問題。那我們怎么知道整個模型穩定了呢?很簡單也很無奈,再運行臟檢測一次,這次a沒變,所以另外兩個也不變了,跟上一次檢測之后的結果一樣,所以就認為它穩定了。

這里我們看到,不管你怎樣,只要變過數據,至少要跑兩次臟檢測。為什么說至少呢,因為我們這種情況剛好把坑給繞過了,來改下代碼:

function Entity() {
    //初始化
    this.a = 1;
    this.b = 1;
    this.c = 1;

//監控語句,偽代碼
this.c = this.b + 1;
this.b = this.a + 1;

}</pre>

沒改什么,只是把兩條監控語句互換了,這個結果就不對了。為什么呢,比如a賦值為1之后,第一遍結果是這樣的:

  • a = 1;
  • c = 2;
  • b = 2;

這里討厭的是c的監控語句先執行了,但b還沒有變,可是我們當時是不知道的。然后,我們想看看模型穩定了沒有,就再檢測一次。所謂的檢測,其實是兩個步驟:把所有監控語句跑一遍,對比本次結果與上次的差異。

那么,這次變成了:

  • a = 1;
  • c = 3;
  • b = 2;

第二輪結束。模型穩定了嗎?其實已經穩定了,但是代碼是不知道的,它判斷穩定的依據是,本次結果與上次相同,可事實是不同的,所以它還得繼續跑。

第三次跑完,終于跟第二次結果一樣了,于是他認為模型穩定了,開始把真正的值拿出去用了。

所以這個過程的效率在很多種情況下偏低,但好在他這個變更不是實時的,而是通過某些東西批量觸發,所以也還湊合。另外有些框架,是每次對數據賦值了就去立刻更新關聯值,這當數據結構比較復雜的時候,這樣比較高效。

Object.observe與之相比,在定義監控的時候比較直觀一些,而且,基于set get的綁定框架,有些會在原始數據的原型上定義一些“私有”方法,相比來說,observe這種方式從數據的外部視角來處理變更,更合理一些。

Angular 2.0的數據綁定機制應該會使用observe重寫,可以期待這個方面有較大的提升。

但不管什么綁定方式,都是有坑的。我知道讀者中有不少壞人,你們看到這里肯定想到很多壞主意了,比如剛才的臟檢測,有沒有辦法把這個過程搞死?很容易,我幫你寫個簡單的:

function Entity() {
    //初始化
    this.a = 1;
    this.b = 1;

//監控語句,偽代碼
this.a = this.b + 1;
this.b = this.a + 1;

}</pre>

這個代碼死循環了,形成了監控閉環。所以,在Angular里面發現循環到一定量的時候,就會覺得它停不下來,終止這個循環。在其他技術實現的綁定框架中,同樣要解決此類問題,所以監控到變更的時候,也不是直接拿去應用。

promise

很奇怪啊,我一直喜歡promise這種編寫異步代碼的方式。可能我對它的喜好來自一些背景,比如說,做可視化組件編程。這個可視化的意思是指通過拖拽配置,配置邏輯流程(注意,不是拖UI)。

比如說,流程細到方法的粒度,每個步驟映射到一個方法,然后拖拽這些步驟,配置出執行流程。這里面有個麻煩就是異步,比如說,某個方法異步了,那就麻煩了,因為在一個純拖動的配置系統中,如果你還要讓他手工調整什么東西甚至改代碼的話,這個事情基本就白做了。

所以你看,promise在這里優勢很大。每一個有異步傾向的方法,我都讓它返回promise,甚至為了一致性,不異步的方法也這么干,每個方法的入參出參都是map,讓promise帶著,是不是就很好了?

以上是我個人見解,可忽略,謝謝。

那么,在Angular 2.0中promise有什么影響呢?

回顧Angular 1.x版本,在其中已經可以看到很多promise的身影,只是那時候用了$q,一個小型的promise實現。在2.0中,promise的使用將更加廣泛,因為更多的東西是異步的了,比如新的路由系統。

promise本身是很容易被降級的,在原生不支持它的瀏覽器中也很容易搞出一個polyfill來。

這個事情在我個人看來是很喜聞樂見的。

Angular 2.0除了作出符合標準的改進,還有一些提升的方面:

依賴注入

Angular大量使用了依賴注入。在JavaScript里面怎樣做依賴注入呢?比如這段代碼:

function foo(moduleA, moduleB) {
    moduleA.aaa(moduleB);
}

a跟b這兩個模塊都要注入進來。對于依賴注入系統而言,首先要知道注入什么,比如這里,至少要先知道a和b是什么,怎么知道呢?很多框架都用一種方式,就是先把foo這個待注入函數toString,這就取得了函數定義的文本,然后使用正則表達式提取參數名。

這個辦法可行,但不可靠,它害怕壓縮。隨便什么壓縮工具,肯定認為形參名是沒用的,隨手就改成a或者b了,這樣你連正確的模塊名都找不到了。那怎么辦呢,只能老土一些:

foo.$inject = ["moduleA", "moduleB"];

這樣總可以了吧?

這樣寫起來還是有些折騰,而且運行時的數據也不夠完全,所以Angular 2.0很激進地引入了一種類似TypeScript的語言叫AtScript,支持類型和注解,比如它的這個例子:

import {Component} from 'angular';
import {Server} from './server';

@Component({selector: 'foo'}) export class MyComponent { constructor(server:Server) { this.server = server; } }</pre>

一些配置信息就可以搞在注解里,類型信息也就豐富了,然后這代碼編譯成ES6或者5,多么美好。更美好的是,2.0借助這種語言,可能把原來的指令、控制器之類的東西統一成組件,使用普通ES6 class加注解的方式來編寫它們的代碼,消除原來那么多復雜冗余的概念。

其實還有很多改進點,比如路由等等,沒法一一列出了,感興趣的可以查閱Angular 2.0已經流出的文檔,或者查閱它的github庫。

小結

Angular 2.0這次的規劃真是脫胎換骨,看了介紹文檔,簡直太喜歡了,之前我考慮過的所有問題都得到了解決。這一次版本跟之前有太大變化,從舊版本遷移可能是個難題,不過相對它所帶來的改進,這代價還是值得的。勇于革自己的命,總比被別人革命好,期待Angular的浴火重生!

</div>

 本文由用戶 RogJolley 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!