Quantcast
Channel: IT瘾javascript推荐
Viewing all 148 articles
Browse latest View live

JavaScript性能优化小知识总结

$
0
0

JavaScript的性能问题不容小觑,这就需要我们开发人员在编写JavaScript程序时多注意一些细节,本文非常详细的介绍了一下JavaScript性能优化方面的知识点,绝对是干货。

前言

一直在学习javascript,也有看过《犀利开发Jquery内核详解与实践》,对这本书的评价只有两个字犀利,可能是对javascript理解的还不够透彻异或是自己太笨,更多的是自己不擅于思考懒得思考以至于里面说的一些精髓都没有太深入的理解。

鉴于想让自己有一个提升,进不了一个更加广阔的天地,总得找一个属于自己的居所好好生存,所以平时会有意无意的去积累一些使用jQuerry的常用知识,特别是对于性能要求这一块,总是会想是不是有更好的方式来实现。

下面是我总结的一些小技巧,仅供参考。(我先会说一个总标题,然后用一小段话来说明这个意思 再最后用一个demo来简单言明)

避免全局查找

在一个函数中会用到全局对象存储为局部变量来减少全局查找,因为访问局部变量的速度要比访问全局变量的速度更快些

        function search() {
            //当我要使用当前页面地址和主机域名
            alert(window.location.href + window.location.host);
        }
        //最好的方式是如下这样  先用一个简单变量保存起来
        function search() {
            var location = window.location;
            alert(location.href + location.host);
        }

定时器

如果针对的是不断运行的代码, 不应该使用setTimeout,而应该是用setInterval,因为setTimeout每一次都会初始化一个定时器,而setInterval只会在开始的时候初始化一个定时器

        var timeoutTimes = 0;
        function timeout() {
            timeoutTimes++;
            if (timeoutTimes < 10) {
                setTimeout(timeout, 10);
            }
        }
        timeout();
        //可以替换为:
        var intervalTimes = 0;
        function interval() {
            intervalTimes++;
            if (intervalTimes >= 10) {
                clearInterval(interv);
            }
        }
        var interv = setInterval(interval, 10);

字符串连接

如果要连接多个字符串,应该少使用+=,如

s+=a;

s+=b;

s+=c;

应该写成s+=a + b + c;

而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存,使用JavaScript数组来收集,最后 使用join方法连接起来

        var buf = [];
        for (var i = 0; i < 100; i++) {
            buf.push(i.toString());
        }
        var all = buf.join("");

避免with语句

和函数类似 ,with语句会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度,由于额外的作用域链的查找,在with语句中执行的代码肯定会比外面执行的代码要慢,在 能不使用with语句的时候尽量不要使用with语句

 with (a.b.c.d) {
            property1 = 1;
            property2 = 2;
        }
        //可以替换为:
        var obj = a.b.c.d;
        obj.property1 = 1;
        obj.property2 = 2;

数字转换成字符串

般最好用”" + 1来将数字转换成字符串,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:

(“” +) > String() > .toString() > new String()

浮点数转换成整型

很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()

各种类型转换

var myVar = "3.14159",
        str = "" + myVar, //  to string  
        i_int = ~ ~myVar,  //  to integer  
        f_float = 1 * myVar,  //  to float  
        b_bool = !!myVar,  /*  to boolean - any string with length 
                                and any number except 0 are true */
        array = [myVar];  //  to array

如果定义了toString()方法来进行类型转换的话,推荐 显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高

多个类型声明

在JavaScript中所有变量都可以使用单个var语句来声明,这样就是组合在一起的语句,以减少整个脚本的执行时间,就如上面代码一样,上面代码格式也挺规范,让人一看就明了。

插入迭代器

如var name=values[i]; i++;前面两条语句可以写成var name=values[i++]

使用直接量

var aTest = new Array(); //替换为
        var aTest = [];
        var aTest = new Object; //替换为
        var aTest = {};
        var reg = new RegExp(); //替换为
        var reg = /../;
        //如果要创建具有一些特性的一般对象,也可以使用字面量,如下:
        var oFruit = new O;
        oFruit.color = "red";
        oFruit.name = "apple";
        //前面的代码可用对象字面量来改写成这样:
        var oFruit = { color: "red", name: "apple" };

使用DocumentFragment优化多次append

一旦需要更新DOM,请考虑使用文档碎片来构建DOM结构,然后再将其添加到现存的文档中。

for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            document.body.appendChild(el);
        }
        //可以替换为:
        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用一次innerHTML赋值代替构建dom元素

对于大的DOM更改,使用innerHTML要比使用标准的DOM方法创建同样的DOM结构快得多。

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //可以替换为:
        var html = [];
        for (var i = 0; i < 1000; i++) {
            html.push('<p>' + i + '</p>');
        }
        document.body.innerHTML = html.join('');

通过模板元素clone,替代createElement

很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低,如果需要直接插入HTML,可以找一个容器元素,比如指定一个div或者span,并设置他们的innerHTML来将自己的HTML代码插入到页面中。通常我们可能会使用字符串直接写HTML来创建节点,其实这样做,1无法保证代码的有效性2字符串操作效率低,所以应该是用document.createElement()方法,而如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点

        var frag = document.createDocumentFragment();
        for (var i = 0; i < 1000; i++) {
            var el = document.createElement('p');
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);
        //替换为:
        var frag = document.createDocumentFragment();
        var pEl = document.getElementsByTagName('p')[0];
        for (var i = 0; i < 1000; i++) {
            var el = pEl.cloneNode(false);
            el.innerHTML = i;
            frag.appendChild(el);
        }
        document.body.appendChild(frag);

使用firstChild和nextSibling代替childNodes遍历dom元素

        var nodes = element.childNodes;
        for (var i = 0, l = nodes.length; i < l; i++) {
            var node = nodes[i];
            //……
        }
        //可以替换为:
        var node = element.firstChild;
        while (node) {
            //……
            node = node.nextSibling;

删除DOM节点

删除dom节点之前,一定要删除注册在该节点上的事件,不管是用observe方式还是用attachEvent方式注册的事件,否则将会产生无法回收的内存。另外,在removeChild和innerHTML=’’二者之间,尽量选择后者. 因为在sIEve(内存泄露监测工具)中监测的结果是用removeChild无法有效地释放dom节点

使用事件代理

任何可以冒泡的事件都不仅仅可以在事件目标上进行处理,目标的任何祖先节点上也能处理,使用这个知识就可以将事件处理程序附加到更高的地方负责多个目标的事件处理,同样, 对于内容动态增加并且子节点都需要相同的事件处理函数的情况,可以把事件注册提到父节点上,这样就不需要为每个子节点注册事件监听了。另外,现有的js库都采用observe方式来创建事件监听,其实现上隔离了dom对象和事件处理函数之间的循环引用,所以应该尽量采用这种方式来创建事件监听

重复使用的调用结果,事先保存到局部变量

        //避免多次取值的调用开销
        var h1 = element1.clientHeight + num1;
        var h2 = element1.clientHeight + num2;
        //可以替换为:
        var eleHeight = element1.clientHeight;
        var h1 = eleHeight + num1;
        var h2 = eleHeight + num2;

注意NodeList

最小化访问NodeList的次数可以极大的改进脚本的性能

        var images = document.getElementsByTagName('img');
        for (var i = 0, len = images.length; i < len; i++) {

        }

编写JavaScript的时候一定要知道何时返回NodeList对象,这样可以最小化对它们的访问

  • 进行了对getElementsByTagName()的调用
  • 获取了元素的childNodes属性
  • 获取了元素的attributes属性
  • 访问了特殊的集合,如document.forms、document.images等等

要了解了当使用NodeList对象时,合理使用会极大的提升代码执行速度

优化循环

可以使用下面几种方式来优化循环

  • 减值迭代

大多数循环使用一个从0开始、增加到某个特定值的迭代器,在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效

  • 简化终止条件

由于每次循环过程都会计算终止条件,所以必须保证它尽可能快,也就是说避免属性查找或者其它的操作,最好是将循环控制量保存到局部变量中,也就是说对数组或列表对象的遍历时,提前将length保存到局部变量中,避免在循环的每一步重复取值。

        var list = document.getElementsByTagName('p');
        for (var i = 0; i < list.length; i++) {
            //……
        }

        //替换为:
        var list = document.getElementsByTagName('p');
        for (var i = 0, l = list.length; i < l; i++) {
            //……
        }
  • 简化循环体

循环体是执行最多的,所以要确保其被最大限度的优化

  • 使用后测试循环

在JavaScript中,我们可以使用for(;;),while(),for(in)三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以,就应该尽量少用。for(;;)和while循环,while循环的效率要优于for(;;),可能是因为for(;;)结构的问题,需要经常跳转回去。

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0;
        for (var i = 0, l = arr.length; i < l; i++) {
            sum += arr[i];
        }

        //可以考虑替换为:

        var arr = [1, 2, 3, 4, 5, 6, 7];
        var sum = 0, l = arr.length;
        while (l--) {
            sum += arr[l];
        }

最常用的for循环和while循环都是前测试循环,而如do-while这种后测试循环,可以避免最初终止条件的计算,因此运行更快。

展开循环

当循环次数是确定的,消除循环并使用多次函数调用往往会更快。

避免双重解释

如果要提高代码性能,尽可能避免出现需要按照JavaScript解释的字符串,也就是

  • 尽量少使用eval函数

使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间,而且使用Eval带来的安全性问题也是不容忽视的。

  • 不要使用Function构造器

不要给setTimeout或者setInterval传递字符串参数

        var num = 0;
        setTimeout('num++', 10);
        //可以替换为:
        var num = 0;
        function addNum() {
            num++;
        }
        setTimeout(addNum, 10);

缩短否定检测

       if (oTest != '#ff0000') {
            //do something
        }
        if (oTest != null) {
            //do something
        }
        if (oTest != false) {
            //do something
        }
        //虽然这些都正确,但用逻辑非操作符来操作也有同样的效果:
        if (!oTest) {
            //do something
        }

条件分支

  • 将条件分支,按可能性顺序从高到低排列:可以减少解释器对条件的探测次数
  • 在同一条件子的多(>2)条件分支时,使用switch优于if:switch分支选择的效率高于if,在IE下尤为明显。4分支的测试,IE下switch的执行时间约为if的一半。
  • 使用三目运算符替代条件分支
        if (a > b) {
            num = a;
        } else {
            num = b;
        }
        //可以替换为:
        num = a > b ? a : b;

使用常量

  • 重复值:任何在多处用到的值都应该抽取为一个常量
  • 用户界面字符串:任何用于显示给用户的字符串,都应该抽取出来以方便国际化
  • URLs:在Web应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的URL
  • 任意可能会更改的值:每当你用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化,如果答案是“是”,那么这个值就应该被提取出来作为一个常量。

避免与null进行比较

由于JavaScript是弱类型的,所以它不会做任何的自动类型检查,所以如果看到与null进行比较的代码,尝试使用以下技术替换

  • 如果值应为一个引用类型,使用instanceof操作符检查其构造函数
  • 如果值应为一个基本类型,作用typeof检查其类型
  • 如果是希望对象包含某个特定的方法名,则使用typeof操作符确保指定名字的方法存在于对象上

避免全局量

全局变量应该全部字母大写,各单词之间用_下划线来连接。尽可能避免全局变量和函数, 尽量减少全局变量的使用,因为在一个页面中包含的所有JavaScript都在同一个域中运行。所以如果你的代码中声明了全局变量或者全局函数的话,后面的代码中载入的脚本文件中的同名变量和函数会覆盖掉(overwrite)你的。

//糟糕的全局变量和全局函数
var current = null;
function init(){
//...
}
function change() {
    //...
}
function verify() {
    //...
}
//解决办法有很多,Christian Heilmann建议的方法是:
//如果变量和函数不需要在“外面”引用,那么就可以使用一个没有名字的方法将他们全都包起来。
(function(){
var current = null;
function init() {
    //...
}
function change() {
    //...
}
function verify() {
    //...
}
})();
//如果变量和函数需要在“外面”引用,需要把你的变量和函数放在一个“命名空间”中
//我们这里用一个function做命名空间而不是一个var,因为在前者中声明function更简单,而且能保护隐私数据
myNameSpace = function() {
    var current = null;

    function init() {
        //...
    }

    function change() {
        //...
    }

    function verify() {
        //...
    }

//所有需要在命名空间外调用的函数和属性都要写在return里面
    return {
        init: init,
        //甚至你可以为函数和属性命名一个别名
        set: change
    };
};

尊重对象的所有权

因为JavaScript可以在任何时候修改任意对象,这样就可以以不可预计的方式覆写默认的行为,所以如果你不负责维护某个对象,它的对象或者它的方法,那么你就不要对它进行修改,具体一点就是说:

  • 不要为实例或原型添加属性
  • 不要为实例或者原型添加方法
  • 不要重定义已经存在的方法
  • 不要重复定义其它团队成员已经实现的方法,永远不要修改不是由你所有的对象,你可以通过以下方式为对象创建新的功能:
  • 创建包含所需功能的新对象,并用它与相关对象进行交互
  • 创建自定义类型,继承需要进行修改的类型,然后可以为自定义类型添加额外功能

循环引用

如果循环引用中包含DOM对象或者ActiveX对象,那么就会发生内存泄露。内存泄露的后果是在浏览器关闭前,即使是刷新页面,这部分内存不会被浏览器释放。

简单的循环引用:

        var el = document.getElementById('MyElement');
        var func = function () {
            //…
        }
        el.func = func;
        func.element = el;

但是通常不会出现这种情况。通常循环引用发生在为dom元素添加闭包作为expendo的时候。

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();

init在执行的时候,当前上下文我们叫做context。这个时候,context引用了el,el引用了function,function引用了context。这时候形成了一个循环引用。

下面2种方法可以解决循环引用:

1)  置空dom对象

       function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替换为:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            el = null;
        }
        init();

将el置空,context中不包含对dom对象的引用,从而打断循环应用。

如果我们需要将dom对象返回,可以用如下方法:

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            return el;
        }
        init();
        //可以替换为:
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
            try {
                return el;
            } finally {
                el = null;
            }
        }
        init();

2)  构造新的context

        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = function () {
                //……
            }
        }
        init();
        //可以替换为:
        function elClickHandler() {
            //……
        }
        function init() {
            var el = document.getElementById('MyElement');
            el.onclick = elClickHandler;
        }
        init();

把function抽到新的context中,这样,function的context就不包含对el的引用,从而打断循环引用。

通过javascript创建的dom对象,必须append到页面中

IE下,脚本创建的dom对象,如果没有append到页面中,刷新页面,这部分内存是不会回收的!

        function create() {
            var gc = document.getElementById('GC');
            for (var i = 0; i < 5000; i++) {
                var el = document.createElement('div');
                el.innerHTML = "test";
                //下面这句可以注释掉,看看浏览器在任务管理器中,点击按钮然后刷新后的内存变化
                gc.appendChild(el);
            }
        }

释放dom元素占用的内存

将dom元素的innerHTML设置为空字符串,可以释放其子元素占用的内存。

在rich应用中,用户也许会在一个页面上停留很长时间,可以使用该方法释放积累得越来越多的dom元素使用的内存。

释放javascript对象

在rich应用中,随着实例化对象数量的增加,内存消耗会越来越大。所以应当及时释放对对象的引用,让GC能够回收这些内存控件。

对象:obj = null

对象属性:delete obj.myproperty

数组item:使用数组的splice方法释放数组中不用的item

避免string的隐式装箱

对string的方法调用,比如’xxx’.length,浏览器会进行一个隐式的装箱操作,将字符串先转换成一个String对象。推荐对声明有可能使用String实例方法的字符串时,采用如下写法:

var myString = new String(‘Hello World’);

松散耦合

1、解耦HTML/JavaScript

JavaScript和HTML的紧密耦合:直接写在HTML中的JavaScript、使用包含内联代码的<script>元素、使用HTML属性来分配事件处理程序等

HTML和JavaScript的紧密耦合:JavaScript中包含HTML,然后使用innerHTML来插入一段html文本到页面

其实应该是保持层次的分离,这样可以很容易的确定错误的来源,所以我们应确保HTML呈现应该尽可能与JavaScript保持分离

2、解耦CSS/JavaScript

显示问题的唯一来源应该是CSS,行为问题的唯一来源应该是JavaScript,层次之间保持松散耦合才可以让你的应用程序更加易于维护,所以像以下的代码element.style.color=”red”尽量改为element.className=”edit”,而且不要在css中通过表达式嵌入JavaScript

3、解耦应用程序/事件处理程序

将应用逻辑和事件处理程序相分离:一个事件处理程序应该从事件对象中提取,并将这些信息传送给处理应用逻辑的某个方法中。这样做的好处首先可以让你更容易更改触发特定过程的事件,其次可以在不附加事件的情况下测试代码,使其更易创建单元测试

性能方面的注意事项

1、尽量使用原生方法

2、switch语句相对if较快

通过将case语句按照最可能到最不可能的顺序进行组织

3、位运算较快

当进行数字运算时,位运算操作要比任何布尔运算或者算数运算快

4、 巧用||&&布尔运算符

        function eventHandler(e) {
            if (!e) e = window.event;
        }
        //可以替换为:
        function eventHandler(e) {
            e = e || window.event;
        }
        if (myobj) {
            doSomething(myobj);
        }
        //可以替换为:
        myobj && doSomething(myobj);

避免错误应注意的地方

1、每条语句末尾须加分号

在if语句中,即使条件表达式只有一条语句也要用{}把它括起来,以免后续如果添加了语句之后造成逻辑错误

2、使用+号时需谨慎

JavaScript 和其他编程语言不同的是,在 JavaScript 中,’+'除了表示数字值相加,字符串相连接以外,还可以作一元运算符用,把字符串转换为数字。因而如果使用不当,则可能与自增符’++’混淆而引起计算错误

        var valueA = 20;
        var valueB = "10";
        alert(valueA + valueB);     //ouput: 2010 
        alert(valueA + (+valueB));  //output: 30 
        alert(valueA + +valueB);    //output:30 
        alert(valueA ++ valueB);     //Compile error

3、使用return语句需要注意

一条有返回值的return语句不要用()括号来括住返回值,如果返回表达式,则表达式应与return关键字在同一行,以避免压缩时,压缩工具自动加分号而造成返回与开发人员不一致的结果

        function F1() {
            var valueA = 1;
            var valueB = 2;
            return valueA + valueB;
        }
        function F2() {
            var valueA = 1;
            var valueB = 2;
            return
            valueA + valueB;
        }
        alert(F1());  //output: 3 
        alert(F2());  //ouput: undefined

==和===的区别

避免在if和while语句的条件部分进行赋值,如if (a = b),应该写成if (a == b),但是在比较是否相等的情况下,最好使用全等运行符,也就是使用===和!==操作符会相对于==和!=会好点。==和!=操作符会进行类型强制转换

        var valueA = "1";
        var valueB = 1;
        if (valueA == valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Equal"
        if (valueA === valueB) {
            alert("Equal");
        }
        else {
            alert("Not equal");
        }
        //output: "Not equal"

不要使用生偏语法

不要使用生偏语法,写让人迷惑的代码,虽然计算机能够正确识别并运行,但是晦涩难懂的代码不方便以后维护

函数返回统一类型

虽然JavaScript是弱类型的,对于函数来说,前面返回整数型数据,后面返回布尔值在编译和运行都可以正常通过,但为了规范和以后维护时容易理解,应保证函数应返回统一的数据类型

总是检查数据类型

要检查你的方法输入的所有数据,一方面是为了安全性,另一方面也是为了可用性。用户随时随地都会输入错误的数据。这不是因为他们蠢,而是因为他们很忙,并且思考的方式跟你不同。用typeof方法来检测你的function接受的输入是否合法

何时用单引号,何时用双引号

虽然在JavaScript当中,双引号和单引号都可以表示字符串, 为了避免混乱,我们建议在HTML中使用双引号,在JavaScript中使用单引号,但为了兼容各个浏览器,也为了解析时不会出错,定义JSON对象时,最好使用双引号

部署

  • 用JSLint运行JavaScript验证器来确保没有语法错误或者是代码没有潜在的问
  • 部署之前推荐使用压缩工具将JS文件压缩
  • 文件编码统一用UTF-8
  • JavaScript 程序应该尽量放在 .js 的文件中,需要调用的时候在 HTML 中以 <script src=”filename.js”> 的形式包含进来。JavaScript 代码若不是该 HTML 文件所专用的,则应尽量避免在 HTML 文件中直接编写 JavaScript 代码。因为这样会大大增加 HTML 文件的大小,无益于代码的压缩和缓存的使用。另外,<script src=”filename.js”> 标签应尽量放在文件的后面,最好是放在</body>标签前。这样会降低因加载 JavaScript 代码而影响页面中其它组件的加载时间。

永远不要忽略代码优化工作,重构是一项从项目开始到结束需要持续的工作,只有不断的优化代码才能让代码的执行效率越来越好


React 入门实例教程

$
0
0

现在最热门的前端框架,毫无疑问是 React

上周,基于 React 的 React Native发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑。

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram的网站。做出来以后,发现这套东西很好用,就在2013年5月 开源了。

由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。

这个项目本身也越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。衍生的 React Native 项目,目标更是宏伟,希望用写 Web App 的方式去写 Native App。如果能够实现,整个互联网行业都会被颠覆,因为同一组人只需要写一次 UI ,就能同时运行在服务器、浏览器和手机(参见 《也许,DOM 不是答案》)。

既然 React 这么热门,看上去充满希望,当然应该好好学一下。从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目。但是,好的 React 教程却不容易找到,这一方面因为这项技术太新,刚刚开始走红,大家都没有经验,还在摸索之中;另一方面因为 React 本身还在不断变动,API 一直在调整,至今没发布1.0版。

我学习 React 时,就很苦恼。有的教程讨论一些细节问题,对入门没帮助;有的教程写得不错,但比较短,无助于看清全貌。我断断续续学了几个月,看过二十几篇教程,在这个过程中,将对自己有帮助的 Demo 都收集下来,做成了一个库 React Demos

下面,我就根据 这个库,写一篇全面又易懂的 React 入门教程。你只需要跟着每一个 Demo 做一遍,就能初步掌握 React 。当然,前提是你必须拥有基本 JavaScript 和 DOM 知识,但是你读完就会发现,React 所要求的预备知识真的很少。

零、安装

React 的安装包,可以到 官网下载。不过, React Demos已经自带 React 源码,不用另外安装,只需把这个库拷贝到你的硬盘就行了。


$ git clone git@github.com:ruanyf/react-demos.git

如果你没安装 git, 那就直接下载 zip 压缩包

下面要讲解的10个例子在各个 Demo 子目录,每个目录都有一个 index.html 文件,在浏览器打开这个文件(大多数情况下双击即可),就能立刻看到效果。

需要说明的是,React 可以在浏览器运行,也可以在服务器运行,但是本教程只涉及浏览器。一方面是为了尽量保持简单,另一方面 React 的语法是一致的,服务器的用法与浏览器差别不大。 Demo11是服务器首屏渲染的例子,有兴趣的朋友可以自己去看源码。

一、HTML 模板

使用 React 的网页源码,结构大致如下。

<!DOCTYPE html><html><head><script src="../build/react.js"></script><script src="../build/JSXTransformer.js"></script></head><body><div id="example"></div><script type="text/jsx">
      // ** Our code goes here! **</script></body></html>

上面代码有两个地方需要注意。首先,最后一个 script 标签的 type 属性为 text/jsx 。这是因为 React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上 type="text/jsx" 。

其次,React 提供两个库: react.js 和 JSXTransformer.js ,它们必须首先加载。其中,JSXTransformer.js 的作用是将 JSX 语法转为 JavaScript 语法。这一步很消耗时间,实际上线的时候,应该将它放到服务器完成。


$ jsx src/ build/

上面命令可以将 src 子目录的 js 文件进行语法转换,转码后的文件全部放在 build 子目录。

二、React.render()

React.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。


React.render(<h1>Hello, world!</h1>,
  document.getElementById('example')
);

上面代码将一个 h1 标题,插入 example 节点(查看 demo01),运行结果如下。

三、JSX 语法

上一节的代码, HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写(查看 Demo02)。


var names = ['Alice', 'Emily', 'Kate'];

React.render(
  <div>
  {
    names.map(function (name) {
      return <div>Hello, {name}!</div>
    })
  }</div>,
  document.getElementById('example')
);

上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析。上面代码的运行结果如下。

JSX 允许直接在模板插入 JavaScript 变量。如果这个变量是一个数组,则会展开这个数组的所有成员(查看 demo03)。


var arr = [<h1>Hello world!</h1>,<h2>React is awesome</h2>,
];
React.render(<div>{arr}</div>,
  document.getElementById('example')
);

上面代码的arr变量是一个数组,结果 JSX 会把它的所有成员,添加到模板,运行结果如下。

四、组件

React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。React.createClass 方法就用于生成一个组件类(查看 demo04)。


var HelloMessage = React.createClass({
  render: function() {
    return <h1>Hello {this.props.name}</h1>;
  }
});

React.render(
  <HelloMessage name="John" />,
  document.getElementById('example')
);

上面代码中,变量 HelloMessage 就是一个组件类。模板插入 <HelloMessage /> 时,会自动生成 HelloMessage 的一个实例(下文的"组件"都指组件类的实例)。所有组件类都必须有自己的 render 方法,用于输出组件。

组件的用法与原生的 HTML 标签完全一致,可以任意加入属性,比如 <HelloMessage name="John" /> ,就是 HelloMessage 组件加入一个 name 属性,值为 John。组件的属性可以在组件类的 this.props 对象上获取,比如 name 属性就可以通过 this.props.name 读取。上面代码的运行结果如下。

添加组件属性,有一个地方需要注意,就是 class 属性需要写成 className ,for 属性需要写成 htmlFor ,这是因为 class 和 for 是 JavaScript 的保留字。

五、this.props.children

this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点(查看 demo05)。


var NotesList = React.createClass({
  render: function() {
    return (<ol>
      {
        this.props.children.map(function (child) {
          return <li>{child}</li>
        })
      }&lt/ol>
    );
  }
});

React.render(
  <NotesList><span>hello</span><span>world</span></NotesList>,
  document.body
);

上面代码的 NoteList 组件有两个 span 子节点,它们都可以通过 this.props.children 读取,运行结果如下。

这里需要注意,只有当子节点多余1个时,this.props.children 才是一个数组,否则是不能用 map 方法的, 会报错。

六、React.findDOMNode()

组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff,它可以极大提高网页的性能表现。

但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 React.findDOMNode 方法(查看 demo06)。


var MyComponent = React.createClass({
  handleClick: function() {
    React.findDOMNode(this.refs.myTextInput).focus();
  },
  render: function() {
    return (<div><input type="text" ref="myTextInput" /><input type="button" value="Focus the text input" onClick={this.handleClick} /></div>
    );
  }
});

React.render(
  <MyComponent />,
  document.getElementById('example')
);

上面代码中,组件 MyComponent 的子节点有一个文本输入框,用于获取用户的输入。这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,文本输入框必须有一个 ref 属性,然后 this.refs.[refName] 就指向这个虚拟 DOM 的子节点,最后通过 React.findDOMNode 方法获取真实 DOM 的节点。

需要注意的是,由于 React.findDOMNode 方法获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个方法,否则会返回 null 。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会调用 React.findDOMNode 方法。

React 组件支持很多事件,除了 Click 事件以外,还有 KeyDown 、Copy、Scroll 等,完整的事件清单请查看 官方文档

七、this.state

组件免不了要与用户互动,React 的一大创新,就是将组件看成是一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染 UI (查看 demo07)。


var LikeButton = React.createClass({
  getInitialState: function() {
    return {liked: false};
  },
  handleClick: function(event) {
    this.setState({liked: !this.state.liked});
  },
  render: function() {
    var text = this.state.liked ? 'like' : 'haven\'t liked';
    return (<p onClick={this.handleClick}>
        You {text} this. Click to toggle.</p>
    );
  }
});

React.render(
  <LikeButton />,
  document.getElementById('example')
);

上面代码是一个 LikeButton 组件,它的 getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。

由于 this.props 和 this.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些一旦定义,就不再改变的特性,而 this.state 是会随着用户互动而产生变化的特性。

八、表单

用户在表单填入的内容,属于用户跟组件的互动,所以不能用 this.props 读取(查看 demo08)。


var Input = React.createClass({
  getInitialState: function() {
    return {value: 'Hello!'};
  },
  handleChange: function(event) {
    this.setState({value: event.target.value});
  },
  render: function () {
    var value = this.state.value;
    return (<div><input type="text" value={value} onChange={this.handleChange} /><p>{value}</p></div>
    );
  }
});

React.render(<Input/>, document.body);

上面代码中,文本输入框的值,不能用 this.props.value 读取,而要定义一个 onChange 事件的回调函数,通过 event.target.value 读取用户输入的值。textarea 元素、select元素、radio元素都属于这种情况,更多介绍请参考 官方文档

九、组件的生命周期

组件的 生命周期分成三个状态:

  • Mounting:已插入真实 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真实 DOM

React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。

  • componentWillMount()
  • componentDidMount()
  • componentWillUpdate(object nextProps, object nextState)
  • componentDidUpdate(object prevProps, object prevState)
  • componentWillUnmount()

此外,React 还提供两种特殊状态的处理函数。

  • componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
  • shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用

这些方法的详细说明,可以参考 官方文档。下面是一个例子(查看 demo09)。


var Hello = React.createClass({
  getInitialState: function () {
    return {
      opacity: 1.0
    };
  },

  componentDidMount: function () {
    this.timer = setInterval(function () {
      var opacity = this.state.opacity;
      opacity -= .05;
      if (opacity < 0.1) {
        opacity = 1.0;
      }
      this.setState({
        opacity: opacity
      });
    }.bind(this), 100);
  },

  render: function () {
    return (
      <div style={{opacity: this.state.opacity}}>
        Hello {this.props.name}</div>
    );
  }
});

React.render(
  <Hello name="world"/>,
  document.body
);

上面代码在hello组件加载以后,通过 componentDidMount 方法设置一个定时器,每隔100毫秒,就重新设置组件的透明度,从而引发重新渲染。

另外,组件的style属性的设置方式也值得注意,不能写成


style="opacity:{this.state.opacity};"

而要写成


style={{opacity: this.state.opacity}}

这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。

十、Ajax

组件的数据来源,通常是通过 Ajax 请求从服务器获取,可以使用 componentDidMount 方法设置 Ajax 请求,等到请求成功,再用 this.setState 方法重新渲染 UI (查看 demo10)。


var UserGist = React.createClass({
  getInitialState: function() {
    return {
      username: '',
      lastGistUrl: ''
    };
  },

  componentDidMount: function() {
    $.get(this.props.source, function(result) {
      var lastGist = result[0];
      if (this.isMounted()) {
        this.setState({
          username: lastGist.owner.login,
          lastGistUrl: lastGist.html_url
        });
      }
    }.bind(this));
  },

  render: function() {
    return (
      <div>
        {this.state.username}'s last gist is<a href={this.state.lastGistUrl}>here</a>.</div>
    );
  }
});

React.render(
  <UserGist source="https://api.github.com/users/octocat/gists" />,
  document.body
);

上面代码使用 jQuery 完成 Ajax 请求,这是为了便于说明。React 没有任何依赖,完全可以使用其他库。

十一、参考链接

  1. React's official site
  2. React's official examples
  3. React (Virtual) DOM Terminology, by Sebastian Markbåge
  4. The React Quick Start Guide, by Jack Callister
  5. Learning React.js: Getting Started and Concepts, by Ken Wheeler
  6. Getting started with React, by Ryan Clark
  7. React JS Tutorial and Guide to the Gotchas, by Justin Deal
  8. React Primer, by Binary Muse
  9. jQuery versus React.js thinking, by zigomir

(完)

文档信息

【译】常见的10个JavaScript动画函数库

$
0
0

018

 

原文: Javascript Animation Libraries

译文:JavaScript的动画函数库

译者:dwqs

 

一、 Snap.svg

SVG是一种创建交互式动画非常棒的方式,独立的分辨率的矢量图形在任何大小的屏幕上看起来效果都很好。Snap.svg库使操作SVG变得更jQuery操作DOM一样简单。

 

二、 Motio

一个基于动画和平移的简单但强大的JavaScript库

 

三、 animo.js

堆栈动画,可以创建跨浏览器平台的模糊效果,在动画完成可以调用回调函数,创建更魔幻的动画。

 

四、 Move.js

Move.js是一个很小的函数库,能简单而优雅的支持CSS3.

 

五、 favico.js

用动画徽章的方式激活你的网站图标。你可以自定义动画类型、位置、背景颜色和文本颜色

 

六、 Textillate.js

一个简单的CSS文本动画插件,Textillate.js结合了一些极好的函数库,目的是为应用CSS3动画的任何文本提供一个简单易用的插件。

 

七、 Anima

用Anima可以很容易的同时激活100多个对象,每一个元素都有大小和黏性去模拟一个真实对象

 

八、 GSAP

GSAP是一套针对原生的、高性能HTML5动画的工具,并能在所有主流浏览器中工作。

 

九、 WOW.js

WOW.js是一个当滚动时显示动画的JavaScript插件。非常适合用Animate.css的朋友

 

十、 Velocity.js

Velocity.js:对于运动设计者来说,这是一个极其迅速的动画渲染引擎。

淡忘~浅思猜你喜欢

【译】排序算法的JavaScript实现

【译】CSS 3动画介绍

JavaScript中的JSON操作

用JavaScript检测浏览器

国外优秀JavaScript资源推荐
无觅

转载请注明: 淡忘~浅思» 【译】常见的10个JavaScript动画函数库

构建自适应的手机页面

$
0
0

从事PC Web开发好多年,但是手机页面开发较晚,所以最开始的时候,为了做微信应用的开发,各种饿补,但是为了将设计稿精准的适配在各种尺寸的手机上还是太坑,所以找了些资料后,借鉴了一些成熟的网站,自己整合了一个简便的方法,分享出来,欢迎拍砖。

首先需要你对viewport有一个了解,建议看 Apple官方文档,虽然啰嗦,但是啃一遍,基本该明白的就全明白了。

第一种方案

html<meta name="viewport" content="target-densitydpi=device-dpi,width=640,user-scalable=no" />

简便易行,适用情况:

  1. 网页设计仅针对手机屏幕,并且没有需要根据手机屏幕尺寸进行UI调整的内容,既没有媒体查询的CSS
  2. 产品层面可以不考虑非主流设备或浏览器的兼容性,例如黑莓的某些设备或搜狗浏览器这类的不支持640定宽渲染的浏览器
  3. 当手机横竖切换时,能够容忍部分手机在横屏浏览网页时,可能出现的问题(潜在问题)。

这个方案是强制让手机浏览器按640的比例来呈现网页,让浏览器去做尺寸适配;这个方案我觉得其实非常好,绝大多数的场景都可以支持,而且开发起来简单高效,不需要考虑各种尺寸和样式表的单位换算问题。

这个方案有点类似css zoom,就是浏览器将整个网页进行了缩放,注意一点:PC端的使用浏览器的开发工具进行缩放的时候,譬如使用iphone 5s 模式预览,可能会有问题,比如border:1px的时候可能会不可见,字体会有些模糊等,但是手机端展现的时候不会出现这些问题,因为绝大多数手机目前屏幕分辨率都足够高,而且专门针对viewport做了处理,所以这个问题可以忽略。

测试地址: 点击这里,这个网页就是按640定宽做的,可以在多数尺寸的网页上做适配

第二种方案

html<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no" />

上面这个不用解释了,绝大多数知名网站都这么做的,但是为了能够让图片和字体去做自适应,通常的做法是使用rem来作为单位,然后再启用javascript判定屏幕宽度,动态改变html元素的fontsize。

为了方便计算,通常会默认将html的font-size设置为100px,这样方便计算,例如你的网页字体是16px,只需要写成0.16rem就行了,然后假如设计稿是按640宽度设计,切图的时候完全按设计稿来进行单位换算,然后在js中获取屏幕宽度,根据比例:100/x = 640/documentWidth 计算html的字体大小即可,当然要加入一个window.resize事件重新计算,以防横竖屏切换。

第二种方案有几个弊端:

  1. 由于背景图片无法做缩放,(background-size:cover|contain)只能针对单张图片,而对于sprite css image无力解决,所以这里要求如果有背景图,全部制作成单张的,然后再加上background-size:cover|contain,这里凸显出使用css font的好处了...
  2. 页面中的所有单位都需要用rem来计算,虽然 X/100是很简单的计算,但是书写的时候还是让人厌烦。
  3. 如果有图片必须指定宽度,不然图片会按原始宽度进行渲染,当屏幕尺寸与设计预期尺寸不一致时,就会出错了。

根据上面的描述,我做了两个工具:

  1. 制作时依然采用px作为单位,通过watch插件监听css文件的改变,然后动态转换 px2rem
  2. 根据图片生成 auto convert css icon文件,类似grunt-sprite,但只生成对应的css,而不合成图片,这样对于将以往的css sprite工具生成的图片可以低成本切换成单一图片ICON

开发思路是这样:

  1. 开发时区分目录,例如src表示源码,dest目录标示转换后的静态文件,目前上规模的工程都这么做,应该没什么成本了,然后css依然使用像素作为单位。
  2. 通过grunt或gulp插件来监听文件改变,然后做copy,sync之类的处理,同时这里将css中的px按设定的比率转换成rem。
  3. 不能在网页中直接嵌入内联style,或者需要自行转换单位之后嵌入。

监听脚本
(注:这里假定开发的时候依据的宽度的640,默认字体是32像素)

javascript// 根据屏幕宽度适配字体大小
$(window).on('resize', function () {
  var width = document.documentElement.clientWidth;
  $('html').css('font-size', (width / 640 * 32) + 'px');
}).triggerHandler('resize');

测试地址: 点击这里

(因为我的工程使用的是grunt,所以写的插件也是针对grunt的,如果你打算使用gulp,需要自行实现上面的工具了)

总结

第一个定宽开发的方案其实非常适合小团队做推广页面,因为这样避免了各种工具的整合,降低了开发复杂度。

第二个方案适合需要适应绝大多数尺寸浏览器的项目,同时如果配置好打包工具,可以很大程度降低难度,将开发和部署解耦,开发代码比较清晰。

[总结贴] 十个 JavaScript 中易犯的小错误

$
0
0

序言

在今天,JavaScript已经成为了网页编辑的核心。尤其是过去的几年,互联网见证了在SPA开发、图形处理、交互等方面大量JS库的出现。

如果初次打交道,很多人会觉得js很简单。确实,对于很多有经验的工程师,或者甚至是初学者而言,实现基本的js功能几乎毫无障碍。但是JS的真实功能却比很多人想象的要更加多样、复杂。JavaScript的许多细节规定会让你的网页出现很多意想不到的bug,搞懂这些bug,对于成为一位有经验的JS开发者很重要。

常见错误一:对于this关键词的不正确引用

我曾经听一位喜剧演员说过:

“我从未在这里,因为我不清楚这里是哪里,是除了那里之外的地方吗?”

这句话或多或少地暗喻了在js开发中开发者对于this关键字的使用误区。This指代的是什么?它和日常英语口语中的this是一个意思吗?

随着近些年js编程不断地复杂化,功能多样化,对于一个程序结构的内部指引、引用也逐渐变多起来

下面让我们一起来看这一段代码:

Game.prototype.restart = function () {   this.clearLocalStorage(); 

    this.timer = setTimeout(function(){     this.clearBoard();        }, 0);

 };

运行上面的代码将会出现如下错误:

Uncaught TypeError: undefined is not a function

这是为什么?this的调用和它所在的环境密切相关。之所以会出现上面的错误,是因为当你在调用 setTimeout()函数的时候, 你实际调用的是window.setTimeout(). 因此,在 setTimeout() 定义的函数其实是在window背景下定义的,而window中并没有 clearBoard() 这个函数方法。

下面提供两种解决方案。第一种比较简单直接的方法便是,把this存储到一个变量当中,这样他就可以在不同的环境背景中被继承下来:

Game.prototype.restart = function () {   this.clearLocalStorage();  

 var self = this;

this.timer = setTimeout(function(){     self.clearBoard();}, 0); };

第二种方法便是用bind()的方法,不过这个相比上一种要复杂一些,对于不熟悉bind()的同学可以在微软官方查看它的使用方法: https://msdn.microsoft.com/zh-cn/library/ff841995

Game.prototype.restart = function () {   this.clearLocalStorage(); 

this.timer = setTimeout(this.reset.bind(this), 0); };      

Game.prototype.reset = function(){     this.clearBoard();};

上面的例子中,两个this均指代的是Game.prototype。

常见错误二:传统编程语言的生命周期误区

另一种易犯的错误,便是带着其他编程语言的思维,认为在JS中,也存在生命周期这么一说。请看下面的代码:

for (var i = 0; i < 10; i++) {   /* ... */ } console.log(i);

如果你认为在运行console.log() 时肯定会报出 undefined 错误,那么你就大错特错了。我会告诉你其实它会返回 10吗。

当然,在许多其他语言当中,遇到这样的代码,肯定会报错。因为i明显已经超越了它的生命周期。在for中定义的变量在循环结束后,它的生命也就结束了。但是在js中,i的生命还会继续。这种现象叫做 variable hoisting。

而如果我们想要实现和其他语言一样的在特定逻辑模块中具有生命周期的变量,可以用let关键字。

常见错误三:内存泄露

内存泄露在js变成中几乎是一个无法避免的问题。如果不是特别细心的话,在最后的检查过程中,肯定会出现各种内存泄露问题。下面我们就来举例说明一下:

var theThing = null; 

var replaceThing = function () { 



    var priorThing = theThing; 

    var unused = function () { 

              if (priorThing) {       console.log("hi");     }   

   }; 

   theThing = {     longStr: new Array(1000000).join('*'),  // 

              someMethod: function () {       console.log(someMessage);     }   
   }; 
};   
setInterval(replaceThing, 1000); 

如果运行上面的代码,你会发现你已经造成了大量的内存泄露,每秒泄露1M的内存,显然光靠GC(垃圾回收器)是无法帮助你的了。由上面的代码来看,似乎是longstr在每次replaceThing调用的时候都没有得到回收。这是为什么呢?

每一个theThing结构都含有一个longstr结构列表。每一秒当我们调用 replaceThing, 它就会把当前的指向传递给 priorThing. 但是到这里我们也会看到并没有什么问题,因为 priorThing 每回也是先解开上次函数的指向才会接受新的赋值。并且所有的这一切都是发生在 replaceThing 函数体当中,按常理来说当函数体结束之后,函数中的本地变量也将会被GC回收,也就不会出现内存泄露的问题了,但是为什么会出现上面的错误呢?

这是因为longstr的定义是在一个闭包中进行的,而它又被其他的闭包所引用,js规定,在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。关于在JS中的内存泄露问题可以查看 http://javascript.info/tutorial/memory-leaks#memory-management-in-java...

常见错误四:比较运算符

JavaScript中一个比较便捷的地方,便是它可以给每一个在比较运算的结果变量强行转化成布尔类型。但是从另一方面来考虑,有时候它也会为我们带来很多不便,下面的这些例子便是一些一直困扰很多程序员的代码实例:

console.log(false == '0'); 

console.log(null == undefined); 

console.log(" \t\r\n" == 0); 

console.log('' == 0);  // And these do too! 

if ({}) // ... 

if ([]) // ...

最后两行的代码虽然条件判断为空(经常会被人误认为转化为false),但是其实不管是{ }还是[ ]都是一个实体类,而任何的类其实都会转化为true。就像这些例子所展示的那样,其实有些类型强制转化非常模糊。因此很多时候我们更愿意用 === 和 !== 来替代== 和 !=, 以此来避免发生强制类型转化。. ===和!== 的用法和之前的== 和 != 一样,只不过他们不会发生类型强制转换。另外需要注意的一点是,当任何值与 NaN 比较的时候,甚至包括他自己,结果都是false。因此我们不能用简单的比较字符来决定一个值是否为 NaN 。我们可以用内置的 isNaN() 函数来辨别:

console.log(NaN == NaN);    // false 

console.log(NaN === NaN);   // false 

console.log(isNaN(NaN));    // true 

常见错误五:低效的DOM操作

js中的DOM基本操作非常简单,但是如何能有效地进行这些操作一直是一个难题。这其中最典型的问题便是批量增加DOM元素。增加一个DOM元素是一步花费很大的操作。而批量增加对系统的花销更是不菲。一个比较好的批量增加的办法便是使用 document fragments :

var div = document.getElementsByTagName("my_div");  

var fragment = document.createDocumentFragment(); 

 for (var e = 0; e < elems.length; e++) { fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true)); 

直接添加DOM元素是一个非常昂贵的操作。但是如果是先把要添加的元素全部创建出来,再把它们全部添加上去就会高效很多。

常见错误六:在for循环中的不正确函数调用

请大家看以下代码:

var elements = document.getElementsByTagName('input');

var n = elements.length; 

for (var i = 0; i < n; i++) {     

elements[i].onclick = function() {         

console.log("This is element #" + i);     }; } 

运行以上代码,如果页面上有10个按钮的话,点击每一个按钮都会弹出 “This is element #10”! 。这和我们原先预期的并不一样。这是因为当点击事件被触发的时候,for循环早已执行完毕,i的值也已经从0变成了。

我们可以通过下面这段代码来实现真正正确的效果:

var elements = document.getElementsByTagName('input'); 

var n = elements.length; 

var makeHandler = function(num) {  // outer function

      return function() { 

console.log("This is element #" + num);      }; }; 

for (var i = 0; i < n; i++) 

{     elements[i].onclick = makeHandler(i+1); }

在这个版本的代码中, makeHandler 在每回循环的时候都会被立即执行,把i+1传递给变量num。外面的函数返回里面的函数,而点击事件函数便被设置为里面的函数。这样每个触发函数就都能够是用正确的i值了。

常见错误七:原型继承问题

很大一部分的js开发者都不能完全掌握原型的继承问题。下面具一个例子来说明:

BaseObject = function(name) {     

if(typeof name !== "undefined") 

{         this.name = name;     } 

else 

{         this.name = 'default'     } }; 

这段代码看起来很简单。如果你有name值,则使用它。如果没有,则使用 ‘default’:

var firstObj = new BaseObject(); 

var secondObj = new BaseObject('unique');  

console.log(firstObj.name);  // -> 结果是'default' 

console.log(secondObj.name); // -> 结果是 'unique'

但是如果我们执行delete语句呢:

delete secondObj.name; 

我们会得到:

console.log(secondObj.name); // -> 结果是 'undefined' 

但是如果能够重新回到 ‘default’状态不是更好么? 其实要想达到这样的效果很简单,如果我们能够使用原型继承的话:

BaseObject = function (name) 

{     if(typeof name !== "undefined") 

{         this.name = name;     } };  

BaseObject.prototype.name = 'default'; 

在这个版本中, BaseObject 继承了原型中的name 属性, 被设置为了 'default'.。这时,如果构造函数被调用时没有参数,则会自动设置为 default。相同地,如果name 属性被从BaseObject移出,系统将会自动寻找原型链,并且获得 'default'值:

 var thirdObj = new BaseObject('unique'); 

 console.log(thirdObj.name);  

 delete thirdObj.name;

 console.log(thirdObj.name);  // -> 结果是 'default' 

常见错误八:为实例方法创建错误的指引

我们来看下面一段代码:

var MyObject = function() {} 

 MyObject.prototype.whoAmI = function() {     

console.log(this === window ? "window" : "MyObj"); }; 

 var obj = new MyObject(); 

现在为了方便起见,我们新建一个变量来指引 whoAmI 方法, 因此我们可以直接用 whoAmI() 而不是更长的obj.whoAmI():

var whoAmI = obj.whoAmI;

接下来为了确保一切都如我们所预测的进行,我们可以将 whoAmI 打印出来:

console.log(whoAmI); 

结果是:

function () {     console.log(this === window ? "window" : "MyObj"); } 

没有错误!

但是现在我们来查看一下两种引用的方法:

obj.whoAmI();  // 输出 "MyObj" (as expected) 

whoAmI();      // 输出 "window" (uh-oh!) 

哪里出错了呢?

原理其实和上面的第二个常见错误一样,当我们执行 var whoAmI = obj.whoAmI;的时候,新的变量 whoAmI 是在全局环境下定义的。因此它的this 是指window, 而不是obj!

正确的编码方式应该是:

var MyObject = function() {}  

MyObject.prototype.whoAmI = function() {     

      console.log(this === window ? "window" : "MyObj"); }; 

var obj = new MyObject(); 

obj.w = obj.whoAmI;   // still in the obj namespace  obj.whoAmI();  // 输出 "MyObj" (as expected) 

obj.w();       // 输出 "MyObj" (as expected) 

常见错误九:用字符串作为setTimeout 或者 setInterval的第一个参数

首先我们要声明,用字符串作为这两个函数的第一个参数并没有什么语法上的错误。但是其实这是一个非常低效的做法。因为从系统的角度来说,当你用字符串的时候,它会被传进构造函数,并且重新调用另一个函数。这样会拖慢程序的进度。

setInterval("logTime()", 1000); 

setTimeout("logMessage('" + msgValue + "')", 1000);

另一种方法是直接将函数作为参数传递进去:

setInterval(logTime, 1000);   

setTimeout(function() { 

logMessage(msgValue); }, 1000); 

常见错误十:忽略 “strict mode”的作用

“strict mode” 是一种更加严格的代码检查机制,并且会让你的代码更加安全。当然,不选择这个模式并不意味着是一个错误,但是使用这个模式可以确保你的代码更加准确无误。

下面我们总结几条“strict mode”的优势:

  1. 让Debug更加容易:在正常模式下很多错误都会被忽视掉,“strict mode”模式会让Debug极致更加严谨。

  2. 防止默认的全局变量:在正常模式下,给一个为经过声明的变量命名将会将这个变量自动设置为全局变量。在strict模式下,我们取消了这个默认机制。

  3. 取消this的默认转换:在正常模式下,给this关键字指引到null或者undefined会让它自动转换为全局。在strict模式下,我们取消了这个默认机制。

  4. 防止重复的变量声明和参数声明:在strict模式下进行重复的变量声明会被抱错,如 (e.g., var object = {foo: "bar", foo: "baz"};) 同时,在函数声明中重复使用同一个参数名称也会报错,如 (e.g., function foo(val1, val2, val1){}),

  5. 让eval()函数更加安全。

  6. 当遇到无效的delete指令的事后报错:delete指令不能对类中未有的属性执行,在正常情况下这种情况只是默默地忽视掉,而在strict模式是会报错的。

结语

正如和其他的技术语言一样,你对JavaScript了解的的越深,知道它是如何运作,为什么这样运作,你才会熟练地掌握并且运用这门语言。相反地,如果你缺少对JS模式的认知的话,你就会碰上很多的问题。了解JS的一些细节上的语法或者功能将会有助于你提高编程的效率,减少变成中遇到的问题。


原文地址: http://www.toptal.com/javascript/10-most-common-javascript-mistakes
译文地址: http://1ke.co/course/136?utm_source=segment&utm_medium=1&utm_c...

async vs defer 属性

$
0
0

<script>元素的async和defer属性支持度已经不错了,是时候详细了解它们了。

图例

719530

<script>

<script>脚本不设置任何属性。HTML文档解析过程中,遇到script文档时,会停止解析HTML文档,发送请求获取script文档(如果是外部文档的话)。脚本执行后,才恢复HTMl文档解析。

936468

<script async>

设置async属性后,在HTML解析的同时,下载script文档。script文档下载完成后,HTMl解析会暂停,来执行script文档。

712163

<script defer>

设置defer属性后,在HTML解析的同时,下载script脚本。但只有在HTML解析完成后,才执行script文档。同时,defer属性保证脚本按照其在文档中出现的顺序执行。

460821

如何选用?

通常情况下,尽可能的使用async属性,然后考虑defer,都不适用时才不设置任何属性。选用规则:

  • 如果脚本是模块化的,并且不依赖其他脚本,那么使用async。
  • 如果脚本依赖其他脚本或被其他脚本依赖,那么使用defer。
  • 如果脚本比较小,并且被一个async脚本依赖,那么使用行内脚本,并放置在async脚本之前。

支持情况

IE9及以下浏览器在实现defer属性上存在糟糕的bug,比如无法保证脚本的执行顺序。如果需要支持<=IE9,不建议使用defer,如果执行顺序非常重要的话,不要使用任何属性。 查看规范

后记

Chrome为了更快的页面加载,引入了两项JavaScript新技术:script streaming 和 code caching。简而言之,前者优化脚本文档的解析(Chrome 41),后者缓存编译后的代码(Chrome 42)。

streaming

ref:

  1. async vs defer attributes
  2. New JavaScript techniques for rapid page loads

我推荐的一些前端开发工具

$
0
0

artTemplate

性能卓越的 js 模板引擎

简洁的模版语法,简单的API,关键还能前后端(Nodejs)共用模板,简直就是前端开发利器。今天有个想法,把 artTemplate封装下,模版 render后给 input等注册几个事件,分分钟就能实现简单的数据双向绑定。

artTemplate

Duo

提供强大的前端静态资源(主要是JS和CSS)构建压缩方案,前身是 Components
其具有丰富的前端 组件库(强烈推荐),生态极其繁荣,依然有很多开发者不断为其贡献组件。

Duo

fastClick

一行代码消除移动短 click事件300ms的延迟,不用过多介绍,只需要这么使用它即可:

$(function() {
    FastClick.attach(document.body);
});

尽情书写 click吧,不用判断移动端使用 touch神马的了~

fastClick

debug.js

手机上看不到 console.log信息?手机检测不到JS报错信息?不用担心,有了 debug.js你就可以做到。

debug.js提供了简洁的API供开发者使用,你可以查看 Demo

当然,如果你想继续使用 console.log,可以这么封装:

if('ontouchend' in window) {
  console.log = debug.log.bind(debug);
}

之后上线时,可以使用 uglify压缩掉所有 console的代码。完美~

debug.js

broadcast.js

broadcast实现了

统一的事件管理中心。

源码非常简单,使用也非常简单,提供两个API:

  • broadcast.on
  • broadcast.fire

简单的功能,会不知不觉成为网页(网站)非常核心的功能,相信如果你真正理解它使用它,你会彻底喜欢上它。

broadcast.js

iconfont.cn

iconfont.cn是由阿里巴巴UX部门推出的矢量图标管理网站。

图标做成字体文件,iconfont.cn是我目前见到的国内最大的图标字体库,由最初的阿里系图标慢慢升级为全图标库,开发者也可以自己上传SVG文件生成iconfont。

iconfont

【译文】Top 10:HTML5、JavaScript 3D游戏引擎和框架

$
0
0

best-3d-javascript-game-engines-frameworks-webgl-html5

由于很多人都在用JavaScript、HTML5和WebGL技术创建基于浏览器的3D游戏,所有JavaScript 3D游戏引擎是一个人们主题。基于浏览器的游戏最棒的地方是平台独立,它们能在iOS、Android、Windows或其他任何平台上运行。

有很多的JavaScript能够用于创建基于浏览器、使用HTML5和WebGL的3D游戏。然后,选择一个合适的游戏引擎是一个不小的挑战,它有时能帮你完成项目或突破项目瓶颈。

为了让你的选择变的容易,我们已经通过分析大多数JavaScript 3D游戏引擎在今天的市场并列出了十大游戏引擎来帮助您用JavaScript创建非常棒的HTML5,WebGL游戏。

1. Babylon.js

毫无疑问,Babylon.JS是最好的JavaScript3D游戏引擎,它能创建可以出售的专业级游戏。
Babylon.JS是David Catuhe对3D游戏引擎热爱的结果,他在用DirectX, OpenGL, 和Silverlight创建3D游戏引擎方面是经验丰富,并最终自己完成了一个游戏引擎。
Babylon.js的一些核心功能包含了场景图与灯光、摄像机、材料和网格、碰撞引擎、物理引擎、音频引擎和优化引擎。
这是一个 Babylon.js试验场,你可以拿你手边的游戏来试试这个游戏引擎。

2.Three.js

Three.js是另一个广泛应用并且功能强大的JavaScript 3D库,从创建简单的3D动画到创建交互的3D游戏, 它都能实现。
Threejs带来的不仅是支持WebGL渲染,也支持SVG、Canvas和CSS3D渲染。然而,从游戏的角度来看,你可以只关注Threejs的WebGL渲染。
获取Three.js 3D引擎的源代码— github

3. Turbulenz

Turbulenz是最好的游戏引擎之一,在2009年,当HTML5和WebGL还在酝酿时,它已经被推出。直到2013年,Turbulenz才基于MIT协议拥抱开源。
Turbulenz包含了很多功能,例如2d物理、3d物理、声音、视频和其他服务,如排行榜、multichat,支付和用户数据。
了解更多: biz.turbulenz.com
获取源代码: turbulenz_engine

4. Famo.us

在HTML5 3D发展的市场中,Famo.us占据了非常重要的地位,并且它是最好的JavaScript 3D开源框架之一。对于famo.us,最好的事情就是包装了3D布局引擎,其完全继承了3D物理驱动的动画引擎。
了解更多— famo.us/docs
获取源代码— famous

5. PlayCanvas.js

PlayCanvas是一个基于 WebGL游戏引擎的企业级开源JavaScript框架,它有许多的开发工具能帮你快速创建3D游戏。PlayCanvas.js由一个专业社区创建,最初并不是开源的,但现在你可以在github上fork PlayCanvas.js,然后在你的下一个3D游戏项目中免费使用。
它还提供了能在浏览器中云心的云编辑器,开始使用PalyCanvas和导航到编辑器的URL一样容易。
了解更多— playcanvas.com
获取源代码— playcanvas/engine

6. Goo Engine

来自于GOO技术家族,Goo引擎有一组功能强大的JavaScript API,可通过使用HTML5和WebGL创建任何3D事物。有一个在线的编辑器goocreate,可以运行在Goo引擎上,并封装了一些功能,例如视觉三维编辑器,材质编辑器、脚本和容易发布选项等。
你可以通过支付一些相关的许可费用,在浏览器中使用在线编辑器。
尽管编辑器需要一些费用,但是Goo引擎是绝对免费的,你可以下载它并在你的3D项目中使用。
了解更多—- labs.gooengine.com
获取源代码— code.gooengine.com

7. CooperLicht

对于创建基于浏览器的游戏,CopperLIcht是最出色的3D引擎之一,也是CopperCube 3D游戏编辑器的后端引擎。
CopperCube是一个支持创建3D游戏和动画所需要的所有功能的编辑器,但是,它并不开源,需要一些相关的许可费用。
了解更多— ambiera.com/copperlicht

8. Voxel.JS

Voxel.Js是开源的,基于JavaScript的一个3D游戏引擎,自从它发布以来,社区成长非常快。如果你喜欢模块化方法,Voxel是一个不错的选择。
Voxel-engine是创建3D游戏的核心模块,其它模块可以根据需要插入。到目前为止,已经有超过200个扩展在npm上发布。
了解更多— maxogden/voxel-engine

9. Blend4Web

在2014年,Blend4Web作为开源的3D框架而发布,它高度集成了Triumph的3D内容创建工具“Blender”,并且本地支持Blender的节点材料、粒子系统、bullet物理引擎和其他功能。
获取源代码— blend4web.com/en/downloads

10. Enchant.js

Enchant.js是一个模块化的、面向对象的JavaScript框架,可用HTML5创建简单的APP和游戏。它是基于MIT协议开源的,因此开源免费使用。3D动画和游戏开源使用额外的插件(基于WebGL)创建。
获取源代码– wise9/enchant.js

本文根据@Nicolas Bevacqua的 《best-3d-javascript-game-engines-frameworks-webgl-html5》所译,整个译文带有我自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: http://noeticforce.com/best-3d-javascript-game-engines-frameworks-webgl-html5

淡忘~浅思猜你喜欢

怎么对HTML 5的特性做检测?

HTML 5:绘制旋转的太极图

【译】利用HTML 5中的Menu和Menuitem元素快速创建菜单

初识HTML 5:关于它的三个三

HTML 5 API的”前生今世”
无觅

转载请注明: 淡忘~浅思» 【译文】Top 10:HTML5、JavaScript 3D游戏引擎和框架


JavaScript 异步机制及应用 入门教程

$
0
0

1. 异步与同步 技术研究

(1). 概念介绍

异步: asynchronous 简写async
同步: synchronous 简写sync

用比方来比喻
异步就是: N个人同时起跑, 起点和出发时间相同, 在起跑时不去关心其他人会啥时候跑完~尼玛这不废话吗?大家都才起跑怎么知道别人多就跑完.
同步就是: N个人接力跑, 起点和出发时间不同, 且后一个人会等待前一个人跑完才能继续跑, 也就是要关心前一个人的结果(上一行代码的返回值).


(2). JS里面的异步/同步

JS运行场景多数是在用户浏览器上, 程序效率优劣会直接影响用户的体验交互. 比如一个网站, 在用户注册时, 会ajax校验输入再发提交表单, 如果用同步就可能会一直卡着等待ajax响应, 好几秒结束后再跳到注册结果页, 这个体验将是非常糟糕的.

说到JS的异步, 不得不提及一个非常有代表意义函数了.

JavaScriptvar url = '/action/';
var data = 'i=1';
xmlHTTP = new XMLHttpRequest();
xmlHTTP.nonce = nonce;
xmlHTTP.open("POST", url);
xmlHTTP.onreadystatechange = function(a) {
    if(a.target.readyState!=4)return false;
    try{
        console.log(a.target.responseText)
    }catch(e){
        return false;
    }
};
xmlHTTP.send(data);

或者在jQuery写作:

JavaScript$.ajax({
    url: '/action/',
    type: 'POST',
    data: 'i=1',
    success: function(responseText){
        console.log(responseText);
    }
})

上面的无论是 xmlHTTP.onreadystatechange, 还是 success, 在JavaScript中均称为 回调方法,
以原生JS的 XMLHttpRequest为例, xmlHTTP变量是个 XMLHttpRequest对象, 他的 onreadystatechange是在每次请求响应状态发生变化时会触发的一个函数/方法, 然后在发出请求 xmlHTTP.send(data)的时候, JS并不会理会 onreadystatechange方法, 而当改送请求到达服务器, 开始响应或者响应状态改变时会调用 onreadystatechange方法:

也就是
1) 请求发出
2) 服务器开始响应数据
3) 执行回调方法, 可能执行多次

以jQuery版为例, $.ajax本身是个函数, 唯一一个参数是{...} 这个对象, 然后回调方法 success是作为这个对象的一个属性传入$.ajax的.
$.ajax()先将数据post到'/action/', 返回结果后再调用 success(如果发生错误会调用 error).
也就是

 1) 请求发出
 2) 服务器开始响应数据
 3) 响应结束执行回调方法

然后作为函数$.ajax, 是函数就应该有返回值(哪怕没有return也会返回undefined), 他本身的返回值是多少呢?
分为 async:trueasync:false两个版本:

async:true版本:

JavaScript$.ajax({'url':'a.html', type:'GET', async:true})> Object {readyState: 1}

async:false版本:

JavaScript$.ajax({'url':'robots.txt', type:'GET', false})> Object {readyState: 4, responseText: "<!DOCTYPE HTML PUBLIC ...", status: 200, statusText: "OK"}

我们可以直接看到, async:true异步模式下, jquery/javascript未将结果返回... 而async:false就将结果返回了.

然后问题就来了, 为什么async:true未返回结果呢?
答案很简单:
因为在返回的时候, 程序不可能知道结果. 异步就是指不用等此操作执行出结果再往下执行, 也就是返回的值中未包含结果.

留下一个问题, 我们是不是为了程序流程的简单化而使用同步呢?


(3). 异步的困惑

先帖一段代码:
a.php

php<?php
sleep(1);      // 休息一秒钟
echo '{}';

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    $.ajax({
    url: 'a.php',
    type: 'POST',
    dataType: 'json',
    data: {data: i},
    async: true,         // 默认即为异步
    success: function(json) {
            console.log(i + ': ' + json); // 打印
        }
    });
}

你们猜猜 打印的那行会最终打印出什么内容?

1: {}
2: {}
3: {}
4: {}

吗?

错!

输出的将是:

4: {}
4: {}
4: {}
4: {}

你TM在逗我?
没有, 这并不是JS的BUG, 也不是jQuery的BUG.
这是因为, PHP休息了一秒, 而js异步地循环从1到4, 远远用不到1秒.
然后在1秒钟后, 才开始返回数据, 触发 success, 此时此刻 i已经自增成了4.
自然而然地, 第一次 console.log(i...)就是4, 第二次也是, 第三次也是, 第四次也是.
那么如果我们希望程序输出也1,2,3,4这样输出怎么办呢?

两种方案:

1) 让后端输出i

a.php

php<?php
sleep(1);
echo '{i: ' . $_POST['data'] . '}'; // 这一行改了

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    $.ajax({
    url: 'a.php',
    type: 'POST',
    dataType: 'json',
    data: {data: i},
    async: true,
    success: function(json) {
            console.log(json.i + ': ' + json); // 这一行改了
        }
    });
}

2) 给回调的事件对象赋属性

a.php

php保持原代码不变

page.js

JavaScriptfor( i = 1; i <= 4; i++ ){
    ajaxObj = $.ajax({          // 将ajax赋给ajaxObj
        url: 'a.php',
        type: 'POST',
        dataType: 'json',
        data: {data: i},
        async: true,
        success: function(json, status, obj) {    // 增加回调参数, jQuery文档有说第三个参数就是ajax方法产生的对象.
                console.log(obj.i + ': ' + json); // 从jQuery.ajax返回的对象中取i
        }
    });
    ajaxObj.i = i;            // 给ajaxObj赋属性i 值为循环的i 
}
有可能你会感到困惑, 为何可以给ajaxObj设置一个i属性然后在回调时用第三个回调参数的i属性呢?

jQuery.ajax文档中写到:

jQuery.ajax( [settings ] )
settings
...
success: Function( Anything data, String textStatus, jqXHR jqXHR )
第1个参数就是响应的文本/HTML/XML/数据/json之类的, 跟你的dataType设置有关
第2个参数就是status状态, 如success
第3个参数就是jqXHR, 也就是jQuery的XMLHttpRequest对象, 当然, 在这里就是$.ajax()生成的对象, 也就是事件的触发者本身, 
给本身设置一个属性(ajaxObj.i = i), 然后再调用本身的回调时, 使用本身的那个属性(obj.i), 当然会保持一致了.

然后
1)输出的结果将是

1: {i:1}
2: {i:2}
3: {i:3}
4: {i:4}

2)输出的结果将是

1: {}
2: {}
3: {}
4: {}

虽然略有区别, 但两者均可达到要求. 若要论代码的逼格, 相信你一定会被第二个方案给震惊的.
凭什么你给ajaxObj赋个属性就可以在 success中用了呢?

请看 (4). 异步的回调机制


(4). 异步的回调机制 ------ 事件

一个有经验的JavaScript程序员一定会将js回调用得得心应手.
因为JavaScript天生异步, 异步的好处是顾及了用户的体验, 但坏处就是导致流程化循环或者递归的逻辑明明在别的语言中无任何问题, 却在js中无法取得期待的值...
而JavaScript异步在设计之初就将这一点考虑到了. 任何流行起来的JS插件方法, 如jQuery的插件, 一定考虑到了这一点了的.

举个例子.

ajaxfileupload插件, 实现原理是将选择的文件$.clone到一个form中, form的target设置成了一个页面中的iframe, 然后定时取iframe的contents().body, 即可获得响应的值.
如果要支持multiple文件上传(一些现代化的浏览器支持), 还是得要用`XMLHttpRequest`

如下面代码:

$('input#file').on('change', function(e){
    for(i = 0; i < e.target.files.length; i++ ){
        var data = new FormData();
        data.append("file", e.target.files[i]);
        xmlHTTP = new XMLHttpRequest();
        xmlHTTP.open("POST", s.url);
        xmlHTTP.onreadystatechange = function(a) { // a 为 事件event对象
            if(a.target.readyState!=4)return false; // a.target为触发这个事件的对象 即xmlHTTP (XMLHttpRequest) 对象
            try{
                console.log(a.target.responseText);
            }catch(e){
                return false;
            }
        };
        xmlHTTP.send(data);
    }
})

你可以很明显地知道, 在 onreadystatechange调用且走到 console.log(a.target.responseText)时, 如果服务器不返回文件名, 我们根本并不知道返回的是哪个文件的URL. 如果根据i去取的话, 那么很容易地, 我们只会取到始终1个或几个, 并不能保证准确.
那么我们应该怎么去保证在 console.log(a.target.responseText)时能知道我信上传的文件的基本信息呢?

$('input#file').on('change', function(e){
    for(i = 0; i < e.target.files.length; i++ ){
        var data = new FormData();
        data.append("file", e.target.files[i]);
        xmlHTTP = new XMLHttpRequest();
        xmlHTTP.file = e.target.files[i];
        xmlHTTP.open("POST", s.url);
        xmlHTTP.onreadystatechange = function(a) {
            if(a.target.readyState!=4)return false;
            try{
                console.log(a.target.file);         //这儿是上面`xmlHTTP.file = e.target.files[i]` 赋进去的
                console.log(a.target.responseText);
            }catch(e){
                return false;
            }
        };
        xmlHTTP.send(data);
    }
})

是不是很简单?


2. 展望

(1). Google对同步JavaScript的态度

在你尝试在chrome打开的页面中执行 async: false的代码时, chrome将会警告你:

Synchronous XMLHttpRequest on the main thread is deprecated because of its detrimental effects to the end user's experience. For more help, check http://xhr.spec.whatwg.org/.

clipboard.png

(2). 职场展望

异步和事件将是JavaScript工程师必备技能

[完]
Reference:

1.《Javascript异步编程的4种方法》     http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
2.《什么是 Event Loop?》            http://www.ruanyifeng.com/blog/2013/10/event_loop.html
3.《JavaScript 运行机制详解:再谈Event Loop》 http://www.ruanyifeng.com/blog/2014/10/event-loop.html

【译】创建优雅表格的8个js工具

$
0
0

当需要呈现数百个表的数据时,展示和可访问性扮演着至关重要的角色。在这种情况下,倘若一个数据网格能够支持大量数据集的HTML Table并提供诸如排序、搜索、过滤和分页等功能,那是棒棒哒。在这篇文章中,将介绍8个用于创建优雅表格的js工具。

SigmaGrid

SigmaGrid是一个开源的Ajax数据表格组件,可以在一个可滚动和可排序的表格中展示和编辑数据。了解更多,戳此: learn more
SigmaGrid

Ingrid

Ingrid是一个低调的JQuery组件,支持数据网格添加行为(列调整、分页、排序和设置列、行的样式等等)了解更多,戳此: learn more
Ingrid

TableKit

TableKit基于Prototype框架,用于增强HTML表格的功能。了解更多,戳此: TableKit
Tablekit

MyTableGrid

MyTableGrid是基于JavaScript的数据表格,并依赖于Prototype。它允许你用一种简单灵活的方式来展现你的数据。了解更多,戳此: MyTableGrid
MyTableGrid

DataTables – Data Table jQuery plugin

DataTables是一个JQuery插件,具有高度的灵活性,添加了高级交互控制。了解更多,戳此: DataTables
DataTables

jTPS – Datatable jQuery Plugin

jTPS提供了分页、动画滚动页面和aa智能自然排序能力,是一个JQuery插件。了解更多,戳此: jTPS
jTPS

SortedTable

SortedTable是另一个数据表格组件,结合了排序、行选择和移动动能。它的排序功能支持不同的数据类型。了解更多,戳此: SortedTable
SortedTable

dhtmlxGrid

dhtmlxGrid是Ajax驱动的网格控件,支持不同的数据格式,如XML、JSON, CSV, HTML table和自定义的XML格式。了解更多,戳此: dhtmlxGrid
dhtmlxGrid

本文根据@Nicolas Bevacqua的 《8 useful js tools for better HTML tables》所译,整个译文带有我自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: http://www.designer-daily.com/8-useful-js-tools-for-better-html-tables-52869/

淡忘~浅思猜你喜欢

js:简单的拖动效果

【译】Impress.js制作酷炫Presentation PPT

问题:关于一个贴友的js留言板的实现

全屏滚动实现:fullPage.js和fullPage

一个js闭包问题的解答
无觅

转载请注明: 淡忘~浅思» 【译】创建优雅表格的8个js工具

Qunit初探

$
0
0

前言

2008年5月, Qunit随JQuery的核心库一起发布,在2009年重构之后,Qunit独立出来,可以用于测试各种Javascript应用和框架,其提供的断言方法遵循了CommonJS维护规范。尽管Qunit能再服务端和命令行运行,但是其主要还是用于测试浏览器端的Javascript。

先看一个简单地测试demo:

  1. <!DOCTYPE html>
  2. <head>
  3. <link rel="stylesheet" href="qunit.css">
  4. <script src="qunit.js"></script>
  5. </head>
  6. <body>
  7. <script>
  8. test("hello", function() {
  9. ok(true, "world");
  10. });
  11. </script>
  12. <h1 id="qunit-header">QUnit Hello World</h1>
  13. <h2 id="qunit-banner"></h2>
  14. <ol id="qunit-tests"></ol>
  15. </body>
  16. </html>

先从 这里下载对应的js和css文件。Qunit提供的 test()方法定义一个为”hello”的测试,第二个参数是传递给 test()的函数,里面才是真正的测试代码,示例中调用 ok()方法,该方法第一个参数是一个布尔值,用于判断测试是否通过,第二个是测试不通过时的输出消息。Qunit会在页面加载完后运行,上面运行后截图如下:

  1. test("hello", function() {
  2. ok(false, "test faild");
  3. });

ok()期待返回一个True,若返回false,测试不会通过:

测试结果中给出了具体的测试结果。和 test()类似的另一个方法是 asyncTest(),后者用于异步测试。

Writing QUnit Tests

给一个稍复杂一点的demo:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>QUnit Test</title>
  5. <link rel="stylesheet" href="qunit.css">
  6. <script src="qunit.js"></script>
  7. <script src="tests.js"></script>
  8. </head>
  9. <body>
  10. <h1 id="qunit-header">QUnit Test</h1>
  11. <h2 id="qunit-banner"></h2>
  12. <div id="qunit-testrunner-toolbar"></div>
  13. <h2 id="qunit-userAgent"></h2>
  14. <ol id="qunit-tests"></ol>
  15. <div id="qunit-fixture">test markup</div>
  16. </body>
  17. </html>

tests.js的文件内容如下:

  1. function format(string, values) {
  2. for (var key in values) {
  3. string = string.replace(new RegExp("\{" + key + "}"), values[key]);
  4. }
  5. return string;
  6. }
  7. test("basics", function() {
  8. var values = {
  9. name: "World"
  10. };
  11. equal( format("Hello, {name}", values), "Hello, World", "single use" );
  12. equal( format("Hello, {name}, how is {name} today?", values),
  13. "Hello, World, how is World today?", "multiple" );
  14. });

这个demo创建一个basics的测试,用于检测 format()方法能否返回我们期待的值。 format()的功能就是模板HTML替换技术,MVC的基础之一。

这里利用 equal()进行断言,它会使用’==’比较传入的第一个(函数的返回值)和第二个(期待的值)参数,第三个参数是输出信息。测试截图如下:

正如我们看到的,对于多行的匹配, format()的函数是有bug的。当然,这个bug也是很容易修复的:

  1. new RegExp("\{" + key + "}", "g")

The Assertion Methods of QUnit

在上面的两个demo中,已经用过两个断言方法: ok()equal()。此外,Qunit提供了很多断言:

  1. deepEqual(value, expected[, message]):跟 equal()类似,但是是使用 ===进行更严格的比较。
  2. notDeepEqual(value, expected[, message]): deepEqual()的反操作
  3. notEqual(value, expected[, message]): equal()的反操作
  4. propEqual(value, expected[, message]):对对象的属性和值进行严格比较,只有当所有 valueexpected严格相等时,测试才会通过。
  5. strictEqual(value, expected[, message]):验证被提供的 valueexpected参数是否严格相等。
  6. notPropEqual(value, expected[, message]): propEqual的反操作
  7. notStrictEqual(value, expected[, message]): strictEqual的反操作
  8. throws(function [, expected ] [, message ]):测试是否有毁掉函数抛出异常,并选择性的抛出错误。

需要说明的是,上述方法中的value是一个函数、方法的返回值,或已经声明的变量的值;expected是期望值;message则是断言的简短描述;function则是一个执行函数,并应该返回一个错误。

看一个demo示例吧:

  1. var App = {
  2. max: function() {
  3. var max = -Infinity;
  4. for (var i = 0; i < arguments.length; i++) {
  5. if (arguments[i] > max) {
  6. max = arguments[i];
  7. }
  8. }
  9. return max;
  10. },
  11. isOdd: function(number) {
  12. return number % 2 !== 0;
  13. },
  14. sortObj: function(array) {
  15. array.sort(function(a, b) {
  16. var date1 = new Date(a.timestamp).getTime();
  17. var date2 = new Date(b.timestamp).getTime();
  18. if (date1 < date2) {
  19. return -1;
  20. } else if (date1 === date2) {
  21. return 0;
  22. } else {
  23. return 1;
  24. }
  25. });
  26. }
  27. };

对象 App包含了三个方法:max、isOdd和sortObj。 sortObj()接受一个数组对象,理想情况下,该数组对象应该有一个 timestamp属性,并基于该属性进行排序。

为了测试这三个方法,一个可能测试集如下:

  1. QUnit.test('max', function (assert) {
  2. assert.strictEqual(App.max(), -Infinity, 'No parameters');
  3. assert.strictEqual(App.max(3, 1, 2), 3, 'All positive numbers');
  4. assert.strictEqual(App.max(-10, 5, 3, 99), 99, 'Positive and negative numbers');
  5. assert.strictEqual(App.max(-14, -22, -5), -5, 'All positive numbers');
  6. });
  7. QUnit.test('isOdd', function (assert) {
  8. assert.ok(App.isOdd(5), '5 is odd');
  9. assert.ok(!App.isOdd(2), '5 is not odd');
  10. assert.ok(!App.isOdd(0), '0 is not odd');
  11. assert.throws(function () {
  12. App.isOdd(null);
  13. },
  14. /The given argument is not a number/,
  15. 'Passing null raises an Error');
  16. assert.throws(function () {
  17. App.isOdd([]);
  18. },
  19. new Error('The given argument is not a number'),
  20. 'Passing an array raises an Error');
  21. });
  22. QUnit.test('sortObj', function (assert) {
  23. var timestamp = Date.now();
  24. var array = [{
  25. id: 1,
  26. timestamp: timestamp
  27. }, {
  28. id: 3,
  29. timestamp: timestamp + 1000
  30. }, {
  31. id: 11,
  32. timestamp: timestamp - 1000
  33. }];
  34. App.sortObj(array);
  35. assert.propEqual(array, [{
  36. id: 11,
  37. timestamp: timestamp - 1000
  38. }, {
  39. id: 1,
  40. timestamp: timestamp
  41. }, {
  42. id: 3,
  43. timestamp: timestamp + 1000
  44. }]);
  45. assert.notPropEqual(App.sortObj(array), array, 'sortObj() does not return an array');
  46. assert.strictEqual(App.sortObj(array), undefined, 'sortObj() returns
  47. });

测试结果如下:

完整地列表戳此: Assert

Module

在DOM操作中,如果清除多于重置,我们可以将其作为测试的一部分。如果有多个测试需要做同样地清除工作,可以用 module()进行重构。 module()最初是为分组测试设计的,例如,多个测试都测试一个特殊的方法,该方法就可以作为单个模块的一部分。为了让测试更加颗粒化,我们可以使用 module()提供的 setupteardown回调。

  1. module("core", {
  2. setup: function() {
  3. // runs before each test
  4. },
  5. teardown: function() {
  6. // runs after each test
  7. }
  8. });
  9. test("basics", function() {
  10. // test something
  11. });

setup会在 test之前运行,而 teardown则在 test之后运行。
我们可以使用这些回调创建对象并在测试中使用,而无需依靠闭包(或全局变量)传给测试。之所以会起作用,是因为 setupteardown以及真实的测试都在一个自定义的可以自动共享和清除的作用域中调用。

下面是一个用于处理货币值的简单demo(不完整):

  1. var Money = function(options) {
  2. this.amount = options.amount || 0;
  3. this.template = options.template || "{symbol}{amount}";
  4. this.symbol = options.symbol || "$";
  5. };
  6. Money.prototype = {
  7. add: function(toAdd) {
  8. this.amount += toAdd;
  9. },
  10. toString: function() {
  11. return this.template
  12. .replace("{symbol}", this.symbol)
  13. .replace("{amount}", this.amount)
  14. }
  15. };
  16. Money.euro = function(amount) {
  17. return new Money({
  18. amount: amount,
  19. template: "{amount} {symbol}",
  20. symbol: "EUR"
  21. });
  22. };

上面的代码创建了Money对象,还有一个为创建美元(dollars)而提供的默认构造函数,为创建欧元(euros)而提供的工厂方法以及两个用于操作和打印的方法。在测试中,不是为每一个单元测试创建Money对象,而是使用 setup回调创建对象并存储在测试作用域中。

  1. module("Money", {
  2. setup: function() {
  3. this.dollar = new Money({
  4. amount: 15.5
  5. });
  6. this.euro = Money.euro(14.5);
  7. },
  8. teardown: function() {
  9. // could use this.dollar and this.euro for cleanup
  10. }
  11. });
  12. test("add", function() {
  13. equal( this.dollar.amount, 15.5 );
  14. this.dollar.add(16.1)
  15. equal( this.dollar.amount, 31.6 );
  16. });
  17. test("toString", function() {
  18. equal( this.dollar.toString(), "$15.5" );
  19. equal( this.euro.toString(), "14.5 EUR" );
  20. });

上代码中, setup回调中创建了两个对象,名叫”add”的测试之使用了一个,”toString”这个测试两个对象都使用了。本例中, teardown回调没必要使用,因为没必要要移除已经创建的Money对象。结果如下截图:

Testing asynchronous code

可以看到,只要代码是同步的,Qunit就能控制什么时候运行测试代码。然而,一旦测试的代码需要使用异步回调(如 setTimeoutAjax请求),你需要给QUnit反馈,以便停止后面的测试,知道你让它再次运行。

利用 stop()start()就能实现Qunit反馈,进行异步测试。看demo:

  1. test("async", function() {
  2. stop();
  3. $.getJSON("resource", function(result) {
  4. deepEqual(result, {
  5. status: "ok"
  6. });
  7. start();
  8. });
  9. });

$.getJSON会去请求”resource”数据,然后判断比较结果。因为 $.getJSON是异步请求,先调用 stop(),随后运行代码,再在 callback结束的时候调用 start(), 告诉QUnit继续运行。

运行异步代码没有调用 stop(),让Qunit停止运行,则会导致本意是其他测试的任意结果,如passed或failed。

正如前文说到得, asyncTest可以代替 test用于异步测试,并且不用调用 stop:

  1. asyncTest("async2", function() {
  2. $.getJSON("resource", function(result) {
  3. deepEqual(result, {
  4. status: "ok"
  5. });
  6. start();
  7. });
  8. });

如果测试的结束点多个-多个回调的顺序随机-我们可以使用QUnit内置信号。调用 stop()与之后调用的 start()同数目,则Qunit会继续运行直到 stop()增加的计数被 start()减少会0.

  1. test("async semaphore", function() {
  2. stop();
  3. stop();
  4. $.getJSON("resource", function(result) {
  5. equal(result.status, "ok");
  6. start();
  7. });
  8. $.getJSON("resource", function(result) {
  9. equal(result.status, "ok");
  10. start();
  11. });
  12. });

Setting Expectations

测试回调的时候,无论是不是异步,我们都不能确定回调会在某些时候被真正调用了。因而,最好的实践是设置一个我们期望的判断个数,这样一来,若一个或多个断言没有被执行,测试就会失败。Qunit提供了 expect()方法来达到这个目的。

  1. expect(assertionsNumber)

了解了这个概念后,之前针对App字面量对象的测试可以写成这样:

  1. QUnit.test('max', function(assert) {
  2. expect(4);
  3. // Assertions here...
  4. });
  5. QUnit.test('isOdd', function(assert) {
  6. expect(5);
  7. // Assertions here...
  8. });
  9. QUnit.test('sortObj', function(assert) {
  10. expect(3);
  11. // Assertions here...
  12. });

戳此看 demo
expect()貌似有个取巧的设置方法,就是给 test()asyncTest()的第二个参数传递一个数字:

  1. asyncTest("async4", 1, function() {
  2. $.getJSON("resource", function(result) {
  3. deepEqual(result, {
  4. status: "ok"
  5. });
  6. start();
  7. });
  8. });

相关文章:
Qunit VS jasmine VS mocha
Automating JavaScript Testing with QUnit
Getting Started with QUnit
QUnit API
边译边学-QUnit下的JavaScript自动化单元测试

淡忘~浅思猜你喜欢

Ubuntu下安装XAMPP

【译】CSS:7个你可能不认识的单位

Linux与Windows的8个不同

画图解释 SQL join 语句

一点思考和新学年目标
无觅

转载请注明: 淡忘~浅思» Qunit初探

Handlebars.js初探

$
0
0

引言

Handlebars是 JavaScript 一个语义模板库,通过对view和data的分离来快速构建Web模板。利用Handlebars处理HTML模板时,一般步骤如下:
1. 获取模板内容
2. 预编译模板
3. 模板数据填充
4. 将结果追加到页面中

下图是html模板被渲染后的结果:

有两种方式来实现这个小小的demo

1.直接在HTML中渲染

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>demo2</title>
  6. <script type="text/javascript" src="./jquery.js"></script>
  7. <script type="text/javascript" src="handlebars.js"></script>
  8. </head>
  9. <body>
  10. <div class="demo">
  11. <div class="tpldemo">
  12. <h1 style="color:{{color}}">{{demo1}}</h1>
  13. <p>{{desc}}</p>
  14. </div>
  15. </div>
  16. <script type="text/javascript">
  17. var context = {
  18. "demo1": "this is a demo",
  19. "desc": "study Handlebars",
  20. "color": "red"
  21. };
  22. //1. 获取模板内容
  23. var tpl = $(".tpldemo").html();
  24. //2. 预编译模板
  25. var template = Handlebars.compile(tpl);
  26. //3. 模板数据填充
  27. var html = template(context);
  28. //4. 将结果追加到页面中
  29. $(".tpldemo").html(html);
  30. </script>
  31. </body>
  32. </html>

2.借用 <script>标签

  1. <div class="demo">
  2. <script id="tpl" type="text/x-handlebars-template">
  3. <div class="tpldemo">
  4. <h1 style="color:{{color}}">{{demo1}}</h1>
  5. <p>{{desc}}</p>
  6. </div>
  7. </script>
  8. </div>
  9. <script type="text/javascript">
  10. var context = {
  11. "demo1": "this is a demo",
  12. "desc": "study Handlebars",
  13. "color": "red"
  14. };
  15. //1. 获取模板内容
  16. var tpl = $("#tpl").html();
  17. //2. 预编译模板
  18. var template = Handlebars.compile(tpl);
  19. //3. 模板数据填充
  20. var html = template(context);
  21. //4. 将结果追加到页面中
  22. $(".demo").html(html);
  23. </script>

需要注意的是,数据名要保持一致,即 {{name}}中的 name要和 context提供的名称保持一致。

基本循环:echo

echo指令可以用于循环json数据对象的属性

  1. //引入插件
  2. <script type="text/javascript" src="./jquery.js"></script>
  3. <script type="text/javascript" src="handlebars.js"></script>
  4. //模板
  5. <script id="table-template" type="text/x-handlebars-template">
  6. {{#each student}}
  7. <tr>
  8. <td>{{name}}</td>
  9. <td>{{sex}}</td>
  10. <td>{{age}}</td>
  11. </tr>
  12. {{/each}}
  13. </script>
  14. //数据处理
  15. <script type="text/javascript">
  16. $(document).ready(function() {
  17. //模拟的json对象
  18. var data = {
  19. "student": [
  20. {
  21. "name": "张三",
  22. "sex": "0",
  23. "age": 18
  24. },
  25. {
  26. "name": "李四",
  27. "sex": "0",
  28. "age": 22
  29. },
  30. {
  31. "name": "妞妞",
  32. "sex": "1",
  33. "age": 18
  34. }
  35. ]
  36. };
  37. //注册一个Handlebars模版,通过id找到某一个模版,获取模版的html框架
  38. var myTemplate = Handlebars.compile($("#table-template").html());
  39. //将json对象用刚刚注册的Handlebars模版封装,得到最终的html,插入到基础table中。
  40. $('#tableList').html(myTemplate(data));
  41. });
  42. </script>

指定上下文:with

with指令可以转移上下文环境,让当前的上下文进入到一个属性中。

  1. //引入插件
  2. <script type="text/javascript" src="./jquery.js"></script>
  3. <script type="text/javascript" src="handlebars.js"></script>
  4. //模板
  5. <script id="table-template" type="text/x-handlebars-template">
  6. {{#each this}}
  7. <tr>
  8. <td>{{name}}</td>
  9. <td>{{sex}}</td>
  10. <td>{{age}}</td>
  11. <td>
  12. {{#with favorite}}
  13. {{#each this}}
  14. <p>{{name}}</p>
  15. {{/each}}
  16. {{/with}}
  17. </td>
  18. </tr>
  19. {{/each}}
  20. </script>
  21. //数据处理
  22. //模拟的json对象
  23. var data = [
  24. {
  25. "name": "张三",
  26. "sex": "0",
  27. "age": 18,
  28. "favorite":
  29. [
  30. {
  31. "name":"唱歌"
  32. },{
  33. "name":"篮球"
  34. }
  35. ]
  36. },
  37. {
  38. "name": "李四",
  39. "sex": "0",
  40. "age": 22,
  41. "favorite":
  42. [
  43. {
  44. "name":"上网"
  45. },{
  46. "name":"足球"
  47. }
  48. ]
  49. },
  50. ];
  51. //注册一个Handlebars模版,通过id找到某一个模版,获取模版的html框架
  52. var myTemplate = Handlebars.compile($("#table-template").html());
  53. //将json对象用刚刚注册的Handlebars模版封装,得到最终的html,插入到基础table中。
  54. $('#tableList').html(myTemplate(data));
  55. });

{{#with favorite}}表示进入到 favorite属性的上下文中,而 favorite属性中又是一个list,因此可以用 {{#each this}}进行遍历,表示遍历当前上下文环境,对于每次遍历,都是map结构,取name属性,最终拿到所有兴趣爱好。

with可以结合handlebars的路径访问一起使用。Handlebars提供了 .来访问属性也可以使用 ../来访问父级属性。

  1. {{#with person}}
  2. <h1>{{../company.name}}</h1>
  3. {{/with}}
  4. //对应的json数据
  5. {
  6. "person":
  7. { "name": "Alan" },
  8. company:
  9. {"name": "Rad, Inc." }
  10. }

this的使用

this表示当前的上下文,在上述示例中已经在 with中使用了 this,当然,也是可以在 each中使用的。在 with中使用 this还有一种方式:

  1. <script id="table-template" type="text/x-handlebars-template">
  2. {{#each this}}
  3. <tr>
  4. <td>{{name}}</td>
  5. <td>{{sex}}</td>
  6. <td>{{age}}</td>
  7. <td>
  8. {{#with favorite}}
  9. {{#each this}}
  10. <p>{{this}}</p>
  11. {{/each}}
  12. {{/with}}
  13. </td>
  14. </tr>
  15. {{/each}}
  16. </script>
  17. //数据处理
  18. <script type="text/javascript">
  19. $(document).ready(function() {
  20. //模拟的json对象
  21. var data = [
  22. {
  23. "name": "张三",
  24. "sex": "0",
  25. "age": 18,
  26. "favorite":
  27. [
  28. "唱歌",
  29. "篮球"
  30. ]
  31. },
  32. {
  33. "name": "李四",
  34. "sex": "0",
  35. "age": 22,
  36. "favorite":
  37. [
  38. "上网",
  39. "足球"
  40. ]
  41. },
  42. {
  43. "name": "妞妞",
  44. "sex": "1",
  45. "age": 18,
  46. "favorite":
  47. [
  48. "电影",
  49. "旅游"
  50. ]
  51. }
  52. ];
  53. //注册一个Handlebars模版,通过id找到某一个模版,获取模版的html框架
  54. var myTemplate = Handlebars.compile($("#table-template").html());
  55. //将json对象用刚刚注册的Handlebars模版封装,得到最终的html,插入到基础table中。
  56. $('#tableList').html(myTemplate(data));
  57. });
  58. </script>

本例和上例不同之处在于 favorite属性中不再是map项,而是普通字符串,因此对于每个项,可以直接用 {{this}}读取, this代表当前字符串。

条件判断:if、unless

if指令可以根据当前上下文是否存在来执行制定的数据处理,常配合 else指令。

  1. //模板
  2. {{#if list}}
  3. <ul id="list">
  4. {{#each list}}
  5. <li>{{this}}</li>
  6. {{/each}}
  7. </ul>
  8. {{else}}
  9. <p>{{error}}</p>
  10. {{/if}}
  11. //对应的json
  12. var data = {
  13. info:['HTML5','CSS3',"WebGL"],
  14. "error":"数据取出错误"
  15. }

这里 {{#if}}判断是否存在 list数组,如果存在则遍历list,如果不存在输出错误信息。

对于 if指令,如果返回的为 undefinednull""[]false任意一个,都会导致最终结果为假。

if指令支持嵌套,如: {{#if name.xxx}},这样写就假设name属性是一个map,检测name属性中是否包含xxx属性。

unless则是和 if指令相反,当判断的值为 false时他会渲染DOM :

  1. {{#unless data}}
  2. <ul id="list">
  3. {{#each list}}
  4. <li>{{this}}</li>
  5. {{/each}}
  6. </ul>
  7. {{else}}
  8. <p>{{error}}</p>
  9. {{/unless}}

自定义Helper

对于复杂的逻辑判断, if指令不能完成时,我们可以自定义Helper。

Handlebars.registerHelper用来定义Helper,它有两个参数,第一个参数是Helper名称,第二个参数是一个回调函数,用来执行核心业务逻辑。

在函数级Helper,回调函数的参数就是模板传来的参数。

  1. <script id="shoe-template" type="x-handlebars-template">​
  2. {{theNameOfTheHelper score}}
  3. ​</script>
  4. //数据处理
  5. var contextObj = {score:85, userName:"Mike"}; //模拟的数据
  6. Handlebars.registerHelper ("theNameOfTheHelper", function (theScore) {
  7. console.log("Grade: " + theScore );
  8. var userGrade = "C";
  9. if (theScore >= 90) {
  10. return "A" ;
  11. }
  12. else if (theScore >= 80 && theScore < 90) {
  13. return "B" ;
  14. }
  15. else if (theScore >= 70 && theScore < 80) {
  16. return "C" ;
  17. }
  18. else {
  19. return "D" ;
  20. }
  21. });

将上下文中读取到的 score作为参数传递给回调函数,回调函数的返回结果会作为模板的填充数据。该示例最终会在html中输出B。

但是在定义块级Helper时,Handlebars会自动在回调函数的最后一个参数加上 options对象。 options对象有一个 fn()inverse()方法一hi两个方法以及一个 hash对象。

The options.fn method:
The fn method takes an object (your data) as a parameter that it uses as the context inside the custom helper block template. You can pass any data object, or if you want to use the same data context referenced in the template, you can use this.

The options.inverse method:
The inverse method is used as the else section of any block statement. So, you would use options.fn to return when the expression in the callback evaluates to a truthy value. But you would use options.inverse when the expression evaluates to falsy (to render content in the else part of the block).

The options.hash object:
Handlebars expressions take not only strings and variables as arguments, but you can pass key-value pairs separated by spaces as well.

  1. <script id="table-template" type="text/x-handlebars-template">
  2. {{#each student}}
  3. {{#if name}}
  4. {{#compare age 20}}
  5. <tr>
  6. <td>{{name}}</td>
  7. <td>{{sex}}</td>
  8. <td>{{age}}</td>
  9. </tr>
  10. {{else}}
  11. <tr>
  12. <td>?</td>
  13. <td>?</td>
  14. <td>?</td>
  15. </tr>
  16. {{/compare}}
  17. {{/if}}
  18. {{/each}}
  19. </script>
  20. //数据处理
  21. //模拟的json对象
  22. var data = {
  23. "student": [
  24. {
  25. "name": "张三",
  26. "sex": "0",
  27. "age": 23
  28. },
  29. {
  30. "sex": "0",
  31. "age": 22
  32. },
  33. {
  34. "name": "妞妞",
  35. "sex": "1",
  36. "age": 18
  37. }
  38. ]
  39. };
  40. //注册一个Handlebars模版,通过id找到某一个模版,获取模版的html框架
  41. var myTemplate = Handlebars.compile($("#table-template").html());
  42. //注册一个比较大小的Helper,判断v1是否大于v2
  43. Handlebars.registerHelper("compare",function(v1,v2,options){
  44. if(v1>v2){
  45. //满足添加继续执行
  46. return options.fn(this);
  47. }else{
  48. //不满足条件执行{{else}}部分
  49. return options.inverse(this);
  50. }
  51. });
  52. //将json对象用刚刚注册的Handlebars模版封装,得到最终的html,插入到基础table中。
  53. $('#tableList').html(myTemplate(data));

其中的age就是当前上下文中读取到的年龄,20是随便写的值,意思是只要age比20大,就显示,否则全显示?。

但是如果传递给函数的是一个空格分开的键值对,则会保存包 options.hash中。

  1. {{#myNewHelper score=30 firstName="Jhonny" lastName="Marco"}}​
  2. Show your HTML content here.
  3. {{/myNewHelper}}
  4. Handlebars.registerHelper ("myNewHelper", function (dataObject, options) {
  5. ​//JSON.stringify used to serialize the object (to a string)​
  6. console.log(JSON.stringify (options.hash));
  7. ​//The output is: {score:30, firstName:"Jhonny", lastName:"Marco"}​
  8. });

HTML编码

在handlebars里, {{expression}}会返回一个经过编码的HTML,也就是说,如果取出的内容中包含html标签,会被转码成纯文本,不会被当成html解析,实际上就是做了类似这样的操作:把 <&lt;替代。如果需要解析html,不要转码,可以使用 {{{

  1. //模板
  2. <script id="table-template" type="text/x-handlebars-template">
  3. {{#each student}}
  4. <tr>
  5. <td>{{name}}</td>
  6. <td>{{sex}}</td>
  7. <td>{{age}}</td>
  8. {{#compare age 20}}
  9. <td>{{homePage}}</td>
  10. {{else}}
  11. <td>{{{homePage}}}</td>
  12. {{/compare}}
  13. </tr>
  14. {{/each}}
  15. </script>
  16. //模拟的json对象
  17. var data = {
  18. "student": [
  19. {
  20. "name": "张三",
  21. "sex": "0",
  22. "age": 18,
  23. "homePage":"<a href='javascript:void(0);'>张三的个人主页</a>"
  24. },
  25. {
  26. "name": "李四",
  27. "sex": "0",
  28. "age": 22,
  29. "homePage":"<a href='javascript:void(0);'>李四的个人主页</a>"
  30. },
  31. {
  32. "name": "妞妞",
  33. "sex": "1",
  34. "age": 19,
  35. "homePage":"<a href='javascript:void(0);'>妞妞的个人主页</a>"
  36. }
  37. ]
  38. };
  39. //注册一个Handlebars模版,通过id找到某一个模版,获取模版的html框架
  40. var myTemplate = Handlebars.compile($("#table-template").html());
  41. //注册一个比较数字大小的Helper,有options参数,块级Helper
  42. Handlebars.registerHelper("compare",function(v1,v2,options){
  43. //判断v1是否比v2大
  44. if(v1>v2){
  45. //继续执行
  46. return options.fn(this);
  47. }else{
  48. //执行else部分
  49. return options.inverse(this);
  50. }
  51. });
  52. //将json对象用刚刚注册的Handlebars模版封装,得到最终的html,插入到基础table中。
  53. $('#tableList').html(myTemplate(data));

本例中,年龄大于20的童鞋个人主页被编码,直接展示出来;而年龄小于20的童鞋,个人主页被当成html解析,显示的是一个超链接。

handlebars不会编码 Handlebars.SafeString。如果你自定义一个helper,返回一段HTML代码,你需要返回 new Handlebars.SafeString(result)。此时,你需要手动对内容进行编码:

  1. Handlebars.registerHelper('link', function(text, url) {
  2. text = Handlebars.Utils.escapeExpression(text);
  3. url = Handlebars.Utils.escapeExpression(url);
  4. var result = '<a href="' + url + '">' + text + '</a>';
  5. return new Handlebars.SafeString(result);
  6. });

文章参考:

handlebars-js-tutorial-learn-everything-about-handlebars-js-javascript-templating

Handlebars.js 模板引擎

Handlebars.js 中文文档

淡忘~浅思猜你喜欢

js:简单的拖动效果

【译】Impress.js制作酷炫Presentation PPT

一个js闭包问题的解答

问题:关于一个贴友的js留言板的实现

DOM笔记(八):JavaScript执行环境和垃圾收集
无觅

转载请注明: 淡忘~浅思» Handlebars.js初探

浏览器事件模型中捕获阶段、目标阶段、冒泡阶段实例详解

$
0
0

如果对事件大概了解,可能知道有事件冒泡这回事,但是冒泡、捕获、传播这些机制可能还没有深入的研究实践一下,我抽时间整理了一下相关的知识。

  • 本文主要对事件机制一些细节进行讨论,过于基础的事件绑定知识方法没有介绍。

  • 特别少的篇幅关注浏览器兼容问题,毕竟原理了解了,兼容性问题可以自己想办法解决了。

在浏览器相对标准化之前,各个浏览器厂商都是自己实现的事件模型,有的用了冒泡,有的用了捕获,W3C为了兼顾之前的标准,将事件发生定义成如下三个阶段:

1、捕获阶段
2、目标阶段
3、冒泡阶段

只是硬生生的说事件机制到底是怎么回事不容易理解,用一个demo为主线说明事件的原理比较容易理解:

HTML

<body><div id="wrapDiv">wrapDiv<p id="innerP">innerP<span id="textSpan">textSpan</span></p></div></body>

CSS

<style>
    #wrapDiv, #innerP, #textSpan{
        margin: 5px;
        padding: 5px;
        box-sizing: border-box;
        cursor: default;
    }
    #wrapDiv{
        width: 300px;
        height: 300px;
        border: indianred 3px solid;
    }
    #innerP{
        width: 200px;
        height: 200px;
        border: hotpink 3px solid;
    }
    #textSpan{
        display: block;
        width: 100px;
        height: 100px;
        border: orange 3px solid;
    }</style>

JavaScript

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);</script>

demo页面效果图
图片描述

这个时候,如果点击一下 textSpan这个元素,控制台会打印出这样的内容:
图片描述

当按下鼠标点击后,到底发生了什么的,现在我基于上面的例子来说一下:

capture=>start: 捕获阶段
window=>operation: window
document=>operation: document
documentElement=>operation: documentElement
body=>operation: body
wrapDiv=>operation: wrapDiv
innerP=>operation: innerP
target=>start: 目标阶段
textSpan=>operation: textSpan
textSpan2=>operation: textSpan
bubble=>start: 冒泡阶段
innerP2=>operation: innerP
wrapDiv2=>operation: wrapDiv
body2=>operation: body
documentElement2=>operation: documentElement
document2=>operation: document
window2=>operation: window
capture->window->document->documentElement->body->wrapDiv->innerP->target->textSpan->textSpan2->bubble->innerP2->wrapDiv2->body2->documentElement2->document2->window2

从上面所画的事件传播的过程能够看出来,当点击鼠标后,会先发生事件的捕获

  • 捕获阶段:首先 window会获捕获到事件,之后 documentdocumentElementbody会捕获到,再之后就是在body中DOM元素一层一层的捕获到事件,有 wrapDivinnerP

  • 目标阶段:真正点击的元素 textSpan的事件发生了两次,因为在上面的JavaScript代码中, textSapn既在 捕获阶段绑定了事件,又在 冒泡阶段绑定了事件,所以发生了两次。但是这里有一点是需要注意,在目标阶段并不一定先发生在捕获阶段所绑定的事件,而是先绑定的事件发生,一会会解释一下。

  • 冒泡阶段:会和捕获阶段相反的步骤将事件一步一步的冒泡到 window

那可能有一个疑问,我们不用 addEventListener绑定的事件会发生在哪个阶段呢,我们来一个测试,顺便再演示一下我在上面的目标阶段所说的 目标阶段并不一定先发生捕获阶段所绑定的事件是怎么一回事。
我们重新改一下 JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);</script>

再看控制台的结果:
图片描述

  • 图中第一个被圈出来的解释: textSpan是被点击的元素,也就是目标元素,所有在 textSpan上绑定的事件都会发生在 目标阶段,在绑定捕获代码之前写了绑定的冒泡阶段的代码,所以在 目标元素上就不会遵守先发生捕获后发生冒泡这一规则,而是先绑定的事件先发生。

  • 图中第二个被圈出来的解释:由于 wrapDiv不是目标元素,所以它上面绑定的事件会遵守先发生捕获后发生冒泡的规则。所以很明显用 onclick直接绑定的事件发生在了冒泡阶段。

target和currentTarget

上面的代码中写了 e.targete.currentTarget,还没有说是什么, targetcurrentTarget都是 event上面的属性, target是真正发生事件的DOM元素,而 currentTarget是当前事件发生在哪个DOM元素上。
可以结合控制台打印出来的信息理解下, 目标阶段也就是 target == currentTarget的时候。我没有打印它们两个因为太长了,所以打印了它们的 nodeName,但是由于 window没有 nodeName这个属性,所以是 undefined

阻止事件传播

说到事件,一定要说的是如何阻止事件传播。总是有很多帖子说 e.stopPropagation()是阻止事件的冒泡的传播,实际上这么说并不是很准确,因为它不仅可以阻止事件在冒泡阶段的传播,还能阻止事件在捕获阶段的传播。
来看一下我们再改一下的JavaScript代码:

<script>
    var wrapDiv = document.getElementById("wrapDiv");
    var innerP = document.getElementById("innerP");
    var textSpan = document.getElementById("textSpan");

    // 测试直接绑定的事件到底发生在哪个阶段
    wrapDiv.onclick = function(){
        console.log("wrapDiv onclick 测试直接绑定的事件到底发生在哪个阶段")
    };

    // 捕获阶段绑定事件
    window.addEventListener("click", function(e){
        console.log("window 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.addEventListener("click", function(e){
        console.log("document 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    document.body.addEventListener("click", function(e){
        console.log("body 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 捕获", e.target.nodeName, e.currentTarget.nodeName);
        // 在捕获阶段阻止事件的传播
        e.stopPropagation();
    }, true);

    innerP.addEventListener("click", function(e){
        console.log("innerP 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    textSpan.addEventListener("click", function(){
        console.log("textSpan 冒泡 在捕获之前绑定的")
    }, false);

    textSpan.onclick = function(){
        console.log("textSpan onclick")
    };

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 捕获", e.target.nodeName, e.currentTarget.nodeName);
    }, true);

    // 冒泡阶段绑定的事件
    window.addEventListener("click", function(e){
        console.log("window 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.addEventListener("click", function(e){
        console.log("document 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.documentElement.addEventListener("click", function(e){
        console.log("documentElement 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    document.body.addEventListener("click", function(e){
        console.log("body 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    wrapDiv.addEventListener("click", function(e){
        console.log("wrapDiv 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    innerP.addEventListener("click", function(e){
        console.log("innerP 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);

    textSpan.addEventListener("click", function(e){
        console.log("textSpan 冒泡", e.target.nodeName, e.currentTarget.nodeName);
    }, false);</script>

我们在事件的捕获阶段阻止了传播,看一下控制台的结果:
图片描述
实际上我们点击的是 textSpan,但是由于在捕获阶段事件就被阻止了传播,所以在 textSpan上绑定的事件根本就没有发生,冒泡阶段绑定的事件自然也不会发生,因为阻止事件在捕获阶段传播的特性, e.stopPropagation()很少用到在捕获阶段去阻止事件的传播,大家就以为 e.stopPropagation()只能阻止事件在冒泡阶段传播。

阻止事件的默认行为

e.preventDefault()可以阻止事件的默认行为发生,默认行为是指:点击a标签就转跳到其他页面、拖拽一个图片到浏览器会自动打开、点击表单的提交按钮会提交表单等等,因为有的时候我们并不希望发生这些事情,所以需要阻止默认行为,这块的知识比较简单,可以自己去试一下。

与事件相关的兼容性问题

这里只是简单提一下兼容性问题,不做过多的展开。对于绑定事件,ie低版本的浏览器是用 attachEvent,而高版本ie和标准浏览器用的是 addEventListenerattachEvent不能指定绑定事件发生在捕获阶段还是冒泡阶段,它只能将事件绑定到冒泡阶段,但是并不意味这低版本的ie没有事件捕获,它也是先发生事件捕获,在发生事件冒泡,只不过这个过程无法通过程序控制。

其实事件的兼容性问题特别的多,比如获取事件对象的方式、绑定和解除绑定事件的方式、目标元素的获取方式等等,由于古老的浏览器终究会被淘汰,不过多展开了。

使用Benchmark.js和jsPerf分析代码性能

$
0
0

前言

前端开发中,掌握好浏览器的特性进行有针对性的性能调优是一项基本工作,同时,比较不同代码的执行速度也是一项关键的工作。

比如,当我们想比较 RegExptest方法和 String对象的 indexOf方法查找字符串谁的速度更快的话, js代码在不同的浏览器,不同的操作系统环境运行的效率可能是不一样的,这就是为什么我们需要对其进行基准测试,在做基准测试方面,我们可以使用 Benchmark.js和使用 jsPerf(一个基于 JSLitmus的基准测试库)。我们可以使用 jsPerf来分享你的基准测试。

Benchmark.js 的使用

github 地址: https://github.com/bestiejs/benchmark.js

我们在很多 github开源项目中,往往都能看到 benchmark文件夹,比如下面这个:

图片描述

于是 Google之,发现这是用来做基准测试的。于是乎:

首先我们在系统根目录下,通过 npm intsall benchmark来安装 benchmark。该模块会被写入 node_modules文件夹中,我们在 test.js文件中通过 require方法引入该模块。

将如下代码写入 test.js文件,该文件置于系统根目录下:

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;

// 添加测试
suite.add('RegExp#test', function() {
    /o/.test('Hello World!');
})
    .add('String#indexOf', function() {'Hello World!'.indexOf('o') > -1;
    })
// add listeners
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').pluck('name'));
    })
// run async
    .run({ 'async': true });

然后在终端执行 node test.js可见输出结果如下:

➜  ~ git:(master) ✗ node test.js
RegExp#test x 9,847,928 ops/sec ±1.47% (83 runs sampled)
String#indexOf x 23,366,017 ops/sec ±0.91% (96 runs sampled)
Fastest is String#indexOf

结果最快的就是 String对象的 indexOf方法,其中, Ops/sec测试结果以每秒钟执行测试代码的次数( Ops/sec)显示,这个数值越大越好。除了这个结果外,同时会显示测试过程中的统计误差,以及相对最好的慢了多少(%)

call和apply的比较

var Benchmark = require('benchmark');
var suite = new Benchmark.Suite;
var arr1 = function (str) {
    return [].slice.apply(str);
};
var str2 = function (str) {
    return [].slice.call(str);
};
// 添加测试
suite.add('arr1', function() {
    arr1('test');
})
    .add('str2', function() {
        str2('test');
    })
// add listeners
    .on('cycle', function(event) {
        console.log(String(event.target));
    })
    .on('complete', function() {
        console.log('Fastest is ' + this.filter('fastest').pluck('name'));
    })
// run async
    .run({ 'async': true });

输出如下内容:

arr1 x 596,505 ops/sec ±1.14% (95 runs sampled)
str2 x 627,822 ops/sec ±1.27% (92 runs sampled)
Fastest is str2

jsPerf 的使用

jsPerf提供了一个简便的方式来创建和共享测试用例,并可以比较不同 JavaScript代码段的性能。 jsPerf也是基于 Benchmark来运行的。

打开 jsPerf站点: http://jsperf.com/,先将必填的项目填了。其中, slug是短名称,会生成一个网址,因此不可与别人的重复。然后在 Code snippets to compare区域填入 title和用于测试的code。最后点击 save test case完成验证即可。浏览器会自动跳转到测试页面

Async选项框是用来测试一些异步调用的性能的,我们的代码没有使用异步方法,所以不必勾选。

运行测试

点击“ Run tests”按钮开始测试两种算法的性能。建议在运行性能测试之前,关闭无关的浏览器页面,关闭其他程序,退出不必要的后台进程,以保证结果不受其他环境的影响。你也可以通过点击个别测试用例的名字单独运行这个例子

点击该链接查看性能比较: http://jsperf.com/huang

jsPerf还会统计所有运行过这个测试用例的浏览器的比较结果,显示在下方的 Browserscope区域,可以通过它直观地看出各个版本浏览器的性能横向和纵向比较情况。

图片描述

可以看到 Firefox下的执行速度明显高于 Chrome

查看别人的测试用例

我们可以通过 http://jsperf.com/browse浏览最新提交的250项最新测试用例。我们也可以使用底部的 Revisions来查看不同的版本,也就是不同浏览器的测试用例情况。

总结

John Resig在其博文 JavaScript 基准测试的质量中提到,应该尽量考虑到每个测试结果的误差并去减小它。扩大测试的样本值,健全的测试执行,都能够起到减少误差的作用。

javaScript跨浏览器事件处理程序

$
0
0

最近在阅读 javascript高级程序设计,事件这一块还是有很多东西要学的,就把一些思考和总结记录下。
在事件处理,事件对象,阻止事件的传播等方法或对象存在着浏览器兼容性问题,开发过程中最好编写成一个通用的事件处理工具。

(function(){
    var EU = {};
    //...
    //在这里添加一些通用的事件处理方法
    //...
    window.EventUtil = EU;
})();

事件处理程序

事件的绑定主要为IE8以下浏览器做兼容处理:

IE8以下只支持事件冒泡
IE事件处理使用 attachEventdetachEvent

绑定事件

EU.addHandler = function(element,type,handler){
    //DOM2级事件处理,IE9也支持
    if(element.addEventListener){
        element.addEventListener(type,handler,false);
    }
    else if(element.attachEvent){
        //type加'on'
        //IE9也可以这样绑定
        element.attachEvent('on' + type,handler);
    }
    //DOM0级事件处理步,事件流也是冒泡
    else{
        element['on' + type] = handler;
    }
};

取消绑定事件

和绑定事件的处理基本一致,有一个注意点:

传入的 handler必须与绑定事件时传入的相同(指向同一个函数)

EU.removeHandler = function(element,type,handler){
    if(element.removeEventListener){
        element.removeEventListener(type,handler);
    }
    else if(element.attachEvent){
       element.detachEvent('on' + type,handler);
    }
    else{
        //属性置空就可以
        element['on' + type] = null;
    }
};

事件对象

注意点:

IE下 event是全局对象,通过 window.event取得

EU.getEvent = function(event){
    return event || window.event;
}

事件的目标

注意点:

IE下通过 attachEvent绑定事件,内部 this并非触发事件的 DOM,而是window;
通过目标对象来获取 DOM节点,IE下是 srcElement属性,等同于其他浏览器的 target属性

EU.addTarget = function(event){
    return event.target || event.srcElement;
}

阻止默认事件

EU.preventDefault = function(event){
    if(event.preventDefault){
        event.preventDefault();
    }
    //IE下处理
    else{
        event.returnValue = false; //默认为true
    }
}

关于 事件默认行为

阻止事件传播

EU.stopPropagation = function(event){
    if(event.stopPropagation){
        event.stopPropagation();
    }
    //IE下处理
    else{
        event.cancelBubble = true;//默认为false,注意区分于returnValue
    }
}

注意点:

阻止的是 DOM层级间的事件传播

比如:对于一个DOM元素,同时绑定捕获事件与冒泡事件,如果在捕获阶段使用 stopPropagation,不会阻断冒泡事件的执行;

Demo地址: http://runjs.cn/detail/hyrdjfyj

如果对子元素和父元素以冒泡形式都绑定'click'事件,在子元素的事件处理中使用 stopPropagation阻止事件传播,父元素绑定的 click事件不会执行。
Demo地址: http://runjs.cn/detail/sf0t1bso


前端性能优化指南

$
0
0

前端性能优化指南

AJAX优化

  • 缓存 AJAX

    • 异步并不等于 即时

  • 请求使用 GET

    • 当使用 XMLHttpRequest时,而URL长度不到 2K,可以使用 GET请求数据, GET相比 POST更快速。

      • POST类型请求要发送两个 TCP数据包。

        • 先发送文件头。

        • 再发送数据。

      • GET类型请求只需要发送一个 TCP数据包。

        • 取决于你的 cookie数量。

COOKIE专题

  • 减少 COOKIE的大小。

  • 使用无 COOKIE的域。

    • 比如图片 CSS等静态文件放在静态资源服务器上并配置单独域名,客户端请求静态文件的时候,减少 COOKIE反复传输时对主域名的影响。

DOM优化

  • 优化节点修改。

    • 使用 cloneNode在外部更新节点然后再通过 replace与原始节点互换。

  • 优化节点添加
    >多个节点插入操作,即使在外面设置节点的元素和风格再插入,由于多个节点还是会引发多次reflow。

    • 优化的方法是创建 DocumentFragment,在其中插入节点后再添加到页面。

      • JQuery中所有的添加节点的操作如 append,都是最终调用 DocumentFragment来实现的,

            createSafeFragment(document) {
                 var list = nodeNames.split( "|" ),
                     safeFrag = document.createDocumentFragment();
  • 优化 CSS样式转换。
    >如果需要动态更改CSS样式,尽量采用触发reflow次数较少的方式。

    • 如以下代码逐条更改元素的几何属性,理论上会触发多次 reflow

          element.style.fontWeight = 'bold' ;
          element.style.marginLeft= '30px' ;
          element.style.marginRight = '30px' ;
    • 可以通过直接设置元素的 className直接设置,只会触发一次 reflow

  • 减少 DOM元素数量

    • console中执行命令查看 DOM元素数量。

          `document.getElementsByTagName( '*' ).length`
    • 正常页面的 DOM元素数量一般不应该超过 1000

    • DOM元素过多会使 DOM元素查询效率,样式表匹配效率降低,是页面性能最主要的瓶颈之一。

  • DOM操作优化。

    • DOM操作性能问题主要有以下原因。

      • DOM元素过多导致元素定位缓慢。

      • 大量的 DOM接口调用。

        • JAVASCRIPTDOM之间的交互需要通过函数 API接口来完成,造成延时,尤其是在循环语句中。

      • DOM操作触发频繁的 reflow(layout)repaint

      • layout发生在 repaint之前,所以layout相对来说会造成更多性能损耗。

        • reflow(layout)就是计算页面元素的几何信息。

        • repaint就是绘制页面元素。

      • DOM进行操作会导致浏览器执行回流 reflow

    • 解决方案。

      • JAVASCRIPT执行时间是很短的。

      • 最小化 DOM访问次数,尽可能在js端执行。

      • 如果需要多次访问某个 DOM节点,请使用局部变量存储对它的引用。

      • 谨慎处理 HTML集合( HTML集合实时连系底层文档),把集合的长度缓存到一个变量中,并在迭代中使用它,如果需要经常操作集合,建议把它拷贝到一个数组中。

      • 如果可能的话,使用速度更快的API,比如 querySelectorAllfirstElementChild

      • 要留意重绘和重排。

      • 批量修改样式时, 离线操作 DOM树。

      • 使用缓存,并减少访问布局的次数。

      • 动画中使用绝对定位,使用拖放代理。

      • 使用事件委托来减少事件处理器的数量。

  • 优化 DOM交互
    >在 JAVASCRIPT中, DOM操作和交互要消耗大量时间,因为它们往往需要重新渲染整个页面或者某一个部分。

    • 最小化 现场更新

      • 当需要访问的 DOM部分已经已经被渲染为页面中的一部分,那么 DOM操作和交互的过程就是再进行一次 现场更新

        • 现场更新是需要针对 现场(相关显示页面的部分结构)立即进行更新,每一个更改(不管是插入单个字符还是移除整个片段),都有一个性能损耗。

        • 现场更新进行的越多,代码完成执行所花的时间也越长。

    • 多使用 innerHTML

      • 有两种在页面上创建 DOM节点的方法:

        • 使用诸如 createElement()appendChild()之类的 DOM方法。

        • 使用 innerHTML

          • 当使用 innerHTML设置为某个值时,后台会创建一个 HTML解释器,然后使用内部的 DOM调用来创建 DOM结构,而非基于 JAVASCRIPTDOM调用。由于内部方法是编译好的而非解释执行,故执行的更快。

  • 回流 reflow

    • 发生场景。

      • 改变窗体大小。

      • 更改字体。

      • 添加移除stylesheet块。

      • 内容改变哪怕是输入框输入文字。

      • CSS虚类被触发如 :hover。

      • 更改元素的className。

      • 当对DOM节点执行新增或者删除操作或内容更改时。

      • 动态设置一个style样式时(比如element.style.width="10px")。

      • 当获取一个必须经过计算的尺寸值时,比如访问offsetWidth、clientHeight或者其他需要经过计算的CSS值。

    • 解决问题的关键,就是限制通过DOM操作所引发回流的次数。

      • 在对当前DOM进行操作之前,尽可能多的做一些准备工作,保证N次创建,1次写入。

      • 在对DOM操作之前,把要操作的元素,先从当前DOM结构中删除:

        • 通过removeChild()或者replaceChild()实现真正意义上的删除。

        • 设置该元素的display样式为“none”。

      • 每次修改元素的style属性都会触发回流操作。

        
                element.style.backgroundColor = "blue";
        • 使用更改 className的方式替换 style.xxx=xxx的方式。

        • 使用 style.cssText = '';一次写入样式。

        • 避免设置过多的行内样式。

        • 添加的结构外元素尽量设置它们的位置为 fixedabsolute

        • 避免使用表格来布局。

        • 避免在 CSS中使用 JavaScript expressions(IE only)

      • 将获取的 DOM数据缓存起来。这种方法,对获取那些会触发回流操作的属性(比如 offsetWidth等)尤为重要。

      • 当对HTMLCollection对象进行操作时,应该将访问的次数尽可能的降至最低,最简单的,你可以将length属性缓存在一个本地变量中,这样就能大幅度的提高循环的效率。

eval优化

  • 避免 eval

    • eval会在时间方面带来一些效率,但也有很多缺点。

      • eval会导致代码看起来更脏。

      • eval会需要消耗大量时间。

      • eval会逃过大多数压缩工具的压缩。

HTML优化

  • 插入 HTML

    • JavaScript中使用 document.write生成页面内容会效率较低,可以找一个容器元素,比如指定一个 div,并使用 innerHTML来将 HTML代码插入到页面中。

  • 避免空的 srchref

    • link标签的 href属性为空、 script标签的 src属性为空的时候,浏览器渲染的时候会把当前页面的 URL作为它们的属性值,从而把页面的内容加载进来作为它们的值。

  • 为文件头指定 Expires

    • 使内容具有缓存性,避免了接下来的页面访问中不必要的HTTP请求。

  • 重构HTML,把重要内容的优先级提高。

  • Post-load(次要加载)不是必须的资源。

  • 利用预加载优化资源。

  • 合理架构,使DOM结构尽量简单。

  • 利用 LocalStorage合理缓存资源。

  • 尽量避免CSS表达式和滤镜。

  • 尝试使用defer方式加载Js脚本。

  • 新特性:will-change,把即将发生的改变预先告诉浏览器。

  • 新特性Beacon,不堵塞队列的异步数据发送。

  • 不同之处:网络缓慢,缓存更小,不令人满意的浏览器处理机制。

  • 尽量多地缓存文件。

  • 使用HTML5 Web Workers来允许多线程工作。

  • 为不同的Viewports设置不同大小的Content。

  • 正确设置可Tap的目标的大小。

  • 使用响应式图片。

  • 支持新接口协议(如HTTP2)。

  • 未来的缓存离线机制:Service Workers。

  • 未来的资源优化Resource Hints(preconnect, preload, 和prerender)。

  • 使用Server-sent Events。

  • 设置一个Meta Viewport。

JITGC优化

  • untyped(无类型)。

    • JAVASCRIPT是个无类型的语言,这导致了如 x=y+z这种表达式可以有很多含义。

      • yz是数字,则 +表示加法。

      • yz是字符串,则 +表示字符串连接。

        而JS引擎内部则使用“ 细粒度”的类型,比如:

      • 32-bit* integer。

      1. 64-bit* floating-point。

        这就要求js类型-js引擎类型,需要做“boxed/unboxed(装箱/解箱)”,在处理一次 x=y+z这种计算,需要经过的步骤如下。

      2. 从内存,读取 x=y+z的操作符。

      3. 从内存,读取 yz

      4. 检查y,z类型,确定操作的行为。

      5. unbox y,z

      6. 执行操作符的行为。

      7. box x

      8. x写入内存。

        只有第 5步骤是真正有效的操作,其他步骤都是为第 5步骤做准备/收尾, JAVASCRIPTuntyped特性很好用,但也为此付出了很大的性能代价。

  • JIT

    • 先看看 JITuntyped的优化,在 JIT下,执行 x=y+z流程。

      1. 从内存,读取 x=y+z的操作符。

      2. 从内存,读取 yz

      3. 检查 yz类型,确定操作的行为。

      4. unbox y,z

      5. 执行 操作符 的行为。

      6. box x

      7. x写入内存。

    • 新引擎还对“对象属性”访问做了优化,解决方案叫 inline caching,简称: IC。简单的说,就是做 cache。但如果当 list很大时,这种方案反而影响效率。

  • Type-specializing JIT
    > Type-specializing JIT引擎用来处理 typed类型(声明类型)变量,但 JAVASCRIPT都是 untype类型的。

    • Type-specializing JIT的解决方案是:

      • 先通过扫描,监测类型。

      • 通过编译优化(优化对象不仅仅只是“类型”,还包括对JS代码的优化,但核心是类型优化),生成类型变量。

      • 再做后续计算。

    • Type-specializing JIT的执行 x=y+z流程:

        • 从内存,读取 x=y+z的操作符。

        • 从内存,读取 yz

        • 检查 yz类型,确定操作的行为。

        • unbox y,z

        • 执行操作符的行为。

        • box x

        • x写入内存。

          代价是:
          
      • 前置的扫描类型

      • 编译优化。

        所以·Type-specializing JIT·的应用是有选择性,选择使用这个引擎的场景包括:
      • 热点代码。

      • 通过启发式算法估算出来的有价值的代码。

        另外,有2点也需要注意:
      • 当变量类型 发生变化时,引擎有2种处理方式:

        • 少量变更,重编译,再执行。

        • 大量变更,交给JIT执行。

      • 数组object properties, 闭包变量 不在优化范畴之列。

js载入优化

  • 加快JavaScript装入速度的工具:

    • Lab.js

      • 借助LAB.js(装入和阻止JavaScript),你就可以并行装入JavaScript文件,加快总的装入过程。此外,你还可以为需要装入的脚本设置某个顺序,那样就能确保依赖关系的完整性。此外,开发者声称其网站上的速度提升了2倍。

  • 使用适当的CDN:

    • 现在许多网页使用内容分发网络(CDN)。它可以改进你的缓存机制,因为每个人都可以使用它。它还能为你节省一些带宽。你很容易使用ping检测或使用Firebug调试那些服务器,以便搞清可以从哪些方面加快数据的速度。选择CDN时,要照顾到你网站那些访客的位置。记得尽可能使用公共存储库。

  • 网页末尾装入JavaScript:

    • 也可以在头部分放置需要装入的一些JavaScript,但是前提是它以异步方式装入。

  • 异步装入跟踪代码:
    >脚本加载与解析会阻塞HTML渲染,可以通过异步加载方式来避免渲染阻塞,步加载的方式很多,比较通用的方法如下。

       var _gaq = _gaq || []; 
           _gaq.push(['_setAccount', 'UA-XXXXXXX-XX']); 
           _gaq.push(['_trackPageview']); 
       (function() { 
           var ga = document.createElement('script'); ga.type = 'text/JavaScript'; ga.async = true; 
           ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 
           var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 
       })();

或者

    function loadjs (script_filename){
         var script = document.createElement( 'script' );
         script.setAttribute( 'type' , 'text/javascript' );
         script.setAttribute( 'src' , script_filename);
         script.setAttribute( 'id' , 'script-id' );

         scriptElement = document.getElementById( 'script-id' );
         if (scriptElement){
             document.getElementsByTagName( 'head' )[0].removeChild(scriptElement);
         }
         document.getElementsByTagName( 'head' )[0].appendChild(script);
    }
    var script = 'scripts/alert.js' ;
    loadjs(script);
  • 把你的JavaScript打包成PNG文件

    • 将JavaScript/css数据打包成PNG文件。之后进行拆包,只要使用画布API的getImageData()。可以在不缩小数据的情况下,多压缩35%左右。而且是无损压缩,对比较庞大的脚本来说,在图片指向画布、读取像素的过程中,你会觉得有“一段”装入时间。

  • 设置Cache-Control和Expires头

    通过Cache-Control和Expires头可以将脚本文件缓存在客户端或者代理服务器上,可以减少脚本下载的时间。
    >
    Expires格式:
    >

       Expires = "Expires" ":" HTTP-date
       Expires: Thu, 01 Dec 1994 16:00:00 GMT
       Note: if a response includes a Cache-Control field with the max-age directive that directive overrides the
       Expires field.

    >
    Cache-Control格式:
    >

       Cache-Control   = "Cache-Control" ":" 1#cache-directive
       Cache-Control: public

具体的标准定义可以参考http1.1中的定义,简单来说Expires控制过期时间是多久,Cache-Control控制什么地方可以缓存 。

with优化

  • 尽可能地少用 with语句,因为它会增加 with语句以外的数据的访问代价。

  • 避免使用 with

    >
    `with`语句将一个新的可变对象推入作用域链的头部,函数的所有局部变量现在处于第二个作用域链对象中,从而使局部变量的访问代价提高。
    

变量专题

  • 全局变量

    • 当一个变量被定义在全局作用域中,默认情况下 JAVASCRIPT引擎就不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。

    • 全局变量缺点。

      • 使变量不易被回收。

        • 多人协作时容易产生混淆。

      • 在作用域链中容易被干扰。

    • 可以通过包装函数来处理 全局变量

  • 局部变量。

    • 尽量选用局部变量而不是全局变量。

    • 局部变量的访问速度要比全局变量的访问速度更快,因为全局变量其实是 window对象的成员,而局部变量是放在函数的栈里的。

  • 手工解除变量引用

    • 在业务代码中,一个变量已经确定不再需要了,那么就可以手工解除变量引用,以使其被回收。

  • 变量查找优化。

    • 变量声明带上 var,如果声明变量忘记了 var,那么 JAVASCRIPT引擎将会遍历整个作用域查找这个变量,结果不管找到与否,都会造成性能损耗。

      • 如果在上级作用域找到了这个变量,上级作用域变量的内容将被无声的改写,导致莫名奇妙的错误发生。

      • 如果在上级作用域没有找到该变量,这个变量将自动被声明为全局变量,然而却都找不到这个全局变量的定义。

    • 慎用全局变量。

      • 全局变量需要搜索更长的作用域链。

      • 全局变量的生命周期比局部变量长,不利于内存释放。

      • 过多的全局变量容易造成混淆,增大产生bug的可能性。
        >

    • 具有相同作用域变量通过一个var声明。

            jQuery.extend = jQuery.fn.extend = function () {
                var options, 
                    name, 
                    src, 
                    copy, 
                    copyIsArray, 
                    clone,target = arguments[0] || {},
                    i = 1,
                    length = arguments.length,
                    deep = false ;
            }
    • 缓存重复使用的全局变量。

      • 全局变量要比局部变量需要搜索的作用域长

      • 重复调用的方法也可以通过局部缓存来提速

      • 该项优化在IE上体现比较明显

  • 善用回调。

    • 除了使用闭包进行内部变量访问,我们还可以使用现在十分流行的回调函数来进行业务处理。

            function getData(callback) {
              var data = 'some big data';
              callback(null, data);
            }
            getData(function(err, data) {
              console.log(data);
            });
      • 回调函数是一种后续传递风格( Continuation Passing Style, CPS)的技术,这种风格的程序编写将函数的业务重点从返回值转移到回调函数中去。而且其相比闭包的好处也有很多。

        • 如果传入的参数是基础类型(如字符串、数值),回调函数中传入的形参就会是复制值,业务代码使用完毕以后,更容易被回收。

        • 通过回调,我们除了可以完成同步的请求外,还可以用在异步编程中,这也就是现在非常流行的一种编写风格。

        • 回调函数自身通常也是临时的匿名函数,一旦请求函数执行完毕,回调函数自身的引用就会被解除,自身也得到回收。

常规优化

  • 传递方法取代方法字符串

    一些方法例如 setTimeout()setInterval(),接受 字符串或者 方法实例作为参数。直接传递方法对象作为参数来避免对字符串的二次解析。

      • 传递方法

           setTimeout(test, 1);
    • 传递方法字符串

  • 使用原始操作代替方法调用

    方法调用一般封装了原始操作,在性能要求高的逻辑中,可以使用原始操作代替方法调用来提高性能。

    • 原始操作

          var min = a<b?a:b;
    • 方法实例

  • 定时器

    如果针对的是不断运行的代码,不应该使用 setTimeout,而应该是用 setIntervalsetTimeout每次要重新设置一个定时器。

  • 避免双重解释

JAVASCRIPT代码想解析 JAVASCRIPT代码时就会存在双重解释惩罚,双重解释一般在使用 eval函数、 new Function构造函数和 setTimeout传一个字符串时等情况下会遇到,如。

    eval("alert('hello world');");
    var sayHi = new Function("alert('hello world');");
    setTimeout("alert('hello world');", 100);

> 上述 alert('hello world');语句包含在字符串中,即在JS代码运行的同时必须新启运一个解析器来解析新的代码,而实例化一个新的解析器有很大的性能损耗。

我们看看下面的例子:

    var sum, num1 = 1, num2 = 2;
    /**效率低**/
    for(var i = 0; i < 10000; i++){
        var func = new Function("sum+=num1;num1+=num2;num2++;");
        func();
        //eval("sum+=num1;num1+=num2;num2++;");
    }
    /**效率高**/
    for(var i = 0; i < 10000; i++){
        sum+=num1;
        num1+=num2;
        num2++;
    }

第一种情况我们是使用了new Function来进行双重解释,而第二种是避免了双重解释。

  • 原生方法更快

    • 只要有可能,使用原生方法而不是自已用JS重写。原生方法是用诸如C/C++之类的编译型语言写出来的,要比JS的快多了。

  • 最小化语句数

    JS代码中的语句数量也会影响所执行的操作的速度,完成多个操作的单个语句要比完成单个操作的多个语句块快。故要找出可以组合在一起的语句,以减来整体的执行时间。这里列举几种模式

    • 多个变量声明

            /**不提倡**/
            var i = 1;
            var j = "hello";
            var arr = [1,2,3];
            var now = new Date();
            /**提倡**/
            var i = 1,
                j = "hello",
                arr = [1,2,3],
                now = new Date();
    • 插入迭代值

            /**不提倡**/
            var name = values[i];
            i++;
            /**提倡**/
            var name = values[i++];
      
    • 使用数组和对象字面量,避免使用构造函数Array(),Object()

            /**不提倡**/
            var a = new Array();
            a[0] = 1;
            a[1] = "hello";
            a[2] = 45;
            var o = new Obejct();
            o.name = "bill";
            o.age = 13;
            /**提倡**/
            var a = [1, "hello", 45];
            var o = {
                name : "bill",
                age : 13
            };
  • 避免使用属性访问方法

    • JavaScript不需要属性访问方法,因为所有的属性都是外部可见的。

    • 添加属性访问方法只是增加了一层重定向 ,对于访问控制没有意义。

      使用属性访问方法示例
      
           function Car() {
              this .m_tireSize = 17;
              this .m_maxSpeed = 250;
              this .GetTireSize = Car_get_tireSize;
              this .SetTireSize = Car_put_tireSize;
           }
      
           function Car_get_tireSize() {
              return this .m_tireSize;
           }
      
           function Car_put_tireSize(value) {
              this .m_tireSize = value;
           }
           var ooCar = new Car();
           var iTireSize = ooCar.GetTireSize();
           ooCar.SetTireSize(iTireSize + 1);
      
      直接访问属性示例
      
           function Car() {
              this .m_tireSize = 17;
              this .m_maxSpeed = 250;
           }
           var perfCar = new Car();
           var iTireSize = perfCar.m_tireSize;
           perfCar.m_tireSize = iTireSize + 1;
      
  • 减少使用元素位置操作

    • 一般浏览器都会使用增量reflow的方式将需要reflow的操作积累到一定程度然后再一起触发,但是如果脚本中要获取以下属性,那么积累的reflow将会马上执行,已得到准确的位置信息。

代码压缩

  • 代码压缩工具

    > 精简代码就是将代码中的 空格注释去除,也有更进一步的会对变量名称 混淆精简。根据统计精简后文件大小会平均减少 21%,即使 Gzip之后文件也会减少 5%

    • YUICompressor

    • Dean Edwards Packer

    • JSMin

    • GZip压缩

      • GZip缩短在浏览器和服务器之间传送数据的时间,缩短时间后得到标题是 Accept-Encoding: gzip, deflate的一个文件。不过这种压缩方法同样也有缺点。

        • 它在服务器端和客户端都要占用处理器资源(以便压缩和解压缩)。

        • 占用磁盘空间。

      • Gzip通常可以减少70%网页内容的大小,包括脚本、样式表、图片等任何一个文本类型的响应,包括 XMLJSONGzipdeflate更高效,主流服务器都有相应的压缩支持模块。

      • Gzip的工作流程为

        • 客户端在请求 Accept-Encoding中声明可以支持 Gzip

        • 服务器将请求文档压缩,并在 Content-Encoding中声明该回复为 Gzip格式。

        • 客户端收到之后按照 Gzip解压缩。

    • Closure compiler

代码优化

  • 优化原则:

  • JS优化总是出现在大规模循环的地方:

    这倒不是说循环本身有性能问题,而是循环会迅速放大可能存在的性能问题,所以第二原则就是以大规模循环体为最主要优化对象。
    

    以下的优化原则,只在大规模循环中才有意义,在循环体之外做此类优化基本上是没有意义的。

    目前绝大多数JS引擎都是解释执行的,而解释执行的情况下,在所有操作中,函数调用的效率是较低的。此外,过深的prototype继承链或者多级引用也会降低效率。JScript中,10级引用的开销大体是一次空函数调用开销的1/2。这两者的开销都远远大于简单操作(如四则运算)。

  • 尽量避免过多的引用层级和不必要的多次方法调用:

    特别要注意的是,有些情况下看似是属性访问,实际上是方法调用。例如所有DOM的属性,实际上都是方法。在遍历一个NodeList的时候,循环 条件对于nodes.length的访问,看似属性读取,实际上是等价于函数调用的。而且IE DOM的实现上,childNodes.length每次是要通过内部遍历重新计数的。(My god,但是这是真的!因为我测过,childNodes.length的访问时间与childNodes.length的值成正比!)这非常耗费。所以 预先把nodes.length保存到js变量,当然可以提高遍历的性能。
    

    同样是函数调用,用户自定义函数的效率又远远低于语言内建函数,因为后者是对引擎本地方法的包装,而引擎通常是c,c++,java写的。进一步,同样的功能,语言内建构造的开销通常又比内建函数调用要效率高,因为前者在JS代码的parse阶段就可以确定和优化。

  • 尽量使用语言本身的构造和内建函数:

动画优化

  • 动画效果在缺少硬件加速支持的情况下反应缓慢,例如手机客户端。

    • 特效应该只在确实能改善用户体验时才使用,而不应用于炫耀或者弥补功能与可用性上的缺陷。

    • 至少要给用户一个选择可以禁用动画效果。

    • 设置动画元素为absolute或fixed。

      • position: staticposition: relative元素应用动画效果会造成频繁的 reflow

      • position: absoluteposition: fixed的元素应用动画效果只需要 repaint

    • 使用一个 timer完成多个元素动画。

      • setIntervalsetTimeout是两个常用的实现动画的接口,用以间隔更新元素的风格与布局。。

    • 动画效果的帧率最优化的情况是使用一个 timer完成多个对象的动画效果,其原因在于多个 timer的调用本身就会损耗一定性能。

            setInterval(function() {
              animateFirst('');
            }, 10);
            setInterval(function() {
              animateSecond('');
            }, 10);
      
       使用同一个`timer`。
      
  • 以脚本为基础的动画,由浏览器控制动画的更新频率。

对象专题

  • 减少不必要的对象创建:

    • 创建对象本身对性能影响并不大,但由于 JAVASCRIPT的垃圾回收调度算法,导致随着对象个数的增加,性能会开始严重下降(复杂度 O(n^2))。

      • 如常见的字符串拼接问题,单纯的多次创建字符串对象其实根本不是降低性能的主要原因,而是是在对象创建期间的无谓的垃圾回收的开销。而 Array.join的方式,不会创建中间字符串对象,因此就减少了垃圾回收的开销。

    • 复杂的 JAVASCRIPT对象,其创建时时间和空间的开销都很大,应该尽量考虑采用缓存。

    • 尽量作用 JSON格式来创建对象,而不是 var obj=new Object()方法。前者是直接复制,而后者需要调用构造器。

  • 对象查找

    • 避免对象的嵌套查询,因为 JAVASCRIPT的解释性, a.b.c.d.e嵌套对象,需要进行 4次查询,嵌套的对象成员会明显影响性能。

    • 如果出现嵌套对象,可以利用局部变量,把它放入一个临时的地方进行查询。

  • 对象属性

    • 访问对象属性消耗性能过程( JAVASCRIPT对象存储)。

      • 先从本地变量表找到 对象

      • 然后遍历 属性

      • 如果在 当前对象属性列表里没找到。

      • 继续从 prototype向上查找。

      • 且不能直接索引,只能遍历。

服务端优化

  • 避免404。

    • 更改404错误响应页面可以改进用户体验,但是同样也会浪费服务器资源。

    • 指向外部 JAVASCRIPT的链接出现问题并返回404代码。

      • 这种加载会破坏并行加载。

      • 其次浏览器会把试图在返回的404响应内容中找到可能有用的部分当作JavaScript代码来执行。

  • 删除重复的 JAVASCRIPTCSS

    • 重复调用脚本缺点。

      • 增加额外的HTTP请求。

      • 多次运算也会浪费时间。在IE和Firefox中不管脚本是否可缓存,它们都存在重复运算 JAVASCRIPT的问题。

  • ETags配置 Entity标签。

    • ETags用来判断浏览器缓存里的元素是否和原来服务器上的一致。

      • last-modified date相比更灵活。

  • 权衡DNS查找次数

    • 减少主机名可以节省响应时间。但同时也会减少页面中并行下载的数量。

      • IE浏览器在同一时刻只能从同一域名下载两个文件。当在一个页面显示多张图片时, IE用户的图片下载速度就会受到影响。

  • 通过Keep-alive机制减少TCP连接。

  • 通过CDN减少延时。

  • 平行处理请求(参考BigPipe)。

  • 通过合并文件或者Image Sprites减少HTTP请求。

  • 减少重定向( HTTP 301和40x/50x)。

类型转换专题

  • 把数字转换成字符串。

    • 应用 ""+1,效率是最高。

      • 性能上来说: ""+字符串> String()> .toString()> new String()

        • String()属于内部函数,所以速度很快。

        • .toString()要查询原型中的函数,所以速度略慢。

        • new String()最慢。

  • 浮点数转换成整型。

    • 错误使用使用 parseInt()

      • parseInt()是用于将 字符串转换成 数字,而不是 浮点数整型之间的转换。

    • 应该使用 Math.floor()或者 Math.round()

      • Math是内部对象,所以 Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。

逻辑判断优化

  • switch语句。

    • 若有一系列复杂的 if-else语句,可以转换成单个 switch语句则可以得到更快的代码,还可以通过将 case语句按照最可能的到最不可能的顺序进行组织,来进一步优化。

内存专题

  • JAVASCRIPT的内存回收机制

    • 以Google的 V8引擎为例,在 V8引擎中所有的 JAVASCRIPT对象都是通过 来进行内存分配的。当我们在代码中 声明变量赋值时, V8引擎就会在 堆内存中分配一部分给这个 变量。如果已申请的 内存不足以存储这个 变量时, V8引擎就会继续申请 内存,直到 的大小达到了 V8引擎的内存上限为止(默认情况下, V8引擎的 堆内存的大小上限在 64位系统中为 1464MB,在 32位系统中则为 732MB)。

    • 另外, V8引擎对 堆内存中的 JAVASCRIPT对象进行 分代管理

      • 新生代。

        • 新生代即存活周期较短的 JAVASCRIPT对象,如临时变量、字符串等

      • 老生代。

        • 老生代则为经过多次垃圾回收仍然存活,存活周期较长的对象,如主控制器、服务器对象等。

  • 垃圾回收算法。

    • 垃圾回收算法一直是编程语言的研发中是否重要的​​一环,而 V8引擎所使用的垃圾回收算法主要有以下几种。

      • Scavange算法:通过复制的方式进行内存空间管理,主要用于新生代的内存空间;

      • Mark-Sweep算法和 Mark-Compact算法:通过标记来对堆内存进行整理和回收,主要用于老生代对象的检查和回收。

  • 对象进行回收。

    • 引用

      • 当函数执行完毕时,在函数内部所声明的对象 不一定就会被销毁。

      • 引用( Reference)是 JAVASCRIPT编程中十分重要的一个机制。

          • 是指 代码对对象的访问这一抽象关系,它与 C/C++的指针有点相似,但并非同物。引用同时也是 JAVASCRIPT引擎在进行 垃圾回收中最关键的一个机制。

                var val = 'hello world';
                function foo() {
                  return function() {
                    return val;
                  };
                }
                global.bar = foo();
        • 当代码执行完毕时,对象 valbar()并没有被回收释放, JAVASCRIPT代码中,每个 变量作为单独一行而不做任何操作, JAVASCRIPT引擎都会认为这是对 对象的访问行为,存在了对 对象的引用。为了保证 垃圾回收的行为不影响程序逻辑的运行, JAVASCRIPT引擎不会把正在使用的 对象进行回收。所以判断 对象是否正在使用中的标准,就是是否仍然存在对该 对象引用

  • JAVASCRIPT引用是可以进行 转移的,那么就有可能出现某些引用被带到了全局作用域,但事实上在业务逻辑里已经不需要对其进行访问了,这个时候就应该被回收,但是 JAVASCRIPT引擎仍会认为程序仍然需要它。

  • IE下闭包引起跨页面内存泄露。

  • JAVASCRIPT的内存泄露处理

    • DOM对象添加的属性是一个对象的引用。

          var MyObject = {};
          document.getElementByIdx_x('myDiv').myProp = MyObject;
      
      解决方法:在window.onunload事件中写上: 
      
          document.getElementByIdx_x('myDiv').myProp = null;
    • DOM对象与JS对象相互引用。

           function Encapsulator(element) {
              this.elementReference = element;
              element.myProp = this;
           }
           new Encapsulator(document.getElementByIdx_x('myDiv'));
      
      解决方法:在onunload事件中写上: 
      
          document.getElementByIdx_x('myDiv').myProp = null;
    • 给DOM对象用attachEvent绑定事件。

           function doClick() {}
           element.attachEvent("onclick", doClick);
      
      解决方法:在onunload事件中写上: 
      
          element.detachEvent('onclick', doClick);
    • 从外到内执行appendChild。这时即使调用removeChild也无法释放。

           var parentDiv =   document.createElement_x("div");
           var childDiv = document.createElement_x("div");
           document.body.appendChild(parentDiv);
           parentDiv.appendChild(childDiv);
      
      解决方法:从内到外执行appendChild:
      
           var parentDiv =   document.createElement_x("div");
           var childDiv = document.createElement_x("div");
           parentDiv.appendChild(childDiv);
           document.body.appendChild(parentDiv);
    • 反复重写同一个属性会造成内存大量占用(但关闭IE后内存会被释放)。

           for(i = 0; i < 5000; i++) {
              hostElement.text = "asdfasdfasdf";
           }
  • 内存不是 缓存

    • 不要轻易将 内存当作 缓存使用。

    • 如果是很重要的资源,请不要直接放在 内存中,或者制定 过期机制,自动销毁 过期缓存

  • CollectGarbage

    • CollectGarbageIE的一个特有属性,用于释放内存的使用方法,将该变量或引用对象设置为 nulldelete然后在进行释放动作,在做 CollectGarbage前,要必需清楚的两个必备条件:(引用)。

      • 一个对象在其生存的上下文环境之外,即会失效。

      • 一个全局的对象在没有被执用(引用)的情况下,即会失效

事件优化

  • 使用事件代理

    • 当存在多个元素需要注册事件时,在每个元素上绑定事件本身就会对性能有一定损耗。

    • 由于DOM Level2事件模 型中所有事件默认会传播到上层文档对象,可以借助这个机制在上层元素注册一个统一事件对不同子元素进行相应处理。

捕获型事件先发生。两种事件流会触发DOM中的所有对象,从document对象开始,也在document对象结束。

<ul id="parent-list"><li id="post-1">Item 1<li id="post-2">Item 2<li id="post-3">Item 3<li id="post-4">Item 4<li id="post-5">Item 5<li id="post-6">Item 6</li></ul>
    // Get the element, add a click listener...
    document.getElementById("parent-list").addEventListener("click",function(e) {
        // e.target is the clicked element!
        // If it was a list item
        if(e.target && e.target.nodeName == "LI") {
            // List item found!  Output the ID!
            console.log("List item ",e.target.id.replace("post-")," was clicked!");
        }
    });

数组专题

  • 当需要使用数组时,可使用 JSON格式的语法

    • 即直接使用如下语法定义数组: [parrm,param,param...],而不是采用 new Array(parrm,param,param...)这种语法。使用 JSON格式的语法是引擎直接解释。而后者则需要调用 Array的构造器。

  • 如果需要遍历数组,应该先缓存数组长度,将数组长度放入局部变量中,避免多次查询数组长度。

    • 根据字符串、数组的长度进行循环,而通常这个长度是不变的,比如每次查询 a.length,就要额外进行一个操作,而预先把 var len=a.length,则每次循环就少了一次查询。

同域跨域

  • 避免跳转

    • 同域:注意避免反斜杠 “/” 的跳转;

    • 跨域:使用Alias或者mod_rewirte建立CNAME(保存域名与域名之间关系的DNS记录)

性能测试工具

  • js性能优化和内存泄露问题及检测分析工具

      • 性能优化ajax工具 diviefirebug

      • [web性能分析工具YSlow]

        • performance性能评估打分,右击箭头可看到改进建议。

        • stats缓存状态分析,传输内容分析。

        • components所有加载内容分析,可以查看传输速度,找出页面访问慢的瓶颈。

        • tools可以查看js和css,并打印页面评估报告。

      • 内存泄露检测工具 sIEve

        • sIEve是基于 IE的内存泄露检测工具,需要下载运行,可以查看dom孤立节点和内存泄露及内存使用情况。

          1. 列出当前页面内所有dom节点的基本信息(html id style 等)

          2. 页面内所有dom节点的高级信息 (内存占用,数量,节点的引用)

          3. 可以查找出页面中的孤立节点

          4. 可以查找出页面中的循环引用

          5. 可以查找出页面中产生内存泄露的节点

      • 内存泄露提示工具 leak monitor

        • leak monitor在安装后,当离开一个页面时,比如关闭窗口,如果页面有内存泄露,会弹出一个文本框进行即时提示。

      • 代码压缩工具

        • YUI压缩工具

        • Dean Edwards Packer

        • JSMin

    • Blink/Webkit浏览器

      • Blink/Webkit浏览器中( Chrome, Safari, Opera),我们可以借助其中的 Developer ToolsProfiles工具来对我们的程序进行内存检查。

  • Node.js中的内存检查

    • Node.js中,我们可以使用 node-heapdumpnode-memwatch模块进​​行内存检查。

          var heapdump = require('heapdump');
          var fs = require('fs');
          var path = require('path');
          fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);

      在业务代码中引入 node-heapdump之后,我们需要在某个运行时期,向 Node.js进程发送 SIGUSR2信号,让 node-heapdump抓拍一份堆内存的快照。

         $ kill -USR2 (cat app.pid)
  • 分析浏览器提供的Waterfall图片来思考优化入口。

  • 新的测试手段(Navigation, Resource, 和User timing。

循环专题

  • 循环是一种常用的流程控制。

    • JAVASCRIPT提供了三种循环。

      • for(;;)

        • 推荐使用for循环,如果循环变量递增或递减,不要单独对循环变量赋值,而应该使用嵌套的 ++–-运算符。

        • 代码的可读性对于for循环的优化。

        • -=1

        • 从大到小的方式循环(这样缺点是降低代码的可读性)。

      • while()

        • for(;;)while()循环的性能基本持平。

      • for(in)

        • 在这三种循环中 for(in)内部实现是构造一个所有元素的列表,包括 array继承的属性,然后再开始循环,并且需要查询hasOwnProperty。所以 for(in)相对 for(;;)循环性能要慢。

  • 选择正确的方法

    • 避免不必要的属性查找。

      • 访问 变量数组O(1)操作。

      • 访问 对象上的 属性是一个 O(n)操作。

        对象上的任何属性查找都要比访问变量或数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索,即属性查找越多,执行时间越长。所以针对需要多次用到对象属性,应将其存储在局部变量。
        
    • 优化循环。

      • 减值迭代。

        • 大多数循环使用一个从0开始,增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加有效。

      • 简化终止条件。

        • 由于每次循环过程都会计算终止条件,故必须保证它尽可能快,即避免属性查找或其它O(n)的操作。

      • 简化循环体。

        • 循环体是执行最多的,故要确保其被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。

      • 使用后测试循环。

        • 最常用的for和while循环都是前测试循环,而如do-while循环可以避免最初终止条件的计算,因些计算更快。

    • 展开循环。

      • 当循环的次数确定时,消除循环并使用多次函数调用往往更快。

      • 当循环的次数不确定时,可以使用Duff装置来优化。

        • Duff装置的基本概念是通过计算迭代的次数是否为8的倍数将一个循环展开为一系列语句。

  • 避免在循环中使用 try-catch

    • try-catch-finally语句在catch语句被执行的过程中会动态构造变量插入到当前域中,对性能有一定影响。

    • 如果需要异常处理机制,可以将其放在循环外层使用。

        • 循环中使用try-catch

              for ( var i = 0; i < 200; i++) {
                try {} catch (e) {}
              }
      • 循环外使用try-catch

                 try {
                   for ( var i = 0; i < 200; i++) {}
                 } catch (e) {}
  • 避免遍历大量元素:

    • 避免对全局 DOM元素进行遍历,如果 parent已知可以指定 parent在特定范围查询。

           var elements = document.getElementsByTagName( '*' );
           for (i = 0; i < elements.length; i++) {
              if (elements[i].hasAttribute( 'selected' )) {}
           }
      
      如果已知元素存在于一个较小的范围内,
      

原型优化

  • 通过原型优化方法定义。

    • 如果一个方法类型将被频繁构造,通过方法原型从外面定义附加方法,从而避免方法的重复定义。

    • 可以通过外部原型的构造方式初始化值类型的变量定义。(这里强调值类型的原因是,引用类型如果在原型中定义,一个实例对引用类型的更改会影响到其他实例。)

      • 这条规则中涉及到 JAVASCRIPT中原型的概念,构造函数都有一个 prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。可以把那些不变的属性和方法,直接定义在 prototype对象上。

        • 可以通过对象实例访问保存在原型中的值。

        • 不能通过对象实例重写原型中的值。

        • 在实例中添加一个与实例原型同名属性,那该属性就会屏蔽原型中的属性。

        • 通过delete操作符可以删除实例中的属性。

运算符专题

  • 使用运算符时,尽量使用 +=-=*=\=等运算符号,而不是直接进行赋值运算。

  • 位运算

    • 当进行数学运算时 位运算较快, 位运算操作要比任何 布尔运算算数运算快,如 取模逻辑与逻辑或也可以考虑用 位运算来替换。

重绘专题

  • 减少页面的 重绘

    • 减少页面 重绘虽然本质不是 JAVASCRIPT优化,但 重绘往往是由 JAVASCRIPT引起的,而 重绘的情况直接影响页面性能。

           var str = "<div>这是一个测试字符串</div>";
           /**效率低**/
           var obj = document.getElementsByTagName("body");
           for(var i = 0; i < 100; i++){
               obj.innerHTML += str + i;
           }
           /**效率高**/
           var obj = document.getElementsByTagName("body");
           var arr = [];
           for(var i = 0; i < 100; i++){
               arr[i] = str + i;
           }
           obj.innerHTML = arr.join("");

    一般影响页面重绘的不仅仅是innerHTML,如果改变元素的样式,位置等情况都会触发页面重绘,所以在平时一定要注意这点。

  • 使用HTML5和CSS3的一些新特性。

  • 避免在HTML里面缩放图片。

  • 避免使用插件。

  • 确保使用正确的字体大小。

  • 决定当前页面是不是能被访问。

字符串专题

  • 对字符串进行循环操作。

    • 替换、查找等操作,使用正则表达式。

      • 因为 JAVASCRIPT的循环速度较慢,而正则表达式的操作是用 C写成的 API,性能比较好。

  • 字符串的拼接。

    • 字符串的拼接在我们开发中会经常遇到,所以我把其放在首位,我们往往习惯的直接用 +=的方式来拼接字符串,其实这种拼接的方式效率非常的低,我们可以用一种巧妙的方法来实现字符串的拼接,那就是利用数组的 join方法,具体请看我整理的: Web前端开发规范文档中的 javaScript书写规范倒数第三条目。

    • 不过也有另一种说法,通常认为需要用 Array.join的方式,但是由于 SpiderMonkey等引擎对字符串的“ +”运算做了优化,结果使用 Array.join的效率反而不如直接用“ +”,但是如果考虑 IE6,则其他浏览器上的这种效率的差别根本不值一提。具体怎么取舍,诸君自定。

作用域链和闭包优化

  • 作用域。

    • 作用域( scope)是 JAVASCRIPT编程中一个重要的 运行机制,在 JAVASCRIPT同步和异步编程以及 JAVASCRIPT内存管理中起着至关重要的作用。

    • JAVASCRIPT中,能形成作用域的有如下几点。

      • 函数的调用

      • with语句

        • with会创建自已的作用域,因此会增加其中执行代码的作用域的长度。

      • 全局作用域。

        以下代码为例:
        
             var foo = function() {
               var local = {};
             };
             foo();
             console.log(local); //=undefined
        
             var bar = function() {
               local = {};
             };
             bar();
             console.log(local); //={}
        
            /**这里我们定义了foo()函数和bar()函数,他们的意图都是为了定义一个名为local的变量。在foo()函数中,我们使用var语句来声明定义了一个local变量,而因为函数体内部会形成一个作用域,所以这个变量便被定义到该作用域中。而且foo()函数体内并没有做任何作用域延伸的处理,所以在该函数执行完毕后,这个local变量也随之被销毁。而在外层作用域中则无法访问到该变量。而在bar()函数内,local变量并没有使用var语句进行声明,取而代之的是直接把local作为全局变量来定义。故外层作用域可以访问到这个变量。**/
        
  • 作用域链

    • JAVASCRIPT编程中,会遇到多层函数嵌套的场景,这就是典型的作用域链的表示。

           function foo() {
             var val = 'hello';
             function bar() {
               function baz() {
                 global.val = 'world;'
               };
               baz();
               console.log(val); //=hello
             };
             bar();
           };
           foo();
  • 减少作用域链上的查找次数

    • JAVASCRIPT代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要遍历当前执行环境的作用域链,而遍历是从这个作用域链的前端一级一级的向后遍历,直到全局执行环境。

  • 闭包

    • JAVASCRIPT中的标识符查找遵循从内到外的原则。

            function foo() {
              var local = 'Hello';
              return function() {
                return local;
              };
            }
            var bar = foo();
            console.log(bar()); //=Hello
      
            /**这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到`延伸`。foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。**/
      • 闭包是 JAVASCRIPT的高级特性,因为把带有​​内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。

      • 良好的闭包管理。

        • 循环事件绑定、私有属性、含参回调等一定要使用闭包时,并谨慎对待其中的细节。

          • 循环绑定事件,我们假设一个场景:有六个按钮,分别对应六种事件,当用户点击按钮时,在指定的地方输出相应的事件。

  • 避开闭包陷阱

    • 闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。

    • 闭包的性能不如使用内部方法,更不如重用外部方法。

      • 由于 IE 9浏览器的 DOM节点作为 COM对象来实现, COM内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦 DOM引用了闭包(例如 event handler),闭包的上层元素又引用了这个 DOM,就会造成循环引用从而导致内存泄漏。

  • 善用函数

    • 使用一个匿名函数在代码的最外层进行包裹。

       ;(function() {
         // 主业务代码
       })();
      

有的甚至更高级一点:

    ;(function(win, doc, $, undefined) {
      // 主业务代码
    })(window, document, jQuery);

甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:

    /**RequireJS**/
    define(['jquery'], function($) {
      // 主业务代码
    });
    /**SeaJS**/
    define('m​​odule', ['dep', 'underscore'], function($, _) {
      // 主业务代码
    });

被定义在全局作用域的对象,可能是会一直存活到进程退出的,如果是一个很大的对象,那就麻烦了。比如有的人喜欢在JavaScript中做模版渲染:

<?php
      $db = mysqli_connect(server, user, password, 'myapp');
      $topics = mysqli_query($db, "SELECT * FROM topics;");
    ?><!doctype html><html lang="en"><head><meta charset="UTF-8"><title>你是猴子请来的逗比么?</title></head><body><ul id="topics"></ul><script type="text/tmpl" id="topic-tmpl"><li class="topic"><h1><%=title%></h1><p><%=content%></p></li></script><script type="text/javascript">
        var data = <?php echo json_encode($topics); ?>;
        var topicTmpl = document.querySelector('#topic-tmpl').innerHTML;
        var render = function(tmlp, view) {
          var complied = tmlp
            .replace(/\n/g, '\\n')
            .replace(/<%=([\s\S]+?)%>/g, function(match, code) {
              return '" + escape(' + code + ') + "';
            });
          complied = ['var res = "";','with (view || {}) {','res = "' + complied + '";','}','return res;'
          ].join('\n');
          var fn = new Function('view', complied);
          return fn(view);
        };
        var topics = document.querySelector('#topics');
        function init()
          data.forEach(function(topic) {
            topics.innerHTML += render(topicTmpl, topic);
          });
        }
        init();</script></body></html>

在从数据库中获取到的数据的量是非常大的话,前端完成模板渲染以后,data变量便被闲置在一边。可因为这个变量是被定义在全局作用域中的,所以 JAVASCRIPT引擎不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。可是如果我们作出一些很简单的修改,在逻辑代码外包装一层函数,这样效果就大不同了。当UI渲染完成之后,代码对data的引用也就随之解除,而在最外层函数执行完毕时, JAVASCRIPT引擎就开始对其中的对象进行检查,data也就可以随之被回收。

GITHUB: 前端性能优化指南

参考和借鉴了大家的经验,收集整理了这一篇开发规范,感谢所有的原作者,众人拾柴火焰高,技术无国界,持续更新中。

初探 performance – 监控网页与程序性能

$
0
0

使用 window.performance 提供了一组精确的数据,经过简单的计算就能得出一些网页性能数据。

配合上报一些客户端浏览器的设备类型等数据,就可以实现简单的统计啦!

额,先看下兼容性如何: http://caniuse.com/#feat=nav-timing

这篇文章中 Demo 的运行环境为最新的 Chrome 的控制台,如果你用的是其他浏览器,自查兼容性哈~

先来看看在 Chrome 浏览器控制台中执行 window.performance 会出现什么:

简单解释下 performance 中的属性:

先看下一个请求发出的整个过程中,各种环节的时间顺序:

// 获取 performance 数据
var performance = {  
    // memory 是非标准属性,只在 Chrome 有
    // 财富问题:我有多少内存
    memory: {
        usedJSHeapSize:  16100000, // JS 对象(包括V8引擎内部对象)占用的内存,一定小于 totalJSHeapSize
        totalJSHeapSize: 35100000, // 可使用的内存
        jsHeapSizeLimit: 793000000 // 内存大小限制
    },

    //  哲学问题:我从哪里来?
    navigation: {
        redirectCount: 0, // 如果有重定向的话,页面通过几次重定向跳转而来
        type: 0           // 0   即 TYPE_NAVIGATENEXT 正常进入的页面(非刷新、非重定向等)
                          // 1   即 TYPE_RELOAD       通过 window.location.reload() 刷新的页面
                          // 2   即 TYPE_BACK_FORWARD 通过浏览器的前进后退按钮进入的页面(历史记录)
                          // 255 即 TYPE_UNDEFINED    非以上方式进入的页面
    },

    timing: {
        // 在同一个浏览器上下文中,前一个网页(与当前页面不一定同域)unload 的时间戳,如果无前一个网页 unload ,则与 fetchStart 值相等
        navigationStart: 1441112691935,

        // 前一个网页(与当前页面同域)unload 的时间戳,如果无前一个网页 unload 或者前一个网页与当前页面不同域,则值为 0
        unloadEventStart: 0,

        // 和 unloadEventStart 相对应,返回前一个网页 unload 事件绑定的回调函数执行完毕的时间戳
        unloadEventEnd: 0,

        // 第一个 HTTP 重定向发生时的时间。有跳转且是同域名内的重定向才算,否则值为 0 
        redirectStart: 0,

        // 最后一个 HTTP 重定向完成时的时间。有跳转且是同域名内部的重定向才算,否则值为 0 
        redirectEnd: 0,

        // 浏览器准备好使用 HTTP 请求抓取文档的时间,这发生在检查本地缓存之前
        fetchStart: 1441112692155,

        // DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupStart: 1441112692155,

        // DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
        domainLookupEnd: 1441112692155,

        // HTTP(TCP) 开始建立连接的时间,如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接开始的时间
        connectStart: 1441112692155,

        // HTTP(TCP) 完成建立连接的时间(完成握手),如果是持久连接,则与 fetchStart 值相等
        // 注意如果在传输层发生了错误且重新建立连接,则这里显示的是新建立的连接完成的时间
        // 注意这里握手结束,包括安全连接建立完成、SOCKS 授权通过
        connectEnd: 1441112692155,

        // HTTPS 连接开始的时间,如果不是安全连接,则值为 0
        secureConnectionStart: 0,

        // HTTP 请求读取真实文档开始的时间(完成建立连接),包括从本地读取缓存
        // 连接错误重连时,这里显示的也是新建立连接的时间
        requestStart: 1441112692158,

        // HTTP 开始接收响应的时间(获取到第一个字节),包括从本地读取缓存
        responseStart: 1441112692686,

        // HTTP 响应全部接收完成的时间(获取到最后一个字节),包括从本地读取缓存
        responseEnd: 1441112692687,

        // 开始解析渲染 DOM 树的时间,此时 Document.readyState 变为 loading,并将抛出 readystatechange 相关事件
        domLoading: 1441112692690,

        // 完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
        // 注意只是 DOM 树解析完成,这时候并没有开始加载网页内的资源
        domInteractive: 1441112693093,

        // DOM 解析完成后,网页内资源加载开始的时间
        // 在 DOMContentLoaded 事件抛出前发生
        domContentLoadedEventStart: 1441112693093,

        // DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕)
        domContentLoadedEventEnd: 1441112693101,

        // DOM 树解析完成,且资源也准备就绪的时间,Document.readyState 变为 complete,并将抛出 readystatechange 相关事件
        domComplete: 1441112693214,

        // load 事件发送给文档,也即 load 回调函数开始执行的时间
        // 注意如果没有绑定 load 事件,值为 0
        loadEventStart: 1441112693214,

        // load 事件的回调函数执行完毕的时间
        loadEventEnd: 1441112693215

        // 字母顺序
        // connectEnd: 1441112692155,
        // connectStart: 1441112692155,
        // domComplete: 1441112693214,
        // domContentLoadedEventEnd: 1441112693101,
        // domContentLoadedEventStart: 1441112693093,
        // domInteractive: 1441112693093,
        // domLoading: 1441112692690,
        // domainLookupEnd: 1441112692155,
        // domainLookupStart: 1441112692155,
        // fetchStart: 1441112692155,
        // loadEventEnd: 1441112693215,
        // loadEventStart: 1441112693214,
        // navigationStart: 1441112691935,
        // redirectEnd: 0,
        // redirectStart: 0,
        // requestStart: 1441112692158,
        // responseEnd: 1441112692687,
        // responseStart: 1441112692686,
        // secureConnectionStart: 0,
        // unloadEventEnd: 0,
        // unloadEventStart: 0
    }
};

具体的含义都在注释里说明了,接下来我们看下能用这些数据做什么?

使用 performance.timing 信息简单计算出网页性能数据

在注释中,我用【重要】标注了我个人认为比较有用的数据,用【原因】标注了为啥要重点关注这个数据

// 计算加载时间
function getPerformanceTiming () {  
    var performance = window.performance;

    if (!performance) {
        // 当前浏览器不支持
        console.log('你的浏览器不支持 performance 接口');
        return;
    }

    var t = performance.timing;
    var times = {};

    //【重要】页面加载完成的时间
    //【原因】这几乎代表了用户等待页面可用的时间
    times.loadPage = t.loadEventEnd - t.navigationStart;

    //【重要】解析 DOM 树结构的时间
    //【原因】反省下你的 DOM 树嵌套是不是太多了!
    times.domReady = t.domComplete - t.responseEnd;

    //【重要】重定向的时间
    //【原因】拒绝重定向!比如,http://example.com/ 就不该写成 http://example.com
    times.redirect = t.redirectEnd - t.redirectStart;

    //【重要】DNS 查询时间
    //【原因】DNS 预加载做了么?页面内是不是使用了太多不同的域名导致域名查询的时间太长?
    // 可使用 HTML5 Prefetch 预查询 DNS ,见:[HTML5 prefetch](http://segmentfault.com/a/1190000000633364)            
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

    //【重要】读取页面第一个字节的时间
    //【原因】这可以理解为用户拿到你的资源占用的时间,加异地机房了么,加CDN 处理了么?加带宽了么?加 CPU 运算速度了么?
    // TTFB 即 Time To First Byte 的意思
    // 维基百科:https://en.wikipedia.org/wiki/Time_To_First_Byte
    times.ttfb = t.responseStart - t.navigationStart;

    //【重要】内容加载完成的时间
    //【原因】页面内容经过 gzip 压缩了么,静态资源 css/js 等压缩了么?
    times.request = t.responseEnd - t.requestStart;

    //【重要】执行 onload 回调函数的时间
    //【原因】是否太多不必要的操作都放到 onload 回调函数里执行了,考虑过延迟加载、按需加载的策略么?
    times.loadEvent = t.loadEventEnd - t.loadEventStart;

    // DNS 缓存时间
    times.appcache = t.domainLookupStart - t.fetchStart;

    // 卸载页面的时间
    times.unloadEvent = t.unloadEventEnd - t.unloadEventStart;

    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;

    return times;
}

使用performance.getEntries() 获取所有资源请求的时间数据

这个函数返回的将是一个数组,包含了页面中所有的 HTTP 请求,这里拿第一个请求 window.performance.getEntries()[0] 举例。 注意 HTTP 请求有可能命中本地缓存,所以请求响应的间隔将非常短 可以看到,与 performance.timing 对比: 没有与 DOM 相关的属性:

  • navigationStart

  • unloadEventStart

  • unloadEventEnd

  • domLoading

  • domInteractive

  • domContentLoadedEventStart

  • domContentLoadedEventEnd

  • domComplete

  • loadEventStart

  • loadEventEnd

新增属性:

  • name

  • entryType

  • initiatorType

  • duration

与 window.performance.timing 中包含的属性就不再介绍了:

var entry = {  
    // 资源名称,也是资源的绝对路径
    name: "http://cdn.alloyteam.com/wp-content/themes/alloyteam/style.css",
    // 资源类型
    entryType: "resource",
    // 谁发起的请求
    initiatorType: "link", // link 即 <link> 标签
                           // script 即 <script>
                           // redirect 即重定向
    // 加载时间
    duration: 18.13399999809917,

    redirectStart: 0,
    redirectEnd: 0,

    fetchStart: 424.57699999795295,

    domainLookupStart: 0,
    domainLookupEnd: 0,

    connectStart: 0,
    connectEnd: 0,

    secureConnectionStart: 0,

    requestStart: 0,

    responseStart: 0,
    responseEnd: 442.7109999960521,

    startTime: 424.57699999795295
};

可以像 getPerformanceTiming 获取网页的时间一样,获取某个资源的时间:

// 计算加载时间
function getEntryTiming (entry) {  
    var t = entry;
    var times = {};

    // 重定向的时间
    times.redirect = t.redirectEnd - t.redirectStart;

    // DNS 查询时间
    times.lookupDomain = t.domainLookupEnd - t.domainLookupStart;

    // 内容加载完成的时间
    times.request = t.responseEnd - t.requestStart;

    // TCP 建立连接完成握手的时间
    times.connect = t.connectEnd - t.connectStart;

    // 挂载 entry 返回
    times.name = entry.name;
    times.entryType = entry.entryType;
    times.initiatorType = entry.initiatorType;
    times.duration = entry.duration;

    return times;
}

// test
// var entries = window.performance.getEntries();
// entries.forEach(function (entry) {
//     var times = getEntryTiming(entry);
//     console.log(times);
// });

使用 performance.now() 精确计算程序执行时间

performance.now() 与 Date.now() 不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。

并且与 Date.now() 会受系统程序执行阻塞的影响不同,performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。

注意 Date.now() 输出的是 UNIX 时间,即距离 1970 的时间,而 performance.now() 输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间。

使用 Date.now() 的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now() 的差值,并不影响我们计算程序执行的精确时间。

// 计算程序执行的精确时间
function getFunctionTimeWithDate (func) {  
    var timeStart = Data.now();

    // 执行开始
    func();
    // 执行结束
    var timeEnd = Data.now();

    // 返回执行时间
    return (timeEnd - timeStart);
}
function getFunctionTimeWithPerformance (func) {  
    var timeStart = window.performance.now();

    // 执行开始
    func();
    // 执行结束
    var timeEnd = window.performance.now();

    // 返回执行时间
    return (timeEnd - timeStart);
}

使用 performance.mark() 也可以精确计算程序执行时间

使用 performance.mark() 标记各种时间戳(就像在地图上打点),保存为各种测量值(测量地图上的点之间的距离),便可以批量地分析这些数据了。

直接上示例代码看注释便明白:

function randomFunc (n) {  
    if (!n) {
        // 生成一个随机数
        n = ~~(Math.random() * 10000);
    }
    var nameStart = 'markStart' + n; 
    var nameEnd   = 'markEnd' + n; 
    // 函数执行前做个标记
    window.performance.mark(nameStart);

    for (var i = 0; i < n; i++) {
        // do nothing
    }

    // 函数执行后再做个标记
    window.performance.mark(nameEnd);

    // 然后测量这个两个标记间的时间距离,并保存起来
    var name = 'measureRandomFunc' + n;
    window.performance.measure(name, nameStart, nameEnd);
}

// 执行三次看看
randomFunc();  
randomFunc();  
// 指定一个名字
randomFunc(888);

// 看下保存起来的标记 mark
var marks = window.performance.getEntriesByType('mark');  
console.log(marks);

// 看下保存起来的测量 measure
var measure = window.performance.getEntriesByType('measure');  
console.log(measure);

// 看下我们自定义的测量
var entries = window.performance.getEntriesByName('measureRandomFunc888');  
console.log(entries);

可以看到,for 循环 measureRandomFunc888 的时候

结束时间为: 4875.1199999969685

开始时间为:4875.112999987323

执行时间为:4875.1199999969685 – 4875.112999987323 = 0.00700000964

标记和测量用完了可以清除掉:

// 清除指定标记
window.performance.clearMarks('markStart888');  
// 清除所有标记
window.performance.clearMarks();

// 清除指定测量
window.performance.clearMeasures('measureRandomFunc');  
// 清除所有测量
window.performance.clearMeasures();

当然 performance.mark() 只是提供了一些简便的测量方式,比如之前我们测量 domReady 是这么测的:

// 计算 domReady 时间
var t = performance.timing  
var domReadyTime = t.domComplete - t.responseEnd;  
console.log(domReadyTime)

其实就可以写成:

window.performance.measure('domReady','responseEnd' , 'domComplete');  
var domReadyMeasure = window.performance.getEntriesByName('domReady');  
console.log(domReadyMeasure);

抛砖引玉:performance 数据能干啥用?

熟悉 Chrome 开发者工具的朋友应该知道:在开发环境下,其实我们自己打开 Chrome 的开发者工具,切换到网络面板,就能很详细的看到网页性能相关的数据。但当我们需要统计分析用户打开我们网页时的性能如何时,我们将 performance 原始信息或通过简单计算后的信息(如上面写到的 getPerformanceTiming() 和 getEntryTiming()) 上传到服务器,配合其他信息(如 HTTP 请求头信息),就完美啦~

传统 Ajax 已死,Fetch 永生

$
0
0

image

原谅我做一次标题党,Ajax 不会死,传统 Ajax 指的是 XMLHttpRequest(XHR),未来现在已被 Fetch替代。

最近把阿里一个千万级 PV 的数据产品全部由 jQuery 的 $.ajax迁移到 Fetch,上线一个多月以来运行非常稳定。结果证明,对于 IE8+ 以上浏览器,在生产环境使用 Fetch 是可行的。

由于 Fetch API 是基于 Promise 设计,有必要先学习一下 Promise,推荐阅读 MDN Promise 教程。旧浏览器不支持 Promise,需要使用 polyfill es6-promise

本文不是 Fetch API 科普贴,其实是讲异步处理和 Promise 的。Fetch API 很简单,看文档很快就学会了。推荐 MDN Fetch 教程和 万能的 WHATWG Fetch 规范

Why Fetch

XMLHttpRequest 是一个设计粗糙的 API,不符合关注分离(Separation of Concerns)的原则,配置和调用方式非常混乱,而且基于事件的异步模型写起来也没有现代的 Promise,generator/yield,async/await 友好。

Fetch 的出现就是为了解决 XHR 的问题,拿例子说明:

使用 XHR 发送一个 json 请求一般是这样:

var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = 'json';

xhr.onload = function() {
  console.log(xhr.response);
};

xhr.onerror = function() {
  console.log("Oops, error");
};

xhr.send();

使用 Fetch 后,顿时看起来好一点

fetch(url).then(function(response) {
  return response.json();
}).then(function(data) {
  console.log(data);
}).catch(function(e) {
  console.log("Oops, error");
});

使用 ES6 的 箭头函数后:

fetch(url).then(response => response.json())
  .then(data => console.log(data))
  .catch(e => console.log("Oops, error", e))

现在看起来好很多了,但这种 Promise 的写法还是有 Callback 的影子,而且 promise 使用 catch 方法来进行错误处理的方式有点奇怪。不用急,下面使用 async/await 来做最终优化:

注:async/await 是非常新的 API,属于 ES7,目前尚在 Stage 1(提议) 阶段,这是它的 完整规范。使用 Babel开启 runtime模式后可以把 async/await 无痛编译成 ES5 代码。也可以直接使用 regenerator来编译到 ES5。

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);
}
// 注:这段代码如果想运行,外面需要包一个 async function

duang~~ 的一声,使用 await后, 写异步代码就像写同步代码一样爽await后面可以跟 Promise 对象,表示等待 Promise resolve()才会继续向下执行,如果 Promise 被 reject()或抛出异常则会被外面的 try...catch捕获。

await/async,generator/yield 可以粗略地认为是 Promise 的语法糖(其实功能上也有增强)。因此它们可以和 Fetch 完美搭配使用。这也是使用标准 Promise 一大好处。最近也把项目中使用第三方 Promise 库的代码全部转成标准 Promise,为以后全面使用 async/await 做准备。

另外,Fetch 也很适合做现在流行的同构应用,有人基于 Fetch 的语法,在 Node 端基于 http 库实现了 node-fetch,又有人封装了用于同构应用的 isomorphic-fetch

注:同构(isomorphic/universal)就是使 前后端运行同一套代码的意思,后端一般是指 NodeJS 环境。

总结一下,Fetch 优点主要有:

  1. 语法简洁,更加语义化

  2. 基于标准 Promise 实现,支持 async/await

  3. 同构方便,使用 isomorphic-fetch

Fetch 启用方法

下面是重点↓↓↓

先看一下 Fetch 原生支持率:
image

原生支持率并不高,幸运的是,引入下面这些 polyfill 后可以完美支持 IE8+ :

  1. 由于 IE8 是 ES3,需要引入 ES5 的 polyfill: es5-shim, es5-sham

  2. 引入 Promise 的 polyfill: es6-promise

  3. 引入 fetch 探测库: fetch-detector

  4. 引入 fetch 的 polyfill: fetch-ie8

  5. 可选:如果你还使用了 jsonp,引入 fetch-jsonp

  6. 可选:开启 Babel 的 runtime 模式,现在就使用 async/await

Fetch polyfill 的基本原理是探测是否存在 window.fetch方法,如果没有则用 XHR 实现。这也是 github/fetch的做法,但是有些浏览器(Chrome 45)原生支持 Fetch,但响应中有 中文时会乱码,老外又不太关心这种问题,所以我自己才封装了 fetch-detectorfetch-ie8只在浏览器稳定支持 Fetch 情况下才使用原生 Fetch。这些库现在 每天有几千万个请求都在使用,绝对靠谱

终于,引用了这一堆 polyfill 后,可以愉快地使用 Fetch 了。但要小心,下面有坑:

Fetch 常见坑

  • Fetch 请求默认是不带 cookie 的,需要设置 fetch(url, {credentials: 'include'})

  • 服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

竟然没有提到 IE,这实在太不科学了,现在来详细说下 IE

IE 使用策略

所有版本的 IE 均不支持原生 Fetch,fetch-ie8 会自动使用 XHR 做 polyfill。但在跨域时有个问题需要处理。

IE8, 9 的 XHR 不支持 CORS 跨域,虽然提供 XDomainRequest,但这个东西就是玩具,不支持传 Cookie!如果接口需要权限验证,还是乖乖地使用 jsonp 吧,推荐使用 fetch-jsonp。如果有问题直接提 issue,我会第一时间解决。

标准 Promise 的不足

由于 Fetch 是典型的异步场景,所以大部分遇到的问题不是 Fetch 的,其实是 Promise 的。ES6 的 Promise 是基于 Promises/A+标准,为了保持 简单简洁,只提供极简的几个 API。如果你用过一些牛 X 的异步库,如 jQuery(不要笑) 、Q.js 或者 RSVP.js,可能会感觉 Promise 功能太少了。

没有 Deferred

Deferred可以在创建 Promise 时可以减少一层嵌套,还有就是跨方法使用时很方便。
ECMAScript 11 年就有过 Deferred 提案,但后来没被接受。其实用 Promise 不到十行代码就能实现 Deferred: es6-deferred。现在有了 async/await,generator/yield 后,deferred 就没有使用价值了。

没有获取状态方法:isRejected,isResolved

标准 Promise 没有提供获取当前状态 rejected 或者 resolved 的方法。只允许外部传入成功或失败后的回调。我认为这其实是优点,这是一种声明式的接口,更简单。

缺少其它一些方法:always,progress,finally

always 可以通过在 then 和 catch 里重复调用方法实现。finally 也类似。progress 这种进度通知的功能还没有用过,暂不知道如何替代。

资料

最后

Fetch 替换 XHR 只是时间问题,现在看到国外很多新的库都默认使用了 Fetch。

最后再做一个大胆预测:由于 async/await 这类新异步语法的出现,第三方的 Promise 类库会逐渐被标准 Promise 替代,使用 polyfill 是现在比较明智的做法。

转至我的博客,原文地址: https://github.com/camsong/blog/issues/2

内存泄露从入门到精通三部曲之排查方法篇

$
0
0

腾讯Bugly特约作者: 姚潮生

最原始的内存泄露测试

重复多次操作关键的可疑的路径,从内存监控工具中观察内存曲线,是否存在不断上升的趋势且不会在程序返回时明显回落。
这种方式可以发现最基本,也是最明显的内存泄露问题,对用户价值最大,操作难度小,性价比极高。


MAT内存分析工具

2.1 MAT分析heap的总内存占用大小来初步判断是否存在泄露

  1. 在Devices 中,点击要监控的程序。

  2. 点击Devices视图界面中最上方一排图标中的“Update Heap”

  3. 点击Heap视图

  4. 点击Heap视图中的“Cause GC”按钮

  5. 到此为止需检测的进程就可以被监视。

Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:

进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。

所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。

2.2 MAT分析hprof来定位内存泄露的原因所在。

这是出现内存泄露后使用MAT进行问题定位的有效手段。

  1. Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:

  2. 分析持有此类对象引用的外部对象

  3. 分析这些持有引用的对象的GC路径

  4. 逐个分析每个对象的GC路径是否正常

从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。

2.3 MAT对比操作前后的hprof来定位内存泄露的根因所在。

为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去

  1. 第一个HPROF 文件(usingFile > Open Heap Dump ).

  2. 打开Histogram view.

  3. 在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket .

  4. 打开第二个HPROF 文件然后重做步骤2和3.

  5. 切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色"!"图标)。

  6. 分析对比结果

可以看出两个hprof的数据对象对比结果。通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。

注意:

如果是用 MAT Eclipse 插件获取的 Dump文件,不需要经过转换则可在MAT中打开,Adt会自动进行转换。

而手机SDk Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下)

首先,要通过控制台进入到你的 android sdk tools 目录下执行以下命令:

./hprof-conv xxx-a.hprof xxx-b.hprof

例如 hprof-conv input.hprof out.hprof
此时才能将out.hprof放在eclipse的MAT中打开。

手机管家内存泄露每日监控方案

目前手机管家的内存泄露每日监控会自动运行并输出是否存在疑似泄露的报告邮件,不论泄露对象的大小。这其中涉及的核心技术主要是AspectJ,MLD自研工具(原理是虚引用)和UIAutomator。

3.1 AspectJ插桩监控代码

手机管家目前使用一个ant脚本加入MLD的监控代码,并通过AspectJ的语法实现插桩。
使用AspectJ的原因是可以灵活分离出项目源码与监控代码,通过不同的编译脚本打包出不同用途的安装测试包:如果测试包是经过Aspect插桩了MLD监控代码的话,那么运行完毕后会输出指定格式的日志文件,作为后续分析工作的数据基础。

3.2 MLD实现监控核心逻辑

这是手机管家内的一个工具工程,正式打包不会打入,BVT等每日监控测试包可以打入。打入后可以通过诸如addObject接口(通过反射去检查是否含有该工具并调用)来加入需要监控的检测对象,这个工具会自动在指定时机(如退出管家)去检测该对象是否发生泄漏。

这个内存泄露检测的基本原理是:

虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列(ReferenceQueue)联合使用(在虚引用函数就必须关联指定)。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,自动把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

基于以上原理,MLD工具在调用接口addObject加入监控类型时,会为该类型对象增加一个虚引用,注意虚引用并不会影响该对象被正常回收。因此可以在ReferenceQueue引用队列中统计未被回收的监控对象是否超过指定阀值。

利用PhantomReferences(虚引用)和ReferenceQueue(引用队列),当PhantomReferences被加入到相关联的ReferenceQueue时,则视该对象已经或处于垃圾回收器回收阶段了。

MLD监控原理核心

目前手机管家已对大部分类完成内存泄露的监控,包括各种activity,service和view页面等,务求在技术上能带给用户最顺滑的产品体验。

接下来简单介绍下这个工具的判断核心。根据虚引用监控到的内存状态,需要通过多种策略来判断是否存在内存泄露。

  1. 最简单的方式就是直接在加入监控时就为该类型设定最大存在个数,举个例子,各个DAO对象理论上只能存在最多一个,因此一旦出现两个相同的DAO,那一般都是泄露了;

  2. 第二种情况是在页面退出程序退出时,检索gc后无法释放的对象列表,这些对象类型也会成为内存泄露的怀疑对象;

  3. 最后一种情况比较复杂,基本原理是根据历史操作判断对象数量的增长幅度。根据对象的增长通过最小二乘法拟合出该对象类型的增长速度,如果超过经验值则会列入疑似泄露的对象列表。

3.3 UIAutomator完成重复操作的自动化

最后一步就很简单了。这么多反复的UI操作,让人工来点就太浪费人力了。我们使用UIAutomator来进行自动化操作测试。

目前手机管家的每日自动化测试已覆盖各个功能的主路径,并通过配置文件的方式来灵活驱动用例的增删改查,最大限度保证了随着版本推移用例的复用价值。

至此手机管家的内存泄露测试方案介绍完毕,也欢迎各路牛人交流沟通更多更强的内存泄露工具盒方案!


Bugly 是腾讯内部产品质量监控平台的外发版本,其主要功能是App发布以后,对用户侧发生的Crash以及卡顿现象进行监控并上报,让开发同学可以第一时间了解到App的质量情况,及时机型修改。目前腾讯内部所有的产品,均在使用其进行线上产品的崩溃监控。

使用Chrome DevTools的Timeline分析页面性能

$
0
0

随着webpage可以承载的表现形式更加多样化,通过webpage来实现更多交互功能,构建web应用程序已经成为很多产品的首要选择。这种方式拥有非常明显的优势:跨平台、开发便捷、便于部署和维护等等,但随着功能的不断积累,web应用程序也会变得越来越复杂。但是,我们仍然想要在webpage支持丰富的呈现形式的同时,让页面效果能够达到>=60fps(帧)/s的刷新频率以避免出现卡顿,就需要我们使用一些比较直观的方式来分析衡量页面的性能问题,为性能优化方案提供依据。

为什么是60fps?
我们的目标是保证页面要有高于每秒60fps(帧)的刷新频率,这和目前大多数显示器的刷新率相吻合(60Hz)。如果网页动画能够做到每秒60帧,就会跟显示器同步刷新,达到最佳的视觉效果。这意味着,一秒之内进行60次重新渲染,每次重新渲染的时间不能超过16.66毫秒。

需求大体明确,就是要找到页面执行过程中的性能瓶颈。而Chrome DevTools的Timeline则正是用来记录和分析应用在运行时所有的活动情况,它是用来排查应用性能瓶颈的最佳工具。

下图是Timeline面板的预览效果:

Timeline预览效果图

Tips:为了避免浏览器插件对分析过程产生影响,建议在隐身模式下进行分析。

Timeline工具栏介绍

Timeline工具会详细检测出在Web应用加载的过程中时间花费情况的概览,包括下载资源、处理DOM事件、页面布局渲染、向屏幕绘制元素等。你可以通过分析Timeline得到的事件、框架和实时的内存用量,找出应用的性能问题。

Timeline的两种模式

在分析页面前,需要首先开启录制功能,记录页面的操作和渲染记录。如上图,左上角的灰色圆点就是录制按钮,点击后会变成红色,然后在页面上进行相关操作后再次按下变成灰色完成录制,这样就完成了一次对操作及加载渲染的记录过程,随后Timeline就会开始分析操作过程中的各项性能参数。

Timeline同时提供了两种查看模式:“事件模式(Event Mode)”和“帧模式(Frame Mode)”。如上图箭头所示。

事件模式:显示重新渲染的各种事件花费的时间。
帧模式:显示每一帧的时间花费情况。

事件模式 (Event Mode)

如果我们的一个页面执行效率不高,我们必须要搞清楚导致页面性能低下的原因,到底是javascript执行出了问题,还是页面渲染出了问题。要了解这里面的执行细节,我们可以使用“事件模式”来进行分析。首先我们需要录制一些需要被分析的操作,录制结束后进入事件模式预览Timeline。下图是得到的事件模式的视图:

事件模式

在上图中,不同的颜色表示不同的事件。一种颜色的区块越长,说明在处理该事件的耗时就越长。单击某一区块,可以在下面的Summary概要中看到详细的事件处理过程及耗时分布。

事件类别

蓝色(Loading):网络通信和HTML解析
黄色(Scripting):JavaScript执行
紫色(Rendering):样式计算和布局,即重排
绿色(Painting):重绘
灰色(other):其它事件花费的时间
白色(Idle):空闲时间

在显示的记录中,浏览器也会为在检测过程中发现的一些可能导致性能问题的过程进行标注,在 Mode View视图区域,可能会出现一些红色的区块段,这些红色的区块段表明,在对应的时间上执行的事件可能存在性能问题,而在对应的 Main Thread视图区域,事件区块的右上角会出现红色的小三角,点击当前区块,在下面的 Summary概要区域内会给出详细的警告内容以及脚本可能出现问题的行数,如下图,浏览器提示“强制同步布局可能会导致性能瓶颈”:

性能问题标注

此外,在关闭Event Mode后,还可以看到Record Detail视图,详细列出一次记录中各类事件的详细内容。

Record Detail视图

Record Detail视图区域的左侧是事件标题,右侧是对应的时间线。点击每一条时间标题可以看到更多信息,如事件发生在脚本的哪一行等。如果你只对某一个时间段内的某些操作感兴趣,可以通过移动时间轴的始末位置来选择要浏览的区域:

选取一部分记录

帧模式 (Frame Mode)

帧模式从页面渲染性能的角度提供了数据支撑,一个柱状“frame”表示渲染过程中的一帧,也就是浏览器为了渲染单个内容块而必须要做的工作,包括:执行js,处理事件,修改DOM,更改样式和布局,绘制页面等。

如前文所述,我们的目标是保证页面要有高于每秒60fps(帧)的刷新频率,这样就能保证页面有高流畅度的渲染。

帧模式视图

中在Frame视图中有两条贯穿该视图的横线,分别标识出60FPS和30FPS的基准,按照前面提到的16.66ms的计算方式,我们可以理解为分别标识了16.6ms和33.3ms两个时间点。下面的一条是60FPS,低于这条线,可以达到每秒60帧;上面的一条是30FPS,低于这条线,可以达到每秒30次渲染。如果色柱都超过30FPS,这个网页就有性能问题了。

图中帧柱的高度表示了该帧的总耗时,帧柱中的颜色分别对应该帧中包含的不停类型的事件。每一帧柱的高度越低越好,上图是艺龙PC首页 www.elong.com的帧渲染图,从图中可以看出,在进行某些帧的渲染时,帧的渲染频率低于30FPS/s,第二帧和第三帧就大幅低于30fps(帧柱高度高于30fps标准线),在实际浏览器渲染中就有可能出现卡顿。对相关的帧进行分析时,可以点击其中某一帧查看渲染详情,也可以选择某个区域的几个帧查看渲染详情。而要找出可能影响性能的原因,点击当前问题帧,在 Summary面板及 Record Detail视图中的详细信息中进行逐条分析。

你可能注意到了在帧柱上存在灰色区域和空白区域,它们分别代表:
灰色区块:那些没有被DevTools感知到的活动
空白区块:显示刷新周期(display refresh cycles)中的空闲时间段

点击某一个帧柱还可以得到该帧的详细记录数据:

帧详情

Warning: 警告信息
ScreenShot: 当前选中帧的渲染截屏
Duration: 该记录及其子记录的总耗时
FPS: 当前帧的渲染频率
CPU Time: CPU耗时
Aggregated Time: 合计耗时分布

总结

发现问题是解决问题的第一步,chrome浏览器的TimeLine工具可以很好地辅助我们分析页面的性能瓶颈,提供详细全面的分析数据,为我们进行性能优化提供数据依据。当然,TimeLine中有用的功能还有很多,比如 Memery Mode, Screen Shot等,使用技巧多种多样,在这里主要介绍了如何去记录一段渲染过程,如何去使用 Event ModeFrame Mode去查看并分析得到性能指标,后续如果有新的体会和发现,还会再做记录~

TimeLine中的事件汇总

Loading事件

事件描述
Parse HTML浏览器执行HTML解析
Finish Loading网络请求完毕事件
Receive Data请求的响应数据到达事件,如果响应数据很大(拆包),可能会多次触发该事件
Receive Response响应头报文到达时触发
Send Request发送网络请求时触发

Scripting事件

事件描述
Animation Frame Fired一个定义好的动画帧发生并开始回调处理时触发
Cancel Animation Frame取消一个动画帧时触发
GC Event垃圾回收时触发
DOMContentLoaded当页面中的DOM内容加载并解析完毕时触发
Evaluate ScriptA script was evaluated.
Eventjs事件
Function Call只有当浏览器进入到js引擎中时触发
Install Timer创建计时器(调用setTimeout()和setInterval())时触发
Request Animation FrameA requestAnimationFrame() call scheduled a new frame
Remove Timer当清除一个计时器时触发
Time调用console.time()触发
Time End调用console.timeEnd()触发
Timer Fired定时器激活回调后触发
XHR Ready State Change当一个异步请求为就绪状态后触发
XHR Load当一个异步请求完成加载后触发

Rendering事件

事件描述
Invalidate layout当DOM更改导致页面布局失效时触发
Layout页面布局计算执行时触发
Recalculate styleChrome重新计算元素样式时触发
Scroll内嵌的视窗滚动时触发

Painting事件

事件描述
Composite LayersChrome的渲染引擎完成图片层合并时触发
Image Decode一个图片资源完成解码后触发
Image Resize一个图片被修改尺寸后触发
Paint合并后的层被绘制到对应显示区域后触发

参考文档

https://developers.google.com/chrome-developer-tools/docs/timeline

http://www.w3cfuns.com/article-1248-1.html

http://www.oschina.net/translate/performance-optimisation-with-timeline-profiles

Viewing all 148 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>