前端性能优化之路-数据存取小结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/s8460049/article/details/82632961

接着上一节讲的,我们说到过,性能优化的一大痛点就是IO读写,这一次我们讨论一下,数据读写的优化,数据存储的位置,介质决定了读取的速度。

主要指的是,应用内存(运行时内存),远程内存(redis等),本地文件系统(localstorage),远程文件系统(数据库等),这其中内存,还是文件系统,还有可能不同,内存可能有堆内存,栈内存,不同文件系统读取查找的算法,也会有相应影响,这里我们主要讨论前端,先介绍一些概念。

变量类型

字面量:
字面量只代表本身,不存储在特定的位置,主要有,字符串,数字,布尔值,对象,数组,函数,函数表达式,null,undefined
原始类型:
Undefined、Null、Boolean、Number 和 String,存储在栈中
引用类型:
主要是值对象,也就是通过new关键词创造出来的。指对象,在栈中存放引用地址,实际内存在堆中。原面说的其中三种原始类型,也有对应的引用对象,Boolean对象,Number对象,String对象。

作用域

作用域概念是理解javascript的关键所在,不仅仅从性能出发,还包括功能,作用域对javascript有许多影响,确定那些变量可以被函数访问,到确定this的赋值,然而要理解作用域对性能的影响,要先说一下作用域的工作原理。

作用域链

(出自高性能javascript)
每一个javascript的函数都是一个对象,是Function对象的一个实例,Function对象和其他对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供javascript引擎存取的内部属性,其中一个内部属性,[[scope]],内部属性[[scope]]包含了一个函数创建的作用域中对象的集合,这个集合被称为函数的作用域链,他决定了那些函数能被函数访问,函数的作用域中的每个对象被成为一个可变对象,每个可变对象都以键值对存在,他的作用域链会被创建此函数的作用域中可访问的数据对象所填充

这是个比较难懂的概念,我们通过一些图形来理解。

解析一下该图,
首先我们通过

function a(num1,num2){
    var sum = num1 + num2
    console.log(sum)
}

的方式声明了一个函数,我们上面说过了,函数不是一个原始类型,是一个引用类型,是Function的一个实例,那么在栈中存在一个地址,指向了堆里的一个内存,每个对象有一个内部属性,[[scope]],指向了他的作用域链,这是function a(){}这句话执行完后,作用域链的内存结构。
这里写图片描述

第二步,我们执行a(1,2)这个函数的时候,当函数执行过程中,也会创建一个执行环境(执行上下文)的内部对象,一个执行环境对应一个了函数执行时的环境,函数每次执行时,执行环境都是独一无二的,所以多次调用同一个函数,会导致创建多个执行环境,执行环境在函数执行完毕后会消失。
而每个执行环境都有自己的作用域链,用于解析标识符,当执行环境被创建时,他的作用域链初始化为当前运行函数的[[scope]]属性中的对象,这些值按照他们出现在函数中的顺序,被复制到执行环境的作用域链中,这个过程一旦完成,一个被称为‘活动对象’的新对象就为执行环境创建好了,活动对象作为函数运行时的变量对象,包含了所有的局部变量,命名参数,参数集合以及this,然后这个对象被推入作用域链的最前端,当执行环境被销毁,活动对象也随之被销毁。

另外一个概念就是执行环境是栈内存中的,当我们script标签执行的时候,会有一个全局的执行环境在栈中,然后在其中执行到一个函数的时候,这个函数的执行环境会被压入栈顶,当这个函数中又有执行某个函数时,被执行的函数的执行环境被压入栈顶,也就是说栈顶永远是最新的函数执行环境。

这里写图片描述

然后函数在执行的过程中,每遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取或者存储数据,该过程就是搜索执行环境的作用域链,查找同名的标识符。搜索过程中,从作用域链头部开始,也就是当前运行函数的活动对象,如果找到,就用这个,如果没找到,就搜索下一个对象,搜索过程会持续进行,若无法找到,则被视为是undefined,在函数执行过程中,每个标识符都会被搜索,就是这个搜索过程影响了性能,说了这么多,总算绕回来了,另外提一句,如果两个标识符的名字一样,会以先找到的那个为准,也就是说第一个遮挡了第二个。

标识符解析的性能

刚刚说了,搜索的过程就是影响性能的关键,其实不管是什么计算机操作,都会产生性能开销,在执行环境的作用域链中,一个标识符隐藏的位置越深,读写就越慢,因此在作用域链的越前端,越快,全局变量总是在最慢的。

举个简单的例子

function bindEvent(){
    while(i<10){
        document.getElementByid('btn'+i).onclick = funciton(){}
    }
}

这里document被用了10次,每次都会去经过一次完整的搜索,

function bindEvent(){
    let doc = document
    while(i<10){
        doc.getElementByid('btn'+i).onclick = funciton(){}
    }
}

简答改一下,这样,就只搜索了一次。当你的应用越大,数据库操作越多,性能优势自然越明显。

存储介质

这个的影响就不多说了,比较简单,localstorage这样的,Cookie持久化的,这些都是写到本地文件了,当你要去读的时候,首先要找到这个文件,然后从这个文件里面找到你要读取的内容,与内存中数据速度的区别就在于说,一个是存储在你大脑中,你可以直接想到,无法是想的时候,需要搜索你的大脑,而文件是你要搜索你的大脑,是放到什么文件里了,搜索到之后,去找到这个文件,把这个文件打开,阅读找到你要找的内容,两者的差距自然而语。

改变作用域链

  1. with(这个关键词,现在基本弃用了,就不做过多介绍了,有兴趣自己百度吧)
  2. tru catch,当在try中的语句执行发生异常的时候,执行过程会跳转到catch子句中,然后把异常对象推入到作用域链的顶部

动态作用域

  1. eval,同样可以动态指定变量,导致实际的变量名无法预测,通常非必要不建议使用。

闭包,作用域,内存

闭包可以说是javascript的一个强大特性,它允许函数访问局部作用域之外的数据,如今这个特性被广泛使用,然而,这个特性确有的性能问题。我们举两个闭包的场景。

function bindEvents(){
    var id = 'dom1'
    docuement.getElementById('btn').onclick = function(){
        triggerEvent('dom1')
    }
}
function fn1(){
    var id = 1
    return function(){
        console.log(id)
    }
}

bindEvents执行之后,会形成一个dom事件,当dom事件触发的时候,却能访问bindEvents函数执行之时执行环境的局部变量,这个时候bindEvents已经执行完,根据前面的介绍,这个时候执行环境消失了,但是dom事件却依然可以访问id这个变量,这个就是闭包,这个闭包能访问变量,必然是有原因的,我们通过内存图看一下。
这里写图片描述

可以看到,闭包的[[scope]]中,把它的上层函数的活动对象给保存下来了,所以在闭包执行的时候,闭包自己的执行环境又会创建一个活动对象,在原来的活动对象的上方,看到这里,大家看到问题所在了嘛,也就是说闭包的[[scope]]是保存了应该消失的执行环境内存,那么就增大了内存的开销,但是对性能的影响呢?
我们再来看,
这里写图片描述

结合图片和函数内容可以看到,在闭包执行的时候,会去访问两个标识符,一个是trggerEvent,和id,这两个标识符都在作用域链的后面位置,那么根据前面说的,每次访问的时候,都会去搜索查找,带来性能的消耗,所以跨作用域访问标识符会有性能损失,还会有内存问题。

当然我们可以通过前面说过的,将跨作用域的变量保存在局部,来减少消耗,不过如果只有一个读取的话,这个消耗就忽略不计了。

前面我们说到,访问对象成员的属性和方法比访问字面量的速度要慢,那么是为什么,这里也讨论一下,又要引入一些概念,

原型

JavaScript的对象是基于原型的,原型是所有对象的基础,它定义了并实现了一个新创建的对象所必须包含的成员列表,

对象通过一个内部属性[[proto]]绑定到它的原型,

var person = {
    name:'satisfy',
    sex:'man'
}

上述是一个对象,他是一个Object的实例,所以的它的[[proto]]就是Object.prototype
这里写图片描述

从图中可以看出,我们如果要访问对象的一个属性或者方法,它的解析过程和之前分析的变量解析过程是类似的,一个是在作用域链中找,一个是先在自己本身找,然后去原型上找,既然如此,就会有搜索的过程,就会有性能的消耗。

原型链

function Person(name,sex){
    this.name = name
    this.sex = sex
}
Person.prototype.say = function(){
    console.log(this.name)
}

var p1 = new Person('1','man')
var p2 = new Person('2','woman')

上述代码执行完成后,内存图如下:
这里写图片描述

我们从上图来分析原型链,
1. Person对应的绿色的线,是function本身的原型链
2. 红色的部分是Person实例的原型链,
3. 黑色部分是Person自己在内存中的对应情况

这个两个Person实例共享一个原型,他们有着各自的属性,也有相同的方法与属性,原型链上的所有的属性与方法都是可以供实例访问的,层级越深,找到它就越慢,不过随着javascript引擎的优化,这个速度代价已经越来越小了,但是这个代价是不能忽略的。

另外提一个,原型链上一定都是对象,原型链的终点是Object.prototype.proto == null,为什么是null,首先原型链上只能是对象,而null又是一个特殊的对象,表示没有指向任何内存的对象。

嵌套成员

因为对象实例还可能有对象,javascript引擎每次遇到点操作符,都会去搜索所有的成员来寻找,所以像这个操作
window.location.href
location.href
前者比后者就要费性能。

另外,大部分浏览器中,点表示法(object.name),扣号表示法(object[‘name’])没有明显区别,但是在safari中,点始终更快,不过这个可以忽略不计

缓存对象成员

同作用域链的原理,我们应该避免使用成员对象,同一个代码中,有一个对象属性被访问多次,我们应该避免使用每次都去查找,把他缓存在局部变量中,然后后面去使用该变量,来减少次数。

function(){
    var obj = {
        name:'name'
    }
    for(var i=0;i<10;i++){
        console.log(obj.name)//每次都要去查找
        var name = obj.name
        console.log(name)//只查找了一次
    }
}

总结

  1. 访问字面量和局部变量的速速最快,访问数组和对象速度相对较慢
  2. 由于局部变量存于作用域链的起始位置,因此访问局部变量比访问跨作用域链变量更快,变量在作用域链的位置越深,访问时长越长,由于全局变量总处在作用域链的最末端,因此访问速度也是最慢了。
  3. 避免使用with,try-catch要小心使用
  4. 嵌套对象的成员访问会影响性能,尽量少用
  5. 属性或者方法在原型链中的位置越深,访问越慢
  6. 通常来说,可以把常用访问的对象成员,数组元素,跨域变量保存在局部变量中来改善javascript性能,因为局部变量访问最快

上述全部遵循,可以大大提供大型web项目中javascript性能。

猜你喜欢

转载自blog.csdn.net/s8460049/article/details/82632961