个人博客:www.vino.wang
实现一个计算斐波那契数列函数
const fibonacci = (n) => {
if (n <= 1) return n
return fibonacci(n - 1) + fibonacci(n - 2)
}
['first','second','third'].forEach(item =>{
console.log(item,fibonacci(40))
})
将上述代码复制到浏览器控制台执行,你会发现一次计算会比较耗时,并且三次执行的耗时是非常接近的。
我们需要做如下优化:执行三次fibonacci(40) 的结果是相同的,但是三次执行消耗了三倍的世间,能不能优化成执行三次,但是只消耗一次执行的时间呢?
Memo 函数
对于上述的需求,我们可以考虑实现一个memo函数,这类似于 Vue的计算属性或者是React.memo的功能。
💡 memo 函数是一个记忆化(memoization)工具,主要用于性能优化。它的主要功能包括:
- 记住上一次计算的结果和依赖项
- 只有当依赖项发生变化时才重新计算
- 如果依赖项没有变化,直接返回缓存的结果
function memo(getDeps,fn) {
let prevDeps
let prevResult
return () => {
// 获取新的依赖项
const newDeps = getDeps() // 返回依赖的数组
// 如果是第一次运行,或者依赖项发生变化
const depsChanged =
!prevDeps ||
newDeps.length !== prevDeps.length ||
newDeps.some((dep, index) => dep !== prevDeps[index])
// 如果依赖项没变,返回缓存的结果
if (!depsChanged) {
return prevResult
}
// 依赖项变化,重新计算
prevDeps = newDeps
prevResult = fn(...newDeps)
return prevResult
}
}
添加memo函数,并对调用做如下修改:
const memoizedFibonacci = memo(() => [40], fibonacci)
['first','second','third'].forEach(item =>{
console.log(item,memoizedFibonacci())
})
复制上述代码在浏览器执行。肉眼可见第二次和第三次执行的速度是非常快的。
-
Typescript 版本
export function memo<TDeps extends any[], TResult>( getDeps: () => TDeps, fn: (...args: TDeps) => TResult, ) { let prevDeps: TDeps | undefined let prevResult: TResult | undefined return () => { // 获取新的依赖项 const newDeps = getDeps() // 如果是第一次运行,或者依赖项发生变化 const depsChanged = !prevDeps || newDeps.length !== prevDeps.length || newDeps.some((dep, index) => dep !== prevDeps![index]) console.log('depsChanged', depsChanged) // 如果依赖项没变,返回缓存的结果 if (!depsChanged) { return prevResult! } // 依赖项变化,重新计算 prevDeps = newDeps prevResult = fn(...newDeps) return prevResult } }
你理解闭包了吗
有没有发现,memo函数运用到的最核心的技巧就是闭包,通过闭包保存了 prevDeps 和 prevResult 的状态。
💡 什么是闭包?
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
在memo 函数中,闭包的关键作用是状态保持
export function memo<TDeps extends any[], TResult>() {
// 这两个变量通过闭包被保存在内存中
let prevDeps: TDeps | undefined
let prevResult: TResult | undefined
return () => {
// 内部函数可以访问并修改这些变量
// ...
}
}
闭包使得:
- prevDeps 和 prevResult 在函数多次调用之间保持状态
- 这些变量对外部不可见,形成了私有变量
- 返回的函数可以持续访问这些变量
实际例子
// 使用示例
const calculateSum = memo(
() => [numbers],
(numbers) => numbers.reduce((a, b) => a + b, 0)
)
// 第一次调用
calculateSum() // 会计算并缓存结果
// 第二次调用
calculateSum() // 如果 numbers 没变,直接返回缓存结果
如果不使用闭包,我们就需要:
- 要么使用全局变量(不安全)
- 要么每次都重新计算(低效)
- 要么需要用户手动管理缓存(麻烦)
闭包的好处
-
封装性
-
缓存变量完全私有
-
外部无法直接修改 prevDeps 和 prevResult
-
状态隔离
// 每次调用 memo 都会创建独立的闭包 const sum1 = memo(/*...*/) const sum2 = memo(/*...*/) // sum1 和 sum2 的缓存是相互独立的 -
内存管理
-
当不再使用 memo 返回的函数时
-
闭包中的变量会被垃圾回收
-
不会造成内存泄漏
这就是为什么闭包在这种缓存场景中特别有用,它提供了一种优雅的方式来维护函数的内部状态。
Tanstack/table的实际应用
在 tanstack/table 源代码里面,运用了大量的 memo函数,用于表格组件的列计算,排序,过滤,分组等操作。通过记忆优化可显著的提升性能。
// packages/table-core/src/core/table.ts 部分源代码
import { createRow } from '../core/row'
import { Table, Row, RowModel, RowData } from '../types'
import { getMemoOptions, memo } from '../utils'
export function getCoreRowModel<TData extends RowData>(): (
table: Table<TData>
) => () => RowModel<TData> {
return table =>
memo(
() => [table.options.data],
(
data
): {
rows: Row<TData>[]
flatRows: Row<TData>[]
rowsById: Record<string, Row<TData>>
} => {
//.....
return rows
}
rowModel.rows = accessRows(data)
return rowModel
},
getMemoOptions(table.options, 'debugTable', 'getRowModel', () =>
table._autoResetPageIndex()
)
)
}
完。