論ES6模塊系統的靜態解析

[size=medium]本文是Dave Herman的《[url=http://calculist.org/blog/2012/06/29/static-module-resolution/]Static module resolution[/url]》一文的編譯。Dave Herman是TC39的成員,ES6 module系統的champion。【ES6 spec太大了,所以分成許多可相對獨立的特性集合,分別交給一個或幾個主導人負責,TC39委員會則會定期開會進行審閱和討論。主導人就稱之爲champion。】


在純JS環境下已經有多種模塊系統。比如CommonJS。所謂純JS系統,就是不依賴其他機制如預處理之類的。純JS系統中的模塊都是一個個對象。客戶代碼導入模塊所導出的定義,實際上是查找module對象上的屬性:[/size]

var { stat, exists, readFile } = require('fs');

[size=medium]
ES6模塊系統則相反,模塊不是對象,而是聲明式的代碼集合。從模塊導入定義也是聲明式的:
[/size]

import { stat, exists, readFile } from 'fs';

[size=medium]
這個import是在編譯時resolve的——即在腳本開始執行之前。事實上,各個模塊之間的依賴關係圖所涉及的所有imports和exports都是在執行之前resolve好了。當然,我們也有lazy loading或按需加載的需求,即在運行時才進行模塊加載。對此,ES6也有異步的模塊動態加載API。不過本文只討論聲明式模塊依賴關係圖的解析。

NodeJS的作者認爲我們應該走漸進的、改良式的道路,認爲ES6的模塊系統應該更接近今天已經存在着的模塊系統。我也相當贊同“pave the cowpaths”哲學(遵循事實標準),並常以此立論,但是必須注意到,現有JS模塊系統的作者們從未有過從語言層面做修改的可能性,而我們現在卻有機會改變JS,選擇在純動態系統中沒可能走的道路,包括:

[b]快速查找[/b]
靜態import(無論是通過import還是如m.foo的引用)可以編譯爲如同簡單的變量引用一樣。在動態模塊系統中,像m.foo這樣的顯式用引(dereference)會得到一個對象引用,通常需要PIC才能優化(Polymorphic Inline Caching,多態內聯緩存,JavaScript引擎在執行時動態修改JIT代碼的高級優化技術)。如果是複製到局部變量,相對來說會較容易進行優化。但是對於靜態模塊來說,總是早期綁定,也就是始終和變量引用一樣高效。這使得模塊化的程序能運行更快,避免了因爲模塊化導致額外性能成本。

[b]早期變量檢查[/b]
依我的經驗,在腳本執行前若能對變量引用——包括imports和exports——進行檢查,非常有助於確保程序頂層的基礎結構是健全的。JavaScript基本上是靜態作用域的,因此可以進行靜態作用域檢查,這也是唯一可以做的檢查。James Burke認爲這只是shallow type checking(淺類型檢查,而不是強類型檢查),不夠有用。但我在其他語言的經驗表明正相反——這超級有用!變量檢查是一個最佳平衡點,你可以寫出富有表達力的動態程序,同時又能捕捉到那些真的很常見的錯誤。如Anton Kovalyov指出的,報告未綁定的變量是JSHint的最常用特性,如果不必藉助額外的lint工具就能捕捉這些bug那就再好不過了。

[b]循環依賴[/b]
允許模塊間循環依賴是非常重要的。現實情況是編程中可能出現相互間的遞歸調用——有時你甚至都沒注意到。如果你將程序拆分模塊後,由於不能處理循環依賴結果系統掛了,那最簡單的workaround就是繼續把所有東西都堆到一個大模塊中。這肯定有問題。無論如何,模塊系統不應該阻止程序員拆分程序,不應該挫傷程序員模塊化的積極性。

不是說動態系統就不可能支持循環依賴,但是我覺得在那些提案中看起來都像是事後補丁。ES6的靜態模塊系統則仔細的考慮了循環依賴問題。聲明式的模塊讓你可以在執行任何代碼前預初始化更多的模塊結構,這樣如果引用尚未賦值的export,能得到更好的錯誤信息。例如,一個let綁定會扔出異常——如果你在它被賦值之前就引用它的話——你可以得到清晰的錯誤信息。而一個動態模塊對象上的屬性如果還未賦值就被引用,得到的是undefined,最終錯誤可能發生在客戶代碼中,必須跟蹤這個錯誤直到源頭——這比異常要難調試太多了。

[b]兼容未來的macro特性[/b]
我非常期待JavaScript未來能讓程序員可以發展他們自己的定製語法擴展,而不必等待TC39。今天,人們自個兒寫編譯器來弄新語法。但是這個極難,而且你不能在同一個源文件裏使用不同編譯器提供的不同語法特性。
有了macro,你就可以實現,比如說一個新的cond語法,來取代連續的? :條件分支,並可以通過庫的方式共享之:
[/size]

import cond from 'cond.js';
...
var type = cond {
case (x === null): "null",
case Array.isArray(x): "array",
case (typeof x === "object"): "object",
default: typeof x
};

[size=medium]
cond這個macro會在程序運行前進行預處理,將這段代碼轉換爲連續的條件分支。而純動態模塊是無法實現預處理的:
[/size]

var cond = require('cond.js');
...
// impossible to preprocess because we haven't evaluated the require!
var type = cond { /* etc */ };

[size=medium]
[b]兼容未來的類型系統[/b]
在悲劇的ES4時代我就加入了TC39,當時委員會在搞一個可選的類型系統。這系統基礎不全最終廢棄。其中一個重要缺失就是模塊系統,通過模塊系統可以將代碼劃定邊界並說“這部分需要類型檢查”。否則你永遠不知道是否有更多後續代碼會影響類型檢查。

爲什麼要有類型系統?一個原因是:JS很快且越來越快,但是也更難準確預測性能。通過類似LLJS的試驗性系統,我在Mozilla的團隊使用帶有類型的JS方言進行預編譯,生成相當獨特的爲當前JIT優化的JS代碼。如果你可以直接用帶類型系統的JS方言寫出高性能核心,現代編譯器可以做得更好而不用如此曲折。

通過聲明性的解析,你可以導入和導出帶有類型信息的定義,並可進行編譯時檢查。動態導入不可能進行靜態檢查。

[b]跨語言的模塊性[/b]
一些人不care或者不想要像macro或類型這樣的特性。但是JavaScript必須適應許多不同的程序員的各種不同的開發實踐和需求。其中一種方式是讓人們使用他們自己的語言,並編譯爲JavaScript。所以即使未來的ECMAScript標準沒有macro和類型,若你可以使用靜態類型或帶有macro的JS方言並編譯爲瀏覽器可執行的JS,也是相當好的。實際上人們已經這樣幹了,比如用Closure compiler的類型檢查、Roy語言、ClojureScript等。靜態模塊系統可以更一致更直接的兼容更多的語言。

[b]成本和收益[/b]
以上是一些我看到的聲明性模塊解析的收益。Isaac Schlueter(NodeJS的作者)說import語法無甚意義。這是不公正和錯誤的。它是有意義的。我也不認爲聲明性的import語法會給ES6和未來的JS版本增加很高的成本。
[/size]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章