做爲一名前端工程師,天天的清晨,你走進公司的大門,回味着前臺妹子的笑容,摘下耳機,泡上一杯茶,打開 Terminal
進入對應的項目目錄下,而後 npm run start / dev
或者 yarn start / dev
就開始了一天的工做。javascript
當你須要進行時間的轉換隻須要使用 dayjs
或者 momentjs
, 當你須要封裝 http 請求的時候,你能夠用 fetch
或者 axios
, 當你須要作數據處理的時候,你可能會用 lodash
或者 underscore
。html
不知道你有沒有意識到,對於今天的咱們而言,這些工具包讓開發效率獲得了巨大的提高,可是這一切是從什麼開始的呢?前端
這些就要從 Modular design (模塊化設計)
提及:java
在我剛接觸前端的時候,常常據說 Modular design (模塊化設計)
這樣的術語,面試時也會常常被問到,「聊聊前端的模塊化」這樣的問題,或許不少人均可以說出幾個熟悉的名詞,甚至是他們之間的區別:node
但就像你閱讀一個項目的源碼同樣,若是從第一個 commit
開始研究,那麼你能收穫的或許不只僅是,知道他們有什麼區別,更重要的是,可以知道在此以前的歷史中,是什麼樣的緣由,致使了區別於舊的規範而產生的新規範,而且基於這些,或許你可以從中體會到這些改變意味着什麼,甚至在未來的某個時刻,你也能成爲這規則的制定者之一。jquery
因此讓咱們回到十年前,來看看是怎麼實現模塊化設計的:ios
IIFE 是 Immediately Invoked Function Expression 的縮寫,做爲一個基礎知識,不少人可能都已經知道 IIFE 是怎麼回事,(若是你已經掌握了 IIFE,能夠跳過這節閱讀後面的內容) 但這裏咱們仍舊會解釋一下,它是怎麼來的,由於在後面咱們還會再次提到它:git
最開始,咱們對於模塊區分的概念,多是從文件的區分開始的,在一個簡易的項目中,編程的習慣是經過一個 HTML 文件加上若干個 JavaScript 文件來區分不一樣的模塊,就像這樣:es6
咱們能夠經過這樣一個簡單的項目來講明,來看看每一個文件裏面的內容:github
這個文件,只是簡單的引入了其餘的幾個 JavaScript 文件:
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>demo</title>
</head>
<script src="main.js"></script>
<script src="header.js"></script>
<script src="footer.js"></script>
<body></body>
</html>
複製代碼
在不一樣的 js 文件中咱們定義了不一樣的變量,分別對應文件名:
var header = '這是一條頂部信息' //header.js
var main_message = '這是一條內容信息' //main.js
var main_error = '這是一條錯誤信息' //main.js
var footer = '這是一條底部信息' //footer.js
複製代碼
像這樣經過不一樣的文件來聲明變量的方式,實際上沒法將這些變量區分開來。
它們都綁定在全局的 window / Global(node 環境下的全局變量) 對象上,嘗試去打印驗證一下:
這簡直就是一場噩夢,你可能沒有意識到這會致使什麼嚴重的結果,咱們試着在 footer.js 中對 header
變量進行賦值操做,讓咱們在末尾加上這樣一行代碼:
header = 'nothing'
複製代碼
打印後你就會發現,window.header
的已經被更改了:
試想一下,你永遠沒法預料在何時什麼地點無心中就改掉了以前定義的某個變量,若是這是在一個團隊中,這是一件多麼可怕的事情。
Okay,如今咱們知道,僅僅經過不一樣的文件,咱們沒法作到將這些變量分開,由於它們都被綁在了同一個 window 變量上。
可是更重要的是,怎麼去解決呢?咱們都知道,在 JavaScript 中,函數擁有本身的做用域 的,也就是說,若是咱們能夠用一個函數將這些變量包裹起來,那這些變量就不會直接被聲明在全局變量 window 上了:
因此如今 main.js 的內容會被修改爲這樣:
function mainWarraper() {
var main_message = '這是一條內容信息' //main.js
var main_error = '這是一條錯誤信息' //main.js
console.log('error:', main_error)
}
mainWarraper()
複製代碼
爲了確保咱們定義在函數 mainWarraper 的內容會被執行,因此咱們必須在這裏執行 mainWarraper() 自己,如今咱們在 window 裏面找不到 main_message
和 main_error
了,由於它們被隱藏在了 mainWarraper
中,可是 mainWarraper
仍舊污染了咱們的 window:
這個方案還不夠完美,怎麼改進呢?
答案就是咱們要說的 IIFE 咱們能夠定義一個 當即執行的匿名函數 來解決這個問題:
(function() {
var main_message = '這是一條內容信息' //main.js
var main_error = '這是一條錯誤信息' //main.js
console.log('error:', main_error)
})()
複製代碼
由於是一個匿名的函數,執行完後很快就會被釋放,這種機制不會污染全局對象。
雖然看起來有些麻煩,但它確實解決了咱們將變量分離開來的需求,不是嗎?然而在今天,幾乎沒有人會用這樣方式來實現模塊化編程。
後來又發生了什麼呢?
在 2009 年的一個冬天, 一名來自 Mozilla 團隊的的工程師 Kevin Dangoor 開始搗鼓了一個叫 ServerJS 的項目,他是這樣描述的:
"What I’m describing here is not a technical problem. It’s a matter of people getting together and making a decision to step forward and start building up something bigger and cooler together."
"在這裏我描述的不是一個技術問題。 這是一個關於你們齊心協力,作出決定向前邁進,而且開始一塊兒建造一些更大更酷的東西的問題。"
這個項目在 2009 年的 8 月份改名爲今日咱們熟悉的 CommonJS 以顯示 API 更普遍的適用性。我以爲那時他可能並無料到,這一規則的制定會讓整個前端發生翻天覆地的變化。
CommonJS 在 Wikipedia 中是這樣描述的:
CommonJS is a project with the goal to establish conventions on module ecosystem for JavaScript outside of the web browser. The primary reason of its creation was a major lack of commonly accepted form of JavaScript scripts module units which could be reusable in environments different from that provided by a conventional web browser e.g. web server or native desktop applications which run JavaScript scripts.
CommonJS 是一個旨在 Web 瀏覽器以外,爲 JavaScript 創建模塊生態系統的約定的項目。 其建立的主要緣由是缺少廣泛接受的 JavaScript 腳本模塊單元形式,而這一形式可讓 JavaScript 在不一樣於傳統網絡瀏覽器提供的環境中重複使用,例如, 運行 JavaScript 腳本的 Web 服務器或本機桌面應用程序。
經過上面這些描述,相信你已經知道 CommonJS 是誕生於怎樣的背景,可是這裏所說的 CommonJS 是一套通用的規範,與之對應的有很是多不一樣的實現:
圖片來源於 wiki
可是咱們關注的是其中 Node.js 的實現部分。
這裏不會解釋
Node.js Modules
的 API 基本用法,由於這些均可以經過閱讀 官方文檔 來了解,咱們會討論爲何會這樣設計,以及你們比較難理解的點來展開。
在 Node.js 模塊系統中,每一個文件都被視爲一個單獨的模塊,在一個Node.js 的模塊中,本地的變量是私有的,而這個私有的實現,是經過把 Node.js 的模塊包裝在一個函數中,也就是 The module wrapper
,咱們來看看,在 官方示例中 它長什麼樣:
(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
// 實際上,模塊內的代碼被放在這裏
});
複製代碼
是的,在模塊內的代碼被真正執行之前,實際上,這些代碼都被包含在了一個這樣的函數中。
若是你真正閱讀了上一節中關於 IIFE 的內容,你會發現,其實核心思想是同樣的,Node.js 對於模塊私有化的實現也仍是經過了一個函數。可是這有哪些不一樣呢?
雖然這裏有 5 個參數,可是咱們把它們先放在一邊,而後嘗試站在一個模塊的角度來思考這樣一個問題:做爲一個模塊,你但願本身具有什麼樣的能力呢?
exports:導出對象
, module:模塊的引用
]require:引用方法
]__filename:絕對文件名
, __dirname:目錄路徑
]爲何咱們要了解 require
方法的實現呢?由於理解這一過程,咱們能夠更好地理解下面的幾個問題:
exports
和 module.exports
有什麼聯繫和區別?在文檔中,有簡易版的 require
的實現:
function require(/* ... */) {
const module = { exports: {} };
((module, exports) => {
// Module code here. In this example, define a function.
// 模塊代碼在這裏,在這個例子中,咱們定義了一個函數
function someFunc() {}
exports = someFunc;
// At this point, exports is no longer a shortcut to module.exports, and
// this module will still export an empty default object.
// 當代碼運行到這裏時,exports 再也不是 module.exports 的引用,而且當前的
// module 仍舊會導出一個空對象(就像上面聲明的默認對象那樣)
module.exports = someFunc;
// At this point, the module will now export someFunc, instead of the
// default object.
// 當代碼運行到這時,當前 module 會導出 someFunc 而不是默認的對象
})(module, module.exports);
return module.exports;
}
複製代碼
回到剛剛提出的問題:
require
作了怎樣一件事情?require 至關於把被引用的 module 拷貝了一份到當前 module 中
exports
和 module.exports
的聯繫和區別?代碼中的註釋以及 require 函數第一行默認值的聲明,很清楚的闡述了,exports
和 module.exports
的區別和聯繫:
exports
是 module.exports
的引用。做爲一個引用,若是咱們修改它的值,實際上修改的是它對應的引用對象的值。
就如:
exports.a = 1
// 等同於
module.exports = {
a: 1
}
複製代碼
可是若是咱們修改了 exports 引用的地址,對於它原來所引用的內容來講,沒有任何影響,反而咱們斷開了這個引用於原來的地址之間的聯繫:
exports = {
a: 1
}
// 至關於
let other = {a: 1} //爲了更加直觀,咱們這樣聲明瞭一個變量
exports = other
複製代碼
exports
從指向 module.exports
變爲了 other
。
CommonJS
這一標準的初衷是爲了讓 JavaScript
在多個環境下都實現模塊化,可是 Node.js 中的實現依賴了 Node.js 的環境變量:module
,exports
,require
,global
,瀏覽器無法用啊,因此後來出現了 Browserify
這樣的實現,可是這並非本文要討論的內容,有興趣的同窗能夠讀讀阮一峯老師的 這篇文章。
說完了服務端的模塊化,接下來咱們聊聊,在瀏覽器這一端的模塊化,又經歷了些什麼呢?
試想一下,假如咱們如今是在瀏覽器環境下,使用相似於 Node.js Module 的方式來管理咱們的模塊(例如 Browserify
),會有什麼樣的問題呢?
由於咱們已經瞭解了 require()
的實現,因此你會發現這實際上是一個複製的過程,將被 require 的內容,賦值到一個 module 對象的屬性上,而後返回這個對象的 exports 屬性。
這樣作會有什麼問題呢?在咱們尚未完成複製的時候,沒法使用被引用的模塊中的方法和屬性。在服務端可能這不是一個問題(由於服務器的文件都是存放在本地,而且是有緩存的),但在瀏覽器環境下,這會致使阻塞,使得咱們後面的步驟沒法進行下去,還可能會執行一個未定義的方法而致使出錯。
相對於服務端的模塊化,瀏覽器環境下,模塊化的標準必須知足一個新的需求:異步的模塊管理
在這樣的背景下,RequireJS 出現了,咱們簡單的瞭解一下它最核心的部分:
require()
define()
官方文檔中的使用的例子:
requirejs.config({
// 默認加載 js/lib 路徑下的module ID
baseUrl: 'js/lib',
// 除去 module ID 以 "app" 開頭的 module 會從 js/app 路徑下加載。
// 關於 paths 的配置是與 baseURL 關聯的,而且由於 paths 可能會是一個目錄,
// 因此不要使用 .js 擴展名
paths: {
app: '../app'
}
});
// 開始主邏輯
requirejs(['jquery', 'canvas', 'app/sub'],
function ($, canvas, sub) {
//jQuery, canvas 和 app/sub 模塊已經被加載而且能夠在這裏使用了。
});
複製代碼
官方文檔中的定義的例子:
// 簡單的對象定義
define({
color: "black",
size: "unisize"
});
// 當你須要一些邏輯來作準備工做時能夠這樣定義:
define(function () {
//這裏能夠作一些準備工做
return {
color: "black",
size: "unisize"
}
});
// 依賴於某些模塊來定義屬於你本身的模塊
define(["./cart", "./inventory"], function(cart, inventory) {
//經過返回一個對象來定義你本身的模塊
return {
color: "blue",
size: "large",
addToCart: function() {
inventory.decrement(this);
cart.add(this);
}
}
}
);
複製代碼
RequireJS 是基於 AMD 規範 實現的,那麼相對於 Node.js 的 Module 它有什麼優點呢?
若是一個 JS 模塊系統沒法提供上述功能,那麼與 AMD 及其相關 API 相比,它將在回調需求,加載器插件和基於路徑的模塊 ID 等方面處於明顯的劣勢。
經過上面的語法說明,咱們會發現一個很明顯的問題,在使用 RequireJS 聲明一個模塊時,必須指定全部的依賴項 ,這些依賴項會被當作形參傳到 factory 中,對於依賴的模塊會提早執行(在 RequireJS 2.0 也能夠選擇延遲執行),這被稱爲:依賴前置。
這會帶來什麼問題呢?
加大了開發過程當中的難度,不管是閱讀以前的代碼仍是編寫新的內容,也會出現這樣的狀況:引入的另外一個模塊中的內容是條件性執行的。
針對 AMD 規範中能夠優化的部分,CMD 規範 出現了,而 SeaJS 則做爲它的具體實現之一,與 AMD 十分類似:
// AMD 的一個例子,固然這是一種極端的狀況
define(["header", "main", "footer"], function(header, main, footer) {
if (xxx) {
header.setHeader('new-title')
}
if (xxx) {
main.setMain('new-content')
}
if (xxx) {
footer.setFooter('new-footer')
}
});
// 與之對應的 CMD 的寫法
define(function(require, exports, module) {
if (xxx) {
var header = require('./header')
header.setHeader('new-title')
}
if (xxx) {
var main = require('./main')
main.setMain('new-content')
}
if (xxx) {
var footer = require('./footer')
footer.setFooter('new-footer')
}
});
複製代碼
咱們能夠很清楚的看到,CMD 規範中,只有當咱們用到了某個外部模塊的時候,它纔會去引入,這回答了咱們上一小節中遺留的問題,這也是它與 AMD 規範最大的不一樣點:CMD推崇依賴就近 + 延遲執行
咱們可以看到,按照 CMD 規範的依賴就近的規則定義一個模塊,會致使模塊的加載邏輯偏重,有時你並不知道當前模塊具體依賴了哪些模塊或者說這樣的依賴關係並不直觀。
並且對於 AMD 和 CMD 來講,都只是適用於瀏覽器端的規範,而 Node.js module 僅僅適用於服務端,都有各自的侷限性。
ECMAScript6 標準增長了 JavaScript 語言層面的模塊體系定義,做爲瀏覽器和服務器通用的模塊解決方案它能夠取代咱們以前提到的 AMD
,CMD
,CommonJS
。(在此以前還有一個 UMD(Universal Module Definition)規範也適用於先後端,可是本文不討論,有興趣能夠查看 UMD文檔 )
關於 ES6 的 Module 相信你們天天的工做中都會用到,對於使用上有疑問能夠看看 ES6 Module 入門,阮一峯,固然你也能夠查看 TC39的官方文檔
爲何要在標準中添加模塊體系的定義呢?引用文檔中的一句話:
"The goal for ECMAScript 6 modules was to create a format that both users of CommonJS and of AMD are happy with"
"ECMAScript 6 modules 的目標是創造一個讓 CommonJS 和 AMD 用戶都滿意的格式"
它憑藉什麼作到這一點呢?
除此以外,它還有更多的優點:
注意這裏的描述裏出現了兩個詞 循環依賴 和 靜態分析,咱們在後面會深刻討論。首先咱們來看看, TC39 的 官方文檔 中定義的 ES6 modules 規範是什麼。
在 15.2.1.15 節 中,定義了 Abstract Module Records (抽象的模塊記錄) 的 Module Record Fields (模塊記錄字段) 和 Abstract Methods of Module Records (模塊記錄的抽象方法)
Field Name(字段名) | Value Type(值類型) | Meaning(含義) |
---|---|---|
[[Realm]] 域 | Realm Record | undefined | The Realm within which this module was created. undefined if not yet assigned. 將在其中建立當前模塊,若是模塊未聲明則爲 undefined。 |
[[Environment]] 環境 | Lexical Environment | undefined | The Lexical Environment containing the top level bindings for this module. This field is set when the module is instantiated. 詞法環境包含當前模塊的頂級綁定。 在實例化模塊時會設置此字段。 |
[[Namespace]] 命名空間 | Object | undefined | The Module Namespace Object if one has been created for this module. Otherwise undefined. 模塊的命名空間對象(若是已爲此模塊建立了一個)。 不然爲 undefined。 |
[[Evaluated]] 執行結束 | Boolean | Initially false, true if evaluation of this module has started. Remains true when evaluation completes, even if it is an abrupt completion 初始值爲 false 當模塊開始執行時變成 true 而且持續到執行結束,哪怕是忽然的終止(忽然的終止,會有不少種緣由,若是對緣由感興趣能夠看下 這個回答) |
Method 方法 | Purpose 目的 |
---|---|
GetExportedNames(exportStarSet) | Return a list of all names that are either directly or indirectly exported from this module. 返回一個今後模塊直接或間接導出的全部名稱的列表。 |
ResolveExport(exportName, resolveSet, exportStarSet) | Return the binding of a name exported by this modules. Bindings are represented by a Record of the form {[[module]]: Module Record, [[bindingName]]: String}. 返回此模塊導出的名稱的綁定。 綁定由此形式的記錄表示:{[[module]]: Module Record, [[bindingName]]: String} |
ModuleDeclarationInstantiation() | Transitively resolve all module dependencies and create a module Environment Record for the module. 傳遞性地解析全部模塊依賴關係,併爲模塊建立一個環境記錄 |
ModuleEvaluation() | Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module. 若是此模塊已經被執行過,則不執行任何操做。 不然,傳遞執行此模塊的全部模塊依賴關係,而後執行此模塊。 ModuleDeclarationInstantiation must be completed prior to invoking this method.ModuleDeclarationInstantiation 必須在調用此方法以前完成 |
也就是說,一個最最基礎的模塊,至少應該包含上面這些字段,和方法。反覆閱讀後你會發現,其實這裏只是告知了一個最基礎的模塊,應該包含某些功能的方法,或者定義了模塊的格式,可是在咱們具體實現的時候,就像原文中說的同樣:
An implementation may parse a sourceText as a Module, analyze it for Early Error conditions, and instantiate it prior to the execution of the TopLevelModuleEvaluationJob for that sourceText.
實現能夠是:將 sourceText 解析爲模塊,對其進行早期錯誤條件分析,並在執行TopLevelModuleEvaluationJob以前對其進行實例化。
An implementation may also resolve, pre-parse and pre-analyze, and pre-instantiate module dependencies of sourceText. However, the reporting of any errors detected by these actions must be deferred until the TopLevelModuleEvaluationJob is actually executed.
實現還能夠是:解析,預解析和預分析,並預先實例化 sourceText 的模塊依賴性。 可是,必須將這些操做檢測到的任何錯誤,推遲到實際執行TopLevelModuleEvaluationJob 以後再報告出來。
經過這些咱們只能得出一個結論,在具體實現的時候,只有第一步是固定的,也就是:
解析:如 ParseModule 這一節中所介紹的同樣,首先會對模塊的源代碼進行語法錯誤檢查。例如 early-errors,若是解析失敗,讓 body 報出一個或多個解析錯誤和/或早期錯誤。若是解析成功而且沒有找到早期錯誤,則將 body 做爲生成的解析樹繼續執行,最後返回一個 Source Text Module Records
那後面會發生什麼呢?咱們能夠經過閱讀具體實現的源碼來分析。
Babel 做爲 ES6 官方指定的編譯器,在現在的前端開發中發揮着巨大的做用,它能夠幫助咱們將開發人員書寫的 ES6 語法的代碼轉譯爲 ES5 的代碼而後交給 JS 引擎去執行,這一行爲讓咱們能夠毫無顧忌的使用 ES6 給咱們帶來的方便。
這裏咱們就以 Babel 中 babel-helper-module-transforms 的具體實現,來看看它是如何實現 ES6 module 轉換的步驟
在這裏我不會逐行的去分析源碼,而是從結構和調用上來看具體的邏輯
首先咱們羅列一下這個文件中出現的全部方法(省略掉方法體和參數)
/** * Perform all of the generic ES6 module rewriting needed to handle initial * module processing. This function will rewrite the majority of the given * program to reference the modules described by the returned metadata, * and returns a list of statements for use when initializing the module. * 執行處理初始化所需的全部通用ES6模塊重寫 * 模塊處理。 這個函數將重寫給定的大部分 * 程序引用返回的元數據描述的模塊, * 並返回初始化模塊時使用的語句列表。 */
export function rewriteModuleStatementsAndPrepareHeader() {...}
/** * Flag a set of statements as hoisted above all else so that module init * statements all run before user code. * 將一組語句標記爲高於其餘全部語句,以便模塊初始化 * 語句所有在用戶代碼以前運行。 */
export function ensureStatementsHoisted() {...}
/** * Given an expression for a standard import object, like "require('foo')", * wrap it in a call to the interop helpers based on the type. * 給定標準導入對象的表達式,如「require('foo')」, * 根據類型將其包裝在對 interop 助手的調用中。 */
export function wrapInterop() {...}
/** * Create the runtime initialization statements for a given requested source. * These will initialize all of the runtime import/export logic that * can't be handled statically by the statements created by * 爲給定的請求源建立運行時初始化語句。 * 這些將初始化全部運行時導入/導出邏輯 * 不能由建立的語句靜態處理 * buildExportInitializationStatements(). */
export function buildNamespaceInitStatements() {...}
/** * Build an "__esModule" header statement setting the property on a given object. * 構建一個「__esModule」頭語句,在給定對象上設置屬性 */
function buildESModuleHeader() {...}
/** * Create a re-export initialization loop for a specific imported namespace. * 爲特定導入的命名空間,建立 從新導出 初始化循環。 */
function buildNamespaceReexport() {...}
/** * Build a statement declaring a variable that contains all of the exported * variable names in an object so they can easily be referenced from an * export * from statement to check for conflicts. * 構建一個聲明,聲明包含對象中全部導出變量名稱的變量的語句,以即可以從export * from語句中輕鬆引用它們以檢查衝突。 */
function buildExportNameListDeclaration() {...}
/** * Create a set of statements that will initialize all of the statically-known * export names with their expected values. * 建立一組將經過預期的值來初始化 全部靜態已知的導出名的語句 */
function buildExportInitializationStatements() {...}
/** * Given a set of export names, create a set of nested assignments to * initialize them all to a given expression. * 給定一組 export names,建立一組嵌套分配將它們所有初始化爲給定的表達式。 */
function buildInitStatement() {...}
複製代碼
而後咱們來看看他們的調用關係:
咱們以 A -> B 的形式表示在 A 中調用了 B
buildNamespaceInitStatements
:爲給定的請求源建立運行時初始化語句。這些將初始化全部運行時導入/導出邏輯
rewriteModuleStatementsAndPrepareHeader
全部通用ES6模塊重寫,以引用返回的元數據描述的模塊。
-> buildExportInitializationStatements
建立全部靜態已知的名稱的 exports
-> buildInitStatement
給定一組 export names,建立一組嵌套分配將它們所有初始化爲給定的表達式。
因此總結一下,加上前面咱們已知的第一步,其實後面的步驟分爲兩部分:
到這裏其實咱們已經能夠很清晰的知道,在 編譯階段 ,咱們一段 ES6 module 中的代碼經歷了什麼:
ES6 module 源碼 -> Babel 轉譯-> 一段能夠執行的代碼
也就是說直到編譯結束,其實咱們模塊內部的代碼都只是被轉換成了一段靜態的代碼,只有進入到 運行時 纔會被執行。
這也就讓 靜態分析 有了可能。
本文咱們從 JavaScript Module 的發展史開始聊起,一直聊到了現在與咱們息息相關的 ES6 代碼的編譯,很感謝前人走出的這些道路,讓現在我這樣的普通人也可以進入到編程的世界,也不得不感嘆,一個問題越深究,纔會發現其中並不簡單。
感謝那些可以耐心讀到這裏的人,由於這篇文章前先後後,也花了4天的時間來研究,時常感嘆有價值的資料實在太少了。
下一篇咱們會接着聊聊靜態分析,和循環引用
我是 Dendoink ,奇舞週刊原創做者,掘金 [聯合編輯 / 小冊做者] 。
對於技術人而言:技 是單兵做戰能力,術 則是運用能力的方法。駕輕就熟,出神入化就是 藝 。在前端娛樂圈,我想成爲一名出色的人民藝術家。
掃一掃關注公衆號 [ 前端惡霸 ] ,我在這裏等你: