leoyuan.github.io icon indicating copy to clipboard operation
leoyuan.github.io copied to clipboard

SeaJS 2.0.0源码浅析 - 还是从use说起

Open LeoYuan opened this issue 12 years ago • 7 comments

本文标题是根据 @pigcan 的一篇旧文实例解析SeaJS的源码而取的,如今SeaJS也更新到了2.0.0,某些代码已经做了重构优化,所以想重新梳理下代码,以此文做记录。 依旧是熟悉的index.js/a.js/b.js,代码分别如下: index.js

seajs.use('./a', function(a) {
  document.getElementById('console').innerHTML = a;
})

a.js

;define(function(require, exports, module) {
  var numInA = 1;
  var b = require('./b');
  return b.numInB + numInA;
});

b.js

;define(function(require, exports, module) {
  return {
    numInB: 2
  }
});

准备工作ready,代码跑起来。 代码运行流程如下:

  1. 首先进入入口函数seajs.use
seajs.use = function(ids, callback) {
  // Load preload modules before all other modules
  // 2) preload函数
  preload(function() {
    use(resolve(ids), callback)
  })
  return seajs
}
  1. preload函数定义,意在优先加载在seajs.config中preload配置的模块,然后再加载指定模块
function preload(callback) {
  var preloadMods = configData.preload
  var len = preloadMods.length

  if (len) {
    // 3) use函数
    use(resolve(preloadMods), function() {
      // Remove the loaded preload modules
      preloadMods.splice(0, len)

      // Allow preload modules to add new preload modules
      preload(callback)
    })
  }
  else {
    callback()
  }
}

注:resolve函数实际调用了id2url函数,在此处,读者只需要认为可以将id转化为url即可, 如 ./a -> http://localhost/test_seajs/a.js 3) use函数定义,执行回调callback时,会将获取到的各个模块的exports对象传入

function use(uris, callback) {
  isArray(uris) || (uris = [uris])
  // 4) load函数
  load(uris, function() {
    var exports = []

    for (var i = 0; i < uris.length; i++) {
      exports[i] = getExports(cachedModules[uris[i]])
    }

    if (callback) {
      callback.apply(global, exports)
    }
  })
}

  1. load函数,seajs中实际加载js/css文件的函数
function load(uris, callback) {
  // 过滤已经加载过的模块,并且为未加载的模块创建Module实例,放入到cachedModules中保存
  var unloadedUris = getUnloadedUris(uris)

  if (unloadedUris.length === 0) {
    callback()
    return
  }

  // Emit `load` event for plugins such as plugin-combo
  emit("load", unloadedUris)

  var len = unloadedUris.length
  var remain = len

  for (var i = 0; i < len; i++) {
    (function(uri) {
      var mod = cachedModules[uri]

      if (mod.dependencies.length) {
        loadWaitings(function(circular) {
          mod.status < STATUS_SAVED ? fetch(uri, cb) : cb()
          function cb() {
            done(circular)
          }
        })
      }
      else {
        // 5) fetch函数
        // 显然第一次走这个分支,执行fetch(uri, loadWaitings)函数
        mod.status < STATUS_SAVED ?
            fetch(uri, loadWaitings) : done()
      }

      // 该函数会在经过如下函数后 fetch -> request -> addOnload -> onRequested, 
      // 最后在onRequested中被调用
      function loadWaitings(cb) {
        cb || (cb = done)

        var waitings = getUnloadedUris(mod.dependencies)
        if (waitings.length === 0) {
          cb()
        }
        // Break circular waiting callbacks
        else if (isCircularWaiting(mod)) {
          printCircularLog(circularStack)
          circularStack.length = 0
          cb(true)
        }
        // Load all unloaded dependencies
        else {
          waitingsList[uri] = waitings
          load(waitings, cb)
        }
      }

      function done(circular) {
        if (!circular && mod.status < STATUS_LOADED) {
          mod.status = STATUS_LOADED
        }

        if (--remain === 0) {
          callback()
        }
      }

    })(unloadedUris[i])
  }
}
  1. fetch函数,回调调用loadWaitings函数
function fetch(uri, callback) {
  cachedModules[uri].status = STATUS_FETCHING

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

  if (fetchedList[requestUri]) {
    callback()
    return
  }

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

  fetchingList[requestUri] = true
  callbackList[requestUri] = [callback]

  // Emit `request` event for plugins such as plugin-text
  var charset = configData.charset
  emit("request", data = {
    uri: uri,
    requestUri: requestUri,
    callback: onRequested,
    charset: charset
  })

  if (!data.requested) {
    // 6) request函数
    // 第一次加载./a走此分支
    request(data.requestUri, onRequested, charset)
  }
  // 8) onRequested回调
  function onRequested() {
    delete fetchingList[requestUri]
    fetchedList[requestUri] = true

    // Save meta data of anonymous module
    if (anonymousModuleData) {
      save(uri, anonymousModuleData)
      anonymousModuleData = undefined
    }

    // Call callbacks
    var fn, fns = callbackList[requestUri]
    delete callbackList[requestUri]
    while ((fn = fns.shift())) fn()
  }
}
  1. request函数,真正往当前页面中插入script/link标签的函数,并且加上了script/link的onload监听回调。 此处我觉得设计极妙,在插入script后,浏览器马上下载a.js,并且执行define代码块,因为此时没有指定模块的id, 于是会生成一个anonymousModuleData变量,记录了a.js的依赖b.js,工厂方法等, 而上面执行完后,script的onload回调被触发,onload内部触发onRequested函数,onRequested函数是定义在fetch内部的闭包,刚好能取到此时的uri参数和anonymousModuleData变量,两者结合,刚好构造出描述a.js完整的Module实例。
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
    }
  }

  addOnload(node, callback, isCSS)

  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 = undefined
}
  1. 执行define代码块 上面已经说明,在执行request函数时,a.js被浏览器下载并执行,生成anonymousModuleData变量。
  2. 执行onRequested回调,onRequested内部又调用了loadWaitings函数,“恰好”loadWaitings是在 load函数中定义的一个闭包,能够访问到此时load函数中加载的模块aMod,继而发现aMod的依赖bMod, 于是执行load('./b', callback),循环第四步到第八步

至此,代码已基本走完,做一些回顾: 总体思路:代码从seajs.use开始,回调一层套一层,直到最后由script的onload调起onRequested函数触发回调, 逐层依次执行,直到回到最初的回调 --- seajs.use的回调,此回调将所有模块的exports对象传入执行。 亮点:

  1. 在load中放置闭包loadWaitings来获取此时load的模块对象;
  2. 在fetch中放置闭包onRequested来获取此时fetch的uri,借此与anonymousModuleData结合生成完整的Module实例, 为getExports方法提供了各模块至关重要的factory方法。

LeoYuan avatar Apr 21 '13 17:04 LeoYuan

@lifesinger 整个SeaJS代码看下来,感觉代码中回调层层嵌套,结合了script onload的回调,并且闭包运用的也恰到好处,大师就是大师,不佩服都不行。:+1: :+1: :+1: 我看代码时一开始也是用脑袋记各个回调,发现到后来完全混乱了,只好在本子上涂涂画画来记忆,整个设计真的很妙,不知玉伯你设计的时候是怎么想的?有辅助工具?

LeoYuan avatar Apr 21 '13 17:04 LeoYuan

@LeoYuan 也不是一开始就写成这样,不断重构的结果,呵呵。辅助工具是纸和笔,不断在大脑中重构,慢慢调整完善到现在的代码。

lifesinger avatar Apr 22 '13 01:04 lifesinger

@lifesinger 呵呵,精益求精,不断打磨,不断做有意义的重构,这应该咱们每个程序员都应该效仿的。 还有一个问题需要求证下,玉伯,我上边对于seajs整个流程的描述不知道有没有理解偏差/错误的地方?

LeoYuan avatar Apr 22 '13 06:04 LeoYuan

没问题的。

2013/4/22 LeoYuan 袁力皓 [email protected]

@lifesinger https://github.com/lifesinger 呵呵,精益求精,不断打磨,不断做有意义的重构,这应该咱们每个程序员都应该效仿的。 还有一个问题需要求证下,玉伯,我上边对于seajs整个流程的描述不知道有没有理解偏差/错误的地方?

— Reply to this email directly or view it on GitHubhttps://github.com/LeoYuan/leoyuan.github.com/issues/7#issuecomment-16756726 .

王保平 / 玉伯(射雕) 送人玫瑰手有余香

lifesinger avatar Apr 22 '13 09:04 lifesinger

少读源码多使用。

afc163 avatar Apr 24 '13 12:04 afc163

@afc163 呵呵,多谢提醒。 不过,其实我也不是源码控,在有时间并且有兴趣的条件下,看一看也未尝不可啊。

LeoYuan avatar Apr 25 '13 01:04 LeoYuan

乍看一眼你的博客地址,以为是片友,震惊了一下。。。

LeoYuan avatar Apr 25 '13 01:04 LeoYuan