随着我们的应用程序不断增长并开始执行复杂的计算,对速度的要求越来越高(🏎?),因此流程的优化成为必须是。当我们忽略这个问题时,我们最终得到的程序在执行过程中会花费大量时间并消耗大量系统资源。缓存是一种优化技术,它通过存储昂贵函数执行的结果并在再次出现相同输入时返回缓存的结果来加速应用程序。如果这对您来说意义不大,那没关系。本文深入解释了为什么需要缓存、什么是缓存、如何实现缓存以及何时应该使用缓存。什么是缓存缓存是一种优化技术,它通过存储昂贵函数执行的结果并在相同输入再次出现时返回缓存的结果来加速应用程序。在这一点上,我们很清楚缓存的目的是减少执行“昂贵的函数调用”所花费的时间和资源。什么是昂贵的函数调用?不要混淆,我们不是来这里花钱的。在计算机程序的上下文中,我们拥有的两个主要资源是时间和内存。因此,昂贵的函数调用是指由于计算量大而在执行过程中消耗大量计算机资源和时间的函数调用。然而,就像金钱一样,我们需要储蓄。为此,缓存用于存储函数调用的结果,以便将来快速轻松地访问。缓存只是保存数据的临时数据存储,以便可以更快地处理未来对该数据的请求。因此,当一个昂贵的函数被调用一次时,结果会存储在缓存中,这样每当在应用程序中再次调用该函数时,就会非常快速地从缓存中提取结果,而无需重新进行任何计算。为什么缓存很重要?下面是一个说明缓存重要性的示例:假设您正在公园里阅读一本封面吸引人的新小说。每次有人经过,都会被封面吸引,于是询问书名和作者。第一次被问到这个问题时,你会打开书并阅读书名和作者姓名。现在越来越多的人来到这里问同样的问题。你是一个好人🙂所以你回答了所有的问题。你会翻开封面告诉他书名和作者的名字,还是凭记忆开始回答?哪个会为您节省更多时间?发现相似之处?使用助记符,当函数提供输入时,它会执行所需的计算并将结果存储在缓存中,然后再返回一个值。如果以后收到相同的输入,它不必一遍又一遍地重复输入,只需要从缓存(内存)中提供答案即可。缓存如何工作JavaScript中的缓存概念主要建立在两个概念之上,它们是:闭包高阶函数(返回函数的函数)闭包闭包是函数和声明它的词法环境的组合。不是很清楚?我也这么认为。为了更好地理解,让我们快速了解一下JavaScript中词法作用域的概念。词法范围只是指程序员在编写代码时指定的变量和块的物理位置。如下代码:functionfoo(a){varb=a+2;functionbar(c){console.log(a,b,c);}bar(b*2);}foo(3);//3,5,10从这段代码中,我们可以识别出三个作用域:全局作用域(其中包含foo作为唯一标识符)foo作用域,其中包含标识符a、b和barbar作用域,其中包含标识符c仔细看在上面,我们注意到函数foo可以访问变量a和b,因为它嵌套在foo中。请注意,我们成功存储了函数bar及其执行环境。因此,我们说bar在foo的作用域上有一个闭包。你可以在遗传学的背景下理解这一点,在遗传学的背景下,个体即使在当前环境之外也有机会获得和展示遗传特征。这个逻辑突出了关闭的另一个因素,导致我们的第二个主要概念。通过接受其他函数作为参数或返回其他函数从函数返回函数的函数称为高阶函数。闭包允许我们在封闭函数之外调用内部函数,同时保持对封闭函数词法范围的访问让我们对前面示例中的代码进行一些调整来解释这一点。functionfoo(){vara=2;functionbar(){console.log(a);}returnbar;}varbaz=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,...]知道每个值都是前两个值的总和。这道题的递归解法是:functionfibonacci(n){if(n<=1){return1}returnfibonacci(n-1)+fibonacci(n-2)}确实简洁准确!但是,有一个问题。请注意,在终止递归之前达到n的值需要大量的工作和时间,因为对序列中的某些值进行了重复评估。看下图,当我们尝试计算fib(5)时,我们注意到我们反复尝试在不同分支的下标0,1,2,3处找到斐波那契数,这称为冗余计算,而这正是什么缓存是为了消除。函数fibonacci(n,memo){memomemo=memo||{}if(memo[n]){returnmemo[n]}if(n<=1){return1}returnmemo[n]=fibonacci(n-1,memo)+fibonacci(n-2,memo)}在上面的代码片段中,我们调整函数以接受可选参数memo。我们使用备忘录对象作为缓存来存储斐波那契数列,以它们各自的索引为键,以便在稍后的执行过程中需要时可以检索它们。memomemo=memo||{}在这里,检查调用函数时是否将memo作为参数接收。如果有,则初始化使用;如果不是,则将其设置为空对象。if(memo[n]){returnmemo[n]}接下来,检查是否有当前键n的缓存值,如果有,则返回它的值。和前面的解决方案一样,我们指定当n小于或等于1时终止递归。***,我们递归调用n值较小的函数,同时将缓存值(memo)传递给每个函数是在计算过程中使用。这确保我们不会在之前计算和缓存该值时再次执行如此昂贵的计算。我们只是从备忘录中取回值。请注意,我们在返回之前将最终结果添加到缓存中。使用JSPerf测试性能您可以使用这些链接来执行性能测试。在那里,我们运行一个测试来评估使用这两种方法执行fibonacci(20)所需的时间。结果如下:哇!!!令人惊讶的是,使用缓存的斐波那契函数是最快的。然而,这个数字相当惊人。它执行126,762ops/sec,这比1,751ops/sec的纯递归解决方案大得多,并且比没有缓存的递归快约99%。注:“ops/sec”表示每秒的操作次数,也就是期望在一秒内执行的测试次数。我们现在已经看到缓存在函数级别对应用程序性能的影响有多大。这是否意味着对于应用程序中的每个昂贵函数,我们都必须创建一个修改后的变量来维护内部缓存?不,回想一下,我们通过从函数返回函数了解到,即使在外部执行它们时,它们也会使它们继承父函数的范围,这使得将某些特征和属性从封闭函数传递给返回函数成为可能。使用函数的方法在下面的代码片段中,我们创建了一个高阶函数记忆器。使用此功能,可以轻松地将缓存应用于任何功能。上面,我们只需创建一个名为memoizer的新函数,该函数接受要缓存的函数fun作为参数。在函数中,我们创建一个缓存对象来存储函数执行的结果以备将来使用。从memoizer函数,我们返回一个新函数,根据上面讨论的闭包原则,无论它在何处执行,都可以访问缓存。在返回的函数中,我们使用if..else语句来检查是否已经存在指定键(参数)n的缓存值。如果有,请将其取出并返回。如果不是,我们使用一个函数来计算结果,以便将其缓存。然后我们使用适当的键n将结果添加到缓存中,以便以后可以从那里访问它。***,我们返回了计算结果。光滑的!要将memoizer函数应用于初始递归斐波那契函数,我们调用memoizer函数,将斐波那契函数作为参数传递。constfibonacciMemoFunction=memoizer(fibonacciRecursive)测试memoizer函数当我们将memoizer函数与上面的示例进行比较时,结果如下:memoizer函数提供最快的解决方案,速度为42,982,762ops/sec,比之前考虑的解决方案快100%快点。关于缓存,我们已经解释了什么是缓存,为什么会有缓存,缓存是如何实现的。现在让我们看看什么时候使用缓存。何时使用缓存当然,使用缓存是非常高效的,您现在可能希望缓存所有函数,这可能会变得非常无益。以下情况适合使用缓存:对于昂贵的函数调用,执行复杂计算的函数。适用于输入范围有限且高度重复的功能。对于具有重复输入值的递归函数。对于纯函数,即每次使用特定输入调用时返回相同输出的函数。缓存库LodashMemoizerFastmemoizeMoizeReselectforRedux总结使用缓存的方法,我们可以防止函数调用函数重复计算相同的结果,现在是你将这些知识付诸实践的时候了。
