• Sea.Js的运行原理(转)


    1.CMD(Common Module Definition)规范

    Sea.js采用了和Node相似的CMD规范,使用require、exports和module来组织模块。但Sea.js比起Node的不同点在于,前者的运行环境是在浏览器中,这就导致A依赖的B模块不能同步地读取过来,所以Sea.js比起Node,除了运行之外,还提供了两个额外的东西:

       a.模块的管理

       b.模块从服务端的同步

    即Sea.js必须分为模块加载期和执行期。加载期需要将执行期所有用到的模块从服务端同步过来,在再执行期按照代码的逻辑顺序解析执行模块。本身执行期与node的运行期没什么区别。

    Sea.js需要三个接口:define 用来wapper模块,指明依赖,同步依赖;use 用来启动加载期;require 实际上是加载期到执行期的桥梁。

    模块标识:模块id的标准参考Module Identifiers,简单说来就是作为一个模块的唯一标识。

    Factory:一个可以产生模块的工厂。node中的工厂就是新的运行时,而在Sea.js中(Tea.js中也同样),factory就是一个函数。这个函数接受三个参数。

    依赖(Dependencies):依赖就是一个id的数组,即模块所依赖模块的标识

    2.依赖加载原理

    有很多语言都有模块化的结构,比如c/c++的#include语句,Ruby的require语句等等。模块的执行,必然需要其依赖的模块准备就绪才能顺利执行。
    c/c++是编译语言,在预编译时,替换#include语句,将依赖的文件内容包含进来,在编译后的执行期,所有的模块才会开始执行;
    而Ruby是解释型语言,在模块执行前,并不知道它依赖什么模块,待到执行到require语句时,执行将暂停,从外部读取并执行依赖,然后再回来继续执行当前模块。
    JavaScript作为一门解释型语言,在复杂的浏览器环境中,Sea.js是如何处理CMD模块间的依赖的呢?

    a.Node的依赖加载原理

    node于Ruby类似,当我们使用node usegreet.js来运行这个模块时,实际上node会构建一个运行的上下文,在这个上下文中运行这个模块。运行到require('./greet')这句话时,会通过注入的API,在新的上下文中解析greet.js这个模块,然后通过注入的exports或module这两个关键字获取该模块的接口,将接口暴露出来给usegreet.js使用,即通过greet这个对象来引用这些接口。

    node的模块方案的特点如下
       (1)使用require、exports和module作为模块化组织的关键字;
       (2)每个模块只加载一次,作为单例存在于内存中,每次require时使用的是它的接口;
        (3)require是同步的,通俗地讲,就是node运行A模块,发现需要B模块,会停止运行A模块,把B模块加载好,获取的B的接口,才继续运行A模块。如果B模块已经加载到内存中了,当然require B可以直接使用B的接口,否则会通过fs模块化同步地将B文件内存,开启新的上下文解析B模块,获取B的API。
          注意:实际上node如果通过fs异步的读取文件的话,require也可以是异步的,所以曾经node中有require.async这个API。

    b.Sea.js加载原理

    由于在浏览器端,采用与node同样的依赖加载方式是不可行的,因为依赖只有在执行期才能知道,但是此时在浏览器端,我们无法像node一样直接同步地读取一个依赖文件并执行!我们只能采用异步的方式。于是Sea.js的做法是,分成两个时期——加载期和执行期;

    加载期即在执行一个模块之前,将其直接或间接依赖的模块从服务器端同步到浏览器端;
    执行期:在确认该模块直接或间接依赖的模块都加载完毕之后,执行该模块。

    加载期:不难想见,模块间的依赖就像一棵树。启动模块作为根节点,依赖模块作为叶子节点。下面是pixelegos(一个开源项目)的依赖树:

    loadingperiod

    如上图,在页面中通过seajs.use('/js/pixelegos')调用,目的是执行pixelegos这个模块。Sea.js并不知道pixelegos还依赖于其他什么模块,只是到服务端加载pixelegos.js,将其加载到浏览器端之后,通过分析发现它还依赖于其他的模块,于是Sea.js又去加载其他的模块。随着更多的模块同步到浏览器端后,一棵依赖树才慢慢地通过递归显现出来。

    执行期:在执行期,执行也是从根节点开始,本质上是按照代码的顺序结构,对整棵树进行了遍历。有的模块可能已经EXECUTED,而有的还需要执行获取其exports。由于在执行期时,所有依赖的模块都加载好了,所以与node执行过程有点类似。

    pixelegos通过同步的require函数获取tool、canvas和menu,后三者同样通过require来执行各自的依赖模块,于是通过这样一个递归的过程,pixelegos就执行完毕了。

    打包模块的加载过程:

    打包的方式有三种,self,relative和all。
    self,只是自己做了transport
    relative,将多有相对路径的模块transport,concat
    all,包括相对路径模块和库模块(即在seajs-modules文件夹中的),transport,concat

    加载方式(以压缩的pixelegos.js为例)
      (1)在use时,定义一个匿名的use_模块,依赖于/dist/pixelegos模块,匿名的use_模块load依赖,开始加载pixelegos.js模块;
      (2)pixelegos.js加载执行,所有打包在里面的模块被define;
      (3)pixelegos.js的onload回调执行,调用/dist/pixelegos模块的load,加载其依赖模块,但依赖的模块都加载好了;
      (4)通知匿名的use_加载完成,开始执行期。

    3.Sea.js的实现

    module.js是Sea.js的核心,Sea.js中为模块定义了六种状态:

    FETCHING:开始从服务端加载模块
    SAVED:模块加载完成
    LOADING:加载依赖模块中
    LOADED:依赖模块加载完成
    EXECUTING:模块执行中
    EXECUTED:模块执行完成

    Sea.use调用Module.use构造一个没有factory的模块,该模块即为这个运行期的根节点

    // 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.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
        }
    
        mod.load()
    }

    模块构造完成,则调用mod.load()来同步其子模块;直接跳过fetching这一步;mod.callback也是Sea.js不纯粹的一点,在模块加载完成后,会调用这个callback。
    在load方法中,获取子模块,加载子模块,在子模块加载完成后,会触发mod.onload():

    // Load module.dependencies and fire onload when all done
    Module.prototype.load = function () {
        var mod = this
    
        // If the module is being loaded, just wait it onload call
        if (mod.status >= STATUS.LOADING) {
            return
        }
    
        mod.status = STATUS.LOADING
    
        // Emit `load` event for plugins such as combo plugin
        var uris = mod.resolve()
        emit("load", uris)
    
        var len = mod._remain = uris.length
        var m
    
        // Initialize modules and register waitings
        for (var i = 0; i < len; i++) {
            m = Module.get(uris[i])
    
            if (m.status < STATUS.LOADED) {
                // Maybe duplicate
                m._waitings[mod.uri] = (m._waitings[mod.uri] || 0) + 1
            }
            else {
                mod._remain--
            }
        }
    
        if (mod._remain === 0) {
            mod.onload()
            return
        }
    
        // Begin parallel loading
        var requestCache = {}
    
        for (i = 0; i < len; i++) {
            m = cachedMods[uris[i]]
    
            if (m.status < STATUS.FETCHING) {
                m.fetch(requestCache)
            }
            else if (m.status === STATUS.SAVED) {
                m.load()
            }
        }
    
        // Send all requests at last to avoid cache bug in IE6-9. Issues#808
        for (var requestUri in requestCache) {
            if (requestCache.hasOwnProperty(requestUri)) {
                requestCache[requestUri]()
            }
        }
    }

    模块的状态是最关键的,模块状态的流转决定了加载的行为;
    是否触发onload是由模块的_remian属性来确定,在load和子模块的onload函数中都对_remain进行了计算,如果为0,则表示模块加载完成,调用onload:

    // Call this method when module is loaded
    Module.prototype.onload = function () {
        var mod = this
        mod.status = STATUS.LOADED
    
        if (mod.callback) {
            mod.callback()
        }
    
        // Notify waiting modules to fire onload
        var waitings = mod._waitings
        var uri, m
    
        for (uri in waitings) {
            if (waitings.hasOwnProperty(uri)) {
                m = cachedMods[uri]
                m._remain -= waitings[uri]
                if (m._remain === 0) {
                    m.onload()
                }
            }
        }
    
        // Reduce memory taken
        delete mod._waitings
        delete mod._remain
    }

    模块的_remain和_waitings是两个非常关键的属性,子模块通过_waitings获得父模块,通过_remain来判断模块是否加载完成。
    当这个没有factory的根模块触发onload之后,会调用其方法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
    }

    这预示着加载期结束,开始执行期;
    而执行期相对比较无脑,首先是直接调用根模块依赖模块的exec方法获取其exports,用它们来调用use传经来的callback。而子模块在执行时,都是按照标准的模块解析方式执行的:

    // Execute a module
    Module.prototype.exec = function () {
        var mod = this
    
        // When module is executed, DO NOT execute it again. When module
        // is being executed, just return `module.exports` too, for avoiding
        // circularly calling
        if (mod.status >= STATUS.EXECUTING) {
            return mod.exports
        }
    
        mod.status = STATUS.EXECUTING
    
        // Create require
        var uri = mod.uri
    
        function require(id) {
            return Module.get(require.resolve(id)).exec()
        }
    
        require.resolve = function (id) {
            return Module.resolve(id, uri)
        }
    
        require.async = function (ids, callback) {
            Module.use(ids, callback, uri + "_async_" + cid())
            return require
        }
    
        // Exec factory
        var factory = mod.factory
    
        var exports = isFunction(factory) ? factory(require, mod.exports = {},
        mod) : factory
    
        if (exports === undefined) {
            exports = mod.exports
        }
    
        // Emit `error` event
        if (exports === null && ! IS_CSS_RE.test(uri)) {
            emit("error", mod)
        }
    
        // Reduce memory leak
        delete mod.factory
    
        mod.exports = exports
        mod.status = STATUS.EXECUTED
    
        // Emit `exec` event
        emit("exec", mod)
    
        return exports
    }

    注意:var exports = isFunction(factory) ? factory(require, mod.exports = {}, mod) : factory 真的,整个Sea.js就是为了这行代码能够完美运行

    4.资源定位

    资源定位与模块标识相关,而在Sea.js中有三种模块标识:

    普通路径:普通路径与网页中超链接一样,相对于当前页面解析。

    相对标识:在define的factory中的相对路径(.. .)是相对标识,相对标识相对当前的URI来解析

    顶级标识:不以.或者'/'开头的模块标识是顶级标识。

    获取真实路径:在Sea.js中,使用data.cwd来代表当前页面的目录;使用data.base来代表sea.js的加载地址。

    5.factory依赖分析

    在Sea.js的API中,define(factory),并没有指明模块的依赖项,那Sea.js是如何获得的呢。

    /**
     * util-deps.js - The parser for dependencies
     * ref: tests/research/parse-dependencies/test.html
     */
    
    var REQUIRE_RE = /"(?:\"|[^"])*"|'(?:\'|[^'])*'|/*[Ss]*?*/|/(?:\/|[^/
    ])+/(?=[^/])|//.*|.s*require|(?:^|[^$])requires*(s*(["'])(.+?)1s*)/g
    var SLASH_RE = /\\/g
    
    function parseDependencies(code) {
      var ret = []
    
      code.replace(SLASH_RE, "")
          .replace(REQUIRE_RE, function(m, m1, m2) {
            if (m2) {
              ret.push(m2)
            }
          })
    
      return ret
    }

    Sea.js就是使用REQUIRE_RE在factory的源码中匹配出该模块的依赖项。从REQUIRE_RE这么长的正则来看,这里坑很多;在CommonJS的wrapper方案中可以使用JS语法分析器来获取依赖会更准确。

  • 相关阅读:
    Qt共享内存实现进程间通信(QSharedMemory)
    Qt5.5制作简单的屏幕截图程序
    006--C++动态内存(简介)
    005--C++字符
    004--C++11的初始化方式
    003--sizeof的使用
    002--C++程序的创建
    001--基础知识准备
    Qt5.5连接MySQL
    vue-cli中如何创建并引入自定义组件
  • 原文地址:https://www.cnblogs.com/zhulongchao/p/4573709.html
Copyright © 2020-2023  润新知