《高性能javascript》一书要点和延伸(下)

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

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

<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),html

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

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

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

执行以下:jquery

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

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

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

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

⑶ 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。

 

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

donate

相关文章
相关标签/搜索