由於一直在使用,所以了解了下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方法做的事情主要就是用一個正規表示式把函數體裡面所有require(XXX)裡面的XXX提取出來,這也就是這個函數依賴到的所有模組了。
方法本身不複雜,但這個正規表示式不簡單:
分析完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的核心邏輯就差不多都看到了。供參考,有理解不到位或表達不準確的地方,歡迎一起探討。
以上所述就是本文的全部內容了,希望大家能夠喜歡。