对函数缓存的理解
1. 什么是缓存?
函数缓存
是一种典型的用空间(内存)去换取时间(计算过程)
的性能优化技术,用于存储函数调用的结果,以便在相同输入
再次出现时直接返回缓存结果,而不是重新计算。这种技术可以显著提高程序性能,特别是在计算密集型
或频繁调用相同输入
的场景中。
函数缓存的工作原理
把参数和对应的结果数据存在一个对象中,调用时判断参数对应的数据是否存在,存在就返回对应的结果数据,否则就返回计算结果。
- 输入参数: 当一个函数被调用时,其输入参数会作为缓存的键。
- 计算结果: 如果缓存中不存在对应的结果,函数将执行并计算结果。
- 存储结果: 计算结果会被存储在缓存中,
键
是输入参数
,值
是计算结果
。 - 返回结果: 如果相同的输入参数再次调用该函数,直接从缓存中返回结果,而不需要再次计算。
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]
先我们分析⼀下这段代码中执⾏上下⽂栈和执⾏上下⽂的变化情况。
- 创建一个
全局执行上下文
,并将其推入执行上下文栈
。
ECStack = [
globalContext //变量 `object` 被声明并初始化为一个对象,该对象包含一个方法 `fn`。
]
- 当
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]]],
}
- 准备⼯作做完,开始执⾏函数,随着函数的执⾏,修改 AO 的属性值
fnContext = {
AO: {
arguments: {
length: 0
},
result: [],
i: 0
},
Scope: [AO, [[Scope]]],
}
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]
。此时虽然匿名函数Context
的AO
被销毁了,每次循环中依然维护着Scope: [result[i].AO, fnContext.AO, globalContext.VO]
,i
活在fnContext.AO
内存中,匿名函数可以通过作用于链找到它,正是因为 JavaScript 做到了这⼀点,从⽽实现了闭包这个概念。
所以,实践⻆度上闭包的定义:
- 即使创建它的上下⽂已经销毁,它仍然存在(⽐如,内部函数从⽗函数中返回);
- 在代码中引⽤了⾃由变量;
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
的计算结果,只有当data
或filter
发生变化时才会重新计算。- 这样避免了每次组件重新渲染时都执行昂贵的过滤和排序操作。
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