深刻seajs源碼系列二

模塊類和狀態類

       參照上文的demo,咱們結合源碼分析在簡單的API調用的背後,到底使用了什麼技巧來實現各個模塊的依賴加載以及模塊API的導出。css

       首先定義了一個Module類,對應與一個模塊node

function Module(uri, deps) {
  this.uri = uri
  this.dependencies = deps || []
  this.exports = null
  this.status = 0

  // Who depends on me
  this._waitings = {}

  // The number of unloaded dependencies
  this._remain = 0
}

       Module有一些屬性,uri對應該模塊的絕對url,在Module.define函數中會有介紹;dependencies爲依賴模塊數組;exports爲導出的API;status爲當前的狀態碼;_waitings對象爲當前依賴該模塊的其餘模塊哈希表,其中key爲其餘模塊的url;_remain爲計數器,記錄還未加載的模塊個數。jquery

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
}

上述爲狀態對象,記錄模塊的當前狀態:模塊初始化狀態爲0,當加載該模塊時,爲狀態fetching;模塊加載完畢而且緩存在cacheMods後,爲狀態saved;loading狀態意味着正在加載該模塊的其餘依賴模塊;loaded表示全部依賴模塊加載完畢,執行該模塊的回調函數,並設置依賴該模塊的其餘模塊是否還有依賴模塊未加載,若加載完畢執行回調函數;executing狀態表示該模塊正在執行;executed則是執行完畢,可使用exports的API。web

模塊的定義

         commonJS規範規定用define函數來定義一個模塊。define能夠接受1,2,3個參數都可,不過對於Module/wrappings規範而言,module.declare或者define函數只能接受一個參數,即工廠函數或者對象。不過原則上接受參數的個數並無本質上的區別,只不過庫在後臺給額外添加模塊名。數組

         seajs鼓勵使用define(function(require,exports,module){})這種模塊定義方式,這是典型的Module/wrappings規範實現。可是在後臺經過解析工廠函數的require方法來獲取依賴模塊並給模塊設置id和url。瀏覽器

// Define a module
Module.define = function (id, deps, factory) {
  var argsLen = arguments.length

  // define(factory)
  if (argsLen === 1) {
    factory = id
    id = undefined
  }
  else if (argsLen === 2) {
    factory = deps

    // define(deps, factory)
    if (isArray(id)) {
      deps = id
      id = undefined
    }
    // define(id, factory)
    else {
      deps = undefined
    }
  }

  // Parse dependencies according to the module factory code
  // 若是deps爲非數組,則序列化工廠函數獲取入參。
  if (!isArray(deps) && isFunction(factory)) {
    deps = parseDependencies(factory.toString())
  }

  var meta = {
    id: id,
    uri: Module.resolve(id), // 絕對url
    deps: deps,
    factory: factory
  }

  // Try to derive uri in IE6-9 for anonymous modules
    // 導出匿名模塊的uri
  if (!meta.uri && doc.attachEvent) {
    var script = getCurrentScript()

    if (script) {
      meta.uri = script.src
    }

    // NOTE: If the id-deriving methods above is failed, then falls back
    // to use onload event to get the uri
  }

  // Emit `define` event, used in nocache plugin, seajs node version etc
  emit("define", meta)

  meta.uri ? Module.save(meta.uri, meta) :
      // Save information for "saving" work in the script onload event
      anonymousMeta = meta
}

模塊定義的最後,經過Module.save方法,將模塊保存到cachedMods緩存體中。緩存

parseDependencies方法比較巧妙的獲取依賴模塊。他經過函數的字符串表示,使用正則來獲取require(「…」)中的模塊名。app

var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/g
var SLASH_RE = /\\\\/g

function parseDependencies(code) {
  var ret = []
  // 此處使用函數序列化(傳入的factory)進行字符串匹配,尋找require(「...」)的關鍵字
  code.replace(SLASH_RE, "")
      .replace(REQUIRE_RE, function(m, m1, m2) {
        if (m2) {
          ret.push(m2)
        }
      })

  return ret
}

異步加載模塊

        加載模塊能夠有多種方式,xhr方式能夠同步加載,也能夠異步加載,可是存在同源問題,所以難以在此使用。另外script tag方式在IE和現代瀏覽器下能夠保證並行加載和順序執行,script element方式也能夠保證並行加載但不保證順序執行,所以這兩種方式均可以使用。less

        在seajs中,是採用script element方式來並行加載js/css資源的,並針對舊版本的webkit瀏覽器加載css作了hack。異步

function request(url, callback, charset) {
  var isCSS = IS_CSS_RE.test(url)
  var node = doc.createElement(isCSS ? "link" : "script")

  if (charset) {
    var cs = isFunction(charset) ? charset(url) : charset
    if (cs) {
      node.charset = cs
    }
  }

  // 添加 onload 函數。
  addOnload(node, callback, isCSS, url)

  if (isCSS) {
    node.rel = "stylesheet"
    node.href = url
  }
  else {
    node.async = true
    node.src = url
  }

  // For some cache cases in IE 6-8, the script executes IMMEDIATELY after
  // the end of the insert execution, so use `currentlyAddingScript` to
  // hold current node, for deriving url in `define` call
  currentlyAddingScript = node

  // ref: #185 & http://dev.jquery.com/ticket/2709
  baseElement ?
      head.insertBefore(node, baseElement) :
      head.appendChild(node)

  currentlyAddingScript = null
}

function addOnload(node, callback, isCSS, url) {
  var supportOnload = "onload" in node

  // for Old WebKit and Old Firefox
  if (isCSS && (isOldWebKit || !supportOnload)) {
    setTimeout(function() {
      pollCss(node, callback)
    }, 1) // Begin after node insertion
    return
  }

  if (supportOnload) {
    node.onload = onload
    node.onerror = function() {
      emit("error", { uri: url, node: node })
      onload()
    }
  }
  else {
    node.onreadystatechange = function() {
      if (/loaded|complete/.test(node.readyState)) {
        onload()
      }
    }
  }

  function onload() {
    // Ensure only run once and handle memory leak in IE
    node.onload = node.onerror = node.onreadystatechange = null

    // Remove the script to reduce memory leak
    if (!isCSS && !data.debug) {
      head.removeChild(node)
    }

    // Dereference the node
    node = null

    callback()
  }
}
// 針對 舊webkit和不支持onload的CSS節點判斷加載完畢的方法
function pollCss(node, callback) {
  var sheet = node.sheet
  var isLoaded

  // for WebKit < 536
  if (isOldWebKit) {
    if (sheet) {
      isLoaded = true
    }
  }
  // for Firefox < 9.0
  else if (sheet) {
    try {
      if (sheet.cssRules) {
        isLoaded = true
      }
    } catch (ex) {
      // The value of `ex.name` is changed from "NS_ERROR_DOM_SECURITY_ERR"
      // to "SecurityError" since Firefox 13.0. But Firefox is less than 9.0
      // in here, So it is ok to just rely on "NS_ERROR_DOM_SECURITY_ERR"
      if (ex.name === "NS_ERROR_DOM_SECURITY_ERR") {
        isLoaded = true
      }
    }
  }

  setTimeout(function() {
    if (isLoaded) {
      // Place callback here to give time for style rendering
      callback()
    }
    else {
      pollCss(node, callback)
    }
  }, 20)
}

其中有些細節還需注意,當採用script element方法插入script節點時,儘可能做爲首個子節點插入到head中,這是因爲一個難以發現的bug:

GLOBALEVAL WORKS INCORRECTLY IN IE6 IF THE CURRENT PAGE HAS <BASE HREF> TAG IN THE HEAD

fetch模塊 

          初始化Module對象時,狀態爲0,該對象對應的js文件並未加載,若要加載js文件,須要使用上節提到的request方法,可是也不可能僅僅加載該文件,還須要設置module對象的狀態及其加載module依賴的其餘模塊。這些邏輯在fetch方法中得以體現:

// Fetch a module
// 加載該模塊,fetch函數中調用了seajs.request函數
Module.prototype.fetch = function(requestCache) {
  var mod = this
  var uri = mod.uri

  mod.status = STATUS.FETCHING

  // Emit `fetch` event for plugins such as combo plugin
  var emitData = { uri: uri }
  emit("fetch", emitData)
  var requestUri = emitData.requestUri || uri

  // Empty uri or a non-CMD module
  if (!requestUri || fetchedList[requestUri]) {
    mod.load()
    return
  }

  if (fetchingList[requestUri]) {
    callbackList[requestUri].push(mod)
    return
  }

  fetchingList[requestUri] = true
  callbackList[requestUri] = [mod]

  // Emit `request` event for plugins such as text plugin
  emit("request", emitData = {
    uri: uri,
    requestUri: requestUri,
    onRequest: onRequest,
    charset: data.charset
  })

  if (!emitData.requested) {
    requestCache ?
        requestCache[emitData.requestUri] = sendRequest :
        sendRequest()
  }

  function sendRequest() {
    seajs.request(emitData.requestUri, emitData.onRequest, emitData.charset)
  }
  // 回調函數
  function onRequest() {
    delete fetchingList[requestUri]
    fetchedList[requestUri] = true

    // Save meta data of anonymous module
    if (anonymousMeta) {
      Module.save(uri, anonymousMeta)
      anonymousMeta = null
    }

    // Call callbacks
    var m, mods = callbackList[requestUri]
    delete callbackList[requestUri]
    while ((m = mods.shift())) m.load()
  }
}

其中seajs.request就是上節的request方法。onRequest做爲回調函數,做用是加載該模塊的其餘依賴模塊。

在下一節,將介紹模塊之間依賴的加載以及模塊的執行。

相關文章
相關標籤/搜索