热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入了解JavaScript缓存

缓存是一种优化技术,通过存储开销大的函数执行的结果,并在相同的输入再次出现时返回已缓存的结果,从而加快应用程序的速度。

随着我们的应用程序的不断增长并开始进行复杂的计算时,对速度的需求越来越高,所以流程的优化变得必不可少。 当我们忽略这个问题时,我们最终的程序需要花费大量时间并在执行期间消耗大量的系统资源。

推荐教程:《Javascript视频教程》

缓存是一种优化技术,通过存储开销大的函数执行的结果,并在相同的输入再次出现时返回已缓存的结果,从而加快应用程序的速度。

如果这对你没有多大意义,那没关系。 本文深入解释了为什么需要进行缓存,缓存是什么,如何实现以及何时应该使用缓存。

什么是缓存

缓存是一种优化技术,通过存储开销大的函数执行的结果,并在相同的输入再次出现时返回已缓存的结果,从而加快应用程序的速度。

在这一点上,我们很清楚,缓存的目的是减少执行“昂贵的函数调用”所花费的时间和资源。

什么是昂贵的函数调用?别搞混了,我们不是在这里花钱。在计算机程序的上下文中,我们拥有的两种主要资源是时间和内存。因此,一个昂贵的函数调用是指一个函数调用中,由于计算量大,在执行过程中大量占用了计算机的资源和时间。

然而,就像对待金钱一样,我们需要节约。为此,使用缓存来存储函数调用的结果,以便在将来的时间内快速方便地访问。

缓存只是一个临时的数据存储,它保存数据,以便将来对该数据的请求能够更快地得到处理。

因此,当一个昂贵的函数被调用一次时,结果被存储在缓存中,这样,每当在应用程序中再次调用该函数时,结果就会从缓存中非常快速地取出,而不需要重新进行任何计算。

为什么缓存很重要?

下面是一个实例,说明了缓存的重要性:

想象一下,你正在公园里读一本封面很吸引人的新小说。每次一个人经过,他们都会被封面吸引,所以他们会问书名和作者。第一次被问到这个问题的时候,你翻开书,读出书名和作者的名字。现在越来越多的人来这里问同样的问题。你是一个很好的人,所以你回答所有问题。

你会翻开封面,把书名和作者的名字一一告诉他,还是开始凭记忆回答?哪个能节省你更多的时间?

发现其中的相似之处了吗?使用记忆法,当函数提供输入时,它执行所需的计算并在返回值之前将结果存储到缓存中。如果将来接收到相同的输入,它就不必一遍又一遍地重复,它只需要从缓存(内存)中提供答案。

缓存是怎么工作的

Javascript 中的缓存的概念主要建立在两个概念之上,它们分别是:

  • 闭包
  • 高阶函数(返回函数的函数)

闭包

闭包是函数和声明该函数的词法环境的组合。

不是很清楚? 我也这么认为。

为了更好的理解,让我们快速研究一下 Javascript 中词法作用域的概念,词法作用域只是指程序员在编写代码时指定的变量和块的物理位置。如下代码:

function foo(a) {
  var b = a + 2;
  function bar(c) {
    console.log(a, b, c);
  }
  bar(b * 2);
}

foo(3); // 3, 5, 10

从这段代码中,我们可以确定三个作用域:

  • 全局作用域(包含 foo 作为唯一标识符)
  • foo 作用域,它有标识符 abbar
  • bar 作用域,包含 c 标识符

仔细查看上面的代码,我们注意到函数 foo 可以访问变量 a 和 b,因为它嵌套在 foo 中。注意,我们成功地存储了函数 bar 及其运行环境。因此,我们说 barfoo 的作用域上有一个闭包。

你可以在遗传的背景下理解这一点,即个体有机会获得并表现出遗传特征,即使是在他们当前的环境之外,这个逻辑突出了闭包的另一个因素,引出了我们的第二个主要概念。

从函数返回函数

通过接受其他函数作为参数或返回其他函数的函数称为高阶函数。

闭包允许我们在封闭函数的外部调用内部函数,同时保持对封闭函数的词法作用域的访问

让我们对前面的示例中的代码进行一些调整,以解释这一点。

function foo(){
  var a = 2;

  function bar() {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();//2

注意函数 foo 如何返回另一个函数 bar。这里我们执行函数 foo 并将返回值赋给baz。但是在本例中,我们有一个返回函数,因此,baz 现在持有对 foo 中定义的bar 函数的引用。

最有趣的是,当我们在 foo 的词法作用域之外执行函数 baz 时,仍然会得到 a 的值,这怎么可能呢?

请记住,由于闭包的存在,bar 总是可以访问 foo 中的变量(继承的特性),即使它是在 foo 的作用域之外执行的。

案例研究:斐波那契数列

斐波那契数列是什么?

斐波那契数列是一组数字,以1 或 0 开头,后面跟着1,然后根据每个数字等于前两个数字之和规则进行。如

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

或者

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …

挑战:编写一个函数返回斐波那契数列中的 n 元素,其中的序列是:

[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …]

知道每个值都是前两个值的和,这个问题的递归解是:

function fibonacci(n) {
  if (n <= 1) {
    return 1
  }
  return fibonacci(n - 1) + fibonacci(n - 2)
}

确实简洁准确!但是,有一个问题。请注意,当 n 的值到终止递归之前,需要做大量的工作和时间,因为序列中存在对某些值的重复求值。

看看下面的图表,当我们试图计算 fib(5)时,我们注意到我们反复地尝试在不同分支的下标 0,1,2,3 处找到 Fibonacci 数,这就是所谓的冗余计算,而这正是缓存所要消除的。

function fibonacci(n, memo) {
  memo = memo || {}
  if (memo[n]) {
    return memo[n]
  }
  if (n <= 1) {
    return 1
  }

  return memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
}

在上面的代码片段中,我们调整函数以接受一个可选参数 memo。我们使用 memo 对象作为缓存来存储斐波那契数列,并将其各自的索引作为键,以便在执行过程中稍后需要时检索它们。

memo = memo || {}

在这里,检查是否在调用函数时将 memo 作为参数接收。如果有,则初始化它以供使用;如果没有,则将其设置为空对象。

if (memo[n]) {
  return memo[n]
}

接下来,检查当前键 n 是否有缓存值,如果有,则返回其值。

和之前的解一样,我们指定了 n 小于等于 1 时的终止递归。

最后,我们递归地调用n值较小的函数,同时将缓存值(memo)传递给每个函数,以便在计算期间使用。这确保了在以前计算并缓存值时,我们不会第二次执行如此昂贵的计算。我们只是从 memo 中取回值。

注意,我们在返回缓存之前将最终结果添加到缓存中。

使用 JSPerf 测试性能

可以使用些链接来性能测试。在那里,我们运行一个测试来评估使用这两种方法执行fibonacci(20) 所需的时间。结果如下:

哇! ! !这让人很惊讶,使用缓存的 fibonacci 函数是最快的。然而,这一数字相当惊人。它执行 126,762 ops/sec,这远远大于执行 1,751 ops/sec 的纯递归解决方案,并且比较没有缓存的递归速度大约快 99%。

注:“ops/sec”表示每秒的操作次数,就是一秒钟内预计要执行的测试次数。

现在我们已经看到了缓存在函数级别上对应用程序的性能有多大的影响。这是否意味着对于应用程序中的每个昂贵函数,我们都必须创建一个修改后的变量来维护内部缓存?

不,回想一下,我们通过从函数返回函数来了解到,即使在外部执行它们,它们也会导致它们继承父函数的范围,这使得可以将某些特征和属性从封闭函数传递到返回的函数。

使用函数的方式

在下面的代码片段中,我们创建了一个高阶的函数 memoizer。有了这个函数,将能够轻松地将缓存应用到任何函数。

function memoizer(fun) {
  let cache = {}
  return function (n) {
    if (cache[n] != undefined) {
      return cache[n]
    } else {
      let result = fun(n)
      cache[n] = result
      return result
    }
  }
}

上面,我们简单地创建一个名为 memoizer 的新函数,它接受将函数 fun 作为参数进行缓存。在函数中,我们创建一个缓存对象来存储函数执行的结果,以便将来使用。

memoizer 函数中,我们返回一个新函数,根据上面讨论的闭包原则,这个函数无论在哪里执行都可以访问 cache

在返回的函数中,我们使用 if..else 语句检查是否已经有指定键(参数) n 的缓存值。如果有,则取出并返回它。如果没有,我们使用函数来计算结果,以便缓存。然后,我们使用适当的键 n 将结果添加到缓存中,以便以后可以从那里访问它。最后,我们返回了计算结果。

很顺利!

要将 memoizer 函数应用于最初递归的 fibonacci 函数,我们调用 memoizer 函数,将 fibonacci 函数作为参数传递进去。

const fibOnacciMemoFunction= memoizer(fibonacciRecursive)

测试 memoizer 函数

当我们将 memoizer 函数与上面的例子进行比较时,结果如下:

memoizer 函数以 42,982,762 ops/sec 的速度提供了最快的解决方案,比之前考虑的解决方案速度要快 100%。

关于缓存,我们已经说明什么是缓存 、为什么要有缓存和如何实现缓存。现在我们来看看什么时候使用缓存。

何时使用缓存

当然,使用缓存效率是级高的,你现在可能想要缓存所有的函数,这可能会变得非常无益。以下几种情况下,适合使用缓存:

  • 对于昂贵的函数调用,执行复杂计算的函数。
  • 对于具有有限且高度重复输入范围的函数。
  • 用于具有重复输入值的递归函数。
  • 对于纯函数,即每次使用特定输入调用时返回相同输出的函数。

缓存库

  • Lodash
  • Memoizer
  • Fastmemoize
  • Moize
  • Reselect for Redux

总结

使用缓存方法 ,我们可以防止函数调用函数来反复计算相同的结果,现在是你把这些知识付诸实践的时候了。

更多编程相关知识,请访问:编程入门!!

以上就是深入了解Javascript 缓存的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • 本文探讨了如何在PHP与MySQL环境中实现高效的分页查询,包括基本的分页实现、性能优化技巧以及高级的分页策略。 ... [详细]
  • 利用无代码平台实现高效业务应用开发
    随着市场环境的变化加速,全球企业都在探索更为敏捷的应用开发模式,以便快速响应新兴的商业机遇。然而,传统的软件开发方式不仅成本高昂,而且耗时较长,这往往导致IT与业务部门之间的合作障碍,进而影响项目的成功。本文将探讨如何通过无代码开发平台解决这些问题。 ... [详细]
  • 本文探讨了程序员这一职业的本质,认为他们是专注于问题解决的专业人士。文章深入分析了他们的日常工作状态、个人品质以及面对挑战时的态度,强调了编程不仅是一项技术活动,更是个人成长和精神修炼的过程。 ... [详细]
  • 我的读书清单(持续更新)201705311.《一千零一夜》2006(四五年级)2.《中华上下五千年》2008(初一)3.《鲁滨孙漂流记》2008(初二)4.《钢铁是怎样炼成的》20 ... [详细]
  • Python 领跑!2019年2月编程语言排名更新
    根据最新的编程语言流行指数(PYPL)排行榜,Python 在2019年2月的份额达到了26.42%,稳坐榜首位置。 ... [详细]
  • 雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04
    雨林木风 GHOST XP SP3 经典珍藏版 YN2014.04 ... [详细]
  • 本文详细介绍如何在华为手机上安装鸿蒙3.0系统及下载兼容鸿蒙系统的新版应用,包括前期准备、升级途径以及应用下载的具体步骤。 ... [详细]
  • 扎根中国十年,这家海外互联网巨头的创新视频广告策略
    在过去十年间,许多海外互联网企业纷纷在中国设立分支机构,旨在挖掘这一市场的巨大潜力。然而,其中一些企业的布局却有着不同的考量。 ... [详细]
  • 本文探讨了在 PHP 的 Zend 框架下,使用 PHPUnit 进行单元测试时遇到的 Zend_Controller_Response_Exception 错误,并提供了解决方案。 ... [详细]
  • 探索OpenWrt中的LuCI框架
    本文深入探讨了OpenWrt系统中轻量级HTTP服务器uhttpd的工作原理及其配置,重点介绍了LuCI界面的实现机制。 ... [详细]
  • 本问题探讨了如何从给定的一个不含任何长度为2或以上的回文子串的字符串中,找到下一个同样不含此类回文子串的字典序字符串。字符串中的字符限定在英文前N个字母内。 ... [详细]
  • Python Selenium WebDriver 浏览器驱动详解与实践
    本文详细介绍了如何使用Python结合Selenium和unittest构建自动化测试框架,重点解析了WebDriver浏览器驱动的配置与使用方法,涵盖Chrome、Firefox、IE/Edge等主流浏览器。 ... [详细]
  • 本文总结了一次针对大厂Java研发岗位的面试经历,探讨了面试中常见的问题及其背后的原因,并分享了一些实用的面试准备资料。 ... [详细]
  • 随着技术的发展,Python因其高效性和灵活性,在多个领域得到了广泛应用,特别是在大数据处理和网络爬虫开发方面。本文将探讨学习Python是否能够胜任大数据和网络爬虫工程师的工作,并分析其职业前景。 ... [详细]
  • protobuf 使用心得:解析与编码陷阱
    本文记录了一次在广告系统中使用protobuf进行数据交换时遇到的问题及其解决过程。通过这次经历,我们将探讨protobuf的特性和编码机制,帮助开发者避免类似的陷阱。 ... [详细]
author-avatar
曉--伍_621
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有