[翻译]High Performance JavaScript(021)

Splitting Up Tasks  分解任务

 

    What we typically think of as one task can often be broken down into a series of subtasks. If a single function is taking too long to execute, check to see whether it can be broken down into a series of smaller functions that complete in smaller amounts of time. This is often as simple as considering a single line of code as an atomic task, even though multiple lines of code typically can be grouped together into a single task. Some functions are already easily broken down based on the other functions they call. For example:

    我们通常将一个任务分解成一系列子任务。如果一个函数运行时间太长,那么查看它是否可以分解成一系列能够短时间完成的较小的函数。可将一行代码简单地看作一个原子任务,多行代码组合在一起构成一个独立任务。某些函数可基于函数调用进行拆分。例如:

 

function saveDocument(id){
  //save the document
  openDocument(id)
  writeText(id);
  closeDocument(id);
  //update the UI to indicate success
  updateUI(id);
}

    If this function is taking too long, it can easily be split up into a series of smaller steps by breaking out the individual methods into separate timers. You can accomplish this by adding each function into an array and then using a pattern similar to the array-processing pattern from the previous section:

    如果函数运行时间太长,它可以拆分成一系列更小的步骤,把独立方法放在定时器中调用。你可以将每个函数都放入一个数组,然后使用前一节中提到的数组处理模式:

 

function saveDocument(id){
  var tasks = [openDocument, writeText, closeDocument, updateUI];
  setTimeout(function(){
    //execute the next task
    var task = tasks.shift();
    task(id);
    //determine if there's more
    if (tasks.length > 0){
      setTimeout(arguments.callee, 25);
    }
  }, 25);
}

    This version of the function places each method into the tasks array and then executes only one method with each timer. Fundamentally, this now becomes an array-processing pattern, with the sole difference that processing an item involves executing the function contained in the item. As discussed in the previous section, this pattern can be encapsulated for reuse:

    这个版本将每个方法放入任务数组,然后在每个定时器中调用一个方法。从根本上说,现在它成为数组处理模式,只有一点不同:处理函数就包含在数组项中。正如前面一节所讨论的,此模式也可封装重用:

 

function multistep(steps, args, callback){
  var tasks = steps.concat(); //clone the array
  setTimeout(function(){
    //execute the next task
    var task = tasks.shift();
    task.apply(null, args || []);
    //determine if there's more
    if (tasks.length > 0){
      setTimeout(arguments.callee, 25);
    } else {
      callback();
    }
  }, 25);
}

    The multistep() function accepts three arguments: an array of functions to execute, an array of arguments to pass into each function when it executes, and a callback function to call when the process is complete. This function can be used like the following:

    multistep()函数接收三个参数:用于执行的函数数组,为每个函数提供参数的参数数组,当处理结束时调用的回调函数。函数用法如下:

 

function saveDocument(id){
  var tasks = [openDocument, writeText, closeDocument, updateUI];
  multistep(tasks, [id], function(){
    alert("Save completed!");
  });
}

    Note that the second argument to multistep() must be an array, so one is created containing just id. As with array processing, this function is best used when the tasks can be processed asynchronously without affecting the user experience or causing errors in dependent code.

    注意传给multistep()的第二个参数必须是数组,它创建时只包含一个id。正如数组处理那样,使用此函数的前提条件是:任务可以异步处理而不影响用户体验或导致依赖代码出错。

 

Timed Code  限时运行代码

 

    Sometimes executing just one task at a time is inefficient. Consider processing an array of 1,000 items for which processing a single item takes 1 millisecond. If one item is processed in each timer and there is a delay of 25 milliseconds in between, that means the total amount of time to process the array is (25 + 1) × 1,000 = 26,000 milliseconds, or 26 seconds. What if you processed the items in batches of 50 with a 25-millisecond delay between them? The entire processing time then becomes (1,000 / 50) × 25 + 1,000 = 1,500 milliseconds, or 1.5 seconds, and the user is still never blocked from the interface because the longest the script has executed continuously is 50 milliseconds. It's typically faster to process items in batches than one at a time.

    有时每次只执行一个任务效率不高。考虑这样一种情况:处理一个拥有1'000个项的数组,每处理一个项需要1毫秒。如果每个定时器中处理一个项,在两次处理之间间隔25毫秒,那么处理此数组的总时间是(25 + 1) × 1'000 = 26'000毫秒,也就是26秒。如果每批处理50个,每批之间间隔25毫秒会怎么样呢?整个处理过程变成(1'000 / 50) × 25 + 1'000 = 1'500毫秒,也就是1.5秒,而且用户也不会察觉界面阻塞,因为最长的脚本运行只持续了50毫秒。通常批量处理比每次处理一个更快。

 

    If you keep 100 milliseconds in mind as the absolute maximum amount of time that JavaScript should be allowed to run continuously, then you can start optimizing the previous patterns. My recommendation is to cut that number in half and never let any JavaScript code execute for longer than 50 milliseconds continuously, just to make sure the code never gets close to affecting the user experience.

    如果你记住JavaScript可连续运行的最大时间是100毫秒,那么你可以优化先前的模式。我的建议是将这个数字削减一半,不要让任何JavaScript代码持续运行超过50毫秒,只是为了确保代码永远不会影响用户体验。

 

    It's possible to track how long a piece of code has been running by using the native Date object. This is the way most JavaScript profiling works:

    可通过原生的Date对象跟踪代码的运行时间。这是大多数JavaScript分析工具所采用的工作方式:

 

var start = +new Date(),
stop;
someLongProcess();
stop = +new Date();
if(stop-start < 50){
  alert("Just about right.");
} else {
  alert("Taking too long.");
}

    Since each new Date object is initialized with the current system time, you can time code by creating new Date objects periodically and comparing their values. The plus operator (+) converts the Date object into a numeric representation so that any further arithmetic doesn't involve conversions. This same basic technique can be used to optimize the previous timer patterns.

    由于每个新创建的Data对象以当前系统时间初始化,你可以周期性地创建新Data对象并比较它们的值,以获取代码运行时间。加号(+)将Data对象转换为一个数字,在后续的数学运算中就不必再转换了。这一技术也可用于优化以前的定时器模板。

 

    The processArray() method can be augmented to process multiple items per timer by adding in a time check:

    processArray()方法通过一个时间检测机制,可在每个定时器中执行多次处理:

 

function timedProcessArray(items, process, callback){
  var todo = items.concat(); //create a clone of the original
  setTimeout(function(){
    var start = +new Date();
    do {
      process(todo.shift());
    } while (todo.length > 0 && (+new Date() - start < 50));

    if (todo.length > 0){
      setTimeout(arguments.callee, 25);
    } else {
      callback(items);
    }
  }, 25);
}

    The addition of a do-while loop in this function enables checking the time after each item is processed. The array will always contain at least one item when the timer function executes, so a post-test loop makes more sense than a pretest one. When run in Firefox 3, this function processes an array of 1,000 items, where process() is an empty function, in 38–43 milliseconds; the original processArray() function processes the same array in over 25,000 milliseconds. This is the power of timing tasks before breaking them up into smaller chunks.

    此函数中添加了一个do-while循环,它在每个数组项处理之后检测时间。定时器函数运行时数组中存放了至少一个项,所以后测试循环比前测试更合理。在Firefox 3中,如果process()是一个空函数,处理一个1'000个项的数组需要38 - 34毫秒;原始的processArray()函数处理同一个数组需要超过25'000毫秒。这就是定时任务的作用,避免将任务分解成过于碎小的片断。

 

Timers and Performance  定时器与性能

 

    Timers can make a huge difference in the overall performance of your JavaScript code, but overusing them can have a negative effect on performance. The code in this section has used sequenced timers such that only one timer exists at a time and new ones are created only when the last timer has finished. Using timers in this way will not result in performance issues.

    定时器使你的JavaScript代码整体性能表现出巨大差异,但过度使用它们会对性能产生负面影响。本节中的代码使用定时器序列,同一时间只有一个定时器存在,只有当这个定时器结束时才创建一个新的定时器。以这种方式使用定时器不会带来性能问题。

 

    Performance issues start to appear when multiple repeating timers are being created at the same time. Since there is only one UI thread, all of the timers compete for time to execute. Neil Thomas of Google Mobile researched this topic as a way of measuring performance on the mobile Gmail application for the iPhone and Android.

    当多个重复的定时器被同时创建会产生性能问题。因为只有一个UI线程,所有定时器竞争运行时间。Google Mobile的Neil Thomas将此问题作为测量性能的方法进行研究,针对iPhone和Android上运行的移动Gmail程序。

 

    Thomas found that low-frequency repeating timers—those occurring at intervals of one second or greater—had little effect on overall web application responsiveness. The timer delays in this case are too large to create a bottleneck on the UI thread and are therefore safe to use repeatedly. When multiple repeating timers are used with a much greater frequency (between 100 and 200 milliseconds), however, Thomas found that the mobile Gmail application became noticeably slower and less responsive.

    Thomas发现低频率的重复定时器——间隔在1秒或1秒以上——几乎不影响整个网页应用的响应。这种情况下定时器延迟远超过使UI线程产生瓶颈的值,因此可安全地重复使用。当多个重复定时器使用更高的频率(间隔在100到200毫秒之间),Thomas发现移动Gmail程序明显变慢,反应较差。

 

    The takeaway from Thomas's research is to limit the number of high-frequency repeating timers in your web application. Instead, Thomas suggests creating a single repeating timer that performs multiple operations with each execution.

    Thomas研究的言外之意是,要在你的网页应用中限制高频率重复定时器的数量。同时,Thomas建议创建一个单独的重复定时器,每次执行多个操作。

 

Web Workers  网页工人线程

 

    Since JavaScript was introduced, there has been no way to execute code outside of the browser UI thread. The web workers API changes this by introducing an interface through which code can be executed without taking time on the browser UI thread. Originally part of HTML 5, the web workers API has been split out into its own specification (http://www.w3.org/TR/workers/); web workers have already been implemented natively in Firefox 3.5, Chrome 3, and Safari 4.

    自JavaScript诞生以来,还没有办法在浏览器UI线程之外运行代码。网页工人线程API改变了这种状况,它引入一个接口,使代码运行而不占用浏览器UI线程的时间。作为最初的HTML 5的一部分,网页工人线程API已经分离出去成为独立的规范(http://www.w3.org/TR/workers/)。网页工人线程已经被Firefox 3.5,Chrome 3,和Safari 4原生实现。

 

    Web workers represent a potentially huge performance improvement for web applications because each new worker spawns its own thread in which to execute JavaScript. That means not only will code executing in a worker not affect the browser UI, but it also won't affect code executing in other workers.

    网页工人线程对网页应用来说是一个潜在的巨大性能提升,因为新的工人线程在自己的线程中运行JavaScript。这意味着,工人线程中的代码运行不仅不会影响浏览器UI,而且也不会影响其它工人线程中运行的代码。

 

Worker Environment  工人线程运行环境

 

    Since web workers aren't bound to the UI thread, it also means that they cannot access a lot of browser resources. Part of the reason that JavaScript and UI updates share the same process is because one can affect the other quite frequently, and so executing these tasks out of order results in a bad user experience. Web workers could introduce user interface errors by making changes to the DOM from an outside thread, but each web worker has its own global environment that has only a subset of JavaScript features available. The worker environment is made up of the following:

    由于网页工人线程不绑定UI线程,这也意味着它们将不能访问许多浏览器资源。JavaScript和UI更新共享同一个进程的部分原因是它们之间互访频繁,如果这些任务失控将导致糟糕的用户体验。网页工人线程修改DOM将导致用户界面出错,但每个网页工人线程都有自己的全局运行环境,只有JavaScript特性的一个子集可用。工人线程的运行环境由下列部分组成:

 

• A navigator object, which contains only four properties: appName, appVersion, userAgent, and platform

  一个浏览器对象,只包含四个属性:appName, appVersion, userAgent, 和platform

 

• A location object (same as on window, except all properties are read-only)

  一个location对象(和window里的一样,只是所有属性都是只读的)

 

• A self object that points to the global worker object

  一个self对象指向全局工人线程对象

 

• An importScripts() method that is used to load external JavaScript for use in the worker

  一个importScripts()方法,使工人线程可以加载外部JavaScript文件

 

• All ECMAScript objects, such as Object, Array, Date, etc.

  所有ECMAScript对象,诸如Object,Array,Data,等等。

 

• The XMLHttpRequest constructor

  XMLHttpRequest构造器

 

• The setTimeout() and setInterval() methods

  setTimeout()和setInterval()方法

 

• A close() method that stops the worker immediately

  close()方法可立即停止工人线程

 

    Because web workers have a different global environment, you can't create one from any JavaScript code. In fact, you'll need to create an entirely separate JavaScript file containing just the code for the worker to execute. To create a web worker, you must pass in the URL for the JavaScript file:

    因为网页工人线程有不同的全局运行环境,你不能在JavaScript代码中创建。事实上,你需要创建一个完全独立的JavaScript文件,包含那些在工人线程中运行的代码。要创建网页工人线程,你必须传入这个JavaScript文件的URL:

 

var worker = new Worker("code.js");

    Once this is executed, a new thread with a new worker environment is created for the specified file. This file is downloaded asynchronously, and the worker will not begin until the file has been completely downloaded and executed.

    此代码一旦执行,将为指定文件创建一个新线程和一个新的工人线程运行环境。此文件被异步下载,直到下载并运行完之后才启动工人线程。

 

Worker Communication  工人线程交互

 

    Communication between a worker and the web page code is established through an event interface. The web page code can pass data to the worker via the postMessage() method, which accepts a single argument indicating the data to pass into the worker.There is also an onmessage event handler that is used to receive information from the worker. For example:

    工人线程和网页代码通过事件接口进行交互。网页代码可通过postMessage()方法向工人线程传递数据,它接收单个参数,即传递给工人线程的数据。此外,在工人线程中还有onmessage事件句柄用于接收信息。例如:

 

var worker = new Worker("code.js");
worker.onmessage = function(event){
  alert(event.data);
};
worker.postMessage("Nicholas");

    The worker receives this data through the firing of a message event. An onmessage event handler is defined, and the event object has a data property containing the data that was passed in. The worker can then pass information back to the web page by using its own postMessage() method:

    工人线程从message事件中接收数据。这里定义了一个onmessage事件句柄,事件对象具有一个data属性存放传入的数据。工人线程可通过它自己的postMessage()方法将信息返回给页面。

 

//inside code.js
self.onmessage = function(event){
  self.postMessage("Hello, " + event.data + "!");
};

    The final string ends up in the onmessage event handler for the worker. This messaging system is the only way in which the web page and the worker can communicate.

    最终的字符串结束于工人线程的onmessage事件句柄。消息系统是页面和工人线程之间唯一的交互途径。

 

    Only certain types of data can be passed using postMessage(). You can pass primitive values (strings, numbers, Booleans, null, and undefined) as well as instances of Object and Array; you cannot pass any other data types. Valid data is serialized, transmitted to or from the worker, and then deserialized. Even though it seems like the objects are being passed through directly, the instances are completely separate representations of the same data. Attempting to pass an unsupported data type results in a JavaScript error.

    只有某些类型的数据可以使用postMessage()传递。你可以传递原始值(string,number,boolean,null和undefined),也可以传递Object和Array的实例,其它类型就不允许了。有效数据被序列化,传入或传出工人线程,然后反序列化。即使看上去对象直接传了过去,实例其实是同一个数据完全独立的表述。试图传递一个不支持的数据类型将导致JavaScript错误。

 

Loading External Files  加载外部文件

 

    Loading extra JavaScript files into a worker is done via the importScripts() method, which accepts one or more URLs for JavaScript files to load. The call to importScripts() is blocking within the worker, so the script won't continue until all files have been loaded and executed. Since the worker is running outside of the UI thread, there is no concern about UI responsiveness when this blocking occurs. For example:

    当工人线程通过importScripts()方法加载外部JavaScript文件,它接收一个或多个URL参数,指出要加载的JavaScript文件网址。工人线程以阻塞方式调用importScripts(),直到所有文件加载完成并执行之后,脚本才继续运行。由于工人线程在UI线程之外运行,这种阻塞不会影响UI响应。例如:

 

//inside code.js
importScripts("file1.js", "file2.js");
self.onmessage = function(event){
  self.postMessage("Hello, " + event.data + "!");
};

    The first line in this code includes two JavaScript files so that they will be available in the context of the worker.

    此代码第一行包含两个JavaScript文件,它们将在工人线程中使用。

 

Practical Uses  实际用途

 

    Web workers are suitable for any long-running scripts that work on pure data and that have no ties to the browser UI. This may seem like a fairly small number of uses, but buried in web applications there are typically some data-handling approaches that would benefit from using a worker instead of timers.

    网页工人线程适合于那些纯数据的,或者与浏览器UI没关系的长运行脚本。它看起来用处不大,而网页应用程序中通常有一些数据处理功能将受益于工人线程,而不是定时器。

 

    Consider, for example, parsing a large JSON string (JSON parsing is discussed further in Chapter 7). Suppose that the data is large enough that parsing takes at least 500 milliseconds. That is clearly too long to allow JavaScript to run on the client, as it will interfere with the user experience. This particular task is difficult to break into small chunks with timers, so a worker is the ideal solution. The following code illustrates usage from a web page:

    考虑这样一个例子,解析一个很大的JSON字符串(JSON解析将在后面第七章讨论)。假设数据足够大,至少需要500毫秒才能完成解析任务。很显然时间太长了以至于不能允许JavaScript在客户端上运行它,因为它会干扰用户体验。此任务难以分解成用于定时器的小段任务,所以工人线程成为理想的解决方案。下面的代码说明了它在网页上的应用:

 

var worker = new Worker("jsonparser.js");
//when the data is available, this event handler is called
worker.onmessage = function(event){
  //the JSON structure is passed back
  var jsonData = event.data;
  //the JSON structure is used
  evaluateData(jsonData);
};
//pass in the large JSON string to parse
worker.postMessage(jsonText);

 

    The code for the worker responsible for JSON parsing is as follows:

    工人线程的代码负责JSON解析,如下:

 

//inside of jsonparser.js
//this event handler is called when JSON data is available

self.onmessage = function(event){
  //the JSON string comes in as event.data
  var jsonText = event.data;
  //parse the structure
  var jsonData = JSON.parse(jsonText);
  //send back to the results
  self.postMessage(jsonData);
};

    Note that even though JSON.parse() is likely to take 500 milliseconds or more, there is no need to write any additional code to split up the processing. This execution takes place on a separate thread, so you can let it run for as long as the parsing takes without interfering with the user experience.

    请注意,即使JSON.parse()可能需要500毫秒或更多时间,也没有必要添加更多代码来分解处理过程。此处理过程发生在一个独立的线程中,所以你可以让它一直运行完解析过程而不会干扰用户体验。

 

    The page passes a JSON string into the worker by using postMessage(). The worker receives the string as event.data in its onmessage event handler and then proceeds to parse it. When complete, the resulting JSON object is passed back to the page using the worker's postMessage() method. This object is then available as event.data in the page's onmessage event handler. Keep in mind that this presently works only in Firefox 3.5 and later, as Safari 4 and Chrome 3's implementations allow strings to be passed only between page and worker.

    页面使用postMessage()将一个JSON字符串传给工人线程。工人线程在它的onmessage事件句柄中收到这个字符串也就是event.data,然后开始解析它。完成时所产生的JSON对象通过工人线程的postMessage()方法传回页面。然后此对象便成为页面onmessage事件句柄的event.data。请记住,此工程只能在Firefox 3.5和更高版本中运行,而Safari 4和Chrome 3中,页面和工人线程之间只允许传递字符串。

 

    Parsing a large string is just one of many possible tasks that can benefit from web workers. Some other possibilities are:

    解析一个大字符串只是许多受益于网页工人线程的任务之一。其它可能受益的任务如下:

 

• Encoding/decoding a large string

  编/解码一个大字符串

 

• Complex mathematical calculations (including image or video processing)

  复杂数学运算(包括图像或视频处理)

 

• Sorting a large array

  给一个大数组排序

 

    Any time a process takes longer than 100 milliseconds to complete, you should consider whether a worker solution is more appropriate than a timer-based one. This, of course, is based on browser capabilities.

    任何超过100毫秒的处理,都应当考虑工人线程方案是不是比基于定时器的方案更合适。当然,还要基于浏览器是否支持工人线程。

 

Summary  总结

 

    JavaScript and user interface updates operate within the same process, so only one can be done at a time. This means that the user interface cannot react to input while JavaScript code is executing and vice versa. Managing the UI thread effectively means ensuring that JavaScript isn't allowed to run so long that the user experience is affected. To that end, the following should be kept in mind:

    JavaScript和用户界面更新在同一个进程内运行,同一时刻只有其中一个可以运行。这意味着当JavaScript代码正在运行时,用户界面不能响应输入,反之亦然。有效地管理UI线程就是要确保JavaScript不能运行太长时间,以免影响用户体验。最后,请牢记如下几点:

 

• No JavaScript task should take longer than 100 milliseconds to execute. Longer execution times cause a noticeable delay in updates to the UI and negatively impact the overall user experience.

  JavaScript运行时间不应该超过100毫秒。过长的运行时间导致UI更新出现可察觉的延迟,从而对整体用户体验产生负面影响。


• Browsers behave differently in response to user interaction during JavaScript execution.
Regardless of the behavior, the user experience becomes confusing and disjointed when JavaScript takes a long time to execute.

  JavaScript运行期间,浏览器响应用户交互的行为存在差异。无论如何,JavaScript长时间运行将导致用户体验混乱和脱节。


• Timers can be used to schedule code for later execution, which allows you to split up long-running scripts into a series of smaller tasks.

  定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务。


• Web workers are a feature in newer browsers that allow you to execute JavaScript code outside of the UI thread, thus preventing UI locking.

  网页工人线程是新式浏览器才支持的特性,它允许你在UI线程之外运行JavaScript代码而避免锁定UI。

 

    The more complex the web application, the more critical it is to manage the UI thread in a proactive manner. No JavaScript code is so important that it should adversely affect the user's experience.

    网页应用程序越复杂,积极主动地管理UI线程就越显得重要。没有什么JavaScript代码可以重要到允许影响用户体验的程度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值