Prepack 介紹(譯)

luobotang發表於2018-12-25

原文:A Gentle Introduction to Prepack (Part 1) 內容更新至:2018-12-24


注意:

計劃在當前指南更完善後,將其引入 Prepack 文件中。 目前我以 gist 方式釋出,以便收集反饋。

Prepack 介紹(第一部分)

如果你在開發 JavaScript 應用,那麼對如下這些將 JavaScript 程式碼轉為等價程式碼的工具應該比較熟悉:

  • Babel 讓你能夠使用更新的 JavaScript 語言特性,輸出相容老的 JavaScript 引擎的等價程式碼。

  • Uglify 讓你能夠編寫可讀的 JavaScript 程式碼,輸出完成相同功能但是位元組數更少的混淆程式碼。

Prepack 是另一個致力於將 JavaScript 程式碼編譯為等價程式碼的工具。但與 Babel 或 Uglify 不同的是,Prepack 的目標不是新特性或程式碼體積。

Prepack 讓你編寫普通的 JavaScript 程式碼,然後輸出執行地更快的等價程式碼。

如果這聽起來讓人興奮,那麼接下來你會了解到 Prepack 是如何工作的,以及你可以怎樣讓它做得更好。

這個指南有什麼?

就我個人而言,當我最終理解 Prepack 能做什麼時,我非常興奮。我認為在未來,Prepack 會解決目前我在開發大型 JavaScript 應用時遇到的很多問題。我很想傳播這一點,讓其他人也興奮起來。

不過,向 Prepack 貢獻力量在一開始會讓人害怕。它的原始碼裡有很多我不熟悉的術語,我花了很長時間才明白 Prepack 做了什麼。編譯器相關程式碼傾向於使用確定的電腦科學術語,但這些術語讓它們聽起來比實際情況要複雜。

我編寫這個指南,就是為了那些沒有電腦科學背景,但對 Prepack 的目標感興趣,並且希望幫助它實現的 JavaScript 開發者。

本指南就 Prepack 如何工作提供了高度的概括,給你參與的起點。Prepack 中的很多概念直接對應到那些你日常使用的 JavaScript 程式碼工具:物件、屬性、條件和迴圈。即使你還不能在專案中使用 Prepack,你也會發現,在 Prepack 上的工作,有助於增強你對每天編寫的 JavaScript 程式碼的理解。

在我們深入之前 ?

注意,Prepack “還沒有為主流做好準備”。你還不能把它像 Babel 或 Uglify 那樣嵌入到構建系統中,並期望它能正常工作。相反,你得把 Prepack 視作你可以參與的正在進行中且有雄心壯志的試驗,並且在未來它會對你有用。由於其目標很廣,所以有很多機會可以參與進來。

不過,這並不意外著 Prepack 不能工作。但由於其目前只關注於特定的一些場景,而且在生產環境中很可能會有讓人不能接受的過多 bug。好訊息是你可以幫助 Prepack 支援更多用例,以及修復 bug。這個指南會幫助你開始。

Prepack 基礎

讓我們重新審視上面提到的 Prepack 的目標:

Prepack 讓你編寫普通的 JavaScript 程式碼,輸出等價但執行更快的 JavaScript 程式碼。

為什麼我們不直接編寫更快的程式碼呢?我們可以嘗試,如果可以的話也的確應該。但是,在很多應用中,撇開由效能工具識別出的瓶頸,其實並沒有很多明顯可以優化的地方。

通常並沒有單獨一處導致程式變慢;相反,程式忍受的是“千刀萬剮”。那些提升關注分離的特性,例如函式呼叫、分配物件和各種抽象,在執行時吃掉了效能。然而,在原始碼中移除這些會導致難以維護,而且也並沒有我們可以應用的容易的優化方式。甚至 JavaScript 引擎在多年的優化工作中也有所限制,特別是在初始化只執行一次的程式碼上。

最明確的提升效能的方式,是少做一些事情。Prepack 根據這個理念引出其邏輯結論:它 在構建階段 執行程式以瞭解程式碼 將要 做什麼,然後生成等價的程式碼,但是減少了計算量。

這聽起來太奇幻,所以我們來看一些例子,瞭解 Prepack 的優勢和限制。我們會使用 Prepack REPL 來線上對一段程式碼應用 Prepack。

計算 2 + 2 的兩種方式

讓我們先開啟 這個例子

(function() {
  var x = 2;
  var y = 2;
  global.answer = x + y;
})();
複製程式碼

輸出為:

answer = 4;
複製程式碼

實際上,執行兩個程式碼片段產生相同的效果:值 4 被賦值到名為 answer 的全域性變數上。不過 Prepack 的版本並沒有包含 2 + 2 的計算。不同的是,Prepack 在編譯階段執行 2 + 2,並將最終的賦值操作進行了 “序列化(serialize)”(“寫入”或“生成”的一種花哨的說法)。

這並沒有特別厲害:例如,Google Closure Compiler 也能將 2 + 2 變為 4。這種優化被稱作 “常量摺疊(constant folding)”。Prepack 的不同在於,它能執行任意 JavaScript 程式碼,不僅僅是常量摺疊或類似的有限優化。 Prepack 也有其自身的限制,我們一會再說。

考慮如下這種有意編寫的超級繞的計算 2 + 2 的情況:

(function() {
  function getNumberCalculatorFactory(injectedServices) {
    return {
      create() {
        return {
          calculate() {
            return injectedServices.operatorProvider.operate(
              injectedServices.xProvider.provideNumber(),
              injectedServices.yProvider.provideNumber()
            )
          }
        };
      }
    }
  }
  
  function getNumberProviderService(number) {
    return { provideNumber() { return number; } };
  }

  function createPlusOperatorProviderService() {
    return { operate(x, y) { return x + y; } };
  }  
  
  var numberCalculatorFactory = getNumberCalculatorFactory({
    xProvider: getNumberProviderService(2),
    yProvider: getNumberProviderService(2),
    operatorProvider: createPlusOperatorProviderService(),
  });

  var numberCalculator = numberCalculatorFactory.create();
  global.answer = numberCalculator.calculate();
})();
複製程式碼

儘量我們並不推薦以這種方式來計算兩個數值的和,不過你會看到 Prepack 輸出了相同的結果

answer = 4;
複製程式碼

在兩個例子中,Prepack 在構建階段 執行 程式碼,計算出環境中的 “結果”(修改),然後**“序列化”**(寫)得到實現相同效果但執行時負擔最小的程式碼。

對於任何其他通過 Prepack 執行的程式碼,抽象來看都是如此。

邊注:Prepack 是如何執行我的程式碼的?

在構建階段“執行”程式碼聽起來很可怕。你不希望 Prepack 因為執行了包含 fs.unlink() 呼叫的程式碼,就將檔案系統中的檔案刪除。

我們要明確 Prepack 並非只是在 Node 環境中 eval 輸入的程式碼。Prepack 包含一個完整的 JavaScript 直譯器的實現,所以可以在“空的”獨立環境中執行任意程式碼。預設地,它並不支援像 Node 的 require()module,或者瀏覽器的 document。我們後面會再提到這些限制。

這並不是說,在“宿主(host)” Node 環境和 Prepack JS 環境之間搭建橋樑是不能的。事實上這在未來會是一個值得探索的有趣的觀點。或許你會是參與者之一?

森林中倒下的一棵樹

你可能聽過這個哲學問題:

如果森林中倒下一棵樹而周圍的人都沒有聽到,那麼它有聲音嗎?

這其實與 Prepack 能做什麼和不能做什麼直接相關。

考慮 第一個例子的簡單變種

var x = 2;
var y = 2;
global.answer = x + y;
複製程式碼

輸出中,很奇怪地,也包含 xy 的定義:

var y, x;
x = 2; // 為什麼這個也會序列化?
y = 2; // 為什麼這個也會序列化?
answer = 4;
複製程式碼

這是由於 Prepack 將輸入程式碼視為指令碼(script),而非模組(module)。一個在函式外部的 var 宣告 變成了全域性變數,所以從 Prepack 的角度來看,好像是我們有意向全域性環境宣告瞭它們:

var x = 2; // 等同:global.x = 2;
var y = 2; // 等同:global.y = 2;
global.answer = x + y;
複製程式碼

這也是為什麼 Prepack 將 xy 保留在輸出中。別忘了 Prepack 目標是產生等價的程式碼,也包括 JavaScript 的陷阱。

最容易的避免這個錯誤的方法是 始終將提供給 Prepack 的程式碼包裹在 IIFE 中,並且明確地將結果以全域性變數記錄

(function() { // 建立函式作用域
  var x = 2; // 不再是全域性變數
  var y = 2; // 不再是全域性變數
  global.answer = x + y;
})(); // 別忘了呼叫!
複製程式碼

產生了預期的輸出

answer = 4;
複製程式碼

這是 另一個容易讓人糊塗的例子

(function() {
  var x = 2;
  var y = 2;
  var answer = 2 + 2;
})();
複製程式碼

Prepack REPL 輸出了有用的警告:

// Your code was all dead code and thus eliminated.
// Try storing a property on the global object.
複製程式碼

這裡,另一個問題出現了:儘管我們執行了計算,但沒有任何效果作用於環境。 如果有其他指令碼隨後執行,它並不能判斷我們的程式碼是否執行過。所以不必序列化任何值。

再一次,為了修復這個問題,我們要將 需要 保留的東西以追加到全域性物件的方式標記,讓 Prepack 忽略其他:

(function() {
  var x = 2; // Prepack 會丟棄這個變數
  var y = 2; // Prepack 會丟棄這個變數
  global.answer = 2 + 2; // 但這個值會被序列化
})();
複製程式碼

概念上,這可能讓你想起 垃圾回收:對於全域性物件“可觸達”的物件,需要“保持活躍”(或者,在 Prepack 中,被序列化)。除了設定全域性屬性外,還有其他的“結果”是 Prepack 支援的,我們後面再講。

殘留堆(Residual Heap)

現在我們可以粗略地描述 Prepack 是如何工作的了。

在 Prepack 解釋執行輸入程式碼時,它構造了程式使用的所有物件的內部表示。對於每一個 JavaScript 值(如物件、函式、數值),都有內部的 Prepack 物件記錄其相關資訊。Prepack 程式碼中有這樣的 class:ObjectValueFunctionValueNumberValue,甚至 UndefinedValueNullValue

Prepack 也會跟蹤所有輸入程式碼對環境產生的“效果”(例如寫入全域性變數)。為了在結果程式碼中反映這些效果,Prepack 在程式碼執行結束後查詢所有仍能通過全域性物件觸及到的值。在上面例子中,global.answer 被視為“可觸及的”,因為不同於區域性變數 xy,外部程式碼未來可以讀取 global.answer。這也是為什麼從輸出中忽略 global.answer 不安全,但忽略 xy 是安全的。

所有全域性物件可觸及的值(這些可能影響後續執行程式碼)被收集到“殘留堆”。這名字聽起來比實際上覆雜多了。“殘留堆”是“堆”(執行程式碼建立的所有物件)在程式碼完成執行後保持“殘留”(例如,在輸出中保留)的一部分。如果丟掉電腦科學的帽子,我們可以稱之為“剩下的東西”。

序列化器(Serializer)

Prepack 是如何產生輸出的程式碼呢?

在 Prepack 在殘留堆上標記所有的“可觸及”的值後,它執行一個 序列化器。序列化器的任務是解決如何將 Prepack 殘留堆上的 JavaScript 的物件、函式和其他值的物件表示,轉為輸出程式碼。

如果你對 JSON.stringify() 比較熟悉,從概念上你可以認為 Prepack 序列化器做了類似的事情。不過,JSON.stringify() 可以避免像物件間的迴圈引用這樣的複雜情況:

var a = {};
var b = {};
a.b = b;
b.a = a;
var x = {a, b};
JSON.stringify(x); // Uncaught TypeError: Converting circular structure to JSON
複製程式碼

JavaScript 程式經常有物件間的迴圈引用,所以 Prepack 序列化器需要支援這樣的情況,並且生成等價的程式碼以重建這些物件。所以 對於這樣的輸入

(function() {
  var a = {};
  var b = {};
  a.b = b;
  b.a = a;
  global.x = {a, b};
})();
複製程式碼

Prepack 生成像這樣的程式碼:

(function () {
  var _2 = { // <-- b
    a: void 0
  };
  var _1 = { // <-- a
    b: _2
  };
  _2.a = _1;
  x = {
    a: _1,
    b: _2
  };
})();
複製程式碼

注意賦值順序是不同的(輸入程式碼先構造 a,但是輸出程式碼從 b 開始)。這是因為這個場景下賦值順序並不重要。同時,這也展示了 Prepack 執行的核心理念:

Prepack 並不轉換輸入程式碼。它執行輸入程式碼,找到殘留堆上的所有值,然後序列化這些值和使用到的效果到輸出的 JavaScript 程式碼中。

邊注:把東西放到全域性物件上好嗎?

上面的例子你可能會疑問:把值放到全域性不是不好的方式嗎?但這是指在生產環境中的程式碼,而如果你在生產環境使用還不能用於生產的試驗性的 JavaScript 抽象直譯器,那才是更大的問題。

對於在類 CommonJS 的環境中通過 module.exports 執行 Prepack 已有部分支援,但現在還很原始(而且也是通過全域性物件實現)。不過,這不重要,因為並沒有從根本上改變程式碼的執行,只有當 Prepack 要和其他工具整合時才有壓力。

殘留函式

假設我們要向程式碼新增一些封裝,將 2 + 2 的計算放到到一個函式中:

(function () {
  global.getAnswer = function() {
    var x = 2;
    var y = 2;
    return x + y;
  };
})();
複製程式碼

如果你 嘗試對此進行編譯,你可能會驚訝於如下的結果:

(function () {
  var _0 = function () {
    var x = 2;
    var y = 2;
    return x + y;
  };

  getAnswer = _0;
})();
複製程式碼

看起來好像 Prepack 並沒有優化我們的計算!為什麼會這樣?

預設情況下,Prepack 只優化“初始化路徑”(立即執行的程式碼)。

從 Prepack 的角度來看,Prepack 執行了所有語句後程式已經結束。程式的效果以全域性變數 getAnswer 對應的函式所記錄。工作已經結束。

如果我們在退出程式前呼叫 getAnswer(),Prepack 會執行它。getAnswer() 的實現是否存在於輸出,取決於函式本身對於全域性物件是否“可觸及”(所以忽略它會不安全)。生成到輸出中的函式,被稱為“殘留函式”(它們是在輸出中“殘留的”,或者剩下的)。

預設情況下,Prepack 會嘗試執行或優化殘留函式。這通常是不安全的。在殘留函式被外部程式碼呼叫的時候,JavaScript 執行時全域性物件如 Object.prototype,以及由輸入程式碼建立的物件都可能會被修改,這超出了 Prepack 的感知範圍。這時 Prepack 可能要使用殘留堆中的舊值,再與原始程式碼中的行為進行比對,或者始終假設任何東西都會修改,這都讓優化變得過於困難。哪種方案都不會讓人滿意,所以殘留函式保持原樣。

不過有個試驗模式,可以讓你選擇優化特定函式,這個後面會提到。

速度 vs. 體積開銷

考慮這個例子:

(function () {
  var x = 2;
  var y = 2;

  function getAnswer() {
    return x + y;
  };
  
  global.getAnswer = getAnswer;
})();

複製程式碼

Prepack 生成如下程式碼,在輸出中保持 getAnswer() 為殘留函式:

(function () {
  var _0 = function () {
    return 2 + 2;
  };

  getAnswer = _0;
})();
複製程式碼

注意 getAnswer() 並沒有被優化,因為它是殘留函式,在初始化階段沒有被執行。運算 + 還是在那裡。我們可以看到 22 替換了 xy,這是由於它們在程式執行期間沒有改變,所以 Prepack 將其視為常量。

如果我們動態生成一個函式,再將其新增到全域性物件上呢?例如:

(function() {
  function makeCar(color) {
    return {
      getColor() { return color; },
    }
  };
  global.cars = ['red', 'green', 'blue', 'yellow', 'pink'].map(makeCar);
})();
複製程式碼

這裡,我們建立了多個物件,每個物件都包含一個 getColor() 函式,返回傳入 makeCar() 的不同值。Prepack 像這樣輸出

(function () {
  var _2 = function () {
    return "red";
  };

  var _5 = function () {
    return "green";
  };

  var _8 = function () {
    return "blue";
  };

  var _B = function () {
    return "yellow";
  };

  var _E = function () {
    return "pink";
  };

  cars = [{
    getColor: _2
  }, {
    getColor: _5
  }, {
    getColor: _8
  }, {
    getColor: _B
  }, {
    getColor: _E
  }];
})();
複製程式碼

注意輸出是怎樣的,Prepack 並沒有保持抽象的 makeCar()。相反,它執行了 makeCar() 呼叫,並將返回的函式進行了序列化。這也是為什麼輸出結果中有多個 getColor(),每個 Car 物件一個。

這個例子也展示了 Prepack 優化執行時效能,但可能有位元組體積上的代價。JavaScript 引擎執行 Prepack 生成的程式碼會更快,因為它不必執行函式呼叫並初始化所有的內嵌閉包。但是,生成的程式碼可能會比輸入程式碼更大 —— 有時候非常明顯。

這種“程式碼爆炸”有助於發現初始化階段哪些程式碼做了過多的昂貴的超程式設計(metaprogramming),但也讓 Prepack 很難用於對打包後體積敏感的專案中(例如 web 專案)。今天,最簡單的處理“程式碼爆炸”的方法是 延遲執行這些程式碼將其移入殘留函式中,這樣就從 Prepack 的執行路徑中移除了。當然,這種情況下 Prepack 也就無法優化它。在未來,Prepack 可能會有更好的啟發,進而對速度和體積開銷有更好的控制。

延遲閉包初始化

在上一個例子中,color 值被內聯到殘留函式中,因為它們是常量。但如果閉包中的 color 值會改變呢?考慮如下的例子:

(function() {
  function makeCar(color) {
    return {
      getColor() { return color; }, // 讀取 color
      paint(newColor) { color = newColor; }, // 修改 color
    }
  };
  global.cars = ['red', 'green', 'blue'].map(makeCar);
})();
複製程式碼

現在 Prepack 不能直接生成一系列包含類似 return "red" 語句的 getColor() 函式,因為外部程式碼會通過呼叫 paint(newColor) 改變顏色。

這是 上面場景生成的程式碼

(function () {
  var __scope_0 = Array(3);

  var __scope_1 = function (__selector) {
    var __captured;
    switch (__selector) {
      case 0:
        __captured = ["red"];
        break;
      case 1:
        __captured = ["green"];
        break;
      case 2:
        __captured = ["blue"];
        break;
      default:
        throw new Error("Unknown scope selector");
    }

    __scope_0[__selector] = __captured;
    return __captured;
  };

  var $_0 = function (__scope_2) {
    var __captured__scope_2 = __scope_0[__scope_2] || __scope_1(__scope_2);
    return __captured__scope_2[0];
  };

  var $_1 = function (__scope_2, newColor) {
    var __captured__scope_2 = __scope_0[__scope_2] || __scope_1(__scope_2);
    __captured__scope_2[0] = newColor;
  };

  var _2 = $_0.bind(null, 0);
  var _4 = $_1.bind(null, 0);
  var _6 = $_0.bind(null, 1);
  var _8 = $_1.bind(null, 1);
  var _A = $_0.bind(null, 2);
  var _C = $_1.bind(null, 2);

  cars = [{
    getColor: _2,
    paint: _4
  }, {
    getColor: _6,
    paint: _8
  }, {
    getColor: _A,
    paint: _C
  }];
})();
複製程式碼

這看起來非常複雜!我們來看看是怎麼回事。

注意:如果你一直搞不明白這一節也是完全沒關係的。我也是在開始寫這一節的時候才搞明白。

可能從下往上讀更容易些。首先,我們可以看到 Prepack 仍然沒有保留 makeCar(),而是將零碎的物件手動拼起來以避免函式呼叫和閉包建立。每個函式例項是不同的:

  cars = [{
    getColor: _2, // redCar.getColor
    paint: _4     // redCar.paint
  }, {
    getColor: _6, // greenCar.getColor
    paint: _8     // greenCar.paint
  }, {
    getColor: _A, // blueCar.getColor
    paint: _C     // blueCar.paint
  }];
複製程式碼

這些函式從哪裡來的?Prepack 在上面宣告瞭:

  var _2 = $_0.bind(null, 0); // redCar.getColor
  var _4 = $_1.bind(null, 0); // redCar.paint

  var _6 = $_0.bind(null, 1); // greenCar.getColor
  var _8 = $_1.bind(null, 1); // greenCar.paint
  
  var _A = $_0.bind(null, 2); // blueCar.getColor
  var _C = $_1.bind(null, 2); // blueCar.paint
複製程式碼

可以看到被繫結的函式($_0$_1)對應 car 的方法(getColorpaint)。Prepack 對所有例項使用複用相同的實現。

不過,這些函式得知道是三個獨立修改的顏色中的 哪一個。Prepack 得知道如何有效模擬 JavaScript 閉包 但不建立巢狀函式。

為了解決這個問題,bind() 的引數(012)給了提示,表示哪個顏色在被函式“捕獲”。在例子中,顏色號 0 初始為 'red',顏色號 1 開始是 'green'2 開始是 'blue'。當前顏色儲存在陣列中,在這個函式之後初始化:

  var __scope_0 = Array(3); // index -> color 對映

  var __scope_1 = function (__selector) { // __selector 為索引
    var __captured;
    switch (__selector) {
      case 0:
        __captured = ["red"];
        break;
      case 1:
        __captured = ["green"];
        break;
      case 2:
        __captured = ["blue"];
        break;
      default:
        throw new Error("Unknown scope selector");
    }

    __scope_0[__selector] = __captured; // 在陣列中儲存初始值
    return __captured;
  };
複製程式碼

在上面程式碼中,__scope_0 是陣列,Prepack 用於記錄顏色所以到顏色值的對應關係。__scope_1 是函式,向陣列特定索引設定初始顏色。

最終,所有 getColor() 的實現從顏色陣列中讀取顏色值。如果陣列不存在,則通過呼叫函式來初始化。

  var $_0 = function (__scope_2) {
    var __captured__scope_2 = __scope_0[__scope_2] || __scope_1(__scope_2);

    return __captured__scope_2[0];
  };
複製程式碼

類似地,paint() 確保陣列存在,然後寫入。

  var $_1 = function (__scope_2, newColor) {
    var __captured__scope_2 = __scope_0[__scope_2] || __scope_1(__scope_2);

    __captured__scope_2[0] = newColor;
  };
複製程式碼

為什麼都有 [0],為什麼向陣列寫入 ["red"] 而不是直接儲存顏色?每個閉包可能包含不只一個變數,所以 Prepack 使用額外的陣列層級來引用它們。在我們的例子中,color 是閉包中唯一的變數,所以 Prepack 使用了單元素的陣列來儲存。

你可能注意到輸出的程式碼有點長。這在經過壓縮後會好些。目前,序列化器的這一部分,專注於正確性而非更有效率的輸出。

更可能地是,輸出可以逐步進行優化,所以如果你發現有更好的優化方案,不要猶豫,直接提交 issue。在一開始,Prepack 並沒有生成可以延遲分配閉包的程式碼。相反,所有捕獲的變數都被提升並初始化到輸出的全域性程式碼中。這也是一個速度與程式碼體積的交換,逐漸會有所變化。

環境影響

這個時候,你可能想試著複製貼上一些現有程式碼到 Prepack REPL 中。不過,你很快就會發現像 windowdocument 這樣的瀏覽器基礎特性,或者 Node 的 require,並不能如你所想地工作。

例如,React DOM 包含如下的特性檢查程式碼,這個 Prepack 不能編譯

var documentMode = null;
if ('documentMode' in document) {
  documentMode = document.documentMode;
}
複製程式碼

錯誤資訊為:

PP0004 (2:23):  might be an object that behaves badly for the in operator
PP0001 (3:18):  This operation is not yet supported on document at documentMode
A fatal error occurred while prepacking.
複製程式碼

多數 Prepack 的錯誤碼對應有錯誤描述的 Wiki 頁面。例如,這是與 PP0004 對應的頁面。(另一個 PP0001 錯誤來自老的錯誤系統,你可以幫忙進行遷移

所以為什麼上面的程式碼不能工作?為了回答這個問題,我們需要回顧 Prepack 的工作原理。為了執行程式碼,Prepack 需要知道不同的值等於什麼。而有的東西只在執行時才知道。

Prepack 無法知道程式碼在瀏覽器中執行時的情況,所以它不能確定 是應該安全地為 document 物件應用 in 運算子,還是應該丟擲異常(如果上面有 try / catch,這會是一個潛在的不同的程式碼路徑)。

這聽起來很槽糕。不過,初始化程式碼從環境中讀取一些在構建階段不清楚的東西是很常見的。對此有兩種方法。

一種是隻對不依賴外部資料的程式碼應用 Prepack,把任何環境檢測的程式碼放到 Prepack 以外。對於可以比較容易分離的程式碼,這是合理的策略。

另一種解決方法是使用 Prepack 最強大的特性:抽象值

在下一節中,我們會深入瞭解抽象值,不過當前 gist 沒有這樣的例子。Prepack 可以在不知道某些表示式的具體值的情況下執行程式碼,你可以為 Node 或瀏覽器 API 或其他未知的輸入提供進一步的提示。

待續

我們涉及了 Prepack 工作原理的基礎部分,但還沒有探討更有趣的特性:

  • 手動優化選擇的殘留函式
  • 在某些值未知情況下執行程式碼
  • Prepack 如何“連線”函式執行流
  • 使用 Prepack 檢視變數可以接收的所有值
  • 試驗性的 React 編譯模式
  • 本地檢出 Prepack 並除錯

我們會在下一篇文章中探索這些話題。

相關文章