
您听说过memoization
吗? 顺便说一句,这是一件非常简单的事情-只是记住您从第一个函数调用中获得的结果,并使用它而不是第二次调用它-不要无故调用真实的东西,不要浪费时间。
跳过一些繁琐的操作是一种非常常见的优化技术。 每次您可能不做某事-不要做。 尝试使用缓存memcache
, file cache
, local cache
-任何缓存! 后端系统的必备组件,过去和现在的任何后端系统的关键部分。

记忆与缓存
记住就像缓存。 只是有些不同。 不缓存,我们称它为kashe。
长话短说,但是记忆不是缓存,不是持久缓存。 它可能是在服务器端,但不能,并且不应是客户端的缓存。 更多有关可用资源,使用模式和使用原因的信息。
问题-缓存需要一个“缓存键”
高速缓存使用字符串高速缓存key
存储和获取数据。 构造一个唯一且可用的密钥已经是一个问题,但是随后您必须对数据进行序列化和反序列化,以再次存储在基于字符串的介质中……简而言之,高速缓存的速度可能不如您想象的那样快。 特别是分布式缓存。
备注不需要任何缓存键
同时-无需密钥即可记忆。 通常*它按原样使用参数,而不是尝试从中创建单个键,并且不像缓存通常那样使用某些全局可用的共享对象来存储结果。
备注和缓存之间的区别在于API接口 !
通常*并不表示总是。 Lodash.memoize默认情况下使用JSON.stringify
将传递的参数转换为字符串缓存(还有其他方法吗?否!)。 只是因为他们将使用此键来访问内部对象,并保存一个缓存的值。 快速存储 ,“最快的可能的存储库”,也可以做到这一点。 这两个命名库都不是备注库,而是缓存库。
值得一提的是,您要记住,JSON.stringify的速度可能比函数慢10倍。
显然-解决该问题的简单方法是不使用缓存键,也不使用该键访问某些内部缓存。 所以-记住最后一个被调用的参数。 像回忆或重新选择一样。
Memoizerific可能是您要使用的唯一通用缓存库。
缓存大小
所有库之间的第二个大区别是关于缓存大小和缓存结构。
您是否曾经想过-为什么reselect
或memoize-one
仅保留一个最后的结果? 不要“不要使用缓存键来存储一个以上的结果” ,而是因为没有理由要存储多个结果而不只是最后一个结果 。
...更多关于:
- 可用资源-单个缓存行非常资源友好
- 使用模式-记住“就地”的东西是一个很好的模式。 “就地”通常只需要一个,最后一个结果。
- 使用-模块化,隔离和内存安全的原因是很好的原因。 就缓存冲突而言,不与应用程序的其余部分共享缓存只会更安全。
一个结果?
是的-唯一的结果。 用一个结果来记忆一些经典的事情是不可能的,例如记忆斐波那契数的产生( 您可能在每篇关于记忆的文章中都可以找到例子 )。 但是,通常情况下,您在做其他事情-前端需要谁做斐波那契? 在后端? 现实世界中的例子离抽象的IT测验还很远。
但是,关于单值备忘录类型仍然存在两个BIG问题。
问题1-它“脆弱”
默认情况下-所有参数都应该匹配,完全相同。 如果一个参数不匹配-游戏结束。 即使这是来自记忆的想法-如今,这可能并不是您想要的。 我的意思是-您想尽可能多地,尽可能多地记住。
甚至高速缓存未命中也是高速缓存擦除爆头。
“今天”与“昨天”之间存在一些差异-不变的数据结构,例如在Redux中使用。
const getSomeDataFromState = memoize(state => compute(state.tasks));
看起来不错吗? 看起来对不对? 但是,当任务没有执行时,状态可能会改变,您只需要匹配任务即可。
结构选择器在这里与他们最强大的战士- 重新选择 -一起拯救您的一天。 Reselect不仅是记忆库,它的功能来自记忆级联或镜头(它们不是,而是将选择器视为光学镜头)。
结果,在数据不可变的情况下-您总是必须首先“聚焦”到您真正需要的数据上,然后-执行计算,否则缓存将被拒绝,而记忆化背后的所有想法将消失。
这实际上是一个大问题,特别是对于新手来说,但这是不可变的数据结构背后的想法,它具有显着的好处- 如果不更改某些内容,则不会更改。 如果某项更改-可能已更改 。 这给了我们超快速的比较,但是有一些假阴性,就像第一个例子一样。
这个想法是关于“关注”您所依赖的数据
我应该有两个时刻-提到:
lodash.memoize
和fast-memoize
lodash.memoize
您的数据转换为用作键的字符串。 这意味着它们是1)不快2)不安全3)可能产生误报-一些不同的数据可能具有相同的字符串表示形式 。 这可能会提高“缓存热速率”,但实际上是非常不好的事情。- 有一种ES6代理方法,用于跟踪给定的所有使用过的变量,并仅检查重要的键。 尽管我个人想创建大量的数据选择器-您可能不喜欢或不了解该过程,但是可能想开箱即用地进行适当的备忘-然后使用memoize-state 。
问题2-这是“一个缓存行”
无限的缓存大小是杀手er。 只要内存非常有限,任何不受控制的缓存都是致命的杀手。 所以-所有最好的库都是“ one-cache-line-long”。 这是一个功能和强大的设计决策。 我刚刚写下了它是多么正确,并且相信我-这是很正确的事情 ,但这仍然是一个问题。 一个大问题。
const tasks = getTasks(state);
一旦同一个选择器必须处理不同的源数据,就不止一个了-一切都坏了。 很容易遇到问题:
- 只要我们使用选择器从状态中获取任务-我们就可以使用相同的选择器从任务中获取某些东西。 强烈来自API本身。 但这是行不通的,因此您只能记住上一次呼叫,但必须使用多个数据源。
- 多个React组件存在相同的问题-它们都是相同的,并且都有些不同,它们获取不同的任务,彼此擦拭结果。
有3种可能的解决方案:

该库将帮助您“保留”备忘录缓存,但不能删除它。 特别是因为它正在实现5种(五种!)不同的缓存策略以适合任何情况。 那是难闻的气味。 如果选择了错误的怎么办?
您已经记住的所有数据-迟早要忘记。 重点不是要记住上一次函数调用-重点是在正确的时间忘记它。 还不算太早,破坏记忆,也不会太晚。
有这个主意吗? 现在算了! 第三变体在哪里?
稍等一下
停下 放松一下 深呼吸。 并回答一个简单的问题-目标是什么? 我们必须做什么才能达到目标? 什么会节省一天?
提示:f ***“缓存”位于何处!

该“缓存”位于何处? 是的-这是正确的问题。 感谢您的询问。 答案很简单-它位于一个闭包中。 在一个隐藏的位置*一个记忆功能。 例如-这是memoize-one
代码:
function(fn) { let lastArgs;
您将获得一个memoizedCall
,它将在其本地闭包内保存附近的最后一个结果,除了memoizedCall之外,任何人都无法访问它。 一个安全的地方。 “这”是一个安全的地方。
Reselect
具有相同的功能,并且是创建带有另一个缓存的“叉子”的唯一方法-创建新的备注关闭。
但是(另一个)主要问题-什么时候(缓存)会“消失”?
TLDR:当函数实例被Garbage Collector吞噬时,函数将“消失”。
实例? 实例! 那么-关于每个实例的备注是什么? 在React文档中有整篇文章
简而言之-如果您使用的是基于类的React组件,则可以执行以下操作:
import memoize from "memoize-one"; class Example extends Component { filter = memoize(
那么- “ lastResult”存储在哪里? 在该类实例内部的备注过滤器的本地范围内。 而且,何时会“消失”?
这次它将与一个类实例“消失”。 一旦组件被卸载-它就消失了。 这是一个真实的“每个实例”,您可以使用this.lastResult
来保存时间结果,并具有完全相同的“记忆”效果。
什么是React.Hooks
我们越来越近了。 Redux钩子有一些可疑的命令,这些命令可能与记忆有关。 像useMemo
, useCallback
, useRef

但是问题是-这次它将在何处存储已存储的值?
简而言之-它存储在“挂钩”中,该挂钩位于VDOM元素的特殊部分内,该部分称为与当前元素关联的光纤。 在并行数据结构中。
并非如此,短钩改变了程序的工作方式,将函数移到另一个函数中,而某些变量位于父闭包内的隐藏点中 。 此类功能称为可暂停或可恢复功能-协程。 在JavaScript中,它们通常称为generators
或async functions
。
但这有点极端。 简而言之-useMemo会在其中存储备注值。 “ this”有点不同。
如果我们想创建一个更好的记忆库,我们应该找到一个更好的“ this”。
ing!
弱地图!
是的 弱地图! 要存储键值,键就是这个,只要WeakMap不接受除此以外的任何东西,即“对象”。
让我们创建一个简单的示例:
const createHiddenSpot = (fn) => { const map = new WeakMap();
这很简单,而且很“正确”。 那么“什么时候会消失”?
- 忘记了weakSelect,整个“地图”将消失
- 忘记待办事项[0],他们的弱项将消失
- 忘记待办事项-记住的数据将消失!
很明显什么时候会“消失”-只有在应该的时候!
神奇地-所有重新选择的问题都消失了。 积极记忆的问题-也可以解决。
这种方法会记住数据,直到需要FORGET为止。 它令人难以置信,但是要更好地记住某件事,您必须能够更好地忘记它。
唯一的持久方法-为这种情况创建一个更强大的API
Kashe-是一个缓存
kashe是基于WeakMap的备忘录库,可以节省您的时间。
该库公开了4个函数
kashe
记忆。box
-用于前缀的备忘录,以增加备忘录的机会。inbox
-嵌套的前缀备忘录,以减少备忘录的更改- fork- 分叉 (很明显)的记忆。
kashe(fn)=> memoizedFn(...参数)
它实际上是上一个示例中的createHiddenSpot。 它将使用第一个参数作为内部WeakMap的键。
const selector = (state, prop) => ({result: state[prop]}); const memoized = kashe(selector); const old = memoized(state, 'x') memoized(state, 'x') === old memoized(state, 'y') === memoized(state, 'y')
第一个参数是键,如果您再次调用函数相同的键,但是参数不同-缓存将被替换,则它仍然是一个缓存行长的备注。 为了使它起作用,您必须为弱案例提供不同的键,就像我对weakSelect示例所做的那样,以提供不同的键来保存结果。 重新选择级联A仍然是问题。
并非所有功能都是Kashe可记忆的。 第一个参数必须是对象,数组或函数。 它应该可用作WeakMap的键。
box(fn)=> memoizedFn2(box,... args)
这是相同的功能,只应用了两次。 一次为fn,一次为memoizedFn,向参数添加前导键。 它可以使任何功能kashe-memoizable。
它非常具有说明性-嘿,功能! 我将结果存储在此框中。
如果您将已记忆的功能装箱-您将增加记忆的机会,就像每个实例的记忆-您可以创建记忆级联。
const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId);
收件箱(fn)=> memoizedFn2(box,... args)
这个与盒子相反,但是几乎一样,命令嵌套缓存将数据存储到提供的盒子中。 从一个角度看-它降低了记忆化的可能性(没有记忆式级联),但是从另一个角度-它消除了缓存冲突,并在进程之间由于某种原因不相互干扰时,可以帮助隔离进程。
声明性很强-嘿! 里面的每个人! 这是一个要使用的盒子
const getAndSet = (task, number) => task.value + number; const memoized = kashe(getAndSet); const inboxed = inbox(getAndSet); const doubleBoxed = inbox(memoized); memoized(task, 1)
叉子(kashe-memoized)=> kashe-memoized
Fork是真正的fork-它具有任何kashe-memoized函数,并返回该函数,但具有另一个内部缓存项。 还记得redux mapStateToProps工厂方法吗?
const mapStateToProps = () => {
重新选择
您还应该知道一件事-Kashe可以取代reselect。 从字面上看。
import { createSelector } from 'kashe/reselect';
它实际上是相同的重新选择,只是使用kashe作为备忘录功能创建的。
密码箱
这是一个小例子 。 另外,您可能还要仔细检查测试 -它们紧凑而合理。
如果您想了解有关缓存和记忆的更多信息,请查看我一年前编写最快的记忆库的方式。
PS:值得一提的是,这种方法的较简单版本–“弱记忆” –在情绪js中使用了一段时间。 没有投诉。 nano-memoize也将WeakMaps用于单个参数情况。
明白了吗? 一种更“弱”的方法将帮助您更好地记住某些事情,并更好地忘记它。
https://github.com/theKashey/kashe
是的,关于忘记某事,-您能在这里看看吗?
