Introduction

I've developed some pretty seriously Javascript intensive sites, where the sheer quantity of Javascript on the page is so much that I worry about the load time for the page getting too big. Often large chunks of the Javascript code are only used very rarely, and it seems a shame to load all that code that the user will probably never use. So I wanted a way to dynamically load Javascript functions on demand. This is sometimes called "lazy loading".

For example, my web paint-by-number site has pages where users can solve logic puzzles. Normally users want to solve the puzzles themselves, but when someone is developing a new puzzle, they may need to test solve the same puzzle dozens of time. Doing this gets a bit dull, so we have a simple AI puzzle solving program they can use to solve their own puzzles. This is a big hunk of rarely used Javascript. We don't want to load it with every puzzle page. We want to load it only when the user clicks the "helper" button for the first time.

There are several ways you can load additional Javascript on demand. The best known one is to use XMLHttpRequest() to fetch a file containing the Javascript, and then use the Javascript eval() to evaluate it.

This essay considers an alternative method: dynamically created <script> tags.

On-Demand Loading

One approach to dynamically loading "helper.js" is to create a

    <script type="text/javascript" src="helper.js"></script>

tag and insert it into the current page's <head> block. You could do that with the following lines of Javascript code:

   var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= 'helper.js';
head.appendChild(script);

This works fine on every modern browser I've tested (IE 5.0 and 7.0; Firefox 2.0; Safari 1.3; Opera 7.54 and 9.10; Konqueror 3.5; iCab 3.0). The only browser I'ved tried that did not work is Macintosh IE (version 5.2), which does not allow the script.src property to be changed either by direct assignment (as shown here) or by a setAttribute() call.

I tried to simplify this further by having the <script> tag already defined in the header of the document, like this:

   <script id="loadarea" type="text/javascript"></script>

And then just setting the src attribute when I wanted to load a file, like this:

   document.getElementById('loadarea').src= 'helper.js';

Unfortunately, this doesn't work so well. It's fine in Windows IE, Opera and iCab. In Gecko browsers, it only works once. If you try to load a second file by changing the src of the <script> tag again, the new source is not loaded. It doesn't work in Safari or Konqueror, and of course it generates the same error as the other method in Macintosh IE.

Pity. It'd be cool to be able to do this with one line of Javascript.

Detecting Load Completion

After we have loaded the helper code, we obviously want to call it to start it running. However, we can't just put a call to helper() after the commands above, because the browser may be loading the "helper.js" file asynchronously, which means that our call may occur before the function has finished (or even started) loading.

One web page suggested setting up some event handlers that will be called when the load is complete. We do that by adding the following lines to the previous code:

   var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.onreadystatechange= function () {
if (this.readyState == 'complete') helper();
}
script.onload= helper;
script.src= 'helper.js';
head.appendChild(script);

Here we set up two different event handlers on the newly created script tag. Depending on the browser, one or the other of these two handlers is supposed to be called when the script has finished loading. The onreadystatechange handler works on IE only. The onload handler works on Gecko browsers and Opera.

The "this.readyState == 'complete'" test doesn't actually entirely work. The readyState theoretically goes through a series of states:

uninitialized
1 loading
2 loaded
3 interactive
4 complete

But in fact, states may be skipped. In my experience with IE 7, you get either a loaded event or a completed event, but not both. It may have something to do with whether you are loading from cache or not but there seem to be other factors that influence which events you get. Sometimes I getloading or interactive events too, and sometimes I don't. It's possible the test should be "this.readyState == 'loaded' || this.readyState == 'complete'", but that risks triggering twice.

By the way, if you are reading about readyState on the net, it is important to understand that most of the discussion is in the context of the request object returned by XMLHttpRequest() calls. This is quite different. Almost all browsers support that, not just IE, and the values are numbers instead of strings. The complete (4) state in that case does reliably occur, though the others are a bit quirky in that context too.

Though the problem in IE is bad enough, there's more. So far as I can tell, no event is fired when the script load is complete in either Safari 1.2, Konqueror, or iCab. I've seen claims that it works on Safari 2.0, so long as you set up the handler before you attach the script element to the document, but haven't confirmed this. It's possible they were talking about the XMLHttpRequest() case.

On the whole, I decided that these loading events are not reliable enough on enough browsers to be usable.

Luckily, for my application, a much simpler approach suffices. It'll work for you if (1) you control the contents of the Javascript file being loaded, and (2) you always want to call the same callback function when it is loaded. If that's the case, just put a call to the callback function on the last line of the Javascript file. After the rest of the file has been loaded, the callback function gets called. What could be simpler?

In my case whenever we load the 'helper.js' file, we always want to execute the helper() function immediately after the load. In this case, it is one of the functions loaded, but it could equally well be a function defined before the load. So, we simply make the last line of the 'helper.js' file a call to the helper() function. Then it will always be executed immediately after everything else has been loaded. No flakey, non-portable event handling is required.

Here's a little test script for script loading events. When you click the "load script" link, a script will be loaded. You will see alerts when the script is started and when readystatechange and load events fire. On either of these events, the loaded function is called, which simply does another alert. The last line of the loaded script also does an alert. Clicking on the "run loaded function" link runs the function (if it is defined).

load script 
run loaded function

Loading Only Once

So, the first time a paint-by-number user clicks on the helper button, we load the helper script and then run the helper() function from a call at the end of the script. But the next time someone clicks on the helper button, we don't want to load it again. We just want to execute the already loaded function.

There are many ways you could do this. We could check if the helper() function is defined, and load it only if it isn't, but there is an easier way. Simply name the function that loads the Javascript the same as the function that is loaded. In our case, we would do something like:

   <input type="button" onclick="helper()" value="Helper">

   <script language="JavaScript">
function helper()
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= 'helper.js';
head.appendChild(script);
}
</script>

The script file being loaded looks something like this:

   function helper()
{
:
lots of code
:
} :
more function and global variable definitions
: helper();

So the "helper" button runs the helper() script when clicked. Initially the helper() function is the dummy function that just loads the 'helper.js' file. That file defines the real helper() function, replacing the previous function definition. The last line of the file calls helper() running the new helper function. The next time the "helper" button is clicked, the loaded version of the helper() function will be run, since the other version doesn't exist anymore.

Multiple Loads

As many people have pointed out, this method of creating a <script> tag to load some Javascript could be used as an alternative way to fetch JSON-formatted data from the server, instead of XMLHttpRequest(). If you do that, then you'll likely want to be making the same, or similar calls multiple times. This is likely to cause a couple easily fixed problems.

First, you need to worry about caching. Typically, the url you're going to be loading is going to be some kind of CGI program. After all, if the output doesn't vary, why bother loading it more than once? Usually browsers are fairly smart about not caching those, but you might want to take some steps to ensure that the second call doesn't just get you the cached copy of the result of the first call. So the CGI should probably be outputting headers to suppress caching. You might also put extra arguments on the URL, perhaps by keeping a count of the number of times you've called it, and adding a "?count=7" argument onto the end of the URL (where 7 is the current count). This makes the URL different on each call, and further ensures that you won't get a cached copy.

Second, you'll tend to accumulate a lot of <script> tags in your header. This doesn't necessarily do a whole lot of harm, but it seems sloppy. So you could probably delete them by doing head.removeChild(script). I think you can actually have the callback function do this immediately after loading. Removing the <script> tag does not undefine functions or variables that were defined by it, so you are done with it the moment it is done loading.

Comparison with XMLHttpRequest()

This method of loading Javascript code or data by dynamically creating new <script> tags in the header does have some advantages over XMLHttpRequest() calls:

  • Can request a file from anywhere on the net, not just the server your page was loaded from.
  • Works in IE even when ActiveX is turned off (though not when Javascript is turned off).
  • Works with a few older browsers that don't support XMLHttpRequest, like Opera 7 (though not Macintosh versions of IE).

There are also some modest disadvantages, including:

  • Returned data has to be formatted as Javascript code. XMLHttpRequest() can be used to fetch data in any format, XML, JSON, plain text, or whatever.
  • Can only do GET requests, not POST requests.
  • Whether the request is synchronous or asynchronous is pot luck, depending on the browser. With XMLHttpRequest() you can control this.
  • When fetching JSON data from an untrusted source, there is no possiblity of checking the data before feeding it to the Javascript parser. With XMLHttpRequest() you can parse the data with something like json2.js instead of eval() for secure parsing.

An additional difference, which may or may not be an advantage depending on your appliction, is that functions and variables defined by loading a file by creating a new <script> tag are always created in the global context, while those created with an eval() call are defined in the current context. If you want the data loaded to be only locally defined, then this is an advantage for the XMLHttpRequest() approach. If you want to be able to load global functions and variables from anywhere in your program, this is an advantage for dynamic <script> tags.

On the whole, I think dynamic <script> tags are sometimes a better way to load additional blocks of code than XMLHttpRequest(), but for fetching data, XMLHttpRequest() usually wins out.

JavaScript Madness: Dynamic Script Loading的更多相关文章

  1. 自己编写jQuery动态引入js文件插件 (jquery.import.dynamic.script)

    这个插件主要是结合jquery或者xhr异步请求来使用的,它可以把已经引入过的js文件记录在浏览器内存中,当下次再引入相同的文件就忽略该文件的引入. 此插件不支持浏览器刷新保存数据,那需要利用cook ...

  2. Javascript Madness: Mouse Events

    http://unixpapa.com/js/mouse.html Javascript Madness: Mouse Events Jan WolterAug 12, 2011 Note: I ha ...

  3. Javascript中alert</script>的方法

    Javascript中alert</script>的方法: <%@ page language="java" import="java.util.*&q ...

  4. javascript动态创建script标签,加载完成后调用回调

    代码如下: var head = document.getElementsByTagName('head')[0]; var script = document.createElement('scri ...

  5. [Algorithms] Solve Complex Problems in JavaScript with Dynamic Programming

    Every dynamic programming algorithm starts with a grid. It entails solving subproblems and builds up ...

  6. javascript正则找script标签, link标签里面的src或者 href属性

    1. [代码]javascript 简单的search    <script(?:(?:\s|.)+?)src=[\"\'](.+?)[\"\'](?!\<)(?:(? ...

  7. JavaScript动态创建script标签并执行js代码

    <script> //创建一个script标签 function loadScriptString(code) { var script = document.createElement( ...

  8. elasticsearch update方法报错: Too many dynamic script compilations within, max: [75/5m]

    PUT _cluster/settings    {        "transient" : {            "script.max_compilations ...

  9. CRM 2013 Script Loading Deep Dive

    关于CRM中脚本的加载次序梳理的很不错,可以看看 https://community.dynamics.com/crm/b/develop1/archive/2013/11/02/crm-2013-s ...

随机推荐

  1. josephus问题

    问题描述 n个人围成一圈,号码为1-n,从1开始报数,报到2的退出,剩下的继续从1开始报数,求最后一个人的号码. 算法分析 最直观的算法是用循环链表模拟.从首节点开始,不断删除第二个节点,直到只剩一个 ...

  2. BMP文件格式分析

    前两天要做一个读取bmp文件的小程序,顺便查找了一些关于BMP格式的文章,现在post上来. 简介 BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,在Windows环境下运 ...

  3. 【风马一族_Android】适合你 --- 大概的描述

    适合你:专注于解决毕业生,离校所遗留的闲置教材的去向问题的一款APP. 目前的现状:毕业生的闲置教材,被清理宿舍的阿姨.大叔所清理到垃圾场,或拿到收破烂的地方,卖掉. 在毕业季中,存在的闲置物品不只有 ...

  4. 【风马一族_Python】 决策树

    <机器学习实战>第三章 决策树 ------------------------------------- #1 trees.py 计算给定数据集的香农熵 ---------------- ...

  5. Silverlight 中DataGrid中全选与非全选问题

    问题:当点击全选时,全选所有的复选框,但是滚动屏幕时,却复选框就会取消选中 一.解决方法(将要展示的实体数据模型添加bool属性,在数据绑定时添加click时间,盘带选中的状态,就可以了) 1. xa ...

  6. 在Linux中三种让crontab每秒执行任务的方法

    第一种方法: 1.创建脚本文件 cat phplog.sh 2.编辑脚本内容 #!/bin/bash while : ;do /home/scripts.sh 2>/dev/null & ...

  7. hadoop数据流转过程分析

    hadoop:数据流转图(基于hadoop 0.18.3):通过一个最简单的例子来说明hadoop中的数据流转. hadoop:数据流转图(基于hadoop 0.18.3): 这里使用一个例子说明ha ...

  8. Docker无法启动 Could not find a free IP address range for interface 'docker0' 最方便的解决办法

    阿里云的CentOS 6.5上安装Docker会无法启动,如果直接运行docker -d会看到错误提示:Could not find a free IP address range for inter ...

  9. JNI 学习笔记

    JNI是Java Native Interface的缩写,JNI是一种机制,有了它就可以在java程序中调用其他native代码,或者使native代码调用java层的代码.也 就是说,有了JNI我们 ...

  10. MySQL参数调优最佳实践

    前言很多时候,RDS用户经常会问如何调优RDS MySQL的参数,为了回答这个问题,写一篇blog来进行解释: 哪一些参数不能修改,那一些参数可以修改:这些提供修改的参数是不是已经是最佳设置,如何才能 ...