回忆忘我炸弹


您听说过memoization吗? 顺便说一句,这是一件非常简单的事情-只是记住您从第一个函数调用中获得的结果,并使用它而不是第二次调用它-不要无故调用真实的东西,不要浪费时间。


跳过一些繁琐的操作是一种非常常见的优化技术。 每次您可能不做某事-不要做。 尝试使用缓存memcachefile cachelocal cache -任何缓存! 后端系统的必备组件,过去和现在的任何后端系统的关键部分。



记忆与缓存


记住就像缓存。 只是有些不同。 不缓存,我们称它为kashe。

长话短说,但是记忆不是缓存,不是持久缓存。 它可能是在服务器端,但不能,并且不应是客户端的缓存。 更多有关可用资源,使用模式和使用原因的信息。


问题-缓存需要一个“缓存键”


高速缓存使用字符串高速缓存key存储和获取数据。 构造一个唯一且可用的密钥已经是一个问题,但是随后您必须对数据进行序列化和反序列化,以再次存储在基于字符串的介质中……简而言之,高速缓存的速度可能不如您想象的那样快。 特别是分布式缓存。


备注不需要任何缓存键


同时-无需密钥即可记忆。 通常*它按原样使用参数,而不是尝试从中创建单个键,并且不像缓存通常那样使用某些全局可用的共享对象来存储结果。


备注和缓存之间的区别在于API接口

通常*并不表示总是。 Lodash.memoize默认情况下使用JSON.stringify将传递的参数转换为字符串缓存(还有其他方法吗?否!)。 只是因为他们将使用此键来访问内部对象,并保存一个缓存的值。 快速存储 ,“最快的可能的存储库”,也可以做到这一点。 这两个命名库都不是备注库,而是缓存库。


值得一提的是,您要记住,JSON.stringify的速度可能比函数慢10倍。

显然-解决该问题的简单方法是不使用缓存键,也不使用该键访问某些内部缓存。 所以-记住最后一个被调用的参数。 像回忆重新选择一样。


Memoizerific可能是您要使用的唯一通用缓存库。

缓存大小


所有库之间的第二个大区别是关于缓存大小和缓存结构。


您是否曾经想过-为什么reselectmemoize-one仅保留一个最后的结果? 不要“不要使用缓存键来存储一个以上的结果” ,而是因为没有理由要存储多个结果而不只是最后一个结果


...更多关于:


  • 可用资源-单个缓存行非常资源友好
  • 使用模式-记住“就地”的东西是一个很好的模式。 “就地”通常只需要一个,最后一个结果。
  • 使用-模块化,隔离和内存安全的原因是很好的原因。 就缓存冲突而言,不与应用程序的其余部分共享缓存只会更安全。

一个结果?


是的-唯一的结果。 用一个结果来记忆一些经典的事情是不可能的,例如记忆斐波那契数的产生( 您可能在每篇关于记忆的文章中都可以找到例子 )。 但是,通常情况下,您在做其他事情-前端需要谁做斐波那契? 在后端? 现实世界中的例子离抽象的IT测验还很远。


但是,关于单值备忘录类型仍然存在两个BIG问题。


问题1-它“脆弱”


默认情况下-所有参数都应该匹配,完全相同。 如果一个参数不匹配-游戏结束。 即使这是来自记忆的想法-如今,这可能并不是您想要的。 我的意思是-您想尽可能多地,尽可能多地记住。


甚至高速缓存未命中也是高速缓存擦除爆头。

“今天”与“昨天”之间存在一些差异-不变的数据结构,例如在Redux中使用。


 const getSomeDataFromState = memoize(state => compute(state.tasks)); 

看起来不错吗? 看起来对不对? 但是,当任务没有执行时,状态可能会改变,您只需要匹配任务即可。


结构选择器在这里与他们最强大的战士- 重新选择 -一起拯救您的一天。 Reselect不仅是记忆库,它的功能来自记忆级联或镜头(它们不是,而是将选择器视为光学镜头)。


 // every time `state` changes, cached value would be rejected const getTasksFromState = createSelector(state => state.tasks); const getSomeDataFromState = createSelector( // `tasks` "without" `state` getTasksFromState, // <---------- // and this operation would be memoized "more often" tasks => compute(state.tasks) ); 

结果,在数据不可变的情况下-您总是必须首先“聚焦”到您真正需要的数据上,然后-执行计算,否则缓存将被拒绝,而记忆化背后的所有想法将消失。


这实际上是一个大问题,特别是对于新手来说,但这是不可变的数据结构背后的想法,它具有显着的好处- 如果不更改某些内容,则不会更改。 如果某项更改-可能已更改 。 这给了我们超快速的比较,但是有一些假阴性,就像第一个例子一样。


这个想法是关于“关注”您所依赖的数据

我应该有两个时刻-提到:


  • lodash.memoizefast-memoize lodash.memoize您的数据转换为用作键的字符串。 这意味着它们是1)不快2)不安全3)可能产生误报-一些不同的数据可能具有相同的字符串表示形式 。 这可能会提高“缓存热速率”,但实际上是非常不好的事情。
  • 有一种ES6代理方法,用于跟踪给定的所有使用过的变量,并仅检查重要的键。 尽管我个人想创建大量的数据选择器-您可能不喜欢或不了解该过程,但是可能想开箱即用地进行适当的备忘-然后使用memoize-state

问题2-这是“一个缓存行”


无限的缓存大小是杀手er。 只要内存非常有限,任何不受控制的缓存都是致命的杀手。 所以-所有最好的库都是“ one-cache-line-long”。 这是一个功能和强大的设计决策。 我刚刚写下了它是多么正确,并且相信我-这是很正确的事情 ,但这仍然是一个问题。 一个大问题。


 const tasks = getTasks(state); // let's get some data from state1 (function was defined above) getDataFromTask(tasks[0]); // Yep! equal(getDataFromTask(tasks[0]), getDataFromTask(tasks[0])) // Ok! getDataFromTask(tasks[1]); // a different task? What the heck? // oh! That's another argument? How dare you!? // TLDR -> task[0] in the cache got replaced by task[1] you cannot use getDataFromTask to get data from different tasks 

一旦同一个选择器必须处理不同的源数据,就不止一个了-一切都坏了。 很容易遇到问题:


  • 只要我们使用选择器从状态中获取任务-我们就可以使用相同的选择器从任务中获取某些东西。 强烈来自API本身。 但这是行不通的,因此您只能记住上一次呼叫,但必须使用多个数据源。
  • 多个React组件存在相同的问题-它们都是相同的,并且都有些不同,它们获取不同的任务,彼此擦拭结果。

有3种可能的解决方案:


  • 如果是redux,请使用mapStateToProps工厂。 它将创建每个实例的备注。
     const mapStateToProps = () => { const selector = createSelector(...); // ^ you have to define per-instance selectors here // usually that's not possible :) return state => ({ data: selector(data), // a usual mapStateToProps }); } 
  • 第二个变体几乎相同(也用于redux)-它使用re-reselect 。 这是一个复杂的库,可以通过区分组件来节省时间。 它只是可以理解,新的调用是针对“另一个”组件的,它可以保留“上一个”组件的缓存。


该库将帮助您“保留”备忘录缓存,但不能删除它。 特别是因为它正在实现5种(五种!)不同的缓存策略以适合任何情况。 那是难闻的气味。 如果选择了错误的怎么办?
您已经记住的所有数据-迟早要忘记。 重点不是要记住上一次函数调用-重点是在正确的时间忘记它。 还不算太早,破坏记忆,也不会太晚。


有这个主意吗? 现在算了! 第三变体在哪里?

稍等一下


停下 放松一下 深呼吸。 并回答一个简单的问题-目标是什么? 我们必须做什么才能达到目标? 什么会节省一天?


提示:f ***“缓存”位于何处!


该“缓存”位于何处? 是的-这是正确的问题。 感谢您的询问。 答案很简单-它位于一个闭包中。 在一个隐藏的位置*一个记忆功能。 例如-这是memoize-one代码:


 function(fn) { let lastArgs; // the last arguments let lastResult;// the last result <--- THIS IS THE CACHE // the memoized function const memoizedCall = function(...newArgs) { if (isEqual(newArgs, lastArgs)) { return lastResult; } lastResult = resultFn.apply(this, newArgs); lastArgs = newArgs; return lastResult; }; return memoizedCall; } 

您将获得一个memoizedCall ,它将在其本地闭包内保存附近的最后一个结果,除了memoizedCall之外,任何人都无法访问它。 一个安全的地方。 “这”是一个安全的地方。


Reselect具有相同的功能,并且是创建带有另一个缓存的“叉子”的唯一方法-创建新的备注关闭。


但是(另一个)主要问题-什么时候(缓存)会“消失”?


TLDR:当函数实例被Garbage Collector吞噬时,函数将“消失”。

实例? 实例! 那么-关于每个实例的备注是什么? 在React文档中有整篇文章


简而言之-如果您使用的是基于类的React组件,则可以执行以下操作:


 import memoize from "memoize-one"; class Example extends Component { filter = memoize( // <-- bound to the instance (list, filterText) => list.filter(...); // ^ that is "per instance" memoization // we are creating "own" memoization function // with the "own" lastResult render() { // Calculate the latest filtered list. // If these arguments haven't changed since the last render, // `memoize-one` will reuse the last return value. const filteredList = this.filter(something, somehow); return <ul>{filteredList.map(item => ...}</ul> } } 

那么- “ lastResult”存储在哪里? 在该类实例内部的备注过滤器的本地范围内。 而且,何时会“消失”?


这次它将与一个类实例“消失”。 一旦组件被卸载-它就消失了。 这是一个真实的“每个实例”,您可以使用this.lastResult来保存时间结果,并具有完全相同的“记忆”效果。


什么是React.Hooks


我们越来越近了。 Redux钩子有一些可疑的命令,这些命令可能与记忆有关。 像useMemouseCallbackuseRef



但是问题是-这次它将在何处存储已存储的值?

简而言之-它存储在“挂钩”中,该挂钩位于VDOM元素的特殊部分内,该部分称为与当前元素关联的光纤。 在并行数据结构中。


并非如此,短钩改变了程序的工作方式,将函数移到另一个函数中,而某些变量位于父闭包内的隐藏点中 。 此类功能称为可暂停可恢复功能-协程。 在JavaScript中,它们通常称为generatorsasync functions


但这有点极端。 简而言之-useMemo会在其中存储备注值。 “ this”有点不同。


如果我们想创建一个更好的记忆库,我们应该找到一个更好的“ this”。

ing!


弱地图!


是的 弱地图! 要存储键值,键就是这个,只要WeakMap不接受除此以外的任何东西,即“对象”。


让我们创建一个简单的示例:


 const createHiddenSpot = (fn) => { const map = new WeakMap(); // a hidden "closure" const set = (key, value) => (map.set(key, value), value); return (key) => { return map.get(key) || set(key, fn(key)) } } const weakSelect = createHiddenSpot(selector); weakSelect(todos); // create a new entry weakSelect(todos); // return an existing entry weakSelect(todos[0]); // create a new entry weakSelect(todos[1]); // create a new entry weakSelect(todos[0]); // return an existing entry! weakSelect(todos[1]); // return an existing entry!! weakSelect(todos); // return an existing entry!!! 

这很简单,而且很“正确”。 那么“什么时候会消失”?


  • 忘记了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') // ^^ another argument // but old !== memoized(state, 'x') // 'y' wiped 'x' cache in `state` 

第一个参数是键,如果您再次调用函数相同的键,但是参数不同-缓存将被替换,则它仍然是一个缓存行长的备注。 为了使它起作用,您必须为弱案例提供不同的键,就像我对weakSelect示例所做的那样,以提供不同的键来保存结果。 重新选择级联A仍然是问题。
并非所有功能都是Kashe可记忆的。 第一个参数必须是对象,数组或函数。 它应该可用作WeakMap的键。


box(fn)=> memoizedFn2(box,... args)


这是相同的功能,只应用了两次。 一次为fn,一次为memoizedFn,向参数添加前导键。 它可以使任何功能kashe-memoizable。


它非常具有说明性-嘿,功能! 我将结果存储在此框中。

 // could not be "kashe" memoized const addTwo = (a,b) => ({ result: a+b }); const bAddTwo = boxed(addTwo); const cacheKey = {}; // any object bAddTwo(cacheKey, 1, 2) === bAddTwo(cacheKey, 1, 2) === { result: 3} 

如果您将已记忆的功能装箱-您将增加记忆的机会,就像每个实例的记忆-您可以创建记忆级联。


 const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId); // 1. try to find result in `this` // 2. try to find result in `todos` // 3. store in `todos` // 4. store in `this` // if multiple `this`(components) are reading from `todos` - // selector is not working (they are wiping each other) // but data stored in `this` - exists. ... } } 

收件箱(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) // ok memoized(task, 2) // previous result wiped inboxed(key1, task, 1) // ok inboxed(key2, task, 2) // ok // inbox also override the cache for any underlaying kashe calls doubleBoxed(key1, task, 1) // ok doubleBoxed(key2, task, 2) // ok 

叉子(kashe-memoized)=> kashe-memoized


Fork是真正的fork-它具有任何kashe-memoized函数,并返回该函数,但具有另一个内部缓存项。 还记得redux mapStateToProps工厂方法吗?


 const mapStateToProps = () => { // const selector = createSelector(...); // const selector = fork(realSelector); // just fork existing selector. Or box it, or don't do anything // kashe is more "stable" than reselect. return state => ({ data: selector(data), }); } 

重新选择


您还应该知道一件事-Kashe可以取代reselect。 从字面上看。


 import { createSelector } from 'kashe/reselect'; 

它实际上是相同的重新选择,只是使用kashe作为备忘录功能创建的。


密码箱


这是一个小例子 。 另外,您可能还要仔细检查测试 -它们紧凑而合理。
如果您想了解有关缓存和记忆的更多信息,请查看一年前编写最快的记忆库的方式。


PS:值得一提的是,这种方法的较简单版本–“弱记忆” –在情绪js中使用了一段时间。 没有投诉。 nano-memoize也将WeakMaps用于单个参数情况。

明白了吗? 一种更“弱”的方法将帮助您更好地记住某些事情,并更好地忘记它。


https://github.com/theKashey/kashe


是的,关于忘记某事,-您能在这里看看吗?


Source: https://habr.com/ru/post/zh-CN446390/


All Articles