JavaScript 模塊化及 SeaJs 源碼分析
網頁的結構越來越復雜,簡直可以看做一個簡單APP,如果還像以前那樣把所有的代碼都放到一個文件里面會有一些問題:
-
全局變量互相影響
-
JavaScript文件變大,影響加載速度
-
結構混亂、很難維護
和后端(比如Java)比較就可以看出明顯的差距。2009年Ryan Dahl創(chuàng)建了node.js項目,將JavaScript用于服務器編程,這標志“JS模塊化編程”正式誕生。
基本原理
模塊就是一些功能的集合,那么可以將一個大文件分割成一些小文件,在各個文件中定義不同的功能,然后在HTML中引入:
- var module1 = new Object({
- _count : 0,
- m1 : function (){
- //...
- },
- m2 : function (){
- //...
- }
- });
這樣做的壞處是:把模塊中所有的成員都暴露了!我們知道函數(shù)的本地變量是沒法從外面進行訪問的,那么可以用立即執(zhí)行函數(shù)來優(yōu)化:
- var module1 = (function(){
- var _count = 0;
- var m1 = function(){
- //...
- };
- var m2 = function(){
- //...
- };
- return {
- m1 : m1, m2 : m2
- };
- })();
大家定義模塊的方式可能五花八門,如果都能按照一定的規(guī)范來,那好處會非常大:可以互相引用!
模塊規(guī)范
在node.js中定義math.js模塊如下:
- function add(a, b){
- return a + b;
- }
- exports.add = add;
在其他模塊中使用的時候使用全局require函數(shù)加載即可:
- var math = require('math');
- math.add(2,3);
在服務器上同步require是沒有問題的,但是瀏覽器在網絡環(huán)境就不能這么玩了,于是有了異步的AMD規(guī)范:
- require(['math'], function (math) {// require([module], callback);
- math.add(2, 3);
- });
模塊的定義方式如下(模塊可以依賴其他的模塊):
- define(function (){ // define([module], callback);
- var add = function (x,y){
- return x+y;
- };
- return { add: add };
- });
用RequireJS可以加載很多其他資源(看這里),很好很強大!在工作中用的比較多的是SeaJS,所使用的規(guī)范稱為CMD,推崇(應該是指異步模式):
as lazy as possible!
對于依賴的模塊的處理方式和AMD的區(qū)別在于:
AMD是提前執(zhí)行(依賴前置),CMD是延遲執(zhí)行(依賴就近)。
在CMD中定義模塊的方式如下:
- define(function(require, exports, module) {
- var a = require('./a');
- a.doSomething();
- var b = require('./b');
- b.doSomething();
- });
使用方式直接看文檔,這里就不贅述了!
SeaJS源碼分析
剛接觸模塊化的時候感覺這個太簡單了,不就是:
創(chuàng)建script標簽的時候設置一下onload和src!
事實上是這樣的,但也不完全是!下面來開始看SeaJS的代碼(sea-debug.js)。一個模塊在加載的過程中可能經歷下面幾種狀態(tài):
- var STATUS = Module.STATUS = {
- // 1 - The `module.uri` is being fetched
- FETCHING: 1,
- // 2 - The meta data has been saved to cachedMods
- SAVED: 2,
- // 3 - The `module.dependencies` are being loaded
- LOADING: 3,
- // 4 - The module are ready to execute
- LOADED: 4,
- // 5 - The module is being executed
- EXECUTING: 5,
- // 6 - The `module.exports` is available
- EXECUTED: 6,
- // 7 - 404
- ERROR: 7
- }
內存中用Modul
對象來維護模塊的信息:
- function Module(uri, deps) {
- this.uri = uri
- this.dependencies = deps || [] // 依賴模塊ID列表
- this.deps = {} // 依賴模塊Module對象列表
- this.status = 0 // 狀態(tài)
- this._entry = [] // 在模塊加載完成之后需要調用callback的模塊
- }
在頁面上啟動模塊系統(tǒng)需要使用seajs.use
方法:
- seajs.use(‘./main’, function(main) {// 依賴及回調方法
- main.init();
- });
加載過程的整體邏輯可以在Module.prototype.load
中看到:
- Module.prototype.load = function() {
- var mod = this
- if (mod.status >= STATUS.LOADING) {
- return
- }
- mod.status = STATUS.LOADING
- var uris = mod.resolve() // 解析依賴模塊的URL地址
- emit("load", uris)
- for (var i = 0, len = uris.length; i < len; i++) {
- mod.deps[mod.dependencies[i]] = Module.get(uris[i])// 從緩存取或創(chuàng)建
- }
- mod.pass(); // 將entry傳遞給依賴的但還沒加載的模塊
- if (mod._entry.length) {// 本模塊加載完成
- mod.onload()
- return
- }
- var requestCache = {};
- var m;
- // 加載依賴的模塊
- for (i = 0; i < len; i++) {
- m = cachedMods[uris[i]]
- if (m.status < STATUS.FETCHING) {
- m.fetch(requestCache)
- } else if (m.status === STATUS.SAVED) {
- m.load()
- }
- }
- for (var requestUri in requestCache) {
- if (requestCache.hasOwnProperty(requestUri)) {
- requestCache[requestUri]()
- }
- }
- }
總體上邏輯很順就不講了,唯一比較繞的就是_entry
數(shù)組了。網上沒有找到比較通俗易懂的文章,于是看著代碼連蒙帶猜地大概看懂了,其實只要記住它的目標即可:
當依賴的所有模塊加載完成后執(zhí)行回調函數(shù)!
換種說法:
數(shù)組_entry中保存了當前模塊加載完成之后、哪些模塊的依賴可能加載完成的列表(依賴的反向關系)!
舉個例子,模塊A依賴于模塊B、C、D,那么經過pass之后的狀態(tài)如下:
此時A中的remain
為3,也就是說它還有三個依賴的模塊沒有加載完成!而如果模塊B依賴模塊E、F,那么在它load的時候會將A也傳遞出去:
有幾個細節(jié):
-
已經加載完成的模塊不會被傳播;
-
已經傳播過一次的模塊不會再次傳播;
-
如果依賴的模塊正在加載那么會遞歸傳播;
維護好依賴關系之后就可以通過Module.prototype.fetch
來加載模塊,有兩種sendRequest
的實現(xiàn)方式:
-
importScripts
-
script
然后根據結果執(zhí)行load
或者error
方法。依賴的所有模塊都加載完成后就會執(zhí)行onload
方法:
- Module.prototype.onload = function() {
- var mod = this
- mod.status = STATUS.LOADED
- for (var i = 0, len = (mod._entry || []).length; i < len; i++) {
- var entry = mod._entry[i]
- if (--entry.remain === 0) {
- entry.callback()
- }
- }
- delete mod._entry
- }
其中--entry.remain
就相當于告訴entry對應的模塊:你的依賴列表里面已經有一個完成了!而entry.remain === 0
則說明它所依賴的所有的模塊都已經加載完成了!那么此時將執(zhí)行回調函數(shù):
- for (var i = 0, len = uris.length; i < len; i++) {
- exports[i] = cachedMods[uris[i]].exec();
- }
- if (callback) {
- callback.apply(global, exports)// 執(zhí)行回調函數(shù)
- }
腳本下載完成之后會馬上執(zhí)行define
方法來維護模塊的信息:
沒有顯式地指定dependencies時會用parseDependencies來用正則匹配方法中的require()片段(指定依賴列表是個好習慣)。
接著執(zhí)行factory
方法來生成模塊的數(shù)據:
- var exports = isFunction(factory) ?
- factory.call(mod.exports = {}, require, mod.exports, mod) :
- factory
然后執(zhí)行你在seajs.use中定義的callback
方法:
- if (callback) {
- callback.apply(global, exports)
- }
當你寫的模塊代碼中require時,每次都會執(zhí)行factory方法:
- function require(id) {
- var m = mod.deps[id] || Module.get(require.resolve(id))
- if (m.status == STATUS.ERROR) {
- throw new Error('module was broken: ' + m.uri)
- }
- return m.exec()
- }
到這里核心的邏輯基本上講完了,補一張狀態(tài)的轉換圖:
以后在用的時候就可以解釋一些詭異的問題了!
總結
模塊化非常好用,因此在ECMAScript 6中也開始支持,但是瀏覽器支持還是比較堪憂的~~