zoukankan      html  css  js  c++  java
  • 深入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 = /"(?:\"|[^"])*"|'(?:\'|[^'])*'|/*[Ss]*?*/|/(?:\/|[^/
    ])+/(?=[^/])|//.*|.s*require|(?:^|[^$])requires*(s*(["'])(.+?)1s*)/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作为回调函数,作用是加载该模块的其他依赖模块。

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

  • 相关阅读:
    记录——framework探测定位程序集与core探测定位程序集
    C# 特定框架适用特定代码
    python读取excel代码
    时间比较
    ORA 01791错误
    MongoDB.1什么是MongoDB
    Mayatis 异常之result maps collection already contains value...
    怎样做好黄焖鸡
    关于foreach
    C#之out,ref关键字
  • 原文地址:https://www.cnblogs.com/accordion/p/4303436.html
Copyright © 2011-2022 走看看