《高性能JavaScript》读书笔记⑥:快速响应的用户界面

浏览器UI线程(The Browser UI Thread)

  • 用于执行JavaScript代码和更新界面的进程被称为 “浏览器UI线程”
  • UI线程的工作基于一个简单的队列系统,任务会被保存到队列中直到线程空闲,一旦空闲队列就被重新提取出来运行。这些任务要么是运行JavaScript代码,要么是执行UI更新,包括重绘和重排。浏览器每一次执行JavaScript代码或者响应用户事件,都可能会导致一个或多个任务加入队列。
  • 案例:
<!DOCTYPE html>
<html>
 <head>
 <meta charset="UTF-8">
 <title>Browser UI Thread Example</title>
 </head>
 <body>
 <button onclick="handleClick()">Click Me</button>
 <script type="text/javascript">
 function handleClick(){
 //创建div元素
 var div = document.createElement("div");
 //div元素添加内部文本
 div.innerHTML = "Clicked!";
 //将div元素append到body中
 document.body.appendChild(div);
 }
 </script>
 </body>
</html>

当按钮被点击时,会触发UI线程创建两个任务并添加到队列中:

  • 第一个任务就是更新按钮的UI,浏览器需要改变按钮外观表明它被点击了;
  • 第二个任务就是执行handleClick()方法中的代码。但是在运行过程中,创建了一个新的 <div> 元素并把它追加在 <body> 元素的末尾,这实际上引发了另一次UI变化,一个新的UI更新任务呗添加到队列中。当方法运行完毕后,UI还会在更新一次。
  • 所有的UI线程任务执行完毕,线程进入空闲状态是最理想的。因为用户所有的交互都会立即出发UI更新,而事实上,大多数浏览器在JavaScript运行时,会停止把新任务添加到UI线程的队列中,也就是说JavaScript任务必须尽快结束,以避免对用户体验造成不良影响。
浏览器限制(Browser Limits)
  • 浏览器对JavaScript任务的运行时间进行了限制,这很有必要,它能确保了不让恶意代码通过密集的操作锁住用户的浏览器。

  • 此类限制分为两种:调用栈大小以及长时间运行(long-running)脚本限制。长时间脚本限制也称为“长时间运行脚本定时器”或“失控脚本定时器”,顾名思义,它的原理就是浏览器记录脚本的运行时间,当达到一定限度时终止它并弹出提示框,这是可用性的问题。对于开发者来说,应该避免此类情况的出现。

  • 长时间运行脚本限制 的实现方式主要分为两种:

    1. 通过脚本执行的语句数量来限制(但要注意,在不同的机器,可用的内存和CPU速度执行单个语句的时间是不同的)
    2. 通过脚本执行的总时长来限制(但是,在不同的机器,指定时间内所运行的脚本数量,也有所差异)

    毫无疑问,不同浏览器检测长时间运行脚本的方法也各不相同:

    • IE注册表中,设置默认限制500万条语句
      (HKEY_CURRENT_USER\Software\Microsoft\InternetExplorer\StylesMaxScriptStatements)
    • Firfox的默认限制为10秒(dom.max_script_run_time)
    • Safari的默认限制为5秒(Disable Runnaway JavaScript Timer)
    • Opera由于自身的架构,没有长运行脚本限制
    • Chrome也没有单独的长运行脚本限制,而是依赖其通过崩溃检测系统来处理此类问题
多久才算“太久”(How Long Is Too Long)
  • 即便主流浏览器有长运行脚本限制的机制,但不意味着开发者也允许自己代码的运行时间达到最大限制值。单个JavaScript脚本运行的最大值不应该超过100毫秒
  • Jakob Nielsen 在其著作《可用性工程》(Morgan Kaufrnann,1944)中指出,如果界面超过100毫秒未响应,用户会感觉自己与界面失去联系。
  • 而主流的浏览器行为大致相同,当脚本执行时,UI不随用户交互而更新。也就是说,当运行JavaScript代码的这段时间内,用户交互行为引发的UI更新会被浏览器自动跳过。因此,在JavaScript脚本运行期间,用户点击一个按钮,可能无法看到它已经被按下的样式,界面呈现“挂起”或“假死”的状态。

使用定时器让出时间片段(Yielding with Timeers)

  • 在大型应用的Web界面的实现中,总会有一些复杂的JavaScript任务不能在100毫秒或更短的时间内完成。此时,最理想的方法是停止执行JavaScript脚本,让出UI线程的控制权,使得UI可以更新,然后再继续执行JavaScript。
定时器基础(Timer Basics)
  • 在JavaScript中通过setTimeout()setInterval()方法创建定时器。其中,setTimeout()方法是创建一次性的定时器,setInterval()方法是创建周期性的定时器。
  • 定时器会重置浏览器长运行代码的限制,正是这一特性使得定时器称为长时间运行JavaScript代码理想的跨浏览器解决方案。
    • 案例:saySomething()方法执行时,先调用oneMethod()方法,然后创建一个定时器,最后调用anotherMethod()方法
function saySomething(){
 oneMethod();
 setTimeout(function(){
 console.info("hello");
 },250);
 anotherMethod();
}

而定时器是创建(调用setTime())后当即开始计时,那么定时器代码有可能在saySomething()方法处理完成之前执行完成。
如案例所示,如果anotherMethod()方法的执行时间超过50毫秒,那么定时器代码会抢先进入UI执行队列。

定时器的精度(Timer Precision)
  • 值得注意的是,当定时器倒计时完成后,往往不会立即执行,而是等待UI队列中其他任务执行完毕后才会执行。这导致了JavaScript定时器的延迟问题,通常相差大约几毫秒。正因为JavaScript的定时器不太精准,所以我们应该避免定时器用于测量实际时间。
  • 其中,Windows系统中定时器分辨率为15毫秒,这意味着, 在Windows系统中,设置定时器的时间间隔不能小于15毫秒,否则会导致浏览器锁定。
使用定时器处理数组(Array Processing with Timers)
  • 一种造成长时间运行脚本的起因是耗时过长的循环,通过把循环的工作分解到一系列定时器中执行,是常见的优化方法。
  • 案例:这类循环执行时间过长的原因,不外乎是process()方法的处理太过复杂,或者是items数组的长度太长
var items = [], //数组
    len = items.length; //数组的长度
//遍历数组中的元素作为参数,执行proccess方法    
for (var i=0;i<len;i++) {
 process(item[i]);
}

如果items数组不需要按顺序处理,并且process()方法的处理过程不需要同步,则可以通过定时器来分解任务,即异步代码模式:

var items = []; //原数组
var todo = items.concat();    //克隆原数组到todo
setTimeout(function(){
    //获取todo数组的第一个元素,并返回删除第一个元素后的数组
    procces(todo.shift());
 
    if(todo.length > 0){
    //arguments.callee表示当前执行的函数
    setTimeout(arguments.callee,25);
    }else{
    return items;
    }
},25);

由于分解任务带来更多代码,我们可以进一步封装,以便多出重用:

var items = []; //原数组
function efficiencyLoopFn(items,process,callback){
       var todo = items.concat();    //克隆原数组到todo 
       setTimeout(function(){
    //获取todo数组的第一个元素,并返回删除第一个元素后的数组
    process(todo.shift());
    if(todo.length > 0){
    //arguments.callee表示当前执行的函数
    setTimeout(arguments.callee,25);
    }else{
    callback(items);
    }
       },25); 
}
分割任务(Splitting Up Tasks)
  • 如果一个函数运行时间太长,那么可以考虑把它拆分成一系列能在较短时间内完成的子函数。
function saveDocument(id){
       openDocument(id);  //进入
       writeText(id);    //写入
       closeDocument(id);    //关闭i
       updateUI(id);    //更新界面
}

另外,还可以把独立的方法通过定时器调用:

function saveDocument(id){
       var tasks = [openDocument,writeText,closeDocument,updateUI];
       setTimeout(function(){
              //执行下一个任务
              var task = tasks.shift();
              task(id);
              //检查是否还有其他任务
              if(tasks.length >0){
                   setTimeout(arguments.callee,25);
              }else{
                   callback();
              }
       },25);
}

同样可以封装成一个公共调用的方法:

function multistep(steps,args,calback){
       var tasks = steps.concat();
       setTimeout(function(){
              //执行下一个任务
              var task = tasks.shift();
              task.apply(null,args || []);
              //检查是否还有其他任务
              if(tasks.length >0){
                   setTimeout(arguments.callee,25);
              }else{
                   callback();
              }
       },25);
}
function saveDocument(id){

       var tasks =[openDocument,writeText,closeDocument,updateUI];
       multistep(tasks,[id],function(){
              console.info("Save completed!");
       });
}
记录代码运行时间(Timed Code)
  • 通过原生Date对象来跟踪代码的运行时间,是大多数JavaScript分析工具的工作原理
var start = +new Date(),stop;
comLongProcess();
//加号可以将Date对象转换成数字
stop = +new Date();
if(stop - start < 50){
       console.info("Just about right");
}else{
       console.info("Taking too long");
}

为了执行JavaScript任务更高效,我们可以进一步优化上面的代码,保证JavaScript代码运行在50毫秒以内。

function timedProcessArray(items,process,callback){
       var todo = items.concat();//
       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();
              }
       },25);
}

定时器与性能(Timers and Performance)
  • 虽然可以通过定时器提升JavaScript代码的性能,但过度使用也会对性能造成负面影响。等上一个定时器结束后再创建新的定时器不会导致性能问题,但同时有多个重复的定时器创建,则会发生性能问题。因为只有一个UI线程,而所有的定时器都在争夺运行时间。

Web Workers

  • 在Web Workers出现之前,没有办法在浏览器UI线程之外运行代码,而Web Workers API能使代码运行且不占用浏览器UI线程的时间。这意味着,每个新的Workers都在自己的线程中运行代码,不会影响其他Worker的运行代码,并且不会影响浏览器UI。
  • Web Workers API已经作为HTML5标准的规范被Firefox、Chrome、Safari浏览器支持。
Worker运行环境(Woker Environment)
  • Workers运行环境由如下部分组成:
    • navigator对象,包括四个属性appName、appVersion、user Agent和platform;
    • location对象(与window.location相同,只不过所有属性是只读的);
    • self对象,指向全局的worker对象;
    • importScript()方法,用来加载Worker所用到的外部JavaScript文件;
    • 以及所有的ECMAScript对象(诸如:Object、Array、Date等);
    • XMLHttpRequest构造器
    • setTimeout()setInterval()方法;
    • 停止WebWorker运行的close()方法;
  • 由于Web Workers拥有独立的运行环境,因此我们需要在一个完全独立的JavaScript文件中编写Worker中运行的代码,然后创建Woker线程中引用它。
var worker = new Worker("code.js");

当以上代码执行时,会创建一个新的线程和一个新的Worker运行环境,code.js文件会被异步下载,等文件下载并执行完成后,启动此Woker。

与Worker通信(Worker Communication)
  • 页面代码通过事件接口与Woker进行通信,通过postMessage()方法给Worker传递数据,通过onmessage事件接收信息。
var worker = new Worker("code.js");
worker.onmessage = function(event){
       console.info(event.data);
};
worker.postMessage("hello");

在Woker代码中,同样也是通过onmessage事件接收信息,通过postMessage()方法发送信息。

self.onmessage = function(event){
       console.info(event.data);
};
self.postMessage("hello too!");
  • 上面提到的消息系统是网页和Worker通信的唯一途径,并且只有特定类型的数据能通过postMessage()传递:原始值(字符串、数字、boolean、null和undefined)和Object和Array的实例。数据传入传出Worker时,会经历序列化和反序列化。
加载外部文件(Loading External Files)
  • Worker通过importScript()方法加载外部一个或多个JavaScript文件。
  • inportScript()的调用过程是阻塞式的,直到素有文件加载并执行完成后,Worker中的脚本才会继续运行。但由于Worker在UI线程之外运行,所以不用担心这种阻塞会影响UI响应。
//code1/2.js文件执行后,引入的变量就可以在Worker线程中使用了
importScript('code1.js','code2.js');
self.onmessage = function(event){
       console.info(event.data);
};
实际应用(Practical Uses)
  • Web Worker适合处理纯数据的、与浏览器UI无关的长时间运行(超过100毫秒)的脚本,比如:
    • 编码/解码大字符串;
    • 图片或视频处理时的复杂数学运算;
    • 大数据排序
  • 案例:现在需要解析一个数据量足够大的JSON字符串,耗时至少需要500毫秒。为了避免干扰用户体验,Worker称为最理想的解决方案。
<!DOCTYPE html>
<html>
       <head>
              <meta charset="{CHARSET}">
              <title>Test4Web Worker</title>
       </head>
       <body>
              <script type="text/javascript">
                     var worker = new Worker("jsonParse.js");
                     //注意:需要先定义onmessage事件,在postMessage()推消息
                     worker.onmessage = function(event){
                            console.info(event.data);
                     };
                     worker.postMessage("{'name':'William','age':26,'sex':'man'}");
              </script>
       </body>
</html>

外部jsonParse.js文件:

self.onmessage = function(event){
       var jsonText = event.data;
       console.info(jsonText);
       self.postMessage("parse done!");
};

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • 最近在阅读这本Nicholas C.Zakas(javascript高级程序设计作者)写的最佳实践、性能优化类的书...
    undefinedR阅读 2,153评论 0 30
  • 用一周的时间看完了《高性能Javascript》,涉及到了javascript很多个方面的性能问题,但由于书比较薄...
    陈坚生阅读 601评论 0 1
  • 葱油,红油,辣椒油是凉菜最常用的三个油,并称凉菜调味三剑客。葱油的熬制方法比较简单,关键是火候。 大葱、小葱、洋葱...
    廖相晖阅读 683评论 0 0
  • Working Holiday Visa,简称WHV,即打工度假签证。 持有某国的打工度假签证WHV,可以在规定时...
    阿嗖阅读 2,399评论 1 6
  • 最近有个舅舅买了房,自己东拼西凑付了首付,然后还要贷款。最近打电话给我妈,也就是他的大表姐要借两万块钱付中介费。我...
    语桐酱阅读 311评论 0 0