深入seajs源码系列二

模块类和状态类

       参照上文的demo,我们结合源码分析在简单的API调用的背后,到底使用了什么技巧来实现各个模块的依赖加载以及模块API的导出。

       首先定义了一个Module类,对应与一个模块

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为计数器,记录还未加载的模块个数。

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。

模块的定义

         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(“…”)中的模块名。

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方式也可以保证并行加载但不保证顺序执行,因此这两种方式都可以使用。

        在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作为回调函数,作用是加载该模块的其他依赖模块。

在下一节,将介绍模块之间依赖的加载以及模块的执行。

你可能感兴趣的:(seajs)