Prepack 介紹(譯)

原文: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%2520%257B%250A%2520%2520var%2520x%2520%253D%25202%253B%250A%2520%2520var%2520y%2520%253D%25202%253B%250A%2520%2520global.answer%2520%253D%2520x%2520%252B%2520y%253B%250A%257D)()%253B)。這種優化被稱作 “常量摺疊(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 中。不過,你很快就會發現像 window
document 這樣的瀏覽器基礎特性,或者 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 並調試

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章