SeaJS是如何做到就近依赖的伪同步效果的?
Author:zhoulujun Date:
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(); });而卢勃大神在知乎给了一个很精彩的解释,这里直接分享下:
通过回调函数的Function.toString函数,使用正则表达式来捕捉内部的require字段,找到require('jquery')内部依赖的模块jquery
根据配置文件,找到jquery的js文件的实际路径
在dom中插入script标签,载入模块指定的js,绑定加载完成的事件,使得加载完成后将js文件绑定到require模块指定的id(这里就是jquery这个字符串)上
回调函数内部依赖的js全部加载(暂不调用)完后,调用回调函数
当回调函数调用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)彻底去除,根本不考虑,有死循环时,自然的报错(比如浏览器的提示)。具体怎么做还有纠结。
从更大的社区来看:
Go 语言非常严肃地不支持循环依赖,认为循环依赖虽然能在某些场景下带来细微好处,但长期来看,会对整体造成很不好的影响。
循环依赖检测可以通过 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