web前端:高性能JavaScript阅读简记

 

1、Loading and Execution 加载和运行javascript

从加载和运行角度优化,源于JavaScript运行会阻塞UI更新,JavaScript脚本的下载、解析、运行过程当中,页面的下载和解析过程都会停下来等待,由于脚本可能在运行过程当中修改页面内容。前端

 

Script Positioning 脚本位置java

将<script>标签放在尽量接近<body>标签底部的位置,尽可能减小对页面下载的影响。web

 

Grouping Scripts 成组脚本编程

旨在减小http请求,将JavaScript脚本文件合并打包,能够经过打包工具实现(固然能够手动合并)或者实时工具,好比“Yahoo! 的 combo handler,任何网站经过一个“联合句柄”URL指出包含YUI文件包中的哪些文件,服务器收到URL请求时,将文件合并在一块儿后返回给客户端。segmentfault

 

Nonblocking Scripts 非阻塞脚本数组

页面加载完成以后,再加载JavaScript源码,也就是window的load事件发出后开始下载代码。浏览器

 

Deferred Scripts 延期脚本缓存

HTML4为<script>标签订义的扩展属性:defer。若是你为<script>指定defer属性,代表此脚本不打算修改DOM,代码能够稍后执行。IE4+/FF3.5+支持。具备defer属性的脚本,能够放在页面的任何位置,能够和页面的其余资源一期并行下载,但会在DOM加载完成,onload事件句柄被调用以前执行。服务器

Dynamic Script Elements 动态脚本元素

建立一个script元素,指定src属性,而后在页面加载完成以后添加到页面的任何地方。一个简单的通用demo:

function loadScript(url, callback) {

    var script = document.createElement("script") script.type = "text/javascript";

    if (script.readyState) { //IE

        script.onreadystatechange = function() {

            if (script.readyState == "loaded" || script.readyState == "complete") {

                script.onreadystatechange = null;

                callback();

            }

        };

    } else { //Others     

        script.onload = function() {

            callback();

        };

    }

    script.src = url;

    document.getElementsByTagName("head")[0].appendChild(script);

}

XMLHttpRequest Script Injection XHR 脚本注入

使用XMLHttpRequest对象,将脚本注入到页面,和动态脚本元素有相似之处,先建立XHR对象,而后经过get方法下载JavaScript文件,接着用动态<script>元素将JavaScript代码注入页面。

var xhr = new XMLHttpRequest();

xhr.open("get","file.js",true);

xhr.onreadystatechange = function () {

    if (xhr.readyState === 4) {

        if(xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){

            var script = document.createElement ("script");

            script.text = xhr.responseText; 

            document.body.appendChild(script); 

        }

    }

}

xhr.send(null);

因为JavaScript的同源策略,脚本文件必须和页面放置在同一个域内,不能经过CDN下载,所以不常见于大型网页。

 

Recommended Nonblocking Pattern 推荐的非阻塞模式

先加载一个加载器,而后加载JavaScript。

好比上文中提到的loadScript方法就能够封装为一个初级的加载器,而后经过loadScript方法来加载其余脚本。只不过这个微型加载器要保证依赖关系会比较丑:

 

loadScript("./file1.js", function () {

    loadScript("./file2.js", function () {

        loadScript("./file3.js", function () {

            //do something

        })

    })

})

再好比YUI3,好比lazyload.js,好比lab.js。

 

2、Data Acess 数据访问

数据存储在哪里,关系到代码运行期间数据被检索到的速度。JavaScript中有四种基本的数据存储位置:Literal values(直接量)、Variables(变量)、Array items(数组项)、Object members(对象成员)。对于直接量和局部变量的访问性能差别微不足道,性能消耗代价高一些的是全局变量、数组项、对象成员。

 

Managing Scope 管理做用

先了解一下做用域的原理

每个JavaScript函数都是一个对象,也能够叫函数实例,函数对象和其余对象同样,拥有编程能够访问的属性和不能被程序访问,仅供JavaScript引擎使用的内部属性,一种有一个叫[[Scope]]的属性。[[Scope]]中包含函数做用域中对象的集合(做用域链),它表示当前函数环境中可访问的数据,以链式的形式存在。当一个函数被建立后,做用域链中被放入可访问的对象。例如:

 

function add (a,b) {

    var result = a + b;

    return result;

}

此时做用域链中被推入一个可变的全局对象(随便取个名叫“房间A”),表明了全部全局范围中的变量,包含window、document、navigator等的访问接口。

在函数运行期间,函数内部会创建一个内部对象,称为运行期上下文。这个对象定义了函数运行时的环境,每次函数运行,这个上下文都是独一的,屡次调用函数就会屡次建立运行期上下文对象,函数执行完毕,这个上下文对象会被销毁。这个上下文环境也有本身的做用域链,用来解析标识符(理解为寻找变量),当一个运行期上下文被建立时,它的做用域链被初始化,函数自己的[[Scope]]属性中的对象,按照原来的顺序被复制到运行期上下文的做用域链中。此时运行期上下文会建立一个新的对象,名叫“激活对象(取名叫“房间B”)”,“房间B”中存储了全部的局部变量、命名参数、参数集合和this的接口。而后“房间B”被推入到做用域链的前端。在刚刚所说的可变全局对象(“房间A”)的前面。

 

函数过程当中,每遇到一个变量,标识符识别过程都要决定从哪里得到或者存储数据。它会搜索运行期上下文的做用域链,查找同名的标识符,搜索工做从做用域链的前端开始查找,也就是刚才的“房间B”那里查找,若是找到了,就是用对应的变量值,若是没找到就进入“房间A”进行查找,若是找到,就用对应的值,没有找到就认为这个标识符是未定义的("undefined");

在以前的add函数运行过程当中,result/a/b三个变量的查找实际上都进行了上述的搜索过程,所以产生性能问题。当一个标识符所处位置越深,读写速度就越慢,因此函数中局部变量的访问速度是最快的,全局变量一般很慢,由于全局变量老是处于做用域链最后一个位置,前面的房间都找过了,没找到,才会过来他这里找。所以,就有了优化性能的办法:

 

用局部变量存储本地范围以外的变量值(若是这个变量值被屡次使用)

好比:

 

function foo() {

    var a = document.getElementById("a"),

        b = document.getElementsByTagName("div");

}

这时候document被查找了两次,并且每次都要先找“房间B”,再找“房间A”才能找到,这时候就能够用一个局部变量暂存document:

 

function foo() {

    var doc = document,

        a   = doc.getElementById("a"),

        b   = doc.getElementsByTagName("div");

}

减小使用动态做用域(Dynamic Scopes)

with()

with能够临时改变函数的做用域链,在某些特殊场景下,能够加快一些变量的访问。好比一个函数内屡次使用document:

 

function foo() {

    var a = document.getElementById("a"),

        b = document.getElementsByTagName("div");

    console.log(a.className);

}

 

能够改写为:

 

function foo() {

    with(document){

        var a = getElementById("a"),

            b = getElementsByTagName("div");

        console.log(a.className);

    }

}    

在这里,document对象以及document对象全部的属性,都被插入到做用域的最前端,页面在寻找"getElementById"方法是会首先从document对象属性中寻找,而不须要从foo()的做用域中查找,而后再到全局做用域中进行查找,下降了二次查找的消耗。可是在document对象的属性被推入做用域链的最前端的同时,其余局部变量都被推入做用域链第二的位置。上例中,在查找a的时候,会先从document对象属性中查找,没有才会从foo()的做用域中进行查找。这样带来的性能消耗每每得不偿失。所以with必须慎用,只有在极个别的场景中才划算。

try-cahch

当try中程序块发生错误而转入catch块中时,程序会自动将异常对象推入做用域链的最前端。一样会改变做用域链,带来性能问题。所以在不得不用try-catch语句的时候,能够采用下面的操做方式:

 

try{

    //do something

}catch(e){

    handleError(e);

}

在catch块中运行错误处理函数,将错误对象做为参数传给错误处理函数,catch块中做用域链的改变就没什么影响了。

others

还有一些其余的状况,好比:

 

function foo(f){

    (f);

    function returnWindow(){

        return window;

    }

    var s = returnWindow();

}

 

正常状况下,上述函数window就是window,可是若是咱们执行:

 

foo("var window = 'I am not window';");

这时候的window就再也不是那个window了。性能上的问题不说,只是变量做用域变得不可控了,带来其余的问题。同时,在一些现代浏览器中,好比Safari的Nitro引擎中,会铜鼓分析代码来肯定哪些变量应该在任意时刻被访问,绕过传统做用域链查找,用标识符索引的方式快速查找,以此来加快标识符识别过程。可是遇到动态做用域的时候,引擎须要切回慢速的基于哈希表的标识符识别方法,这里的浏览器引擎作的努力就没办法了。

 

closures 慎用闭包

慎用闭包有两个方面的缘由。一是闭包必然存在函数嵌套,闭包内访问外部变量都会通过最少两次的查找。更重要的问题在于,闭包须要访问外部变量,所以致使函数运行期的激活对象被保存,没法销毁。引用始终存在于闭包的[[Scope]]属性中,不只消耗更多的内存开销,在IE中还会致使内存泄露。

 

Object Members 对象成员

JavaScript中一切皆对象,对象的命名成员能够包含任意数据类型,固然就能够包含函数。这里所说的对象成员,指的就是函数对象,函数对象的访问速度,比直接亮和局部变量要慢,某些浏览器的实现中,甚至比数组还要慢。找到优化办法以前,须要先了解缘由。

 

Prototypes 原型

JavaScript中的对象是基于原型的,原型是对象的基础,定义并实现了一个新对象所必须具备的成员。原型对象为全部给定类型的对象实例共享,全部的实例共享原型对象的成员。一个对象经过一个内部属性绑定到本身的原型,在FF/Safari/Chrome中,这一对象被称为_proto_,任什么时候候建立一个内置类型的实例,这些实例将自动拥有一个Object做为他们的原型。

所以一个对象拥有成员能够分为两类:实例成员(own成员)和原型成员。实例成员直接存在于实例自身,而原型成员则从对象成员继承。例:

 

var cat = {

    name:"xiaohua",

    age:1

}

 

在这里,cat的实例成员就是name和age,原型成员就是cat._proto_中的成员属性,而cat._proto_属性是Object.prototype,在这里就是Object,以下调用时:

 

console.log(cat.name);

在调用cat.name属性时,如今cat实例成员中查找,若是调用cat.toString()方法时,一样先在cat的实例成员中查找,找不到的时候再到其原型成员中查找,和处理变量的过程相似,一样也就致使了性能问题。

 

Prototype Chains 原型链

对象的原型决定了一个实例的类型,默认状况下,全部对象都是Object的实例,并继承了全部基本方法,当咱们使用构造器建立实例时,就建立了另一种类型的原型。

function Animal(name,age){

    this.name = name,

    this.age = age

}

Animal.prototype.sayHello = function(){

    console.log("Hello,I am a " + this.name);

}

var cat = new Animal("cat",1);

var dog = new Animal("dog",1);

cat是Animal的实例,cat._proto_是Animal.prototype,Animal.prototype._proto_是Object;dog和cat共享一个原型链,但各自拥有本身的实例成员name和age。若是咱们调用了cat.toString()方法时,搜索路径以下:

 

cat---cat._proto_(Animal.prototype)---cat._proto_.constructor(Animal)---cat._proto_.Constructor._proto_(Object);

原型链每深刻一个层级,就会带来更大的性能消耗,速度也就会更慢。而对于实例成员的搜索开销自己就大于访问直接量或者是局部变量,所以这种性能消耗仍是很值得去优化的。

 

Nested Members 嵌套成员

例如:window.local.href ;每遇到一个 . ;JavaScript引擎就会在该对象成员上执行一次解析过程。好比若是href并非local的实例属性,解析引擎就会去local的原型链去进行搜索,由此带来严重的性能消耗。

Caching Object Member Values 缓存对象成员的值

上述的性能问题都是和对象成员有关,所以要尽可能避免对对象成员的搜索,好比:

function foo(ele,className1,className2) {

    return ele.className == className1 || ele.className == className2;

}

在这里,咱们访问了两次ele的className属性,可是这两次访问时,ele的className属性值是同样的,所以能够在这里用一个变量暂存ele.className的值,避免两次访问致使的两次搜索过程。处理嵌套属性更须要用这种办法来处理。

 

针对数据访问致使的相关性能问题,主要的解决办法就是对数据进行暂存,好比将全局变量暂存为局部变量,减小做用域链的深刻搜索;将实例的属性暂存,减小对原型链的屡次深刻搜索;另外一个就是减小使用动态做用域和闭包。

 

l  转自:https://segmentfault.com/a/1190000010453813

l  做者:前端老李

|  知海匠库web前端系统课程学习:http://www.zhihaijiangku.com

相关文章
相关标签/搜索