第六章 快速响应的用户界面

本章开篇介绍了浏览器UI线程的概念,我也突然想到一个小例子,这是写css3动画的朋友都经常会碰到的一个问题:

<head>
<meta charset="UTF-8">
<title>Title</title>
<style>
div{width:50px; height:50px; background:yellow;}
.act{width:100px;transition:width 0.5s;}
</style>
</head>
<body>
<div class="act"></div>
<button>click me</button>
<script>
var btn = document.querySelector('button');
var div = document.querySelector('div'); btn.onclick = function(){
div.className = '';
div.className = 'act';
}
</script>
</body>

如代码所示,我们希望点击按钮的时候,div能通过移除class瞬间变回50px,然后再给其加回class来触发动画(0.5秒内,宽度由50px延伸到100px),

不过这段代码的执行效果是——没有效果(录屏软件在win10下有点兼容bug,鼠标都偏移了)

其解决方案却也简单——套上一个setTimeout即可:

    btn.onclick = function(){
div.className = '';
setTimeout(function(){
div.className = 'act';
}, 0)
}

执行如下:

原理是,我们通过 setTimeout,将div的第一次UI事件得以优先执行,而非放到 div.className = 'act' 的后方执行。

在用户点击按钮(未加setTimeout时的代码)的时候其实发生了这样的事情:

⑴ UI事件——更新按钮的UI,让用户能“看到”它被点击了。同时把回调事件放入事件队列。

⑵ JS事件A——执行回调事件,先执行首行的 div.className = '' ,移除div的类名,这时候会生成一个UI事件A(重渲染div)放入事件队列中等候空闲。

⑶ JS事件B——继续执行回调事件,给div加上名为“act”的类,这时候依旧又生成了一个UI事件B(重新渲染div)并放入队列中等候。

⑷ UI事件A——鉴于浏览器的UI线程已不存在任何执行中的任务(回调已执行完毕,处空闲状态),那么事件队列中的UI事件便开始以FIFO的形式进入UI线程来被处理。

⑸ UI事件B——跟UI事件A是一样的,即根据div的当前样式来做渲染处理。

(制图的时候没记清楚,把事件A/B写为事件1/2了,大家自行脑部替换吧)

而加上 setTimeout 之后则变为:

⑴ UI事件——更新按钮的UI,让用户能“看到”它被点击了。同时把回调事件(JS事件A和B)放入事件队列。

⑵ JS事件A——执行回调事件,先执行首行的 div.className = '' ,移除div的类名,这时候会生成一个UI事件A(重渲染div)放入事件队列中等候空闲。

⑶ UI事件A——由于JS事件B带延迟特性,故先放行事件队列后方的队列成员,让UI事件A先执行。这时候div失去了类,依据当前有效样式,将其渲染为50px宽度。

⑷ JS事件B——继续执行回调事件,给div加上名为“act”的类,依旧又生成了一个UI事件B(重新渲染div)并放入队列中等候。

⑸ UI事件B——div加上了类,故根据当前的有效样式,将其渲染为100px宽度。

⑹ UI事件C——鉴于div的宽度发生了变化,故触发动画事件。

综上我们稍微了解了浏览器UI线程(主线程)的一个工作流程,但常规浏览器并不仅仅只有一个线程在运作,其主要线程可归类为:

另外我们回过头看看 setTimeout/setInterval 这两个时间机制,它们实际上只是把回调事件放入队列中以“礼让”的状态等候,若后方有事件成员则礼让给后方先出队。

这点跟 node 的 setImmediate 是一样的,不同的是 setImmediate 不受延时限制,当event loop当轮结束时则执行。

那么给 setTimeout 配置一个数值为 0 的延时,是否就实现了 setImmediate 的功能呢?答案是否定的,在书中“定时器精度”一节有提及,js的时间机制是不精准的,它受到了系统/客户端定时器分辨率(如window下为15毫秒)的影响,所以会存在毫秒级的偏差。

不过这里需要了解的事实是—— JS中的时间机制并不是一个纯粹的异步事件,它依旧走的UI单线程,只是当事件队列为空时候才“见缝插针”到UI线程中去执行,营造出了一种“异步”的假象。

顺道也在这里提一提,JS中真正走了异步的应该是下面几个事件:

1. Ajax
2. event(如监听click)
3. requestAninmationFrame
4. WebSQL、IndexDB
5. Web Worker
6. postMessage

第七章 Ajax

“动态脚本注入”一节介绍了JSONP原理——前后端约定好一个回调名,让script请求的回包数据包裹在该回调名内,客户端拉取到该回包时通过 eval 来即时触发回调函数。

除了 JSONP 我们还是能有许多跨域通信的实现,可参照我的旧文章

本章提及的“Multipart XHR”其实是域名收敛的一种实现,比如下面的单条请求就一口气返回了对应的多个脚本资源:

http://imgcache.gtimg.cn/c/=/club/qv/pkg/qv_1.x.x.js,/clubact/premin/jquery.webStorage.min.js,/clubact/common/oz.js,/clubact/common/aid.js,/clubact/common/mustache.js,/clubact/common/nav/youxi_nav.js

不过这里提及了一个有趣的处理——若MXHR响应的出局非常多,等到全部数据返回过来才做处理有点慢,我们可以通过监听XHR的 readyState 来提前处理。

当 readyState 为3时其实表示客户端已经开始下载回包(含报头)了,这时候我们就可以通过轮询来提前处理(主要是拆开、提取回包中的合并资源):

var req = new XMLHttpRequest();
var getLatestPacketInterval, lastLength = 0;
req.open('GET', 'rollup_images.php', true);
req.onreadystatechange = readyStateHandler;
req.send(null);
function readyStateHandler{
if (req.readyState === 3 && getLatestPacketInterval === null) {
// 开始轮询
getLatestPacketInterval = window.setInterval(function() {
getLatestPacket();
}, 15);
}
if (req.readyState === 4) {
// 停止轮询
clearInterval(getLatestPacketInterval);
// 获取最后一个数据包
getLatestPacket();
}
}
function getLatestPacket() {
var length = req.responseText.length;
var packet = req.responseText.substring(lastLength, length);
processPacket(packet);
lastLength = length;
}

接着提及的 Beacons 其实是一种 image ping 技术,常规也是用来跨域通信的(主要用于统计)。不过这里提及的服务端响应处理还是值得一看:

1. 服务端返回真实的图片数据,客户端可通过判断图片宽度来了解状态;

2. 若客户端无须了解服务端状态,则返回不带消息正文的204即可。

第八章 编程实践

本章提供一些建议,让读者能避免使用一些性能上不太好的编程习惯。

1. 避免双重求值

js中提供了某些接口允许你输入字符串来编译执行,eval是其中最耳熟能详的方法了。除却eval还包括如下方法:

⑴ 以 new Function() 的形式来创建函数;   ⑵ 让 setTimeout/setInterval 执行字符串。

这些方法都会让js引擎先做字符串解析,再做求值处理,导致了双重求值,性能开销会变大,所以常规不建议这么来使用。

如果不得已要解析服务端返回的大规模json字符串,可以开个 Web Worker 做异步处理。

2. 使用 Object/Array 直接量

//不推荐
var o = {};
o.a = 1;
o.b = 2; //推荐
var o = {
a: 1,
b: 2
} //不推荐
var arr = new Array();
arr[0] = 1;
arr[1] = 2; //推荐
var arr = [1, 2];

使用“推荐”的直接量处理来定义一个对象将获得更快的执行速度也有助减小文件体积。

3. 避免重复工作

大部分开发都会忽略的地方,即封装在某个方法中的功能分支判断,在每次方法被调用的时候都会重新做一次冗余判断:

function addHandler(target, eventType, handler){
if(target.addEventListener){
target.addEventListener(eventType, handler, false)
} else {
target.attachEvent('on'+eventType, handler)
}
}

如上述的事件绑定接口在每次被调用时,都需要做一次事件添加句柄判断。

解决该问题的方法是内部重写接口(延迟加载):

function addHandler(target, eventType, handler){
if(target.addEventListener){
addHandler = function(target, eventType, handler){
target.addEventListener(eventType, handler, false)
} } else {
addHandler = function(target, eventType, handler){
target.attachEvent('on'+eventType, handler)
} } addHandler(target, eventType, handler); //延迟加载
}

4. 用速度最快的部分

⑴ 位操作

JS的位操作会相比其它的计算处理快得多,若妥当使用可以提升脚本执行速度。

例如常规我们会以 if(i%2) 来判断 i 是奇数或偶数,若把条件更改为 if(i & 1) 会得到一样的结果,不过速度快了50%。

本节也提及了“位掩码”的使用,是种有趣的逻辑识别处理。

打个比方,在手Q web 页面开发中,我们会通过一个“_wv”的参数来知会客户端(手Q)是否显示返回按钮、分享按钮,以及如何显示分享面板等功能。

关于这个参数有类似这样的映射:

当我们给 url 的 _wv 参数取值 21 (即 16 + 4 + 1)的时候,手Q针对该参数的值来隐藏返回按钮和底栏,并配置分享面板中不出现空间的选项。

而常规我们在写JS时,可以利用位掩码来实现相同处理。

我们依旧使用上方的映射表,不过不再使用累加处理,而是使用位处理:

var wv = 16 | 4 | 1;

//识别处理
if(wv & 1){
//隐藏返回按钮
}
if(wv & 2){
//隐藏分享按钮
}
...//省略4和8的分支
if(wv & 16){
//分享面板隐藏空间分享
}

⑵ 原生方法

即多使用原生的 Math 接口来实现复杂的计算,多使用原生的选择器(如 querySelector)来选择DOM。

至于后面两章主要提及的是前端构建和检测工具,其中部分技术还是淘汰掉的东西就不赘述了。共勉~

《高性能javascript》一书要点和延伸(下)的更多相关文章

  1. 《高性能javascript》一书要点和延伸(上)

    前些天收到了HTML5中国送来的<高性能javascript>一书,便打算将其做为假期消遣,顺便也写篇文章记录下书中一些要点. 个人觉得本书很值得中低级别的前端朋友阅读,会有很多意想不到的 ...

  2. 《高性能javascript》 领悟随笔之-------DOM编程篇(二)

    <高性能javascript> 领悟随笔之-------DOM编程篇二 序:在javaSctipt中,ECMASCRIPT规定了它的语法,BOM实现了页面与浏览器的交互,而DOM则承载着整 ...

  3. 高性能JavaScript 编程实践

    前言 最近在翻<高性能JavaScript>这本书(2010年版 丁琛译),感觉可能是因为浏览器引擎的改进或是其他原因,书中有些原本能提高性能的代码在最新的浏览器中已经失效.但是有些章节的 ...

  4. 高性能javascript学习笔记系列(4) -算法和流程控制

    参考高性能javascript for in 循环  使用它可以遍历对象的属性名,但是每次的操作都会搜索实例或者原型的属性 导致使用for in 进行遍历会产生更多的开销 书中提到不要使用for in ...

  5. 高性能JavaScript 达夫设备

    前言 在<高性能JavaScript>一书的第四章算法和流程控制中,提到了减少迭代次数加速程序的策略—达夫设备(Duff's device).达夫设备本身很好理解,但是其效果是否真的像书中 ...

  6. 高性能JavaScript(您值得一看)

    众所周知浏览器是使用单进程处理UI更新和JavaScript运行等多个任务的,而同一时间只能有一个任务被执行,如此说来,JavaScript运行了多长时间就意味着用户得等待浏览器响应需要花多久时间. ...

  7. 【JavaScript】【译】编写高性能JavaScript

    英文链接:Writing Fast, Memory-Efficient JavaScript 很多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是专门为需要快速执 ...

  8. 编写高性能JavaScript【转】

    英文链接:Writing Fast, Memory-Efficient JavaScript 很多JavaScript引擎,如Google的V8引擎(被Chrome和Node所用),是专门为需要快速执 ...

  9. 《高性能javascript》学习总结

    本文是学习<高性能javascript>(Nichols C. Zakes著)的一些总结,虽然书比较过时,里面的知识点也有很多用不上了,但是毕竟是前人一步步探索过来的,记录着javascr ...

随机推荐

  1. 记一次debug记录:Uncaught SyntaxError: Unexpected token ILLEGAL

    在使用FIS3搭建项目的时候,遇到了一些问题,这里记录下. 这里是发布搭建代码: // 代码发布时 fis.media('qa') .match('*.{js,css,png}', { useHash ...

  2. Python 爬虫模拟登陆知乎

    在之前写过一篇使用python爬虫爬取电影天堂资源的博客,重点是如何解析页面和提高爬虫的效率.由于电影天堂上的资源获取权限是所有人都一样的,所以不需要进行登录验证操作,写完那篇文章后又花了些时间研究了 ...

  3. CENTOS 6.5 平台离线编译安装 PHP5.6.6

    一.下载php源码包 http://cn2.php.net/get/php-5.6.6.tar.gz/from/this/mirror 二.编译 编译之前可能会缺少一些必要的依赖包,加载一个本地yum ...

  4. 从零开始编写自己的C#框架(26)——小结

    一直想写个总结,不过实在太忙了,所以一直拖啊拖啊,拖到现在,不过也好,有了这段时间的沉淀,发现自己又有了小小的进步.哈哈...... 原想框架开发的相关开发步骤.文档.代码.功能.部署等都简单的讲过了 ...

  5. dagger2系列之依赖方式dependencies、包含方式(从属方式)SubComponent

    本篇是实战文章,从代码的角度分析这两种方式.本文参考自下列文章: http://www.jianshu.com/p/1d42d2e6f4a5 http://www.jianshu.com/p/94d4 ...

  6. C#通过NPOI操作Excel

    参考页面: http://www.yuanjiaocheng.net/webapi/create-crud-api-1-post.html http://www.yuanjiaocheng.net/w ...

  7. 浅谈java异常[Exception]

    学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:589809992 我们一起学Java! 一. 异常的定义 在<java编程思想 ...

  8. java统计字符串单词的个数

    在一些项目中可能需要对一段字符串中的单词进行统计,我在这里写了一个简单的demo,有需要的同学可以拿去看一下. 本人没怎么写个播客,如果有啥说的不对的地方,你来打我啊 不说废话了直接贴代码: 实现代码 ...

  9. Jquery 获得当前标签的名称和标签属性

    得到标签的名称 $("#name").prop("tagName"); 或者 $("#name")[0].tagName; 注意:1.得到的 ...

  10. [Django]用户权限学习系列之权限管理界面实现

    本系列前三章: http://www.cnblogs.com/CQ-LQJ/p/5604331.htmlPermission权限基本操作指令 http://www.cnblogs.com/CQ-LQJ ...