這篇文章主要講解了“Angular優(yōu)化的方法步驟”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Angular優(yōu)化的方法步驟”吧!
成都創(chuàng)新互聯(lián)公司一直秉承“誠信做人,踏實做事”的原則,不欺瞞客戶,是我們最起碼的底線! 以服務為基礎,以質量求生存,以技術求發(fā)展,成交一個客戶多一個朋友!為您提供成都做網站、成都網站建設、成都網頁設計、小程序設計、成都網站開發(fā)、成都網站制作、成都軟件開發(fā)、APP應用開發(fā)是成都本地專業(yè)的網站建設和網站設計公司,等你一起來見證!
變更檢測機制
不同于網絡傳輸優(yōu)化,運行時優(yōu)化更加關注于 Angular 的運行機制以及如何編碼才能有效地避免性能問題(最佳實踐)。而要弄明白 Angular 的運行機制,首先需要理解它的變更檢測機制(也被稱為臟檢查)——如何將狀態(tài)的變更重新渲染到視圖之中。而如何將組件狀態(tài)的變化反應到視圖中,也是前端三大框架都需要解決的一個問題。不同框架的解決方案既有類似的思路也有各自的特色。
首先,Vue 和 React 都是采用虛擬 DOM 來實現視圖更新,不過具體實現上還是有所區(qū)別:
對于 React:
通過使用 setState
或 forceUpdate
來觸發(fā) render
方法更新視圖
父組件更新視圖時,也會判斷是否需要 re-render
子組件
對于 Vue:
Vue 會遍歷 data
對象的所有屬性,并使用 Object.defineProperty
把這些屬性全部轉為經過包裝的 getter
和 setter
每個組件實例都有相應的 watcher
實例對象,它會在組件渲染的過程中把屬性記錄為依賴
當依賴項的 setter
被調用時,會通知 watcher
重新計算,從而使它關聯(lián)的組件得以更新
而 Angular 則是通過引入 Zone.js 對異步操作的 API 打補丁,監(jiān)聽其觸發(fā)來進行變更檢測。關于 Zone.js 的原理在之前的一篇文章中有詳細的介紹。簡單來說,Zone.js 通過 Monkey patch (猴補?。┑姆绞剑┝Φ貙g覽器或 Node 中的所有異步 API 進行了封裝替換。
比如瀏覽器中的 setTimeout
:
let originalSetTimeout = window.setTimeout; window.setTimeout = function(callback, delay) { return originalSetTimeout(Zone.current.wrap(callback), delay); } Zone.prototype.wrap = function(callback) { // 獲取當前的 Zone let capturedZone = this; return function() { return capturedZone.runGuarded(callback, this, arguments); }; };
或者 Promise.then
方法:
let originalPromiseThen = Promise.prototype.then; // NOTE: 這里做了簡化,實際上 then 可以接受更多參數 Promise.prototype.then = function(callback) { // 獲取當前的 Zone let capturedZone = Zone.current; function wrappedCallback() { return capturedZone.run(callback, this, arguments); }; // 觸發(fā)原來的回調在 capturedZone 中 return originalPromiseThen.call(this, [wrappedCallback]); };
Zone.js 在加載時,對所有異步接口進行了封裝。因此所有在 Zone.js 中執(zhí)行的異步方法都會被當做為一個 Task 被其統(tǒng)一監(jiān)管,并且提供了相應的鉤子函數(hooks),用來在異步任務執(zhí)行前后或某個階段做一些額外的操作。因此通過 Zone.js 可以很方便地實現記錄日志、監(jiān)控性能、控制異步回調執(zhí)行的時機等功能。
而這些鉤子函數(hooks),可以通過Zone.fork()
方法來進行設置,具體可以參考如下配置:
Zone.current.fork(zoneSpec) // zoneSpec 的類型是 ZoneSpec // 只有 name 是必選項,其他可選 interface ZoneSpec { name: string; // zone 的名稱,一般用于調試 Zones 時使用 properties?: { [key: string]: any; } ; // zone 可以附加的一些數據,通過 Zone.get('key') 可以獲取 onFork: Function; // 當 zone 被 forked,觸發(fā)該函數 onIntercept?: Function; // 對所有回調進行攔截 onInvoke?: Function; // 當回調被調用時,觸發(fā)該函數 onHandleError?: Function; // 對異常進行統(tǒng)一處理 onScheduleTask?: Function; // 當任務進行調度時,觸發(fā)該函數 onInvokeTask?: Function; // 當觸發(fā)任務執(zhí)行時,觸發(fā)該函數 onCancelTask?: Function; // 當任務被取消時,觸發(fā)該函數 onHasTask?: Function; // 通知任務隊列的狀態(tài)改變 }
舉一個onInvoke
的簡單列子:
let logZone = Zone.current.fork({ name: 'logZone', onInvoke: function(parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) { console.log(targetZone.name, 'enter'); parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source) console.log(targetZone.name, 'leave'); } }); logZone.run(function myApp() { console.log(Zone.current.name, 'queue promise'); Promise.resolve('OK').then((value) => {console.log(Zone.current.name, 'Promise', value) }); });
最終執(zhí)行結果:
理解了 Zone.js 的原理之后,通過走讀 Angular 的源碼,可以知道 Zone.js 在 Angular 被用來實現只要有異步方法或事件的調用,就會觸發(fā)變更檢測。大體如下:
首先,在 applicatoin_ref.ts 文件中,當 ApplicationRef
構建時就訂閱了微任務隊列為空的回調事件,其調用了 tick
方法(即變更檢測):
其次,在 checkStable 方法中,會判斷當微任務隊列清空時觸發(fā) onMicrotaskEmpty
事件(結合上來看,等價于會觸發(fā)變更檢測):
最后,能夠觸發(fā) checkStable 方法的調用的地方分別在 Zone.js 的三個鉤子函數中,分別是 onInvoke
、 onInvokeTask
和 onHasTask
:
比如 onHasTask
—— 檢測到有或無 ZoneTask
時觸發(fā)的鉤子:
另外 Zone.js 中對于異步任務總共分為三類:
Micro Task(微任務):由 Promise
等創(chuàng)建, native
的 Promise
是在當前事件循環(huán)結束前就要執(zhí)行的,而打過補丁的 Promise
也會在事件循環(huán)結束前執(zhí)行。
Macro Task (宏任務):由 setTimeout
等創(chuàng)建,native
的 setTimeout
會在將來某個時間被處理。
Event Task :由 addEventListener
等創(chuàng)建,這些 task
可能被觸發(fā)多次,也可能一直不會被觸發(fā)。
其實如果站在瀏覽器的角度, Event Task其實可以看做是宏任務,換句話說,所有事件或異步 API 都可以理解成是宏任務或微任務中的一種,而它們的執(zhí)行順序在之前的一篇文章中有詳細分析,簡單來說:
(1)主線程執(zhí)行完后,會優(yōu)先檢查微任務隊列是否還有任務需要執(zhí)行
(2)第一次輪詢結束后,會檢查宏任務隊列是否還有任務執(zhí)行,執(zhí)行完之后檢查微任務列表是否還有任務執(zhí)行,之后將重復這個過程
性能優(yōu)化原理
頁面性能的好壞,最直觀的判斷是看頁面響應是否流暢、是否響應得快。而頁面響應其本質上就是把頁面狀態(tài)的變更重新渲染到頁面上的過程,站在相對宏觀的視角來看, Angular 的變更檢測其實只是整個事件響應周期中的一環(huán)。用戶與頁面的所有交互都是通過事件來觸發(fā),其整個響應過程大致如下:
如果考慮優(yōu)化頁面響應的速度,可以從各個階段入手:
(1)對于觸發(fā)事件階段,可以減少事件的觸發(fā),來減少整體的變更檢測次數和重新渲染
(2)對于 Event Handler 執(zhí)行邏輯階段,可以通過優(yōu)化復雜代碼邏輯來減少執(zhí)行時間
(3)對于 Change Detection 檢測數據綁定并更新 DOM 階段,可以減少變更檢測和模板數據的計算次數來減少渲染時間
(4)對于瀏覽器渲染階段,則可能需要考慮使用不同瀏覽器或從硬件配置上進行提升
對于第二、四階段的相關優(yōu)化這里不做過多討論,結合上面提到的 Angular 對于異步任務的分類,針對第一、三階段的優(yōu)化方式可以進一步明確:
(1)針對 Macro task 合并請求,盡量減少 tick 的次數
(2)針對 Micro task 合并 tick
(3)針對 Event task 減少 event 的觸發(fā)和注冊事件
(4)tick 分為 check 和 render 兩個階段,減少 check 階段的計算以及不必要的渲染
前面有提到,大多數情況通過觀察頁面是否流暢可以判斷頁面的是否存在性能問題。雖然這種方式簡單、直觀,但也相對主觀,并非是通過精確的數字反映頁面的性能到底如何。換言之,我們需要用一個更加有效、精確的指標來衡量什么樣的頁面才是具備良好性能的。而 Angular 官方也提供了相應的方案,可以通過開啟 Angular 的調試工具,來實現對變更檢測循環(huán)(完成的 tick
)的時長監(jiān)控。
首先,需要使用 Angular 提供的 enableDebugTools
方法,如下:
之后只需要在瀏覽器的控制臺中輸入 ng.profiler.timeChangeDetection()
,即可看到當前頁面的平均變更檢測時間:
從上面可以看出,執(zhí)行了 692 次變更檢測循環(huán)(完整的事件響應周期)的平均時間為 0.72 毫秒。如果多運行幾次,你會發(fā)現每次運行的總次數是不一樣、隨機的。
官方提供了這樣一個判斷標準:理想情況下,分析器打印出的時長(單次變更檢測循環(huán)的時間)應該遠低于單個動畫幀的時間(16 毫秒)。一般這個時長保持在 3 毫秒下,則說明當前頁面的變更檢測循環(huán)的性能是比較好的。如果超過了這個時長,則就可以結合 Angular 的變更檢測機制分析一下是否存在重復的模板計算和變更檢測。
性能優(yōu)化方案
在理解 Angular 優(yōu)化原理的基礎上,我們就可以更有針對性地去進行相應的性能優(yōu)化:
(1)針對異步任務 ——減少變更檢測的次數
使用 NgZone 的 runOutsideAngular 方法執(zhí)行異步接口
手動觸發(fā) Angular 的變更檢測
(2)針對 Event Task —— 減少變更檢測的次數
將 input 之類的事件換成觸發(fā)頻率更低的事件
對 input valueChanges 事件做的防抖動處理,并不能減少變更檢測的次數
如上圖,防抖動處理只是保證了代碼邏輯不會重復運行,但是 valueChanges 的事件卻隨著 value 的改變而觸發(fā)(改變幾次,就觸發(fā)幾次),而只要有事件觸發(fā)就會相應觸發(fā)變更檢測。
(3)使用 Pipe ——減少變更檢測中的計算次數
將 pipe 定義為 pure pipe(@Pipe
默認是 pure pipe,因此也可以不用顯示地設置 pure: true
)
import { Piep, PipeTransform } from '@angular/core'; @Pipe({ name: 'gender', pure, }) export class GenderPiep implements PipeTransform { transform(value: string): string { if (value === 'M') return '男'; if (value === 'W') return '女'; return ''; } }
關于 Pure/ImPure Pipe:
Pure Pipe:如果傳入 Pipe 的參數沒有改變,則會直接返回之前一次的計算結果
ImPure Pipe:每一次變更檢測都會重新運行 Pipe 內部的邏輯并返回結果。(簡單來說, ImPure Pipe 就等價于普通的 formattedFunction,如果一個頁面觸發(fā)了多次的變更檢測,那么 ImPure Pipe 的邏輯就會執(zhí)行多次)
(4)針對組件 ——減少不必要的變更檢測
組件使用 onPush 模式
只有輸入屬性發(fā)生變化時,該組件才會檢測
只有該組件或者其子組件中的 DOM 事件觸發(fā)時,才會觸發(fā)檢測
非 DOM 事件的其他異步事件,只能手動觸發(fā)檢測
聲明了 onPush 的子組件,如果輸入屬性未變化,就不會去做計算和更新
@Component({ ... changeDetection: ChangeDetectionStrategy.OnPush, }) export class XXXComponent { .... }
在 Angular 中 顯示的設置 @Component
的 changeDetection
為 ChangeDetectionStrategy.OnPush
即開啟 onPush 模式(默認不開啟),用 OnPush 可以跳過某個組件或者某個父組件以及它下面所有子組件的變化檢測,如下所示:
(5)針對模板 ——減少不必要的計算和渲染
列表的循環(huán)渲染使用 trackBy
盡量使用緩存值,避免使用方法調用和 get 屬性的調用
模板中如果確實有需要調用函數的地方,且是多處調用可以使用模板緩存
ngIf 控制組件的展示,放到調用組件的地方控制
(6)其他編碼優(yōu)化建議
不要使用 try/catch 來做流程控制,其會造成很大的時間消耗(記錄大量堆棧信息等)
過多的動畫會導致頁面加載卡頓
長列表可以使用虛擬滾動
針對 preload module 盡量延遲 load, 因為瀏覽器的 http 請求線程的并發(fā)數是有限制的,一旦超過了限制數,后面的請求都會被阻塞掛起
等等
感謝各位的閱讀,以上就是“Angular優(yōu)化的方法步驟”的內容了,經過本文的學習后,相信大家對Angular優(yōu)化的方法步驟這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關知識點的文章,歡迎關注!