說起立即執行函式(IIFE,Immediately-invoked function expression)大家應該都不陌生,在 JavaScript 中可以宣告一個函式然後立即執行它:
1 2 3 |
(function(){/* 函式體 */})() !function(){/* 函式體 */}() |
IIFE 通常用於實現私有變數、實現獨立模組等等地方,比如喜聞樂見的 jQuery 最頂層的結構就是這樣的:
1 2 3 4 5 |
(function(global, factory) { //...... })(typeof window !== "undefined" ? window : this, function(window, noGlobal) { //...... }) |
但我們今天要說的不是 IIFE 怎麼用,而是關於它針對 JS 引擎的一處效能優化。
先從一個小問題說起吧,想實現一個立即執行的函式,我們有很多種寫法,比如下面這兩種:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 方法一,傳入一個匿名函式 function run(f){ return f(); } run(function(){ //...... }); // 方法二,使用IIFE (function(){ //...... })() |
是的,這兩種寫法完全是等價的,無論怎麼看都不會有什麼區別,但是在一些 JavaScript 引擎中,它們其實效能相差甚遠。
我使用 Node 分別對兩種情況執行了十萬次,方法一的執行時間平均在360毫秒左右,方法二平均是 50 毫秒左右,效能相差7倍還多。
為什麼使用 IIFE 之後效能會提升那麼多呢?這就要從 JS 引擎(比如V8、SpiderMonkey)對於函式的優化上說起了。
現在的 JS 引擎都是十分聰明的,它們在真正執行程式碼之前會對程式碼中函式宣告做一遍 pre-parse(預解析),為啥要做 pre-parse 呢?因為實際情況中大多數的函式都不是立即被使用的(甚至完全沒被呼叫過),不需要對它們做一次完整的解析,只需要做一次效能開銷更小的 pre-parse(比如檢查一下語法錯誤),等函式真正被呼叫時,再進行完整的 full-parse。
1 2 3 4 5 6 7 |
// 下面的函式會先進行pre-parse function foo() { //...... } // 2秒之後函式被執行,又會進行一遍full-parse setTimeout(foo, 2000); |
但是有個問題!對於立即執行函式這種奇葩來說,它不適用於上面的規則,應該直接進行 full-parse。現在的大多數引擎也完全考慮到了這一點:
1 2 3 4 |
// 只會進行一次full-parse (function() { //...... })(); |
但是還有個問題!!現在的大多數引擎檢測 IIFE 的時候都不完全,大部分都是通過判別函式宣告前有沒有類似『 ( 』或者『 ! 』這樣的字元來實現的,比如下面這種情況就被忽略掉了:
1 2 3 4 5 6 7 |
// 這裡要進行pre-parse和full-parse,而前者是多餘的 function run(f){ return f(); } run(function(){ //...... }) |
所以我們可以通過一個小 trick 來優化這裡的效能(加了一對括號),這樣引擎就會把這裡識別為立即執行函式,然後只做一次 full-parse:
1 2 3 4 5 6 7 |
// 只進行一次full-parse function run(f){ return f(); } run((function(){ //...... })); |
所以針對這個問題,有一個專門的小工具來解決:
還有一個相關的討論:
Turn off negate_iife by default as it hurts V8 performance. · Issue #886 · mishoo/UglifyJS2
這個看似不起眼的 trick 實際對於效能有很顯著的提升:
這個問題本質上來講是 JS 引擎對於立即執行函式的識別有遺漏導致的,比如在 Safari 10 中這個問題基本不會發生,而 Chrome 的 V8 中就經常出現。不過感覺隨著引擎版本的迭代,這個問題應該會得到修復。