问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501
你好,欢迎来到懂视!登录注册
当前位置: 首页 - 正文

如何让浏览器加载js脚本

发布网友 发布时间:2022-04-20 19:38

我来回答

2个回答

懂视网 时间:2022-05-16 10:48

在es6之前,js不像其他语言自带成熟的模块化功能,页面只能靠插入一个个script标签来引入自己的或第三方的脚本,并且容易带来命名冲突的问题。js社区做了很多努力,在当时的运行环境中,实现"模块"的效果。

通用的js模块化标准有CommonJS与AMD,前者运用于node环境,后者在浏览器环境中由Require.js等实现。此外还有国内的开源项目Sea.js,遵循CMD规范。(目前随着es6的普及已经停止维护,不论是AMD还是CMD,都将是一段历史了)

浏览器端js加载器

实现一个简单的js加载器并不复杂,主要可以分为解析路径、下载模块、解析模块依赖、解析模块四个步骤。

首先定义一下模块。在各种规范中,通常一个js文件即表示一个模块。那么,我们可以在模块文件中,构造一个闭包,并传出一个对象,作为模块的导出:

define(factory() {
 var x = {
 a: 1
 };
 return x;
});

define函数接收一个工厂函数参数,浏览器执行该脚本时,define函数执行factory,并把它的return值存储在加载器的模块对象modules里。

如何标识一个模块呢?可以用文件的uri,它是唯一标识,是天然的id。

文件路径path有几种形式:

绝对路径:http://xxx, file://xxx

相对路径:./xxx , ../xxx , xxx(相对当前页面的文件路径)

虚拟绝对路径:/xxx /表示网站根目录

因此,需要一个resolvePath函数来将不同形式的path解析成uri,参照当前页面的文件路径来解析。

接着,假设我们需要引用a.js与b.js两个模块,并设置了需要a与b才能执行的回调函数f。我们希望加载器去拉取a与b,当a与b都加载完成后,从modules里取出a与b作为参数传给f,执行下一步操作。这里可以用观察者模式(即订阅/发布模式)实现,创建一个eventProxy,订阅加载a与加载b事件;define函数执行到最后,已经把导出挂载modules里之后,emit一个本模块加载完成的事件,eventProxy收到后检查a与b是否都加载完成,如果完成,就传参给f执行回调。

同理,eventProxy也可以实现模块依赖加载

// a.js
define([ 'c.js', 'd.js' ], factory (c, d) {
 var x = c + d;
 return x;
});

define函数的第一个参数可以传入一个依赖数组,表示a模块依赖c与d。define执行时,告诉eventProxy订阅c与d加载事件,加载好了就执行回调函数f存储a的导出,并emit事件a已加载。

浏览器端加载脚本的原始方法是插入一个script标签,指定src之后,浏览器开始下载该脚本。

那么加载器中的模块加载可以用dom操作实现,插入一个script标签并指定src,此时该模块为下载中状态。

PS:浏览器中,动态插入script标签与初次加载页面dom时的script加载方式不同:

初次加载页面,浏览器会从上到下顺序解析dom,碰到script标签时,下载脚本并阻塞dom解析,等到该脚本下载、执行完毕后再继续解析之后的dom(现代浏览器做了preload优化,会预先下载好多个脚本,但执行顺序与它们在dom中顺序一致,执行时阻塞其他dom解析)

动态插入script,

var a = document.createElement('script'); a.src='xxx'; document.body.appendChild(a);

浏览器会在该脚本下载完成后执行,过程是异步的。

下载完成后执行上述的操作,解析依赖->加载依赖->解析本模块->加载完成->执行回调。

模块下载完成后,如何在解析它时知道它的uri呢?有两种发发,一种是用srcipt.onload获取this对象的src属性;一种是在define函数中采用document.currentScript.src。

实现基本的功能比较简单,代码不到200行:

var zmm = {
 _modules: {},
 _configs: {
 // 用于拼接相对路径
 basePath: (function (path) {
 if (path.charAt(path.length - 1) === '/') {
 path = path.substr(0, path.length - 1);
 }
 return path.substr(path.indexOf(location.host) + location.host.length + 1);
 })(location.href),
 // 用于拼接相对根路径
 host: location.protocol + '//' + location.host + '/'
 }
};
zmm.hasModule = function (_uri) {
 // 判断是否已有该模块,不论加载中或已加载好
 return this._modules.hasOwnProperty(_uri);
};
zmm.isModuleLoaded = function (_uri) {
 // 判断该模块是否已加载好
 return !!this._modules[_uri];
};
zmm.pushModule = function (_uri) {
 // 新模块占坑,但此时还未加载完成,表示加载中;防止重复加载
 if (!this._modules.hasOwnProperty(_uri)) {
 this._modules[_uri] = null;
 }
};
zmm.installModule = function (_uri, mod) {
 this._modules[_uri] = mod;
};
zmm.load = function (uris) {
 var i, nsc;
 for (i = 0; i < uris.length; i++) {
 if (!this.hasModule(uris[i])) {
 this.pushModule(uris[i]);
 // 开始加载
 var nsc = document.createElement('script');
 nsc.src = uri;
 document.body.appendChild(nsc);
 }
 }
};
zmm.resolvePath = function (path) {
 // 返回绝对路径
 var res = '', paths = [], resPaths;
 if (path.match(/.*://.*/)) {
 // 绝对路径
 res = path.match(/.*://.*?//)[0]; // 协议+域名
 path = path.substr(res.length);
 } else if (path.charAt(0) === '/') {
 // 相对根路径 /开头
 res = this._configs.host;
 path = path.substr(1);
 } else {
 // 相对路径 ./或../开头或直接文件名
 res = this._configs.host;
 resPaths = this._configs.basePath.split('/');
 }
 resPaths = resPaths || [];
 paths = path.split('/');
 for (var i = 0; i < paths.length; i++) {
 if (paths[i] === '..') {
 resPaths.pop();
 } else if (paths[i] === '.') {
 // do nothing
 } else {
 resPaths.push(paths[i]);
 }
 }
 res += resPaths.join('/');
 return res;
};
var define = zmm.define = function (dependPaths, fac) {
 var _uri = document.currentScript.src;
 if (zmm.isModuleLoaded(_uri)) {
 return;
 }
 var factory, depPaths, uris = [];
 if (arguments.length === 1) {
 factory = arguments[0];
 // 挂载到模块组中
 zmm.installModule(_uri, factory());
 // 告诉proxy该模块已装载好
 zmm.proxy.emit(_uri);
 } else {
 // 有依赖的情况
 factory = arguments[1];
 // 装载完成的回调函数
 zmm.use(arguments[0], function () {
 zmm.installModule(_uri, factory.apply(null, arguments));
 zmm.proxy.emit(_uri);
 });
 }
};
zmm.use = function (paths, callback) {
 if (!Array.isArray(paths)) {
 paths = [paths];
 }
 var uris = [], i;
 for (i = 0; i < paths.length; i++) {
 uris.push(this.resolvePath(paths[i]));
 }
 // 先注册事件,再加载
 this.proxy.watch(uris, callback);
 this.load(uris);
};
zmm.proxy = function () {
 var proxy = {};
 var taskId = 0;
 var taskList = {};
 var execute = function (task) {
 var uris = task.uris,
 callback = task.callback;
 for (var i = 0, arr = []; i < uris.length; i++) {
 arr.push(zmm._modules[uris[i]]);
 }
 callback.apply(null, arr);
 };
 var deal_loaded = function (_uri) {
 var i, k, task, sum;
 // 当一个模块加载完成时,遍历当前任务栈
 for (k in taskList) {
 if (!taskList.hasOwnProperty(k)) {
 continue;
 }
 task = taskList[k];
 if (task.uris.indexOf(_uri) > -1) {
 // 查看这个任务中的模块是否都已加载好
 for (i = 0, sum = 0; i < task.uris.length; i++) {
 if (zmm.isModuleLoaded(task.uris[i])) {
 sum ++;
 }
 }
 if (sum === task.uris.length) {
 // 都加载完成 删除任务
 delete(taskList[k]);
 execute(task);
 }
 }
 }
 };
 proxy.watch = function (uris, callback) {
 // 先检查一遍是否都加载好了
 for (var i = 0, sum = 0; i < uris.length; i++) {
 if (zmm.isModuleLoaded(uris[i])) {
 sum ++;
 }
 }
 if (sum === uris.length) {
 execute({
 uris: uris,
 callback: callback
 });
 } else {
 // 订阅新加载任务
 var task = {
 uris: uris,
 callback: callback
 };
 taskList['' + taskId] = task;
 taskId ++;
 }
 };
 proxy.emit = function (_uri) {
 console.log(_uri + ' is loaded!');
 deal_loaded(_uri);
 };
 return proxy;
}();

循环依赖问题

"循环加载"指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。这是一种应该尽量避免的设计。

浏览器端

用上面的zmm工具加载模块a:

// main.html
zmm.use('/a.js', function(){...});
// a.js
define('/b.js', function(b) {
 var a = 1;
 a = b + 1;
 return a;
});
// b.js
define('/a.js', function(a) {
 var b = a + 1;
 return b;
});

就会陷入a等待b加载完成、b等待a加载完成的死锁状态。sea.js碰到这种情况也是死锁,也许是默认这种行为不应该出现。

seajs里可以通过require.async来缓解循环依赖的问题,但必须改写a.js:

// a.js
define('./js/a', function (require, exports, module) {
 var a = 1;
 require.async('./b', function (b) {
 a = b + 1;
 module.exports = a; //a= 3
 });
 module.exports = a; // a= 1
});
// b.js
define('./js/b', function (require, exports, module) {
 var a = require('./a');
 var b = a + 1;
 module.exports = b;
});
// main.html
seajs.use('./js/a', function (a) {
 console.log(a); // 1
});

但这么做a就必须先知道b会依赖自己,且use中输出的是b还没加载时a的值,use并不知道a的值之后还会改变。

在浏览器端,似乎没有很好的解决方案。node模块加载碰到的循环依赖问题则小得多。

node/CommonJS

CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

// a.js
var a = 1;
module.exports = a;
var b = require('./b');
a = b + 1;
module.exports = a;
// b.js
var a = require('./a');
var b = a + 1;
module.exports = b;
// main.js
var a = require('./a');
console.log(a); //3

上面main.js的代码中,先加载模块a,执行require函数,此时内存中已经挂了一个模块a,它的exports为一个空对象a.exports={};接着执行a.js中的代码;执行var b = require('./b');之前,a.exports=1,接着执行require(b);b.js被执行时,拿到的是a.exports=1,b加载完成后,执行权回到a.js;最后a模块的输出为3。

CommonJS与浏览器端的加载器有着实现上的差异。node加载的模块都是在本地,执行的是同步的加载过程,即按依赖关系依次加载,执行到加载语句就去加载另一个模块,加载完了再回到函数调用点继续执行;浏览器端加载scripts由于天生限制,只能采取异步加载,执行回调来实现。

ES6

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。因此,ES6模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

这导致ES6处理"循环加载"与CommonJS有本质的不同。ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

来看一个例子:

// even.js
import { odd } from './odd';
export var counter = 0;
export function even(n) { counter++; return n == 0 || odd(n - 1);}
// odd.js
import { even } from './even';
export function odd(n) { return n != 0 && even(n - 1);}
// main.js
import * as m from './even.js';
m.even(10); // true; m.counter = 6

上面代码中,even.js里面的函数even有一个参数n,只要不等于0,就会减去1,传入加载的odd()。odd.js也会做类似作。

上面代码中,参数n从10变为0的过程中,foo()一共会执行6次,所以变量counter等于6。第二次调用even()时,参数n从20变为0,foo()一共会执行11次,加上前面的6次,所以变量counter等于17。

而这个例子要是改写成CommonJS,就根本无法执行,会报错。

// even.js
var odd = require('./odd');
var counter = 0;
exports.counter = counter;
exports.even = function(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
var even = require('./even').even;
module.exports = function(n) {
return n != 0 && even(n - 1);
}
// main.js
var m = require('./even');
m.even(10); // TypeError: even is not a function

上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成"循环加载"。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于null,等到后面调用even(n-1)就会报错。

热心网友 时间:2022-05-16 07:56

在body 里面 输入 <body onload="hxtime()"> 加粗的地方是你用的函数名。
声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。
E-MAIL:11247931@qq.com
吉利博越的发动机是什么牌子 吉利博越的发动机是什么品牌 吉利博越汽车搭载的是什么发动机? 博越发动机是什么牌子? 吉利博越是什么发动机? 消费冲正什么时候到账 怎么开淘宝会员? 在抖音怎么开通会员呢? 爱奇艺怎么开通会员便宜吗? 我手机oppor7,下了个1.8gb的大游戏,运行内存怎么不够,下面是图 弹弹堂出徒宝箱怎样高几率开出天天向上? 在浏览器调用js脚本会不会被服务器发现? JS怎样检测浏览器内的脚本 JS脚本语言是干什么用的 浏览器怎么添加js格式的脚本 怎么开启浏览器的js脚本功能 请给姓朱的小男孩起名字 父亲姓朱,给孩子取个好名字 给孩子起名字,姓朱,叫朱玉什么好听 想给朱姓男宝宝起名 姓朱,帮忙给孩子起名字!! 在AI里面总有一个大框,怎么样去除呢 AI ,自己画的图和嵌入的图片,只想要一部分显示,... AI怎么擦除画多余的部分? AI里我用打印拼贴工具在画板上点击了,它就在画板... AI画板上有个固定的外框选不中 删不掉又不是出血是... ai软件怎么删除页面? 怎样去掉AI里画板名称“画板1.2.3,副本”,导出文件... 求问: AI画板删除 但是原来画板的底色还在。见图... AI 软件 怎么清除多余的部分 AI illustrator 如何能删掉边缘多余的部分 如何将javascript脚本在IE浏览器中运行? 您的浏览器禁用了JS脚本运行,请启用该功能,请问... 我在网页中加入JS脚本后,运行时浏览器自动阻止了J... 如何让浏览器中的每个网页都执行我写的一个js脚本? 怎么让浏览器打开的网页执行一段js脚本 怎样在IE浏览器中开启javascript脚本功能 win7浏览器如何启用javascript脚本 把一个网页下载下来,里面会有很多的js脚本,我想... js 网页脚本 浏览器禁止了JS脚本运行是怎么回事?怎么启动啊 如何禁止浏览器运行 JavaScript 脚本 华为畅玩7x报价多少钱 华为荣耀畅享7x多少钱 华为畅玩7x参数配置 请问当初华为荣耀畅玩7x,新机多少钱,谢谢 华为荣耀7x多少钱 华为荣耀7x价格,参数 华为畅享7X多少钱 7X多少钱华为荣耀畅玩7X价格介绍 荣耀畅玩7x什么时候上市 新机价格大概是多少 华为荣耀畅玩7X的屏幕有多大?
  • 焦点

最新推荐

猜你喜欢

热门推荐