深入探尋seajs的模組化與載入方式,探尋seajs
由於一直在使用,所以瞭解了下seajs的原始碼。這裡是我對下面幾個問題的理解:
1、seajs的require(XXX)的方法是怎樣實現模組載入的?
2、為什麼需要預先載入?
3、為什麼需要構建工具?
4、構建前後的代碼究竟有些什麼區別,為什麼要這麼做?
問題1: seajs的require(XXX)的方法是怎樣實現模組載入的?
代碼邏輯比較繞,對原始碼的理解放在文章的末尾,這裡先簡單梳理下模組載入的邏輯:
1、從seajs.use方法入口,開始載入use到的模組。
2、use到的模組這時mod緩衝當中一定是不存在的。seajs建立一個新的mod,賦予一些初始的狀態。
3、執行mod.load方法
4、一堆邏輯之後走到seajs.request方法,請求模組檔案。模組載入完成之後,執行define方法。
5、define方法分析提模數塊的相依模組,儲存起來。緩衝factory但不執行。
6、模組的相依模組再被載入,如果繼續有相依模組,則繼續載入。直至所有被依賴的模組都載入完畢。
7、所有的模組載入完畢之後,執行use方法的callback.
8、模組內部邏輯從callback開始執行。require方法在這個過程當中才被執行。
問題2:為什麼需要預先載入?
我們看到seajs.use方法實際上是在所有相依模組都載入完了之後才執行callback。可以理解成在商務邏輯代碼在執行之前,必須先預先載入所有被依賴的模組代碼。那麼為什麼是一個這樣必須先做預先載入的邏輯?
答案在於邏輯代碼裡面引用其他模組方法的這個require方法的執行方法:
var mod = require(id);
這個文法決定了mod的取得是個同步執行的過程,如果模組代碼在此之前沒有被預先載入的話,就只能採用非同步載入回調的方法來實現了,那麼整個seajs的執行邏輯將完全會是另一個樣子。因為非同步你會搞不懂模組的執行順序,邏輯會變的難以掌控。
問題3:為什麼需要構建工具?
可以看到沒有構建前各個相依模組都是單獨載入的。這會產生過多的模組請求,對於頁面的載入效能是不利的。構建工具本質上就是為瞭解決模組合併載入的問題。
問題4:構建前後的代碼究竟有些什麼區別,為什麼要這麼做?
構建工具究竟做了些什麼。我們說它本質上是為瞭解決代碼合併載入的問題,那麼它所做的只是簡單的將各個模組檔案合并成一個檔案?
當然不是。測試一下,你如果只是簡單把幾個模組檔案合并到一個檔案以後,會發現這個檔案根本沒有辦法正常執行。
原因在於define方法的實現。
seajs是推崇定義模組的時候只在define方法傳入factory參數的。回顧define方法內部,當沒有傳入id(姑且等同於模組的url)時,會通過getCurrentScript()方法去取得當前正在執行的這個模組檔案的url路徑,然後把這個路徑作為索引值與模組本身一起緩衝到cachedMods。這裡很關鍵的一點是,整個seajs內部的這個模組緩衝機制其實是依賴每個模組的url來做緩衝的索引值。require(id)方法,歸根結底也是通過url索引值到。require(id)方法,歸根結底也是通過url索引值到cachedMods裡面去找相應的模組。這個索引值不能重複不能出錯,不然模組的對應關係就混亂了。如果把a、b、c幾個模組檔案簡單合并到一個目標檔案x之後,getCurrentScript()只能擷取到x的路徑,三個模組的索引值就沒法做出區別了,執行肯定出錯。
所以如果要把幾個模組檔案合并在一起,就必須為每個模組明確uri。也就是define方法必須都傳入id參數。當id傳入的時候,seajs會將這個id轉換為url用作緩衝的索引值。
如果只傳id和factory,也就是 define(id, factory),那麼deps = undefined,define方法就會去執行parseDependencies(factory.toString())方法提取factory裡面的相依模組,後續會走到解析模組路徑,線上單獨載入各個模組的邏輯裡面去,這個時候就失去了合併載入的意義了。
所以合併載入,define方法必須正確的傳入id,deps,factory三個參數才能正確執行。
seajs 所謂CMD的模組定義方法,是提倡大家寫模組的階段都只傳factory一個參數的,其他兩個參數在後期代碼構建的階段來產生。上面解釋了為什麼這兩參數在構建後是必須的。
至於為什麼提倡定義模組的時候只傳factory,我看主要是因為手工傳入的id和deps參數,極易出錯,不便維護。工具可以提高效率並保證參數的正確。
附: 對seajs 主要代碼邏輯的理解。
說明:原始碼版本是Sea.js 2.3.0
1、先看看define方法做了些什麼
Module.define = function (id, deps, factory)
define方法的時候,支援三個參數。其中id,deps是選填的。factory必須。代碼裡面通過以下邏輯來控制:
但其實deps是必須的,因為seajs必須知道每個模組依賴了哪些模組,不然無法執行載入。
所以,當factory是函數,並且deps沒有被主動傳入的時候,就需要使用parseDependencies方法來分析出factory當中的相依模組了。
parseDependencies方法做的事情主要就是用一個Regex把函數體裡面所有require(XXX)裡面的XXX提取出來,這也就是這個函數依賴到的所有模組了。
方法本身不複雜,但是這個Regex不簡單:
分析完deps之後,將模組定義存入緩衝:
注意,我們會發現define方法純粹只是分析模組、儲存模組,並沒有執行模組。
2、真正執行模組,是在require方法裡面。我們接下來看require。
簡而言之require方法就是根據id在define定義儲存的模組緩衝中找到相應的模組,並執行它,獲得模組定義返回的方法:
整個這個大步驟中,有一個很關鍵的步驟,有必要細說:
Module.get(require.resolve(id))。
require一個模組的時候,首先要找到這個模組。 Module.get方法就起這個作用。
cachedMods裡面沒有的話,就建立一個新的Module並緩衝到cachedMods裡面:
define和rquire方法這樣看來不算複雜。seajs主要還是模組載入的邏輯有點複雜。
3、seajs真正執行的入口,是use方法:
通過use方法,從這裡的ids開始觸發模組的載入和執行。
可以看到載入的關鍵點在mod.load方法。
load方法代碼有點長,其中的主要邏輯是:判斷mod的目前狀態是否為已載入或者載入中。
在Module的舒適化函數中,我們可以看到status預設值是0.
所以沒有載入過的新模組,到這裡都是: mod.status = STATUS.LOADING 狀態設定為載入中,並執行後續載入邏輯。
接來下是擷取模組的依賴urls
mod.resolve方法:
Module.resolve方法本質上就是把相對路徑、配置的path、別名等轉換成一個絕對路徑。不貼代碼了。
更新模組載入狀態。
載入模組的邏輯:
主要是m.fetch方法,裡面其他邏輯這裡略過。
可以看到 seajs.request最終會去執行模組檔案的載入:
當所有相依模組載入完了之後,執行mod的onload方法
這裡是 mod.onload()方法
到此,seajs的核心邏輯就差不多都看到了。供參考,有理解不到位或者表達不準確的地方,歡迎一起探討。
以上所述就是本文的全部內容了,希望大家能夠喜歡。