浴火重生的Angular
來自: 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>