`
wenbois2000
  • 浏览: 44584 次
  • 性别: Icon_minigender_1
  • 来自: 湖南
社区版块
存档分类
最新评论

JavaScript Patterns 读书笔记(七)

    博客分类:
  • Web
阅读更多

7.Client Pattern

  • DOM Access
       DOM access is expensive; it’s the most common bottleneck when it comes to JavaScript performance. This is because the DOM is usually implemented separately from the JavaScript engine. From a browser’s perspective, it makes sense to take this approach, because a JavaScript application may not need DOM at all. And also languages other than JavaScript (for example, VBScript in IE) can be used to work with the page’s DOM. The bottom line is that DOM access should be reduced to minimum. This means:

                • Avoiding DOM access in loops
                • Assigning DOM references to local variables and working with the locals
                • Using selectors API where available
                • Caching the length when iterating over HTML collections

       Consider the following example where the second (better) loop, despite being longer, will be tens to hundreds of times faster, depending on the browser:
    // antipattern
    for (var i = 0; i < 100; i += 1) {
    	document.getElementById("result").innerHTML += i + ", ";
    }
    
    // better - update a local variable
    var i, content = "";
    for (i = 0; i < 100; i += 1) {
    	content += i + ",";
    }
    document.getElementById("result").innerHTML += content;
    
        In the next snippet, the second example (using a local variable style) is better, although it requires one more line of code and one more variable:
    // antipattern
    var padding = document.getElementById("result").style.padding,
    margin = document.getElementById("result").style.margin;
    
    // better
    var style = document.getElementById("result").style,
    padding = style.padding,
    margin = style.margin;
    
     
       Using selector APIs means using the methods:
      
    document.querySelector("ul .selected");
    document.querySelectorAll("#widget .class");
       These methods accept a CSS selector string and return a list of DOM nodes that match the selection. The selector methods are available in modern browsers (and in IE since version 8) and will always be faster than if you do the selection yourself using other DOM methods. Recent versions of popular JavaScript libraries take advantage of the selector APIs, so you should make sure you use an up-to-date version of your preferred library.
        It will also help if you add id="" attributes to elements you’ll be accessing often, because document.getElementById(myid) is the easiest and fastest way to find a node.

  • DOM Manipulation
       In addition to accessing the DOM elements, you often need to change them, remove some of them, or add new ones. Updating the DOM can cause the browser to repaint the screen and also often reflow (recalculate elements’ geometry), which can be expensive.
       Again, the general rule of thumb is to have fewer DOM updates, which means batching changes and performing them outside of the “live” document tree. When you need to create a relatively big subtree, you should do so without adding to the live document until the end. For this purpose you can use a document fragment to contain all your nodes.
      Here’s how not to append nodes:
    // antipattern
    // appending nodes as they are created
    var p, t;
    p = document.createElement('p');
    t = document.createTextNode('first paragraph');
    p.appendChild(t);
    document.body.appendChild(p);
    
    p = document.createElement('p');
    t = document.createTextNode('second paragraph');
    p.appendChild(t);
    document.body.appendChild(p);
    
        A better version will be to create a document fragment, update it “offline,” and add it to the live DOM when it’s ready. When you add a document fragment to the DOM tree, the content of the fragment gets added, not the fragment itself. And this is really convenient. So the document fragment is a good way to wrap a number of nodes even when you’re not containing them in a suitable parent (for example, your paragraphs are not in a div element).
        Here’s an example of using a document fragment:
    var p, t, frag;
    frag = document.createDocumentFragment();
    
    p = document.createElement('p');
    t = document.createTextNode('first paragraph');
    p.appendChild(t);
    frag.appendChild(p);
    
    p = document.createElement('p');
    t = document.createTextNode('second paragraph');
    p.appendChild(t);
    frag.appendChild(p);
    
    document.body.appendChild(frag);
    
         In this example the live document is updated only once, causing a single reflow/repaint,as opposed to one for every paragraph, as was the case in the previous antipattern snippet.
         The document fragment is useful when you add new nodes to the tree. But when you update an existing part of the tree, you can still batch changes. You can make a clone of the root of the subtree you’re about to change, make all the changes to the clone, and then when you’re done, swap the original with the clone:
    var oldnode = document.getElementById('result'),
    clone = oldnode.cloneNode(true);
    // work with the clone...
    // when you're done:
    oldnode.parentNode.replaceChild(clone, oldnode);
    
     
  • Events
       Another area of browser scripting that is full of inconsistencies and a source of frustration is working with the browser events, such as click, mouseover, and so on. Again, a JavaScript library can take away much of the double work that needs to be done to support IE (before version 9) and the W3C-conforming implementations.
       Let’s go over the main points, because you may not always be using an existing library for simple pages and quick hacks, or you may be creating your own library.

    Event Handling
         It all starts with attaching event listeners to elements. Say you have a button that increments a counter each time you click it. You can add an inline onclick attribute and that will work across all browsers but will be violating the separation of concerns and the progressive enhancement. So you should strive for attaching the listener in JavaScript, outside of any markup.
        Say you have the following markup:
    <button id="clickme">Click me: 0</button>
        You can assign a function to the onclick property of the node, but you can do this only once:
    // suboptimal solution
    var b = document.getElementById('clickme'),
    count = 0;
    b.onclick = function () {
    	count += 1;
    	b.innerHTML = "Click me: " + count;
    };
        If you want to have several functions executed on click, you cannot do it with this pattern while maintaining loose coupling. Technically you can check if onclick already contains a function and if so, add the existing one to your own function and replace the onclick value with your new function. But a much cleaner solution is to use the addEventListener() method. This method doesn’t exist in IE up to and including version 8, so you need attachEvent() for those browsers.
        When we looked at the init-time branching pattern. you saw an example of implementing a good solution for defining a cross-browser event listener utility.Without going into all the details right now, let’s just attach a listener to our button:
    var b = document.getElementById('clickme');
    if (document.addEventListener) { // W3C
    	b.addEventListener('click', myHandler, false);
    } else if (document.attachEvent) { // IE
    	b.attachEvent('onclick', myHandler);
    } else { // last resort
    	b.onclick = myHandler;
    }
    
         Now when this button is clicked, the function myHandler() will be executed. Let’s have this function increment the number in the “Click me: 0” button label. To make it a little more interesting, let’s assume we have several buttons and a single myHandler() for all of them. Keeping a reference to each button node and a counter for the number will be inefficient, given that we can get that information from the event object that is created on every click.
        Let’s see the solution first and comment on it after:
    function myHandler(e) {
    	var src, parts;
    	// get event and source element
    	e = e || window.event;
    	src = e.target || e.srcElement;
    
    	// actual work: update label
    	parts = src.innerHTML.split(": ");
    	parts[1] = parseInt(parts[1], 10) + 1;
    	src.innerHTML = parts[0] + ": " + parts[1];
    	// no bubble
    	if (typeof e.stopPropagation === "function") {
    		e.stopPropagation();
    	}
    	if (typeof e.cancelBubble !== "undefined") {
    		e.cancelBubble = true;
    	}
    	// prevent default action
    	if (typeof e.preventDefault === "function") {
    		e.preventDefault();
    	}
    	if (typeof e.returnValue !== "undefined") {
    		e.returnValue = false;
    	}
    }
    
        There are four parts in the event handler function:

       • First, we need to gain access to the event object, which contains information about the event and the page element that triggered that event. This event object is passed to the callback event handler, but not when using the onclick property where it’s accessible through the global property window.event instead.

      • The second part is doing the actual work of updating the label.

      • Next is canceling the propagation of the event. This is not required in this particular. example, but in general if you don’t do it, then the event bubbles up all the way to the document root or even the window object. Again we need to do it two ways: the W3C standard way (stopPropagation()) and then differently for IE (using cancelBubble).

      • Finally, prevent the default action, if this is required. Some events (clicking a link,submitting a form) have default actions, but you can prevent them by using preventDefault() (or for IE, by setting returnValue to false).

    As you see there’s quite a bit of duplicate work involved, so it makes sense to create your event utility with façade methods.

  • Event Delegation
        The event delegation pattern benefits from the event bubbling and reduces the number of event listeners attached to separate nodes. If there are 10 buttons inside a div element, you can have one event listener attached to the div as opposed to 10 listeners attached to each button. So we’re working with the following markup:
    <div id="click-wrap">
    	<button>Click me: 0</button>
    	<button>Click me too: 0</button>
    	<button>Click me three: 0</button>
    </div>
    
        Instead of attaching listeners to each button, you attach one to the “click-wrap” div.Then you can use the same myHandler() function from the previous example with only one little change: you have to filter out clicks you’re not interested in. In this case you look only for clicks on any button, so all other clicks in the same div could be ignored.The change to myHandler() would be to check if the nodeName of the source of the event is a “button”:
    // ...
    // get event and source element
    e = e || window.event;
    src = e.target || e.srcElement;
    if (src.nodeName.toLowerCase() !== "button") {
    	return;
    }
    // ...
    
         The drawback of the event delegation is the slightly more code to filter out the events that happen in the container that are not interesting for you. But the benefits—performance and cleaner code—outweigh the drawbacks significantly, so it’s a highly recommended pattern.
         Modern JavaScript libraries make it easy to use event delegation by providing convenient APIs. For example YUI3 has the method Y.delegate(), which enables you to specify a CSS selector to match the wrapper and another selector to match the nodes you’re interested in. This is convenient because your callback event handler function will actually never be called when the event happens outside the nodes you care about. In this case, the code to attach an event listener would be simply:
    Y.delegate('click', myHandler, "#click-wrap", "button");
        And thanks to the abstraction of all browser differences done in YUI and the fact that the source of the event is determined for us, the callback function will be much simpler:
    function myHandler(e) {
    	var src = e.currentTarget,
    	parts;
    	parts = src.get('innerHTML').split(": ");
    	parts[1] = parseInt(parts[1], 10) + 1;
    	src.set('innerHTML', parts[0] + ": " + parts[1]);
    	e.halt();
    }
    
     
  • Long-Running Scripts
        You have noticed that sometimes the browser complains that a script has been running for too long and asks the user if the script should be stopped. You don’t want this to happen in your application, no matter how complicated your task is. Also, if the script works too hard, the browser’s UI becomes unresponsive to the point where the user cannot click anything. This is bad for the experience and should be avoided.In JavaScript there are no threads, but you can simulate them in the browser by using setTimeout() or, in more modern browsers, web workers.

    setTimeout()
          The idea is to split a big amount of work into smaller chunks and perform each chunk with a timeout of 1ms. Using 1ms timeout chunks can cause the task to be completed slower overall, but the user interface will remain responsive, and the user is more comfortable and in control of the browser.

    Web Workers
          Recent browsers offer another solution to long-running scripts: web workers. Web workers provide background thread support in the browser. You put your heavy computations in a separate file, for example, my_web_worker.js, and then call it from the main program (page) like so:
    var ww = new Worker('my_web_worker.js');
    ww.onmessage = function (event) {
    	document.body.innerHTML +=
    		"<p>message from the background thread: " + event.data + "</p>";
    };
    
         The source for a web worker that does a simple arithmetic operation 1e8 times (1 with 8 zeros) is shown here:
    var end = 1e8, tmp = 1;
    postMessage('hello there');
    while (end) {
    	end -= 1;
    	tmp += end;
    	if (end === 5e7) { // 5e7 is the half of 1e8
    		postMessage('halfway there, `tmp` is now ' + tmp);
    	}
    }
    postMessage('all done');
    
         The worker uses postMessage() to communicate with the caller and the caller subscribes to the onmessage event to receive updates. The onmessage callback receives an event object as an argument and this object contains the property data, which can be anything that the worker wants to pass. Similarly the caller can pass data to the worker using (in this example) ww.postMessage() and the worker can subscribe to those messages using an onmessage callback.
    The previous example will print in the browser:
    message from the background thread: hello there
    message from the background thread: halfway there, `tmp` is now 3749999975000001
    message from the background thread: all done
     
  • Remote Scripting
        Today’s web applications often use remote scripting to communicate with the server without reloading the current page. This enables for much more responsive and desktop-like web applications. Let’s consider a few ways to communicate with the server from JavaScript.

    XMLHttpRequest
         XMLHttpRequest is a special object (a constructor function) available in most browsers today, which lets you make an HTTP request from JavaScript. There are three steps to making a request:

                1. Set up an XMLHttpRequest object (called XHR for short).
                2. Provide a callback function to be notified when the request object changes state.
                3. Send the request.

    The first step is as easy as:
      

    var xhr = new XMLHttpRequest();
      But in IE prior to version 7, the XHR functionality was implemented as an ActiveX object, so a special case is needed there. The second step is providing a callback to the readystatechange event:
    xhr.onreadystatechange = handleResponse;
        The last step is to fire off the request—using two methods open() and send(). The open() method sets up the HTTP request method (for example, GET, POST) and the URL. The send() method passes any POST data or just a blank string in the case of GET. The last parameter to open() specifies whether the request is asynchronous. Asynchronous means that the browser will not block waiting for the response. This is definitely the better user experience, so unless there’s a strong reason against it, the asynchronous parameter should always be true:
    xhr.open("GET", "page.html", true);
    xhr.send();
    
    // construct xhr object:
    
    var i, xhr, activeXids = [
    	'MSXML2.XMLHTTP.3.0',
    	'MSXML2.XMLHTTP',
    	'Microsoft.XMLHTTP'
    ];
    if (typeof XMLHttpRequest === "function") { // native XHR
    	xhr = new XMLHttpRequest();
    } else { // IE before 7
    	for (i = 0; i < activeXids.length; i += 1) {
    		try {
    			xhr = new ActiveXObject(activeXids[i]);
    			break;
    		} catch (e) {}
    	}
    }
    
     
  • JSONP
        JSONP (JSON with padding) is another way to make remote requests. Unlike XHR, it’s not restricted by the same-domain browser policy, so it should be used carefully because of the security implications of loading data from third-party sites. The response to an XHR request can be any type of document:

                • XML documents (historically)
                • HTML chunks (quite common)
                • JSON data (lightweight and convenient)
                • Simple text files and others

       With JSONP the data is most often JSON wrapped in a function call, where the function name is provided with the request. An example JSONP request URL would commonly look like this:
    http://example.org/getdata.php?callback=myHandler
         getdata.php could be any type of page or script. The callback parameter specifies which JavaScript function will handle the response.The URL is then loaded into a dynamic <script> element, like so:
    var script = document.createElement("script");
    script.src = url;
    document.body.appendChild(script);
    
         The server responds with some JSON data passed as a parameter to the callback function. The end result is that you’ve actually included a new script in the page, which happens to be a function call. For example:
    myHandler({"hello": "world"});
    
     
  • Frames and Image Beacons
         An alternative way to do remote scripting is to use frames. With JavaScript you can create an iframe and change its src URL. The new URL can contain data and function calls that update the caller—the parent page outside the iframe.
        The simplest form of remote scripting is when all you need to do is to send data to the server, and you’re not expecting a response. In those cases you can create a new image and point its src to the script on the server:
    new Image().src = "http://example.org/some/page.php";
         This pattern is called an image beacon and is useful when you want to send data to be logged by the server, for example for collecting visitor statistics. Because you have no use for a response of such a beacon, a common practice (but an antipattern) is to have the server respond with a 1×1 GIF image. A better option will be to respond with a “204 No Content” HTTP response. This means that only a header and no response body is sent back to the client.

  • Deploying JavaScript
        There are a few performance considerations when it comes to serving JavaScript. Let’s discuss the most important ones at a high level.

  • Combining Scripts
         The first rule when building fast-loading pages is to have as few external components as possible, because HTTP requests are expensive. When it comes to JavaScript, that means you can speed up the page loading times significantly by combining external script files together.
         Let’s say your page is using the jQuery library. This is one .js file. Then you’re also using        a few jQuery plugins, which also come each in a separate file. This way you can quickly end up with 4 or 5 files before you’ve written a single line. It makes sense to combine these files into one, especially given that some of them will be small in size (2 to 3 Kb) and the HTTP overhead will cost more time than the actual download. Combining the scripts simply means creating a new file and pasting the contents of each. Of course, this concatenation of files should happen only right before the code goes to production and not in development, where it will make debugging more painful. The drawbacks of combining script files are:

       • It’s one more step before going live, but it can easily be automated and done on the command line, for example using cat in Linux/Unix:  $ cat jquery.js jquery.quickselect.js jquery.limit.js > all.js

       • Losing some of the caching benefits—when you make a small change in one of the files, you invalidate the whole bundle. That’s why it’s good to have a release schedule for bigger projects or consider having two bundles: one that contains files that are expected to change and one “core” bundle that hardly changes at all.

       • You need to come up with some naming and versioning pattern for the bundle, such as using a timestamp: all_20100426.js or a hash of the file’s content.

       The drawbacks can be summed up as mainly an inconvenience, but the benefit is well  worth the trouble.

  • Minifying and Compressing
         When you think from the user perspective, there’s no reason why they should have to dwnload all the comments in your code, which serve no purpose for the way the application works.
          The benefit of minification can be different depending on how generously you use comments and white space, and also on the specific minification tools you use. But on average you would be looking at around 50% file size reduction.Serving the script file compressed is also something you should always do. It’s a simple one-time server configuration to enable gzip compression, and it gives you an instant speed up. Even if you’re using a shared hosting provider that doesn’t give you much freedom in terms of server configuration, most providers at least allow you to use
    Apache’s .htaccess configuration files. So in your web root, add this to the .htaccess file:
    AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml
    application/javascript application/json
        Compression will give you 70% smaller files on average. Combining compression with minification, you can expect your users to download a mere 15% of the file size of unminified, uncompressed source code you wrote.

  • Expires Header
        Contrary to the popular belief, files do not get to stay for too long in the browser cache. You can do your due diligence and increase the chances of having your files in the cache for repeat visits by using an Expires header. Again, this is a one-off server configuration you can do in .htaccess:

    ExpiresActive On
    ExpiresByType application/x-javascript "access plus 10 years"
         The drawback is that if you want to change the file, you also need to rename it, but you’ll probably be doing this already if you’ve established a naming convention for your bundles of combined files.

  • Using a CDN
          CDN stands for Content Delivery Network. This is a paid (sometimes quite pricey) hosting service that lets you distribute copies of your files in different data centers around the world and have them served quicker to your users, while still keeping the same URL in your code.
          Even if you don’t have a budget for CDN, you can still benefit from some free options:

          • Google hosts a number of popular open-source libraries, which you can link to for
                  free and benefit from its CDN.
          • Microsoft hosts jQuery and its own Ajax libraries.
          • Yahoo! hosts YUI library on its CDN.

  • Loading Strategies
          how you include a script in a web page seems like a straightforward question at a first glance—you use a <script> element and either inline the JavaScript code or link to a separate file in the src attribute:
    // option 1
    <script>
    	console.log("hello world");
    < /script>
    
    // option 2
    <script src="external.js">< /script>
           But there are some more patterns and considerations you should be aware of when your goal is building high-performance web applications. As a side note, there are some common attributes developers tend to use with the <script> element:

       • language="JavaScript"
              In many forms of capitalizing “JavaScript” and sometimes with a version number.The language attribute shouldn’t be used, because it’s implied that the language is JavaScript. The version number doesn’t work that well and is considered a mistake in retrospect.

       • type="text/javascript"
             This attribute is required by the HTML4 and XHTML1 standards, but it shouldn’t be, because the browsers assume JavaScript anyway. HTML5 is making this attribute not required. Other than satisfying markup validators, there’s no other reason to use the type attribute.

       • defer
             (And better yet, HTML5’s async) is also a way, albeit not widely supported, to specify that downloading the external script file shouldn’t block the rest of the page. More on blocking next.

  • The Place of the <script> Element
          The script elements block progressive page downloads. Browsers download several components at a time, but when they encounter an external script, they stop further downloads until the script file is downloaded, parsed, and executed. This hurts the overall page time, especially if it happens several times during a page load.To minimize the blocking effect, you can place the script element toward the end of the page, right before the closing </body> tag. This way there will be no other resources for the script to block. The rest of the page components will be downloaded and already engaging the user.
    The worst antipattern is to use separate files in the head of the document:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
    	<head>
    		<title>My App</title>
    		<!-- ANTIPATTERN -->
    		<script src="jquery.js"></script>
    		<script src="jquery.quickselect.js"></script>
    		<script src="jquery.lightbox.js"></script>
    		<script src="myapp.js"></script>
    	</head>
    	<body>
    		...
    	</body>
    </html>
    
        And the best option is to put the combined script at the very end of the page:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html>
    	<head>
    		<title>My App</title>
    	</head>
    	<body>
    		...
    		<script src="all_20100426.js"></script>
    	</body>
    </html>
    
     
  • HTTP Chunking
          The HTTP protocol supports the so-called chunked encoding. It enables you to send the page in pieces. So if you have a complicated page, you don’t have to wait for all the server-side work to complete before you start sending the more or less static head of the page.
          One simple strategy is to send the <head> content of the page with the first chunk while the rest of the page is being assembled. In other words you can have something like this:
    <!doctype html>
    <html>
    	<head>
    		<title>My App</title>
    	</head>
    	<!-- end of chunk #1 -->
    	<body>
    		...
    		<script src="all_20100426.js"></ script>
    	</body>
    </html>
    <!-- end of chunk #2 -->
    
          A simple improvement would be to also move the JavaScript back into the <head> and serve it with the first chunk. This way the browser has a head start downloading the script file while the rest of the page is not ready yet on the server side;
          An even better option would be to have a third chunk, which contains only the script at the very bottom of the page. You can also send a part of the body with the first chunk if you have a somewhat static header at the top of every page:
         This approach fits well in the spirit of progressive enhancement and unobtrusive JavaScript. Right after the end of the second chunk of HTML you should have a completely loaded, displayed, and usable page, just as if JavaScript were disabled in the browser. Then when the JavaScript arrives with the third chunk, it enhances the page, adding all the bells and whistles.

  • Dynamic <script> Element for Nonblocking Downloads
          As mentioned already, JavaScript blocks the download of the files that follow it. But several patterns enable you to prevent this:

        • Loading the script with an XHR request and then eval() it as a string. This approach suffers from same-domain restrictions and also involves eval(), which is an antipattern of itself.

        • Using defer and async attributes, but these don’t work across all browsers.

        • Using a dynamic <script> element.

       The last one is a good and doable pattern. Similar to what you saw with JSONP, you create a new script element, set its src, and append it to the page. This is an example that will load a JavaScript file asynchronously without blocking the rest of the downloads:
    var script = document.createElement("script");
    script.src = "all_20100426.js";
    document.documentElement.firstChild.appendChild(script);
    
        The drawback of this pattern is that you cannot have any other script elements that follow this pattern if they rely on the main .js being loaded. The main .js is loaded asynchronously, so there’s no guarantee when it will arrive, and the scripts that come after that might assume objects that are not yet defined.
         To solve this drawback you can have all the inline scripts not execute right away but be collected as functions in an array. Then when the main script arrives, it can execute all the function collected in the buffer array. So there are three steps in achieving this. First, create an array to store all inline code, as early in the page as possible:
    var mynamespace = {
    	inline_scripts: []
    };
         Then you need to wrap all the individual inline scripts into a function and append each   function to the inline_scripts array. In other words:
    // was:
    // <script>console.log("I am inline");</ script>
    // becomes:
    <script>
    	mynamespace.inline_scripts.push(function () {
    		console.log("I am inline");
    	});
    </ script>
    
     
         And the last step is to have your main script loop through the buffer of inline scripts and execute all of them:
    var i, scripts = mynamespace.inline_scripts, max = scripts.length;
    	for (i = 0; i < max; max += 1) {
    	scripts[i]();
    }
    
     
  • Appending the <script> element
           Commonly scripts are appended to the <head> of the document, but you can append them to any element, including the body (as with the JSONP example).In the previous example we used documentElement to append to the <head>, because documentElement is the <html> and its first child is the <head>:
    document.documentElement.firstChild.appendChild(script);
     This is also commonly written as:
    document.getElementsByTagName("head")[0].appendChild(script);
         This is fine when you control the markup, but what if you’re creating a widget or an ad and you have no idea what type of page will host it? Technically you may have no <head> and no <body> on the page; although document.body will most certainly work even without a <body> tag; But there’s actually one tag that will always exist on the page where your script is run—a script tag. If there were no script tag (for an inline or external file), then your code would not run. To use that fact you can insertBefore() the first available script element on the page:
    var first_script = document.getElementsByTagName('script')[0];
    first_script.parentNode.insertBefore(script, first_script);
        Here first_script is the script element that’s guaranteed to be on the page and script is the new script element you create.

  • Lazy-Loading
        The technique known as lazy-loading refers to loading an external file after the page  load event. It’s often beneficial to split a large bundle of code into two parts:

    • One part that is required for the page to initialize and attach event handlers to the UI elements

     • A second part that is only needed after user interaction or other conditions

      The goal is to load the page progressively and give the use something to work with as soon as possible. Then the rest can be loaded in the background while the user is engaged and looking around the page.
       The way to load the second part of the JavaScript is again to simply use a dynamic script element appended to the head or the body:
       For many applications, the lazy part of the code will most often be bigger than the core part, because the interesting “action” (such as drag and drop, XHR, and animations) happens only after the user initiates it.

  • Loading on Demand
       The previous pattern loaded additional JavaScript unconditionally after page load, assuming that the code will likely be needed. But can we do better and load only parts of the code and only the parts that are really needed? Imagine you have a sidebar on the page with different tabs. Clicking on a tab makes an XHR request to get content, updates the tab content, and animates the update fading the color. And what if this is the only place on the page you need your XHR and animation libraries, and what if the user never clicks on a tab? Enter the load-on-demand pattern. You can create a require() function or method that takes a filename of a script to be loaded and a callback function to be executed when the additional script is loaded.The require() function can be used like so:

    require("extra.js", function () {
    	functionDefinedInExtraJS();
    });
    
         Let’s see how you can implement such a function. Requesting the additional script is straightforward—you just follow the dynamic <script> element pattern. Figuring out when the script is loaded is a little trickier due to the browser differences:
    function require(file, callback) {
    	var script = document.getElementsByTagName('script')[0],
    	newjs = document.createElement('script');
    
    	// IE
    	newjs.onreadystatechange = function () {
    		if (newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
    			newjs.onreadystatechange = null;
    			callback();
    		}
    	};
    
    	// others
    	newjs.onload = function () {
    		callback();
    	};
    	newjs.src = file;
    	script.parentNode.insertBefore(newjs, script);
    }
    
     
    A few comments on this implementation:

        • In IE you subscribe to the readystatechange event and look for a readyState “loaded” or “complete.” All other browsers will ignore this.

        • In Firefox, Safari, and Opera, you subscribe to the load event via the onload property.

        • This approach doesn’t work in Safari 2. If this browser is a requirement, to make it work there you’ll have to set up an interval to periodically check if a specified variable (which you define in the additional file) has been defined. When it becomes defined, it means the new script has been loaded and executed.

  • Preloading JavaScript
        In the lazy-loading pattern and the on-demand pattern, we post-load scripts required by the current page. In addition, you can also post-load scripts that are not needed on the current page but on the page that is likely to follow. This way, when the user lands on the second page, the user already has the script preloaded, and the overall experience becomes much faster.
    Preloading can be implemented simply by using the dynamic script pattern. But this means that the script will be parsed and executed. Although parsing merely adds to the total time spent in preloading, the execution can additionally cause JavaScript errors when the preloaded script assumes it’s running on the second page and, for example, expects to find certain DOM nodes.
        It is possible to load scripts without parsing and executing them; this works for CSS and images, too. In IE you can make a request with the familiar image beacon pattern:
    new Image().src = "preloadme.js";
        In all other browsers you can use an <object> instead of a script element and set its data attribute to point to the URL of the script:
      
    var obj = document.createElement('object');
    obj.data = "preloadme.js";
    document.body.appendChild(obj);
        To prevent the object from being visible, you should also set its width and height attributes to 0. You can create a general-purpose preload() function or method and also use the inittime branching pattern to handle the browser differences:
    var preload;
    if (/*@cc_on!@*/false) { // IE sniffing with conditional comments
    	preload = function (file) {
    		new Image().src = file;
    	};
    } else {
    	preload = function (file) {
    		var obj = document.createElement('object'),
    		body = document.body;
    		obj.width = 0;
    		obj.height = 0;
    		obj.data = file;
    		body.appendChild(obj);
    	};
    }
    
    //Using the new function:
    preload('my_web_worker.js');
    
             The drawback of this pattern is the presence of user agent sniffing, but it cannot be avoided because, in this case, the capability detection doesn’t tell us enough about the browser behavior. In this pattern, for example, theoretically you can test if typeof Image is a “function” and use that instead of the sniffing. However this won’t help here, because all browsers support new Image(); it’s just that some have a separate cache for images, which means that preloading components as an image will not be used as a script from the cache on the second page but will be downloaded again.
          Browser sniffing using conditional comments is interesting in itself. It is slightly safer than looking for strings in navigator.userAgent, because these strings are easy to change by the user.    Having this:
    var isIE = /*@cc_on!@*/false;
          will set isIE to false in all browsers (because they ignore the comment), but it will be true in Internet Explorer, because of the negation '!' in the conditional comment. It’s as if IE sees:
    var isIE = !false; // true
          The preloading pattern can be used for all kinds of components, not only scripts. It’s useful, for example, on login pages. When the user starts typing his or her username, you can use this typing time to start preloading (nothing sensitive, of course), because it’s likely that the user will end up on the second logged-in page.
      

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics