seajs和requirejs的区别
写在前面
前段时间在看一些前端面试相关的东西,发现有些比较有意思的经典面试题网上少有能说的通俗易懂的文章。
比如说,seajs 和 requirejs 的区别,defer 和 async 的区别。这两道面试题应该算是非常old school了,搜索引擎一艘前端面试题,发现老早就能看到这两道题的身影。
不知道小伙伴们被问这两道题的多吗,至少我还没被问过哈哈。。虽然感觉被问的概率比较低,题目也比较经典,但如果你深入了解这两道题,也许你会发现新大陆,你会大受震撼,直呼:厉害啊,还能这么搞,牛x
所以打算新开一个系列讲讲这个,目前打算先写上面说到的两篇,这是我自己想捣鼓的两道题,后续有什么想看的小伙伴可以评论区留言~
简单说说API
在讲 seajs 和 requirejs 的原理之前,先讲讲这两个库的基本用法,大概有个印象
seajs
声明模块
define 是用来声明模块的,传入的参数是一个工厂函数,这个函数就是我们所说的模块,用法上跟 cjs 规范差不多
define(function(require, exports, module) {
// exports、module暴露接口
exports.a = { a: 'a' }
module.exports = { b: 'b' }
})
注册模块
用 define 声明完一个模块之后,还需要注册到 seajs 里面,否则无法引入,具体参数可以参考 官网
seajs.config({
base: "../lib/",
alias: {
"jquery": "jquery.min.js",
}
})
引用模块
使用 use API可以引入模块,参数一是引入的模块,参数二是引入的回调,回调参数是模块的到处内容
seajs.use(['jquery','../static/main.js'],function($,main){
...
});
requirejs
声明模块
声明自己的模块
define(function(){
const a = 'a'
const b = 'b'
// 直接返回导出内容
return {
a, b
};
})
声明需要依赖的模块。requirejs 推崇的是依赖前置,意思是,在模块声明前先引入需要依赖的模块。当然也只是推崇,在 requirejs 中你也可以使用依赖就近
// 注册有依赖关系的模块
define(['依赖的模块路径'], function(依赖模块导出内容){
// 依赖就近的引入方式
require(['otherModule'],function(moduleExport){
...
})
return {
};
});
注册模块
同样的,requirejs 也需要将声明的模块注册到用全局对象上,更多参数可以参考 官网
require.config({
paths:{
"jquery":'../lib/jquery.min'
}
});
引用模块
// 引用单个
require(['jquery'],function($){
//通过此方式引入jquery才能使用$,接下来正常写jquery代码就好
})
// 引用多个
require(['A', 'B', 'C'],function(A, B, C){
// 引入的模块会按顺序放入到回调函数的参数中
})
// 嵌套引用(有依赖关系的话需要嵌套引入)
require(['jquery'],function($){
require(['A'],function(A){
...
})
})
用法总结
看了上面的用法之后,可以简单概括为三类API,注册、声明和引入。
两者的区别
原则上的区别
看过网上讲解两者区别的小伙伴都知道,requirejs 属于 amd 规范,seajs 是属于 cmd 规范。
amd 规范推崇的是依赖前置,cmd 推崇的是依赖就近。这两个新名词看起来比较抽象,但实际上可以非常简单的理解为:
- 依赖前置就是在模块声明/引入前,先把该模块需要的依赖先引入
- 依赖就近就是模块声明/引入不需要提前引入依赖,在模块内部再引入
在 seajs 和 requirejs 中这两种原则其实都是支持的,两者看似不一样,但本质都离不开以下四步:
- 收集依赖
- 加载依赖
- 执行模块的工厂函数,导出模块内容
- 执行依赖引入的回调
而 seajs 和 requirejs 的区别更多是在于收集依赖这一块的区别
- seajs 的依赖是需要手动收集的
- requirejs 的依赖是以函数的参数形式传递的,也就是说在代码执行阶段我已经知道这个模块需要引入哪些依赖
seajs 的依赖是手动收集是什么意思??这里先按下不表,这一操作我认为是 seajs 的非常亮眼的特性,下一节详细展开
使用上的区别
对于 requirejs,其实也是支持依赖就近的,但并不保证按模块加载顺序执行,模块在加载完毕后就会立即执行。
define(function(require, exports, module) {
console.log('require module: main');
var mod1 = require('./mod1');
mod1.hello();
var mod2 = require('./mod2');
mod2.hello();
return {
hello: function() {
console.log('hello main');
}
};
});
// seajs
require module: main
require module: mod1
hello mod1
require module: mod2
hello mod2
helo main
// requirejs
require module: mod2
require module: mod1
require module: main
hello mod1
hello mod2
helo main
从上面代码结果看,seajs 的结果更符合我们常规的代码执行顺序
所以,amd 规范里面使用依赖就近会有一个很明显的问题,模块执行不按顺序,如果某个加载的比较快的模块依赖了一个加载的比较慢的模块,就会出现问题。所以这种情况在 requirejs 里面最好是用回调的 api 来模拟依赖就近原则
define(function(require, exports, module) {
console.log('require module: main');
require('./mod1', function(mod1) {
mod1.hello();
});
require('./mod2', function(mod2) {
mod2.hello();
});
return {
hello: function() {
console.log('hello main');
}
};
});
揭秘seajs实现原理
requirejs 的源码太长且不好看,所以没有逐行代码认真去看,但是具体流程跟上一节说的步骤差不多。seajs 的代码相对就好看很多了,有兴趣的小伙伴可以去看看,很有意思。
seajs 具体的实现,我们可以从刚刚的三类API入手,分别注册、声明和引入
注册模块
seajs.config 的主要功能是收集传入的各个模块的url地址
function Seajs() {
this.moduleConfig = {}
}
// 注册模块
Seajs.prototype.config = function(config) {
this.moduleConfig = config
}
window.seajs = new Seajs()
代码很简单,因为就是用来存储配置的,下面我们看看怎么使用
seajs.config({
alias: {
"sea1": "./sea1.js",
"sea2": "./sea2.js",
}
})
这样我们就收集到了两个模块的 url 地址
声明模块
声明模块的入口函数是 define,我们可以猜一下这个函数是做什么的。define 的参数是一个工厂函数,这个工厂函数就是模块的核心了。所以 define 就是接收并执行这个工厂函数?
答案:不是。
从上面 API 使用示例我们知道,引入模块首先要加载所需依赖,再执行引入的回调。对于 依赖前置 我们可以以参数形式提前知道这个模块需要的依赖,但 依赖就近 是无法提前得知这个模块需要哪些依赖的。
那 seajs 是从哪里得知这个模块需要什么依赖的呢?关键在于将工厂函数转换为字符串
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
}
function define(factory) {
// 将工厂函数转换为字符串
var factoryStr = factory.toString()
const deps = parseDependencies(factoryStr)
// 以下内容将会在 script 标签的 onload 里面用到
anonymousMeta = {
deps: deps,
factory: factory
}
}
没看源码之前,我是怎么都不会想到还有这种操作,将模块字符串化,再提取依赖,玉伯大佬真的太强了。
当然,这种操作在目前的模块化规范里面其实已经用不到了,我们可以观察 webpack 打包的产物,模块化都依赖 IIFE,不需要像 seajs 和 requirejs 那样需要独立的js文件做处理
引入模块
对于引入模块,可以细分为上面说的四个步骤
- 收集依赖
- 加载依赖
- 执行模块的工厂函数,导出模块内容
- 执行依赖引入的回调
收集依赖在上一小节讲了,接下来也是从API use 入手
let count = 0
Seajs.prototype.use = function(deps, callback) {
// 基于此次引入创建一个模块对象
var mod = Module.get(`__use__${count++}`, deps)
// 该模块没有被谁依赖,所以自己就是入口本身
mod._entry.push(mod)
// 记录该模块的依赖数量
mod.remain = deps.length
// 模块依赖加载完毕后执行的回调
mod.callback = function() {
// 模块导出
var exports = []
deps.map(i => {
exports.push(cachedMods[i].exec())
})
执行依赖引入的回调
if (callback) callback.apply(window, exports)
}
// 执行模块的加载
mod.load()
}
上面就是 use 函数的主要逻辑,整体逻辑都是依赖 Module 对象运行的,下面看看 Module 到底是什么
function Module(deps) {
this.dependences = deps ? deps instanceof Array ? deps : [deps] : []
this.deps = {}
// 0 尚未加载 1 加载中 2 加载完毕 3 缓存结果
this.status = 0
this._entry = []
}
Module.get = function(moduleName, deps) {
// 已存在这个模块对象就复用,否则就新建
return cachedMods[moduleName] || (cachedMods[moduleName] = new Module(deps))
}
Module 对象主要存放的是模块的依赖,也就是 use 函数的第一个参数。而 Module.get 则是起到一个创建对象的作用。
use 函数的最后执行的是 load 函数,我们看看 load 的逻辑
Module.prototype.load = function() {
if (!this.status) {
this.status = 1
const dependenceList = []
// 根据模块的各个依赖创建 Module 对象
this.dependences.map(dep => {
const moduleName = dep.toString()
const m = Module.get(moduleName)
if (m.status == 0) {
// 记录当前依赖的uri
m.uri = seajs.moduleConfig.alias[moduleName]
// 依赖的入口就是当前 Module 对象
m._entry.push(this)
this.deps[moduleName] = m
// 模块加载完毕的回调
m.callback = () => {
m.status = 2
m.remain = 0
this.onload.call(this)
}
dependenceList.push(m)
}
})
// 如果 dependenceList 为空,就代表当前模块的依赖已经加载过了,直接使用缓存即可
if (!dependenceList.length) {
this.fetch(this.onload.bind(this))
} else {
// 否则逐个请求依赖资源
dependenceList.map(m => {
m.status = 1
m.fetch(this.onload.bind(this))
})
}
}
}
load 函数主要做的是将模块的依赖封装成 Module 对象,然后去请求依赖的资源,这里其实类似于是一个向下递归加载依赖的过程。接下来看看 fetch 函数
function requestScript(url, cb) {
var node = document.createElement("script")
node.async = true
node.src = url
node.onload = () => {
cb()
document.head.removeChild(node)
}
document.head.appendChild(node)
}
Module.prototype.fetch = function(resolve) {
// 如果当前模块没有uri(代表不是第三方加载的脚本),则直接加载模块内容
if (!this.uri) {
resolve()
return
}
requestScript(this.uri, () => {
this.factory = anonymousMeta.factory
if (anonymousMeta.deps.length) {
const moduleName = anonymousMeta.deps.toString()
const m = Module.get(moduleName)
// 将收集到的依赖信息记录到当前模块对象中
this.dependences = this.dependences.concat(anonymousMeta.deps)
this.deps[moduleName] = m
this.remain = anonymousMeta.deps.length
m.uri = seajs.moduleConfig.alias[moduleName]
m.callback = this.callback
// 将当前模块的对象加入到依赖的entry属性里面
m._entry.push(this)
anonymousMeta = null
// 加载依赖
m.load()
} else resolve()
})
}
fetch 函数主要做的是创建 script 标签,并将 src 设置为当前 Module 对象的 uri 属性,也就是刚刚 load 函数里面处理的逻辑。等脚本 onload 了之后,承接为我们上一节说到的 anonymousMeta
anonymousMeta 有两个属性:
- deps:deps是当前这个模块的依赖,也就是上一节说到的
define函数收集到的依赖 - factory:是模块的工厂函数,等这个模块的所有依赖都被加载完了,就会执行这个工作函数,得到模块导出内容
脚本资源文件加载完毕之后,接下来要看这个模块是否有依赖别的脚本
- 没有:代表当前模块的依赖都加载完了,可以执行这个模块的工厂函数了,执行完再回调给这个模块的入口(_entry属性),告诉入口模块我已经加载完毕了
- 有:如果依赖了别的脚本,需要先把这些依赖的脚本加载完了,才能回调给当前模块的入口
接下来看下回调给入口模块的逻辑
Module.prototype.onload = function() {
this.status = 2
// 执行模块的工厂函数
if (this.factory) this.exec()
// 回调给各个入口模块
this._entry.map(e => {
if (!this.remain || --this.remain == 0) {
e.callback()
}
})
}
Module.prototype.exec = function() {
// 使用缓存
if (this.status == 3) return this.exports
this.status = 3
// 没有缓存的化,就执行工厂函数
this.exports = this.factory(require)
return this.exports
}
源码总结
先上一张思维导图
总的来说,对于 seajs 来说,代表当前模块加载完毕的标志,就是当前模块工厂函数执行完毕,而允许执行当前工厂函数的前提是,当前模块的所有依赖都需要加载完毕。
下面放一个完整的代码和使用示例,小伙伴们可以本地调试
源码:
/** --------------------- Module对象 ---------------------**/
function requestScript(url, cb) {
var node = document.createElement("script")
node.async = true
node.src = url
node.onload = () => {
cb()
document.head.removeChild(node)
}
document.head.appendChild(node)
}
var cachedMods = {}, anonymousMeta = null
function require(dep) {
return cachedMods[dep].exports
}
function Module(deps) {
this.dependences = deps ? deps instanceof Array ? deps : [deps] : []
this.deps = {}
// 0 尚未加载 1 加载中 2 加载完毕 3 缓存结果
this.status = 0
this._entry = []
}
Module.prototype.load = function() {
if (!this.status) {
this.status = 1
const dependenceList = []
this.dependences.map(dep => {
const moduleName = dep.toString()
const m = Module.get(moduleName)
if (m.status == 0) {
// 记录当前模块的uri
m.uri = seajs.moduleConfig.alias[moduleName]
m._entry.push(this)
this.deps[moduleName] = m
m.callback = () => {
m.status = 2
m.remain = 0
this.onload.call(this)
}
dependenceList.push(m)
}
})
if (!dependenceList.length) {
this.fetch(this.onload.bind(this))
} else {
dependenceList.map(m => {
m.status = 1
m.fetch(this.onload.bind(this))
})
}
}
}
Module.prototype.fetch = function(resolve) {
// 如果当前模块没有uri(代表不是第三方加载的脚本),则直接加载模块内容
if (!this.uri) {
resolve()
return
}
requestScript(this.uri, () => {
this.factory = anonymousMeta.factory
if (anonymousMeta.deps.length) {
const moduleName = anonymousMeta.deps.toString()
const m = Module.get(moduleName)
// 将收集到的依赖信息记录到当前模块对象中
this.dependences = this.dependences.concat(anonymousMeta.deps)
this.deps[moduleName] = m
this.remain = anonymousMeta.deps.length
m.uri = seajs.moduleConfig.alias[moduleName]
m.callback = this.callback
// 将当前模块的对象加入到依赖的entry属性里面
m._entry.push(this)
anonymousMeta = null
// 加载依赖
m.load()
} else resolve()
})
}
Module.prototype.onload = function() {
this.status = 2
if (this.factory) this.exec()
this._entry.map(e => {
if (!this.remain || --this.remain == 0) {
e.callback()
}
})
}
Module.prototype.exec = function() {
if (this.status == 3) return this.exports
this.status = 3
this.exports = this.factory(require)
return this.exports
}
Module.get = function(moduleName, deps) {
return cachedMods[moduleName] || (cachedMods[moduleName] = new Module(deps))
}
/** --------------------- Module对象 ---------------------**/
/** --------------------- 声明模块 ---------------------**/
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
}
function define(factory) {
var factoryStr = factory.toString()
const deps = parseDependencies(factoryStr)
anonymousMeta = {
deps: deps,
factory: factory
}
}
/** --------------------- 声明模块 ---------------------**/
/** --------------------- seajs全局对象 ---------------------**/
let count = 0
function Seajs() {
this.moduleConfig = {}
}
// 注册模块
Seajs.prototype.config = function(config) {
this.moduleConfig = config
}
// 引入模块
Seajs.prototype.use = function(deps, callback) {
var mod = Module.get(`__use__${count++}`, deps)
mod._entry.push(mod)
mod.remain = deps.length
mod.callback = function() {
var exports = []
deps.map(i => {
exports.push(cachedMods[i].exec())
})
if (callback) callback.apply(window, exports)
}
mod.load()
}
window.seajs = new Seajs()
/** --------------------- seajs全局对象 ---------------------**/
使用示例:
index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<!-- <script src="./lib/sea.js"></script> -->
<script src="./lib/sea-mini.js"></script>
<style>
</style>
<body>
<div id="root">
<button onclick="clickMethod()">加载sea1模块</button>
</div>
</html>
<script>
function clickMethod() {
seajs.use(['sea1'], function name(params) {
console.log('params', params)
})
}
seajs.config({
alias: {
"sea1": "./sea1.js",
"sea2": "./sea2.js",
}
})
</script>
sea1.js
define(function(require, factory) {
const sea2 = require('sea2')
return {
a: 'a',
b: 'b',
sea2
}
});
sea2.js
define(function(require, factory) {
return {
c: 'c',
d: 'd'
}
});
总结
上面放的其实不是 seajs 的源码,是我看了源码过后自己简单的整理出来的,是一个能跑通的小 demo,相比真正的 seajs 还差了不少细节,比如说:
- Module 对象的一些状态,seajs会更丰富一些
- 没有实现依赖前置,seajs里面是有的
- 对于那三大类API,没有完善参数接收和校验
- 没有实现异步引入,也就是
require.async
即使如今 seajs 已经不是主流的规范了,但并不减我对他的兴趣和赞叹,factory.toString() 这句代码所引申出来的设计和思想我认为永不过时。
今天是2021年的最后一天了,还在努力肝文章呜呜,啥时候能像大佬们那样一篇文章几百个赞。。。最近也准备出击找工作了,看到这篇文章的大佬们如果觉得小弟可以,也可以捞捞小弟~~