寫在前面:本文僅針對 AngularJS,不針對 Angular,望區分。

AngularJS 是 Google 於 2009 年發布的 Web 增強框架,一度作為 Web 開發主流方案,但是隨著 Google 宣布 AngularJS 的 final LTS 計劃,已經正式步入了過氣網紅的行列。

然而過氣並不意味著不會被繼續消費(關鍵字「使徒子 猴賽雷」),即便已經很少出現在新項目當中,仍然時常現身於各種文章中,作為對照組出現。

不過在各種與 AngularJS 的比較當中,往往作者自身並不了解 AngularJS,僅僅是為了襯托出其它產品的優勢。然而結果是對 AngularJS 的描述錯誤百出,但很少有人願意再為過氣網紅去發聲。

這裡將簡要總結一下對 AngularJS 理解中的常見誤區。(當然,也並不指望有人再去認真學習 AngularJS

常見誤區一:AngularJS 存在全局的 watchers 列表

AngularJS 採用了准 MVVM 的設計理念,承擔 ViewModel 這一職責的類型叫做 Scope,Scope 中提供的 $new 方法可以創建繼承於* 該 Scope 的 Child Scope。一個完整的應用中,會存在一個 Scope Tree,每次檢查時會從 Root Scope 出發,以深度優先搜索的方式遍歷完整的 Tree

* Isolated Scope 雖然在原型鏈上不會包含父 Scope,但在數據結構組織上仍然是 Parent Scope 的子節點。

而在每個 Scope 當中,存在一個 $$watchers 數組,用於記錄當前 Scope 中所有的綁定內容以及綁定更新時的後續操作。對於單個模版中的數據綁定,其添加順序由編譯器保證,父元素的綁定會先於子元素被添加*,同級元素間先出現的元素先被添加,因此仍然是自頂向底的有序執行

* 實現上而言是在數組中反向添加而後用 while(index--) 遍歷,對執行結果沒有影響。

總而言之,對於一個 AngularJS 應用,其綁定內容的數據結構是 Tree<Array<Watcher>>,而 Array<Watcher> 本身是依照模版相應 DOM Tree 遍歷的結果,自始自終都是一個有序的樹結構。這個數據結構的模型基本適用於任何具備精確綁定行為的 MDV 實現,即便到了今天也是如此。

由於是樹結構,因此可以通過剪枝的方式來優化性能,在 AngularJS 中通過 $suspend 及 $resume 方法實現。不過操作仍然較為繁瑣,缺乏像 Angular 的 OnPush 那樣的自動化剪枝方案。

要違背這一設定創造亂序的 Watcher,只能夠通過 $watch 方法手動添加,只要願意甚至可以應用於 Scope 之外的數據,這時候造成的數據流組織問題並不是 AngularJS 的問題,而是用戶水平問題。

常見誤區二:AngularJS 的綁定內容至少會被檢查兩次

我們已經知道,AngularJS 會以深度優先搜索遍歷所有 Scope,對於每個 Scope 遍歷其 $$watchers 數組,從而檢查所有綁定內容是否發生變化。

而在 AngularJS 的 Digest 行為中,這個過程會重複進行,直到所有 Watcher 都不再發生變化而已。因此常常有人認為所有 Watcher 中記錄的內容都至少會被檢查兩次(即便狀態在第一輪後已經穩定)。

這個結論並不正確。直接上示例:

angularjs-iovneu - StackBlitz?

stackblitz.com

這裡定義了一個計數器(基於 button),使用 getter 來記錄每個模版表達式被檢測的次數,實際情況是:

這個示例中初始狀態經過了三輪檢測穩定,並且每次的點擊均在一輪檢測中立即穩定,可以發現在點擊次數之前的模版表達式每次點擊中被檢測了兩次,點擊次數之後的模版表達式以及 Child Scope 中的模版表達式均僅被檢測一次。

這裡直接說結論:最後一輪檢測並不是完整的檢測,只檢測到上一輪中最後變化的 Watcher 為止(如果其不再變化)。

因此所有檢測順位在最後變化項之後的 Watcher 均不會被額外檢測,包括:

  • Child Scopes;
  • Incoming Sibling Scopes;
  • Incoming Watchers in Same Scope;

所以對於數據組織良好的 AngularJS 應用(一輪檢測後穩定),並不是所有綁定都會被檢測兩次。

常見誤區三:雙向綁定不是單向數據流

為了方便討論,這裡給雙向綁定做一個簡單定義:使用單一的語法結構同時定義視圖到模型的更新過程以及模型到視圖的更新過程

那麼我們先來思考一下,對於一個由 ng-model 定義的雙向綁定,需要幾輪檢測才會穩定呢?我們可以再次使用後置模版表達式,直接得到精確次數:

angularjs-itwany - StackBlitz?

stackblitz.com

實際結果如下:

對於 5 次輸入操作,只觸發了 5 次檢測(初始為 3 次),也就是說每次輸入僅有一次檢測操作。

於是問題來了,對於一個 ng-model 定義的雙向綁定,存在多少個 Watcher 呢?是不是:

  • $watch(modelValue, updateViewValue) 以及
  • $watch(viewValue, updateModelValue)

這兩個呢?

然而並不是,自始至終只存在 $watch(modelValue, updateViewValue) 這一個 Watcher。那麼 view 到 model 的更新過程又是如何實現的呢?當然就只是普通的事件監聽*,addEventListener(input, callback)

* 實際使用 jqLite 進行監聽,並且 event 可設定。

首先我們確認了雙向綁定不會引起額外的數據震蕩(除非有用戶自定義的攔截行為導致用戶操作的結果被篡改,但不論是否基於雙向綁定都會導致額外的視圖操作),那麼接下來的問題是,雙向綁定是不是就會導致雙向數據流呢?

仍然先作出定義:當同一個數據存儲位點具備兩個或更多的輸入源時,即為非單項數據流。(讀取是不會產生環路的,只關心寫入就行)

目前現在可以公開的情報有:

  • ng-model 存在對 modelValue 的 Write 行為;
  • ng-model 存在對 viewValue 的 Write 行為;

顯然,已經使用 ng-model 的情況下並不會有代碼再去修改 viewValue,那麼會有代碼修改 modelValue 嗎?有可能,但是在大部分應用中不會出現也沒有必要出現*,ng-model 的存在意義是單方面收集用戶輸入,並不是在兩個活躍數據源間進行同步

* Create 並不是 Write,僅在 View 建立之前產生作用,在能夠進行交互時已經沒有通路存在,雙向綁定中 modelValue 的默認值並不會引起環路。

當然還可能會有人說,「我壓根不關心數據的流向,只在乎好不好 Debug,我也不會用 Debugger 的 Watch 功能,就只會打斷點該怎麼辦?」

答案也很簡單,把 getter 和 setter 分離即可, AngularJS 的 ng-model 自帶了函數支持:

// <input ng-model="name" ng-model-options="{ getterSetter: true }">

class MyController {
_name =

name = (value) => {
if (value !== undefined) {
this._name = value
return
}
return this._name
}
}

當然如果只用於 ES5(IE9)以上環境也可以基於 JavaScript 自身的 Accessor 實現:

// <input ng-model="name">
class MyController {
_name =

set name(value) {
this._name = value
}

get name() {
return this._name
}
}

歸根到底,雙向綁定僅僅是顯式定義受控屬性的一個模式,所有實際數據流純粹是無關的業務實現。

常見誤區四:AngularJS 會定時檢查綁定內容

都不知道是多老的謠言了,完全沒有解釋的必要。

AngularJS 當且僅當調用 Scope#$digest 的時候才會檢查綁定,而 Scope#$apply 會調用 Scope#$digest,為此一般常常把所有非同步行為的操作都封裝成自帶 Scope#$apply 的版本,比如 $timeout$http

常見誤區五:AngularJS 不能使用 TypeScript

AngularJS 雖然不能像 Angular 一樣對模版內容進行類型檢查,但對其它還是有非常優秀的 .d.ts 支持,特別是 Injector 還有根據字元串的重載:

get<T>(name: string, caller?: string): T;
get(name: $anchorScroll): IAnchorScrollService;
get(name: $cacheFactory): ICacheFactoryService;
get(name: $compile): ICompileService;
get(name: $controller): IControllerService;
get(name: $document): IDocumentService;
get(name: $exceptionHandler): IExceptionHandlerService;
get(name: $filter): IFilterService;
get(name: $http): IHttpService;
get(name: $httpBackend): IHttpBackendService;
get(name: $httpParamSerializer): IHttpParamSerializer;
get(name: $httpParamSerializerJQLike): IHttpParamSerializer;
get(name: $interpolate): IInterpolateService;
get(name: $interval): IIntervalService;
get(name: $locale): ILocaleService;
get(name: $location): ILocationService;
get(name: $log): ILogService;
get(name: $parse): IParseService;
get(name: $q): IQService;
get(name: $rootElement): IRootElementService;
get(name: $rootScope): IRootScopeService;
get(name: $sce): ISCEService;
get(name: $sceDelegate): ISCEDelegateService;
get(name: $templateCache): ITemplateCacheService;
get(name: $templateRequest): ITemplateRequestService;
get(name: $timeout): ITimeoutService;
get(name: $window): IWindowService;
get<T>(name: $xhrFactory): IXhrFactory<T>;

常見誤區六:AngularJS 不區分調試和生產模式

官網專門 寫了一篇專題 Guide 來說明,就只需要一行調用:

myApp.config([$compileProvider, function ($compileProvider) {
$compileProvider.debugInfoEnabled(false);
}]);

不說內容,事實上很多人連目錄都不願意過一遍。

總結

AngularJS 雖然過時,但即便放在今天也並不糟糕,相反,其很多優秀的理念在今天也仍然得到沿用。技術領域中消費前人的行為雖然無法避免,但也應當儘可能的尊重事實,對不了解的內容應當自行確認,而不是盲目傳謠。

推薦閱讀:

相关文章