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