1. 程式人生 > >seajs原始碼分析-執行機制淺析(一)

seajs原始碼分析-執行機制淺析(一)

前端技術發展簡直是日新月異,隨著angularjs,vuejs,reactjs等等這些框架的不斷興起,轉眼間jquery,seajs,Backbone這些框架已經成了清朝的框架了,再加上es6本身對於模組化的支援,也許,seajs模組化在將來的某天可能會徹底成為歷時,儘管如此,我認為對於框架的學習,不僅僅是學會怎麼用它,更重要的理解其中的一些思想,理解框架的思維,才能幹翻框架,框架始終是個輔助。廢話到此為止。
其實seajs程式碼量也就近千行,而且做的事情並不多,也就是模組化和按需非同步載入,光光實現這兩點需求,我們自己也可以寫一個簡單的框架,200行程式碼也許就搞定了,但是框架的使命並不僅僅是能用,這才是我們需要從中學習的。本篇就先系統講解一下seajs的執行機制。
先介紹一下seajs對外最重要的幾個元素,seajs.config,seajs.use,define,require,exports,module
再貼兩段虛擬碼:

    seajs.config({
        base:'./',
        alias:{
            'todomodule':'app-modules/todo'
        },
        paths:{
            'todomodule':'modules/pathtest/todo',
            'app-modules':'app/modules'
        }
    });
    seajs.use('./app.js');

app.js內容如下:

define(function(require,exports,module){
    var todo = require
('todomodule'); ... });

以上程式碼,已經是一個seajs專案的骨架了,就是這麼簡單。
接下來就來說說,看上去的這麼點程式碼,seajs具體做了什麼呢。首先來看config方法,這個配置方法暫時不多做細節上的介紹,先提一點,alias和paths變數不要弄混淆了,具體的後面有機會再細說。接下來說說入口函式use,解釋use之前,還有個方法論需要普及,seajs最重要的內容其實就是在於模組,也就是模組載入和執行,其實seajs程式碼理解的邏輯很清晰,只要抓住模組的生命週期來分析程式碼,就很清晰了,看如下這段程式碼代表的模組的生命週期:

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 }

以上就是seajs模組生命週期的所有狀態變化,程式碼執行的不同時期,根據模組的狀態就知道了。好了,接下來回到use這個入口函式。
use函式其實就是呼叫了Module.use,那麼Module.use做了啥呢,它的註釋是這麼說的// Use function is equal to load a anonymous module,這裡就不翻譯成中文了,意會一下,那麼,這個函式主要是初始化了一個Module物件mod,好了,接下來所有發生的事情都可以對應上這個mod的狀態。所有節點故事的發生都是源於第一個Module物件,後面所有載入的模組都是作為依賴或者子依賴載入進來的,而use函式就是負責開枝散葉的工作,use函式中定義的Module物件會分配一個預設的ID,http://domain:port/app/_use_0,這是一個虛擬的根模組,也是入口模組,該模組的依賴只有一個,那就是seajs.use函式所傳入的uri,因為這是一個虛擬的模組,所以我們應該從他這個唯一依賴模組入手,還是拿上述虛擬碼來說,這裡的唯一依賴模組就是’app.js’,嚴格來說,這才是真正的入口模組。use函式除了初始化工作之外,呼叫了mod.load()函式,
Module.use函式簡化虛擬碼如下:

// Use function is equal to load a anonymous module
Module.use = function (ids, callback, uri) {
  var mod = Module.get(uri, isArray(ids) ? ids : [ids])
  mod._entry.push(mod)
  ...
  mod.callback = function() {}
  mod.load()
}

load函式簡化虛擬碼如下:

1.    Module.prototype.load = function() {
2.      var mod = this
3.      // If the module is being loaded, just wait it onload call
4.      if (mod.status >= STATUS.LOADING) {
5.        return
6.      }
7.    mod.status = STATUS.LOADING
8.    var uris = mod.resolve()
9.    emit("load", uris)
10. 
11.   for (var i = 0, len = uris.length; i < len; i++) {
12.     mod.deps[mod.dependencies[i]] = Module.get(uris[i])
13.   }
14.   // Pass entry to it's dependencies
15.   mod.pass()
16.   // If module has entries not be passed, call onload
17.   if (mod._entry.length) {
18.     mod.onload()
19.     return
20.   }
21.   // Begin parallel loading
22.   var requestCache = {}
23.   var m
24.   for (i = 0; i < len; i++) {
25.     m = cachedMods[uris[i]]
26.     if (m.status < STATUS.FETCHING) {
27.       m.fetch(requestCache)
28.     }
29.     else if (m.status === STATUS.SAVED) {
30.       m.load()
31.     }
32.   }
33.   // Send all requests at last to avoid cache bug in IE6-9. Issues#808
34.   for (var requestUri in requestCache) {
35.     if (requestCache.hasOwnProperty(requestUri)) {
36.       requestCache[requestUri]()
37.     }
38.   }
39. }

use函式中mod物件定義了兩個重要屬性,_entry和callback,其中_entry顧名思義就是入口模組的意思,callback就是個回撥函式,這兩個元素的重要作用後面會提到。先說說load函式,之前有說根模組是個虛擬的模組,實際第一個載入的模組是虛擬根模組唯一依賴模組,既然我們需要抓住這個唯一依賴模組來說事,那這個模組的生命週期開始是在哪裡呢,對了,就是在load函式中開始的,上文我們說到除了根模組,其他所有模組都是作為其依賴模組或者子依賴模組來載入的,那麼在這裡,就根據這個原理來看這段程式碼。首先看第8行,module.resolve函式其實是解析模組的依賴,所以uris陣列存放的就是當前模組的依賴模組,這裡是根模組,所以它的uris存放的就是我們要找的實際根模組的uri。然後看到27行,m.fetch(),其中m就是我們需要尋找的實際根模組,uri是app.js對應的模組,FETCHING是它的第一個狀態,這裡開始它的生命週期。fetch函式獲取該模組的uri並開始載入模組指令碼檔案,載入完畢後立即執行define函式。
接下來又是seajs最重要的函式之一define函式,這個函式主要進行解析當前模組的所有依賴模組,並建立且初始化當前模組(也就是app.js模組),開始進入到SAVED狀態,而後當前模組繼續呼叫load函式,根據解析獲取的依賴模組路徑陣列準備載入所有的依賴模組,進入LOADING狀態,並開始載入依賴模組,直到所有依賴模組載入完畢進入LOADED狀態,所有的模組都進行周而復始,直到所有模組都進入LOADED狀態,即已經準備就緒可以進入EXECTING狀態。
這時候該來說說use函式中mod物件定義的那兩個重要的屬性,_entry和callback,其中_entry主要作用就是傳遞入口,load函式中第15行程式碼呼叫mod.pass()函式,其實就是把這裡的虛擬根模組層層傳遞給依賴模組,直到最後一個依賴,根據該模組_entry陣列是否有值來判斷是否最後一個依賴模組載入完畢,若載入完畢,那麼就呼叫這個入口模組的callback函式,也就是use函式定義的mod物件的callback函式。接下來看看這個callback函式做了什麼,該函式程式碼如下:

mod.callback = function() {
    var exports = []
    var uris = mod.resolve()
    for (var i = 0, len = uris.length; i < len; i++) {
      exports[i] = cachedMods[uris[i]].exec()
    }
    if (callback) {
      callback.apply(global, exports)
    }
    delete mod.callback
    delete mod.history
    delete mod.remain
    delete mod._entry
  }

直接看第5行程式碼,cachedMods[uris[i]].exec(),沒錯,從這裡開始執行我們實際入口模組(app.js)的回撥,所有模組開始進入EXECTING狀態,直到EXCUTED狀態,執行完畢。
到此為止,seajs框架最主要的執行機制就說完了,我們這裡主要是圍繞著模組生命週期中的狀態來進行簡單的系統性的分析,並沒有拆的很細,如果有興趣或者有幫助的話,後面我會細細道來。
感謝大家,原始碼解析畢竟都是實戰中得來,而且跟常用的知識點相關,可能會有片面性,甚至有誤,如果有錯請大家指正。
感謝。