• home > webfront > ECMAS > AMD/CMD >

    SeaJS是如何做到就近依赖的伪同步效果的?

    Author:zhoulujun Date:

    require js和sea js都是在执行模块前预加载了依赖的模块,并没有比require js显得更“懒加载”,只是所依赖模块的代码执行时机不同。require js加载时执行,而sea js是使用时执行。

    RequireJS是一个非常小巧的JavaScript模块载入框架,是AMD(依赖前置、提前执行,define的时候就引入,然后作为回调函数的参数使用)规范最好的实现者之一。

    SeaJS 是CMD (依赖就近、延迟执行,哪里需要哪里require) 规范的最好实践者(没有之一)

    依赖声明和异步加载:

    SeaJS 模块通过 define 函数来声明模块及其依赖项。加载器会首先解析依赖关系,并异步地加载所有依赖的模块。当所有依赖项都加载完成后,才会执行模块的主体代码。这种方式确保了模块之间的依赖关系得到满足,即使实际的加载过程是异步的。

    define(function(require, exports, module) {
        var moduleA = require('moduleA');
        var moduleB = require('moduleB');
        // 依赖模块已经加载完毕,可以安全使用
        moduleA.doSomething();
        moduleB.doSomething();
    });

    按道理加载模块,就是需要等moduleA 、moduleB 加载完毕才能使用,应该是一个异步的过程,为什么可以写成同步的形式呢?这是用了什么黑科技?

    原来作者玉伯大佬用了一个小魔法来“欺骗”我们。

    // 定义一个模块
    define(function(require, exports, module) {
      // 加载jquery模块
      var $ = require('jquery');
      // 直接使用模块里的方法
      $('#header').hide();
    });

    而卢勃大神在知乎给了一个很精彩的解释,这里直接分享下:

    1. 通过回调函数的Function.toString函数,使用正则表达式来捕捉内部的require字段,找到require('jquery')内部依赖的模块jquery

    2. 根据配置文件,找到jquery的js文件的实际路径

    3. 在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上

    4. 回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数

    5. 当回调函数调用require('jquery'),即执行绑定在'jquery'这个id上的js文件,即刻执行,并将返回值传给var b

    也就是说,require.js和sea.js都是在执行模块前预加载了依赖的模块,并没有比require.js显得更“懒加载”,只是所依赖模块的代码执行时机不同。require.js加载时执行,而sea.js是使用时执行。

    两者在性能上并没有太多差异。因为最影响页面渲染速度的当然是资源的加载速度,既然都是预加载,那么加载模块资源的耗时是一样的(网络情况相同时)。

    而模块代码的执行时机并没有那么影响性能(除非你的模块太大),现在的js引擎如V8引擎足够强,没什么压力。



    同步执行依赖模块:

    当模块被加载和解析后,SeaJS 确保所有依赖的模块按照定义的顺序同步执行,这样在模块主体代码运行时,所有依赖模块已经存在于内存中。

    异步模块的同步化:

    SeaJS 内部实现了一套异步加载模块的管理系统。每个模块在加载时都会返回一个 Promise-like 对象,当所有依赖模块的 Promise 被 resolve 后,SeaJS 会触发主模块的执行,这种方式在用户感知上实现了同步效果


    factory的依赖分析

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

    这段是Sea.js的源码:

    /**
     * util-deps.js - The parser for dependencies
     * ref: tests/research/parse-dependencies/test.html
     */
    
    var REQUIRE_RE = /"(?:\\"|[^"])*"|'(?:\\'|[^'])*'|\/\*[\S\s]*?\*\/|\/(?:\\\/|[^\/\r\n])+\/(?=[^\/])|\/\/.*|\.\s*require|(?:^|[^$])\brequire\s*\(\s*(["'])(.+?)\1\s*\)/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
    }

    REQUIRE_RE这个硕大无比的正则就是关键(坑很多)。推荐使用regexper来看看这个正则表达式。非native的函数factory我们可以通过的toString()方法获取源码,Sea.js就是使用REQUIRE_RE在factory的源码中匹配出该模块的依赖项。

    加载过程

    1、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。

    2、在load方法中,获取子模块,加载子模块,在子模块加载完成后,会触发mod.onload()

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

    4、当这个没有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
    }

    这预示着加载期结束,开始执行期;

    5、而执行期相对比较无脑,首先是直接调用根模块依赖模块的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
    }



    seaJS循环依赖的解决原理

    循环依赖的概念:

    seajs在使用a模块时,会先下载a模块,a模块下载时,a模块的状态是FETCHING,下载好了之后,a模块的状态是FETCHED,然后解析a模块,这时会从a模块的回调函数中寻找a模块依赖的模块,这里a模块依赖的模块是b模块。这时,a模块的状态变成了SAVED,然后去下载b模块,这时b模块的状态是FETCHING,下载好了之后,b模块的状态是FETCHED,然后解析b模块,这时会从b模块的回调函数中寻找b模块依赖的模块,这里b模块依赖的模块是a模块。这里就产生了循环依赖的问题。

    seajs里面是如何解决循环依赖?

    seajs中处理这个问题的第一步:

    在a模块的回调函数中寻找a模块依赖的模块后(这时a模块的状态是SAVED了),会判断a模块所依赖的模块是否跟自己有循环依赖的关系,如果有,就不去下载,seajs是通过getPureDependencies方法进行判断的,由于这时b模块还不存在于cachedModules中,所以这里不会检查出a与b有循环依赖的关系。

    因此,去下载b模块,下载好了之后,b模块的状态是FETCHED,然后解析b模块,这时就会从b模块的回调函数中寻找b模块依赖的模块,这里检查出来了是a模块,这时b模块的状态变成了SAVED的。然后判断a模块是否与自己(b模块)循环依赖。由于此时a模块存在,并且状态是SAVED,这时就会检查出来了有依赖,因此b模块的状态就会直接变成了READY。

    这时,a模块的状态就会变成READY(如果a模块还依赖其他的模块,比如c模块,那么等c模块变成READY后,a模块才会变成READY状态)。这时,就会去编译a模块,a模块的状态是COMPILING,编译过程,其实就是执行a模块的回调函数,(如果这里a模块依赖的是c模块,这时,就会执行c模块的回调函数,也就是编译c模块,编译结束后,c模块变成了COMPILED),紧接着,a模块变成COMPILED。

    但是,这里依赖的是b模块,因此,在执行a模块的回调函数时(在编译a模块时),会执行b模块的回调函数,也就是编译b模块,等编译结束,b模块变成了COMPILED,紧接着a模块就变成了COMPILED。


    Sea.js 2.1 去掉循环依赖支持

    具体看:

    https://github.com/seajs/seajs/issues/713

    https://github.com/seajs/seajs/issues/732


    去除对循环依赖的支持。目前 Sea.js 是支持循环依赖的,当有死循环时,也会给出适当的提示。但就如 Go 语言设计者所说,支持循环依赖,看起来很 cool,也能在某些场景下给设计带来简化,但从长远上来,支持循环依赖,会给整体增加复杂性,让依赖关系等都变得复杂。从这个角度上,对循环依赖的支持是一种心理上的完美,而非工程上的完美。去除对循环依赖的支持,有两个做法:1)保留循环依赖时的提示功能,这样对代码层面其实不会有太多精简。2)彻底去除,根本不考虑,有死循环时,自然的报错(比如浏览器的提示)。具体怎么做还有纠结。


    从更大的社区来看:

    1. Go 语言非常严肃地不支持循环依赖,认为循环依赖虽然能在某些场景下带来细微好处,但长期来看,会对整体造成很不好的影响。

    2. 循环依赖检测可以通过 spm 等工具提前检测。线上有循环依赖导致的死循环时,通过浏览器自身的堆栈就可以分析出来。

    从简单性考虑,Sea.js 2.1 中将彻底移除掉对循环依赖的支持




    参考文章:

    Sea.js是如何工作的? https://github.com/island205/HelloSea.js/blob/master/how-seajs-works.md

    sea.js的同步魔法 https://juejin.cn/post/6844903924525170701

    SeaJS从入门到原理 https://jelly.jd.com/article/6006b1045b6c6a01506c87b6



    转载本站文章《SeaJS是如何做到就近依赖的伪同步效果的?》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/AMD/2019_1213_9175.html