JS模块化简单实现
来源:互联网 发布:淘宝退款售后订单删除 编辑:程序博客网 时间:2024/06/15 05:25
曾经花了一段时间去看RequireJS
源码,虽然就两千行左右的代码,但原谅我的愚笨,真的有点看不懂。想想,还是自己实现一个再说。
简单目标
实现类似requireJS的功能,涉及到的功能
// 定义模块,三个参数define(id, deps, factory)// 执行模块,两个参数require(deps, factory)
简单分析
先上一个简单的例子,进行分析:
require(['main'], function(main) { console.log('start~~~'); main.hello();});
- 过程分析
通过deps
参数,很容易知道该文件依赖的脚本是main.js
,然后去加载依赖的脚本,加载完之后执行一遍并且将执行的结果作为回调函数factory
的参数,然后执行该匿名的回调函数即可。
问题分析
如何加载脚本
这个比较容易想到两个方式,一种用ajax请求,另一种用Dom元素添加让浏览器加载;由于ajax请求会有跨域问题,我们采用Dom加载的方式。
如何判断模块加载完毕
曾经我想用script的
onload
事件来判断对应的模块加载,但问题在于onload
只是说明了脚本已经加载,但是并没有执行。调整一下思路,定义模块的状态,在加载的时候,将状态标为加载中;记录模块的依赖数,当该模块的所有依赖全部加载完成,将执行回调函数作为该模块的对应输出,模块加载完成。如何暂存和执行回调函数
比如上面这个简单的例子,将回调函数作为模块
main
加载完毕后的回调函数即可;若依赖的模块有多个,每个依赖加载完后进行就判断依赖是否加载完毕,是就执行回调。注意,此时也意味着当前模块加载完毕,可以执行返回结果了。循环依赖问题
这么难的问题,留到后面再说吧!
数据结构
首先需要一个全部的模块缓存对象,记录所有已经加载的&正在加载的模块;将模块的数据结构设计成如下:
module[name] = { status: String, // 记录模块的加载状态'loading' or 'loaded' export: Object , // 记录该模块的输出结果 onload: Arrray[function] // 记录模块加载完成后的回调函数}
简单实现
小女不才,实现主要参考了叶小钗博客的实现,读懂了他/她(不确定性别)的实现之后,自己实现了一遍。
// 利用闭包创建局部作用域(function(){ // 缓存所有的模块 var moduleMap = {}; //主要函数定义 function define(id, deps, factory){ var name; //存该模块对应的参数,即factory的参数 var params = []; if(typeof id == "object" ){ // 对应require(deps, factory)情况 factory = deps; deps = id; } else{ // 对应deine(id,deps, factory)和require(id)情况 name = id; } // 记录模块的依赖数 var depCount = deps && deps.length; // 如果没有依赖,直接保存或执行模块 if( !depCount || depCount == 0){ return saveModule(name, null ,factory) } else{ deps.forEach(function(dep, i){ // 用闭包解决经典的问题 (function(i){ // 加载依赖的模块,加载完之后的回调函数 loadScript(dep, function(param){ // 加载一个,计数器减一 depCount--; params[i] = param; //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块 if(depCount==0){ return saveModule(name, params, factory); } }) }(i)) }) } } function getURL(name){ if( name.indexOf('.js') < 0){ return name + '.js'; } return name; } function loadScript(name, callback){ var mod = moduleMap[name]; if( mod ){ // 如果加载完毕,直接执行回调 if( mod.status == 'loaded' ){ callback & callback(mod.export); } else{ // 正在加载,将回掉函数存起来 mod.onload.push(callback); } } else{ // 若该模块从没加载过,定义并且利用Dom加载 moduleMap[name] = { status: 'loading', export: null, onload: [callback] } var url = getURL(name); var el = document.createElement('script'); el.src = url; el.id = name; document.body.appendChild(el); } } // 保存或者执行模块 function saveModule(name, params, callback){ var mod = moduleMap[name]; if( mod ){ //设置状态为完成加载 mod.status = 'loaded'; //执行模块的函数,将结果存在export中 mod.export = callback && callback.apply(this, params); // 执行上一级模块的回调函数 while( fn = mod.onload.pop() ){ fn && fn(mod.export); } } else{ //若该模块未定义,直接执行返回结果,对应require(deps, factory)的情况 callback.apply(this, params) } } // 将局部变量暴露出去 window.define = define; window.require = define;})()
测试分析
上个简单的例子,源码Demo点击这里。
<!--index.html--><!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title> requireJS Demo -- my version </title></head><body><script src="./myRequire.js"></script> <script src="./index.js"></script> </body></html>// index.jsrequire(['main'], function(main) { console.log('start~~~'); main.hello();});// main.js define('main', [], function() { console.log('require module: main'); return { hello: function() { console.log('hello main'); } };}); // myRequire.js// 见上一部分
下面我们来对照着分析一下执行过程,帮助理解。
- 入口函数
require
,此时name
为空,depCount
为1;发现依赖main
,执行loadScript
函数 由于
moduleMap['main']
未定义,定义它,并且创建Dom元素去加载main.js
。此时定义如下:moduleMap['main'] = { status: 'loading', export: null, onload:[function(param){ // depCount = 1 depCount--; params[i] = param; //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块 if(depCount==0){ return saveModule('', params, factory); } }] }
main.js
加载完毕之后,执行里面的函数define('main'...)
.- 由于该函数没有依赖,直接进入
saveModule('main'...)
因为
moduleMap['main']
已经被定义,将状态改为loaded
,执行它的回调函数,并赋值给export
属性// 回调函数function() { console.log('require module: main'); return { hello: function() { console.log('hello main'); } };}
控制台输出
'require module: main'
,此时的moduleMap['main']
变成:moduleMap['main'] = { status: 'loaded', export: { hello: function() { console.log('hello main'); } }, onload:[function(param){ // 加载一个,计数器减一 depCount--; params[i] = param; //将计数器为0,说明依赖加载完毕,可以保存或执行当前的模块 if(depCount==0){ return saveModule('', params, factory); } }] }
执行上一级模块的回调函数
moduleMap['main'].onload
,将moduleMap['main'].export
作为参数- 此时
params
变成[moduleMap['main'].export]
,depCount
刚开始为1,现在为0,正好执行上一级模块的saveModule('', params, factory)
由于
moduleMap['']
未定义,直接执行回调函数,即:function(main) { console.log('start~~~'); main.hello();}/* 其中 main = { hello: function() { console.log('hello main'); }}*/
10.控制台输出'start~~~'
,以及'hello main'
。
运行到此结束。
遗留问题
很遗憾,这个实现并没解决循环依赖问题。我也看过ES6和CommonJS对循环依赖的处理,ES6使用的是引用的方式,所以不存在问题;CommonJS是只执行已经定义的部分。
幸运的是,这个实现如果有循环依赖,程序要因为不满足条件而终止执行,不会进行无数次的循环而导致崩溃。
参考资料:
- 叶小钗 【模块化编程】理解requireJS-实现一个简单的模块加载器
- 阮一峰 JavaScript 模块的循环加载
- 景庄 从零开始构建JavaScript模块化加载器
- JS模块化简单实现
- Extjs3+sea.js 实现模块化
- vue.js+webpack 实现模块化
- 简单模拟js的模块化依赖
- Android 实现简单的插件化模块化
- JS 模块化
- 模块化js
- js模块化
- js模块化
- js模块化
- JS模块化
- js模块化
- js 模块化
- js模块化
- JS模块化
- JS模块化
- js模块化
- Browserify —— 利用Node.js实现JS模块化加载
- Git使用详细教程
- weblogic 10.3密码重置
- Android截屏方案
- BASE64加密算法
- 第二节:Maven的安装与配置
- JS模块化简单实现
- 808 协议的解析
- Camera源码解析之数据传递
- Android Fragment应用实战,使用碎片向ActivityGroup说再见
- Android开发人员不得不收集的代码(持续更新中)
- 安卓常用调试工具
- Android滚动截屏,ScrollView截屏,Listview截屏,Recyclerview截屏
- QT 的信号与槽机制介绍
- 基于TCP/IP和UDP的socket编程