JS从代码执行流程解析性能优化

JSBench

JSBench 是在线测试JS执行效率的网站。

其他工具还有 JSPerf ,但已停止维护。

在这里插入图片描述

使用 JSBench 进行测试的时候,建议只打开一个标签页,因为浏览器是支持多线程的,随着打开的标签页越来越多,其他标签页会抢占当前标签页的资源。

在运行测试的时候,最好保持在当前页面,或者说当前的进程不要关掉,因为操作系统是可以同时进行多件事情的,代码的执行是需要消耗时间的,如果将当前页面关闭(最小化)去做其他事情,它有可能会被挂起,等到再回来看的时候,计算的时间不一定是准确的。

另外做测试的时候,应该多执行几次脚本,不能用一次的结果作为最终结论。

注意,声明变量的时候使用 var,否则 JSBench 识别不了。

堆栈中代码执行流程

示例代码

let a = 10
function foo(b) {
  let a = 2
  function baz(c) {
    console.log(a+b+c)
  }
  return baz
}
let fn = foo(2)
fn(3)

流程图示

在这里插入图片描述

  • EC(Execution Context):执行上下文

    • 代码执行所在的词法作用域
  • Stack:栈

  • ECStack(Execution Context Stack):栈内存(执行环境栈),用于存储执行上下文

  • Heap:堆内存,允许程序在运行时动态申请空间存储数据

  • VO/AO:存放变量的对象

    • 在每一个上下文代码执行的时候,都可能会创建变量
    • 每个上下文中都会有一个存储当前上下文中变量的空间
    • 全局上下文称为VO(Variable Object),私有上下文中称为AO(Active Object,活动对象)
  • 字面量的值是基本数据类型,存放在栈内存,因为执行上下文也在栈内存,所以访问字面量数据速度很快

  • 函数属于引用数据类型,它需要在堆内存中申请一个空间去存储,然后将地址返回给上下文

    • 这就是为什么在使用引用类型的时候,如果嵌套层级很深,查询速度会很慢,因为变量会依次去每个层级的引用地址中查找。
  • 函数创建的执行上下文

    • 首先确定 this 的指向
    • 接着初始化作用域链
    • 然后才会初始化 AO
      • 初始化 arguments
      • 为参数变量赋值

流程介绍

  • JS 代码在开始执行后,首先会创建一个执行环境栈(栈内存),用于存放执行上下文
  • 首先会创建全局上下文,存放到执行环境栈,称为入栈
    • 首先初始化当前上下文的变量对象 VO
      • 基本数据类型直接存放在栈内存中,例如 全局变量 a
      • 引用数据类型要在堆内存申请空间去存储,例如 函数 foo
        • 函数在堆内存中存储的内容包括:
          • 函数的定义
          • 函数的形参
          • 函数的参数数量
        • 上下文中给函数变量赋值的就是堆内存中访问这个数据的地址 例如 foo = AB1
  • 当运行到函数调用的代码 foo(2),就会创建该函数的本地执行上下文,并入栈
    • 函数执行上下文中
      • 首先确定 this 的指向,foo 函数的 this 指向 window
      • 接着初始化作用域链
      • 然后初始化 AO
        • 初始化参数 arguments
        • 为参数变量赋值:b = 2
        • 初始化 baz 的时候又在堆内存开辟了空间去存储数据
      • 最终将 baz 的访问地址返回
    • 当前函数执行完毕,就回去判断是否产生了闭包
      • 由于外部调用的 fn 实际上就是函数内定义的 baz,baz 内部有使用了 foo 函数中定义的 a 变量,所以产生了闭包
      • 所以此时 foo 的上下文还不能被销毁,它会被下移
      • foo 中申请的堆内存 AB2 也不能被回收
  • 初始化 fn 的值是 baz 的访问地址,当 fn 被调用时,实际上就是 baz 被调用,于是继续创建 baz 的执行上下文并入栈
    • baz 函数执行上下文中初始化的过程同 foo 一样
    • 当执行到打印命令的时候,会从作用域链查找每个变量的值
  • 当 baz(3) 被执行完毕,判断没有产生闭包就会销毁 baz 的上下文(出栈)
  • 接着全局上下文出栈,AB2 被回收
  • foo(2) 的执行上下文出栈,AB1 被回收

汇总

  • JS 代码在开始执行后,首先会创建一个执行环境栈(栈内存),用于存放执行上下文
  • 首先会创建存储一个全局执行上下文,然后每当函数被调用时都会创建存储这个函数的本地执行上下文。
  • 上下文初始化变量时:
    • 基本数据类型的值直接存放在栈内存,由 JS 主线程进行回收(出栈)。
    • 引用类型的值存放在堆内存中,由 GC 进行回收。
  • 每个上下文的代码执行完成以后,由是否产生闭包来决定
    • 当前的上下文中引用的堆是否要释放掉
    • 当前上下文是否要出栈,不出栈就会下移

减少判断层级

在编写代码的过程中,有可能出现 if else 多层嵌套的场景,可以通过提前 return 无效的条件,减少判断和嵌套层级,达到优化效果。

// 多层嵌套
function doSomething(name, age) {
  const persons = ['张三', '李四', '王五', '赵六']
  if (name) {
    if (persons.includes(name)) {
      console.log('是名单上的人')
      if (age < 16) {
        console.log('年龄不达标')
      }
    }
  } else {
    console.log('请确认姓名')
  }
}
doSomething('李四', 6)
// 减少判断层级
function doSomething(name, age) {
  const persons = ['张三', '李四', '王五', '赵六']
  if (!name) {
    console.log('请确认姓名')
    return
  }
  if (!persons.includes(name)) return
  console.log('是名单上的人')
  if (age < 16) {
    console.log('年龄不达标')
  }
}
doSomething('李四', 6)

JSBench 测试:

在这里插入图片描述

减少判断层级本质上是解决问题的算法作了调整,与内存、作用域关系不大。

if else 更适合做区间性的条件判断,如果判断存在大量 else if 并且判断的值都是固定的,可以使用 switch,这样代码更简洁且便于维护。

减少作用域链查找层级

代码在访问变量的时候会根据作用域链一层一层去查找,减少作用域链的查找层级,可以提高代码运行的速度。

var name = 'Tom'

function foo() {
  name = 'Jack' // 全局的 name
  function baz() {
    var age = 30
    console.log(age)
    console.log(name)
  }
  baz()
}

foo()

这段代码一共创建了三个作用域:全局作用域 -> foo 作用域 -> baz 作用域

尽管 foo 作用域中为 name 变量重新赋值,但是 name 还是属于全局作用域。

在打印 name 的时候会先在 baz 作用域查找变量 name,接着去 foo 作用域查找,最后去全局作用域查找。

如果在 foo 作用域重新声明一个 name 变量,则会减少查找的层级,提高运行速度:

var name = 'Tom'

function foo() {
  var name = 'Jack' // 全局的 name
  function baz() {
    var age = 30
    console.log(age)
    console.log(name)
  }
  baz()
}

foo()

在这里插入图片描述

不过这种方式虽然提高了运行速度,但是又额外占用了内存,因为 foo 作用域的 name 变量会占用一部分内存。

如果 name 存放的数据量比较大,第一段代码相对会节省空间。

字面量与构造式

声明一个基本类型的数据,可以直接使用字面量,或使用构造函数创建。

使用构造函数,相当于增加一个函数调用的工作,所以速度相对慢很多。

并且构造函数创建的是一个对象类型,会占用更多的内存。

// 构造式
var str = new String('工作使我快乐')
console.log(str)
// 字面量
var str = '工作使我快乐'
console.log(str)

如果创建的是引用类型,效率也不会差很多。

var obj = {}
// or
var obj = new Object()

采用事件委托

事件委托的本质就是采用 JS 冒泡的机制把原本需要绑定在子元素的响应事件委托给了父元素,让父元素去完成事件的监听,这样可以大量减少内存的占用和事件的注册。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>遍历注册事件</title>
</head>
<body>
  <ul id="ul">
    <li>TOM</li>
    <li>18</li>
    <li>工作使我快乐</li>
  </ul>
  <script>
    function showText(e) {
      console.log(e.target.innerHTML)
    }
    var list = document.querySelectorAll('li')
    for (var item of list) {
      item.onclick = showText
    }
  </script>
</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>采用事件委托</title>
</head>
<body>
  <ul id="ul">
    <li>TOM</li>
    <li>18</li>
    <li>工作使我快乐</li>
  </ul>
  <script>
    function showText(e) {
      if (e.target.nodeName.toLowerCase() === 'li') {
        console.log(e.target.innerHTML)
      }
    }

    var ul = document.querySelector('ul')
    ul.addEventListener('click', showText, true)
  </script>
</body>
</html>

PS:不要用 JSBench 调试,会卡死。

其它优化方法

以下优化方法在 JSBench 测试执行速度上并没有明显提升或更快。

减少数据读取次数

在 JS 中经常使用的数据表现形式主要包括:字面量局部的变量数组元素对象属性

其中字面量局部的变量接存储在栈区,所以访问速度是最快的。

数组元素对象属性,访问速度相对较慢,这是因为访问它们需要按照引用关系,先找到它们在堆内存中的位置,而且对象属性的访问,往往还要考虑原型链上的查找。

与作用域链的道理一样,要减少查询时间的消耗,就应该尽量减少对象属性的查找次数属性的嵌套层级

常见的做法就是提前把对象的数据进行缓存,方便后续进行使用。

减少循环体中的活动

把每次循环都要操作的不变的数据,都放到循环外面完成。

减少声明及语句数

减少声明和语句数实际上是减少 JS 词法分析的工作。

JS 代码在运行之前会进行词法分析,将代码按照一定的规则拆分成多个词法单元,接着做语法分析,生成 AST 抽象语法树,最终将语法树转化成代码去执行。

选择

虽然有些方法可以提高JS的执行速度,但可能是建立在内存损失的前提下。

所以不但要考虑执行速度,也要考虑内存占用,是否易于维护和阅读等因素。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值