Require.js 源码分析
本文将简单介绍下个人对require.js的源码分析,简单分析实现原理
一.require加载资源的流程
require中,根据AMD(Asynchronous Module Definition)的思想,即异步模块加载机制,其思想就是把代码分为一个个的模块来分块按需加载,这样我们可以组装很多UI或者功能组件,从而实现代码的复用性。
1.data-main 函数从requirejs方法开始调用,newContext方法调用makeRequire方法。
1.1 makeRequire方法判断module是否defined,如果是yes就调用localRequire方法 其中可以通过getModule获取已保存的资源
1.2 如果module没有defined,调用makeRequire方法通过 递归 迭代 不停的注册module 然后放入makeModuleMap中,调用callGetModule方法
1.3 callGetModule方法只用来加载js资源的 module.init 调用 module.fetch 调用 module.load 最后调用 req.load req.load 在遍历 调用req.createNode
1.4 其中有个checkLoaded方法使用定时器不停扫描资源加载状态,这里有3种状态
1.4.1 stillLoading 还在加载 enabling 对该模块的依赖进行加载和模块化 defining 对正在处理的模块进行加载,并运行模块中的callback enabled处理完成的
如果判断js加载完成,调用原生js的一些监听事件 如:onreadystatechange
req.load = function (context, moduleName, url) {
var config = (context && context.config) || {},
node;
if (isBrowser) {
//In the browser so use a script tag
node = req.createNode(config, moduleName, url); node.setAttribute('data-requirecontext', context.contextName);
node.setAttribute('data-requiremodule', moduleName); if (node.attachEvent && !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
!isOpera) { useInteractive = true; node.attachEvent('onreadystatechange', context.onScriptLoad); } else {
node.addEventListener('load', context.onScriptLoad, false);
node.addEventListener('error', context.onScriptError, false);
}
node.src = url;
if (config.onNodeCreated) {
config.onNodeCreated(node, config, moduleName, url);
} currentlyAddingScript = node;
if (baseElement) {
head.insertBefore(node, baseElement);
} else {
head.appendChild(node);
}
currentlyAddingScript = null; return node;
...
如何引入资源 ,其实是插入标签,async等于true,
req.createNode = function (config, moduleName, url) {
var node = config.xhtml ?
document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
document.createElement('script');
node.type = config.scriptType || 'text/javascript';
node.charset = 'utf-8';
node.async = true;
return node;
};
但事实是我们在查看源代码的时候没有看到很多<script>标签,因为require.js最后在加载完成后删除了,在checkLoaded方法种会扫描资源加载状态,最后把加载完毕的资源删除。
function removeScript(name) {
if (isBrowser) {
each(scripts(), function (scriptNode) {
if (scriptNode.getAttribute('data-requiremodule') === name &&
scriptNode.getAttribute('data-requirecontext') === context.contextName) {
scriptNode.parentNode.removeChild(scriptNode);
return true;
}
});
}
}
... if (!mod.inited && expired) {
if (hasPathFallback(modId)) {
usingPathFallback = true;
stillLoading = true;
} else {
noLoads.push(modId);
removeScript(modId);
}
}
...
二.require定义模块,这个函数根据参数类型和个数,最后定义module的依赖和回调,还记得官网大篇幅的文档如果定义module吧
http://requirejs.org/docs/api.html#define,一一揣摩就明白参数和回调的堆栈。
define = function (name, deps, callback) {
var node, context; //Allow for anonymous modules
if (typeof name !== 'string') {
//Adjust args appropriately
callback = deps;
deps = name;
name = null;
} //This module may not have dependencies
if (!isArray(deps)) {
callback = deps;
deps = null;
} //If no name, and callback is a function, then figure out if it a
//CommonJS thing with dependencies.
if (!deps && isFunction(callback)) {
deps = []; if (callback.length) {
callback
.toString()
.replace(commentRegExp, commentReplace)
.replace(cjsRequireRegExp, function (match, dep) {
deps.push(dep);
}); deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps);
}
} if (useInteractive) {
node = currentlyAddingScript || getInteractiveScript();
if (node) {
if (!name) {
name = node.getAttribute('data-requiremodule');
}
context = contexts[node.getAttribute('data-requirecontext')];
}
} if (context) {
context.defQueue.push([name, deps, callback]);
context.defQueueMap[name] = true;
} else {
globalDefQueue.push([name, deps, callback]);
}
}; define.amd = {
jQuery: true
};
在这里用到了2个很长的正则
commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg, 替换回调种的注释
cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, 匹配require中的参数,把产生提取出来,后面在判断是否Common.js来拼接Commonjs的三元素
最后重新附加到参数队尾。
三.require方法内部的主要功能实现
3.1 加载依赖资源什么时候才结束呢,这里require会在load方法中 触发js加载事件 回调onScriptLoad
onScriptLoad方法会调用completeLoad方法,
completeLoad: function (moduleName) {
var found, args, mod,
shim = getOwn(config.shim, moduleName) || {},
shExports = shim.exports; takeGlobalQueue(); while (defQueue.length) {
args = defQueue.shift();
if (args[0] === null) {
args[0] = moduleName;
//If already found an anonymous module and bound it
//to this name, then this is some other anon module
//waiting for its completeLoad to fire.
if (found) {
break;
}
found = true;
} else if (args[0] === moduleName) {
//Found matching define call for this script!
found = true;
} callGetModule(args);
}
context.defQueueMap = {}; //Do this after the cycle of callGetModule in case the result
//of those calls/init calls changes the registry.
mod = getOwn(registry, moduleName); if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) {
if (config.enforceDefine && (!shExports || !getGlobal(shExports))) {
if (hasPathFallback(moduleName)) {
return;
} else {
return onError(makeError('nodefine',
'No define call for ' + moduleName,
null,
[moduleName]));
}
} else {
//A script that does not call define(), so just simulate
//the call for it.
callGetModule([moduleName, (shim.deps || []), shim.exportsFn]);
}
} checkLoaded();
},
这个函数主要 遍历defQueue,获取一个module,传递moduleName,并调用callGetModule
去调用模块
function callGetModule(args) {
//Skip modules already defined.
if (!hasProp(defined, args[0])) {
getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
}
}
callGetModule会调用函数判断是否有这个moduleName的module是否定义了,如果没定义使用makeModuleMap创建一个module返回
在makeModuleMap可以看到一个module有那些属性,为了保证moduleId唯一性,可以看到有前缀后缀,以及id拼接规则。
suffix = prefix && !pluginModule && !isNormalized ?
'_unnormalized' + (unnormalizedCounter += 1) :
''; return {
prefix: prefix,
name: normalizedName,
parentMap: parentModuleMap,
unnormalized: !!suffix,
url: url,
originalName: originalName,
isDefine: isDefine,
id: (prefix ?
prefix + '!' + normalizedName :
normalizedName) + suffix
};
getModule方法有是如何实现的,在require内部定义了Module类
,而这个方法则会为当前的ModuleMap中,其中包含了这个模块的路径等信息。这里要注意的是getModule方法里面拥有一个基于
registry变量中,这里则是用缓存根据ModuleMap来实例化的Module,并将其保存在了registry变量中。
这里我们也可以看到Module类的定义
Module = function (map) {
this.events = getOwn(undefEvents, map.id) || {};
this.map = map;
this.shim = getOwn(config.shim, map.id);
this.depExports = [];
this.depMaps = [];
this.depMatched = [];
this.pluginMaps = {};
this.depCount = 0;
}; Module.prototype = {
//init Module
init : function (depMaps, factory, errback, options) {}, //define dependencies
defineDep : function (i, depExports) {}, //call require for plugins
fetch : function () {}, //use script to load js
load : function () {}, //Checks if the module is ready to define itself, and if so, define it.
check : function () {}, //call Plugins if them exist and defines them
callPlugin : function () {}, //enable dependencies and call defineDep
enable : function () {}, //register event
on : function (name, cb) {}, //trigger event
emit : function (name, evt) {}
}
创建一个Module的时候,会使用init方法,其中enable和check方法是module重要的方法 ,enable遍历调用check方法,全部依赖都检查无误之后,改变(资源加载中)enabling状态为false,
check方法不停的遍历依赖,最后改变module状态defined为真。
3.2多层依赖如何加载,重复加载的问题如何解决,比如 A依赖 BC ,B依赖 CD C依赖 DE
当如果我去requrie(A)时,require去查找defined中是否有A模块,如果没有,则去调用makeModuleMap来为即将调用的模块实例一个ModuleMap并加入到defined中,再用ModuleMap实例化一个Module加入到registry中,但是这时候的Module是一个空壳,它是只存储了一些模块相关的依赖等,模块里的exports或者callback是还没有被嵌进来,因为这个文件根本没有被加载
只有在触发module.init方法的时候才会真正的加载资源文件,但是如何保证加载是否重复,这时候会用到chekLoaded()方法,这个方法会检查依赖是否已经define的
function checkLoaded() {
var err, usingPathFallback,
waitInterval = config.waitSeconds * 1000,
//It is possible to disable the wait interval by using waitSeconds of 0.
expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(),
noLoads = [],
reqCalls = [],
stillLoading = false,
needCycleCheck = true; //Do not bother if this call was a result of a cycle break.
if (inCheckLoaded) {
return;
} inCheckLoaded = true; //Figure out the state of all the modules.
eachProp(enabledRegistry, function (mod) {
var map = mod.map,
modId = map.id; //Skip things that are not enabled or in error state.
if (!mod.enabled) {
return;
} if (!map.isDefine) {
reqCalls.push(mod);
} if (!mod.error) {
//If the module should be executed, and it has not
//been inited and time is up, remember it.
if (!mod.inited && expired) {
if (hasPathFallback(modId)) {
usingPathFallback = true;
stillLoading = true;
} else {
noLoads.push(modId);
removeScript(modId);
}
} else if (!mod.inited && mod.fetched && map.isDefine) {
stillLoading = true;
if (!map.prefix) {
//No reason to keep looking for unfinished
//loading. If the only stillLoading is a
//plugin resource though, keep going,
//because it may be that a plugin resource
//is waiting on a non-plugin cycle.
return (needCycleCheck = false);
}
}
}
}); if (expired && noLoads.length) {
//If wait time expired, throw error of unloaded modules.
err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads);
err.contextName = context.contextName;
return onError(err);
} //Not expired, check for a cycle.
if (needCycleCheck) {
each(reqCalls, function (mod) {
breakCycle(mod, {}, {});
});
} //If still waiting on loads, and the waiting load is something
//other than a plugin resource, or there are still outstanding
//scripts, then just try back later.
if ((!expired || usingPathFallback) && stillLoading) {
//Something is still waiting to load. Wait for it, but only
//if a timeout is not already in effect.
if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) {
checkLoadedTimeoutId = setTimeout(function () {
checkLoadedTimeoutId = 0;
checkLoaded();
}, 50);
}
} inCheckLoaded = false;
}
定义依赖方法,官网推荐使用
define(dependencies, callback)
而不使用
define( callback(
require(a);
require(b);
));
第二种方法会先把callback.toString,然后查找require,效率上差了些。
不管是第一种还是第二种,都必须依赖加载完成的时候才能回调,所以日常工作中我们需要确定依赖的先后顺序,一些主要暂时模块需要优先加载,一些不影响主页面的东西依赖的优先级要放低,依赖要原子项,不能多方依赖,多方依赖导致仅仅一个小功能需要加载很多额外的东西
Require.js 源码分析的更多相关文章
- require.js源码分析
写的寥寥草草,博客园的布局怎么弄还没有研究,再保存一份草稿,日后在完善,深度研究 require.js 加载顺序 1:加载html主页,require.js文件 2:脚本执行到html中的script ...
- basket.js 源码分析
basket.js 源码分析 一.前言 basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用.因此 ...
- events.js 源码分析
events.js 源码分析 1. 初始化 // 使用 this.ee = new EventEmitter(); // 源码 // 绑定this域,初始化 _events,_eventsCount和 ...
- Backbone.js源码分析(珍藏版)
源码分析珍藏,方便下次阅读! // Backbone.js 0.9.2 // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. // Backbone ...
- Vue.js 源码分析(二十七) 高级应用 异步组件 详解
当我们的项目足够大,使用的组件就会很多,此时如果一次性加载所有的组件是比较花费时间的.一开始就把所有的组件都加载是没必要的一笔开销,此时可以用异步组件来优化一下. 异步组件简单的说就是只有等到在页面里 ...
- Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
当使用is特性切换不同的组件时,每次都会重新生成组件Vue实例并生成对应的VNode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如: <!DOCTYPE html> ...
- Vue.js 源码分析(三十) 高级应用 函数式组件 详解
函数式组件比较特殊,也非常的灵活,它可以根据传入该组件的内容动态的渲染成任意想要的节点,在一些比较复杂的高级组件里用到,比如Vue-router里的<router-view>组件就是一个函 ...
- Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解
对于过度动画如果要同时渲染整个列表时,可以使用transition-group组件. transition-group组件的props和transition组件类似,不同点是transition-gr ...
- Vue.js 源码分析(二十八) 高级应用 transition组件 详解
transition组件可以给任何元素和组件添加进入/离开过渡,但只能给单个组件实行过渡效果(多个元素可以用transition-group组件,下一节再讲),调用该内置组件时,可以传入如下特性: n ...
随机推荐
- $.ajax()所有参数详解
原文:https://www.cnblogs.com/everest33Tong/p/6159700.html [一]$.ajax()所有参数详解 url: 要求为String类型的参数,(默认为当前 ...
- layer模态窗简单使用
layer.open({ type: 1,//模态窗种类 skin: "layui-layer-rim", title: "编辑信息", area: [&quo ...
- mvn install 报错Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:2.3.2 错误: 找不到符号
报错信息 在Eclipse中执行mvn install时,console端显示如下报错信息: [ERROR] Failed to execute goal org.apache.maven.plugi ...
- BeginEditorCommand的原理
代码来源 :http://www.arch-pub.com/problem-about-CPropertyPage_10682271.html CWnd* pAcadWnd = CWnd::FromH ...
- Objective-C适用C数学函数 <math.h>
在实际工作中有些程序不可避免的需要使用数学函数进行计算,比如地图程序的地理坐标到地图坐标的变换.Objective-C做为ANSI C的扩展,使用C标准库头文件<math.h>中定义的数学 ...
- 不用split调转字符串
- easyui页面上显示和PL/SQL编码问题
在页面上,只需要显示人们看的懂的文字就行,但是在数据库里面就不一定了,一般情况下,在数据库里面存字母,数字等除了汉字以外的字符,存汉字有个问题,就是有时候不同oracle数据库的客户端会出现乱码问题: ...
- python使用python-docx导出word
#!/usr/bin/env python # -*- coding: utf-8 -*- ''' ''' from docx import Document from docx.shared imp ...
- leetcode-682-Baseball Game
题目描述: You're now a baseball game point recorder. Given a list of strings, each string can be one of ...
- Maven 远程仓库下载慢的的解决方案
配置很简单,修改conf文件夹下的settings.xml文件,添加如下镜像配置: 我直接去设置maven目录下面的setttings文件 添加镜像站点 <mirrors> <mir ...