likes
comments
collection
share

对函数缓存的理解

作者站长头像
站长
· 阅读数 62

1. 什么是缓存?

函数缓存是一种典型的用空间(内存)去换取时间(计算过程)的性能优化技术,用于存储函数调用的结果,以便在相同输入再次出现时直接返回缓存结果,而不是重新计算。这种技术可以显著提高程序性能,特别是在计算密集型频繁调用相同输入的场景中。

函数缓存的工作原理

把参数和对应的结果数据存在一个对象中,调用时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果。

  1. 输入参数: 当一个函数被调用时,其输入参数会作为缓存的键。
  2. 计算结果: 如果缓存中不存在对应的结果,函数将执行并计算结果。
  3. 存储结果: 计算结果会被存储在缓存中,输入参数计算结果
  4. 返回结果: 如果相同的输入参数再次调用该函数,直接从缓存中返回结果,而不需要再次计算。

2.为什么需要做函数缓存?

  • 提高性能: 减少重复计算,特别是对于计算密集型函数。
  • 减少资源消耗: 减少对资源的重复访问,如数据库查询、网络请求等。
  • 优化递归函数: 对于递归函数,如计算斐波那契数列,可以避免大量重复计算。

3.JS实现函数缓存

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

  • 闭包
  • 高阶函数

3.1 闭包

闭包 = 函数 + 函数能够访问的⾃由变量

var object = {
    fn: function (){
        let result = new Array()
        for(var i =0; i < 10; i++) {
            result[i] = function (num) {
                return num;
            }(i)
        }
        return result
    }
}
          
console.log(object.fn());
//[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

先我们分析⼀下这段代码中执⾏上下⽂栈和执⾏上下⽂的变化情况。

  1. 创建一个全局执行上下文,并将其推入执行上下文栈
ECStack = [
    globalContext //变量 `object` 被声明并初始化为一个对象,该对象包含一个方法 `fn`。
]
  1. object.fn()被调用时,会创建一个新的函数执行上下文,并将其推入执行上下文栈
ECStack = [
    fnContext,
    globalContext
]

3.fn函数并不⽴刻执⾏,开始做准备⼯作。

//第⼀步:复制函数`[[scope]]属性`创建作⽤域链
fnContext = { 
    Scope: fn.[[scope]], 
}
//第⼆步:⽤ arguments `创建活动对象`,随后`初始化活动对象`,加⼊`形参`、`函数声明`、`变量声明`
fnContext = {
    AO: {
        arguments: {
            length: 0
        },
        result: undefined,
        i: undefined,
    },
    Scope: fn.[[scope]],
}
//第三步:将`活动对象`压⼊ fn 函数作⽤域链顶端
fnContext = {
    AO: {
        arguments: {
            length: 0
        },
        result: undefined,
        i: undefined
    },
    Scope: [AO, [[Scope]]],
}
  1. 准备⼯作做完,开始执⾏函数,随着函数的执⾏,修改 AO 的属性值
fnContext = {
    AO: {
        arguments: {
            length: 0
        },
        result: [],
        i: 0
    },
    Scope: [AO, [[Scope]]],
}
  1. for循环中result[0]被调用时,会创建一个新的匿名函数执行上下文,并将其推入执行上下文栈
ECStack = [
    匿名函数Context,
    fnContext,
    globalContext
]

result[0] 函数的作⽤域链为:

result[0]Context = {
    AO: {
        arguments: { 
            num: 0
            length: 1, 
        },
    },
    Scope: [AO, fnContext.AO, globalContext.VO]
}

在每次循环中,匿名函数 function(num) { return num; } 被立即调用,并传入当前的 i 值作为参数 num。由于这个匿名函数是立即调用的,它会创建一个新的函数执行上下文,然后立即被销毁。匿名函数执行并返回 num 的值,然后将这个值赋给 result[i]。此时虽然匿名函数ContextAO被销毁了,每次循环中依然维护着Scope: [result[i].AO, fnContext.AO, globalContext.VO]i活在fnContext.AO内存中,匿名函数可以通过作用于链找到它,正是因为 JavaScript 做到了这⼀点,从⽽实现了闭包这个概念。

所以,实践⻆度上闭包的定义:

  1. 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回);
  2. 在代码中引⽤了⾃由变量;

ES5只有函数作用域全局作用域, 这带来很多不合理的场景。比如:1. 内层变量可能会覆盖外层变量;2. 用来计数的循环变量泄露为全局变量。此时可能会需要用到闭包。

不过ES6新增了let const 关键字,有独特的块级作用域,因此,块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式不再必要了。

3.2 柯里化

function currying 把接受多个参数的函数转换成接受一个单一参数的函数

// 非函数柯里化
var add = function (x,y) {
    return x+y;
}
add(3,4) //7

// 函数柯里化
var add2 = function (x) {
    //**返回函数**
    return function (y) {
        return x+y;
    }
}
add2(3)(4) //7

在上面的例子中,我们将多维参数的函数拆分,先接受第一个函数,然后返回一个新函数,用于接收后续参数。

就此,我们得出一个初步的结论:柯里化后的函数,如果形参个数等于实参个数,返回函数执行结果,否者,返回一个柯里化函数。

3.3 高阶函数

通过接收其他函数作为参数或返回其他函数的函数

下面咱们来看一个高阶函数和闭包结合的小例子:

var add = function() {
    var num = 0
    return function(a) {
        return num = num + a
    }
}
add()(1)      // 1
add()(2)      // 2

注意:这里的两个add()(1)和add()(2)不会互相影响,可以理解为每次运行add函数后返回的都是不同的匿名函数,就是每次add运行后return的function其实都是不同的,所以运行结果也是不会影响的。主要是利用闭包来保持着作用域。

如果换一种写法,比如:

var add = function() {
    var num = 0
    return function(a) {
        return num = num + a
    }
}
var adder = add()
adder(1); // 1
adder(2); // 3

这样的话就会在之前运算结果基础上继续运算,意思就是这两个 adder 运行的时候都是调用的同一个 num。

最终实现

这里可以利用高阶函数的思想来实现一个简单的缓存,在函数内部用一个对象存储输入的参数,如果下次再输入相同的参数,那就比较一下对象的属性,把值从这个对象里面取出来,不必再继续运行,这样就极大的节省了客户端等待的时间。

  const memorize = function(fn) {
    const cache = {}       // 存储缓存数据的对象
    return (...key) => {        // 这里用到数组的扩展运算符
      return cache[key] || (cache[key] = fn.apply(fn, key))  // 如果已经缓存过,直接取值。否则重新计算并且缓存
    }
  }
  
  const add = function(a, b) {
    return a + b
  }

  const adder = memorize(add)
  adder(100,200)
  adder(100,200) // 缓存得到的结果

4. React中函数缓存的应用体现

在React中,函数缓存可以显著优化组件的性能。特别是当组件需要执行昂贵的计算或处理大量数据时,通过缓存函数结果可以避免不必要的重复计算,提升应用的响应速度和性能。 React对函数缓存的应用主要体现在以下几个方面:

4.1 useMemo

  • useMemo 用于记忆计算结果,只有当依赖项发生变化时才会重新计算。
  • 适用于需要进行昂贵计算的情况,比如处理大量数据或复杂计算。

示例:使用 useMemo 进行函数缓存

假设有一个组件需要对大量数据进行过滤和排序:

import React, { useState, useMemo } from 'react';

const ExpensiveComponent = ({ data }) => {
  const [filter, setFilter] = useState('');

  // 使用 useMemo 缓存计算结果
  const filteredData = useMemo(() => {
    console.log('Expensive computation');
    return data
      .filter(item => item.includes(filter))
      .sort();
  }, [data, filter]);

  return (
    <div>
      <input
        type="text"
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="Filter"
      />
      <ul>
        {filteredData.map(item => (
          <li key={item}>{item}</li>
        ))}
      </ul>
    </div>
  );
};

export default ExpensiveComponent;

在这个示例中:

  • useMemo 缓存了 filteredData 的计算结果,只有当 datafilter 发生变化时才会重新计算。
  • 这样避免了每次组件重新渲染时都执行昂贵的过滤和排序操作。

4.2 useCallback

  • useCallback 用于记忆函数定义,只有当依赖项发生变化时才会重新创建函数。
  • 适用于需要将回调函数传递给子组件以防止不必要的重新渲染。

示例:使用 useCallback 进行函数缓存

假设有一个父组件需要将回调函数传递给子组件:

import React, { useState, useCallback } from 'react';

const ChildComponent = React.memo(({ onClick }) => {
  console.log('Child component rendered');
  return <button onClick={onClick}>Click me</button>;
});

const ParentComponent = () => {
  const [count, setCount] = useState(0);

  // 使用 useCallback 缓存回调函数
  const handleClick = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Count: {count}</p>
      <ChildComponent onClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

在这个示例中:

  • useCallback 缓存了 handleClick 函数,只有当依赖项发生变化时才会重新创建。
  • 因为 handleClick 函数地址没有变化,ChildComponent 不会因为父组件的重新渲染而重新渲染,避免了不必要的渲染。

4.3 使用自定义缓存 Hook

有时,我们需要自定义的缓存机制来满足特定需求。可以创建一个自定义 Hook 来实现。

import { useState, useRef, useEffect } from 'react';

function useCache(key, compute, dependencies) {
  const cache = useRef({});

  useEffect(() => {
    if (!cache.current[key] || dependencies.some(dep => cache.current[key].deps.includes(dep))) {
      cache.current[key] = {
        value: compute(),
        deps: dependencies
      };
    }
  }, [key, compute, dependencies]);

  return cache.current[key].value;
}

// 使用自定义缓存 Hook 的组件
const CachedComponent = ({ num }) => {
  const expensiveCalculation = (n) => {
    console.log('Calculating...');
    return n * n;
  };

  const result = useCache('expensiveCalculation', () => expensiveCalculation(num), [num]);

  return (
    <div>
      <p>Result: {result}</p>
    </div>
  );
};

在这个示例中:

  • useCache 是一个自定义 Hook,用于缓存计算结果。
  • num 变化时,useCache 会重新计算并缓存结果。
  • 使用这种方式可以灵活实现多种缓存策略。

通过这些方法,React能够有效地缓存函数结果和回调函数,优化组件性能,减少不必要的重新渲染和重复计算。

转载自:https://juejin.cn/post/7379060488352481290
评论
请登录