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框架最主要的运行机制就说完了,我们这里主要是围绕着模块生命周期中的状态来进行简单的系统性的分析,并没有拆的很细,如果有兴趣或者有帮助的话,后面我会细细道来。
感谢大家,源码解析毕竟都是实战中得来,而且跟常用的知识点相关,可能会有片面性,甚至有误,如果有错请大家指正。
感谢。

猜你喜欢

转载自blog.csdn.net/jiangcs520/article/details/52643444