【问题标题】:how to memoize a TypeScript getter如何记忆 TypeScript getter
【发布时间】:2019-10-02 13:08:27
【问题描述】:

我正在使用以下方法来使用装饰器来记忆 TypeScript getter,但想知道是否有更好的方法。我正在使用 npm 中流行的 memoizee 包,如下所示:

import { memoize } from '@app/decorators/memoize'

export class MyComponent {

  @memoize()
  private static memoizeEyeSrc(clickCount, maxEyeClickCount, botEyesDir) {
    return clickCount < maxEyeClickCount ? botEyesDir + '/bot-eye-tiny.png' : botEyesDir + '/bot-eye-black-tiny.png'
  }

  get leftEyeSrc() {
    return MyComponent.memoizeEyeSrc(this.eyes.left.clickCount, this.maxEyeClickCount, this.botEyesDir)
  }
}

并且 memoize 装饰器是:

// decorated method must be pure
import * as memoizee from 'memoizee'

export const memoize = (): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    const func = descriptor.value
    descriptor.value = memoizee(func)
    return descriptor
  }
}

有没有办法做到这一点,而无需在 MyComponent 中使用两个单独的函数,而是直接将装饰器添加到 TypeScript getter?

这里的一个考虑因素是修饰函数必须是纯函数(在这种情况下),但如果您的答案不满足这一点,请随意忽略这一点,因为我对如何解决这个问题很感兴趣。

【问题讨论】:

  • 但是装饰器如何决定是否应该使用缓存版本?您需要以某种方式标记影响 getter 的成员
  • 是的,我明白了,感谢您的评论 - 但是,这只是这个问题的一个可选要求 - 请参阅下面我对 @estus 答案的评论

标签: node.js angular typescript


【解决方案1】:

装饰器可以扩展为支持原型方法和getter:

export const memoize = (): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    if ('value' in descriptor) {
      const func = descriptor.value;
      descriptor.value = memoizee(func);
    } else if ('get' in descriptor) {
      const func = descriptor.get;
      descriptor.get = memoizee(func);
    }
    return descriptor;
  }
}

并且可以直接在getter上使用:

  @memoize()
  get leftEyeSrc() {
    ...
  }

【讨论】:

  • 但是在这种情况下缓存永远不会失效,对吧?
  • 绝妙的答案 - 当我有机会时会尝试并接受它是否有效 - 这在使用 memoize 时无济于事,因为装饰函数必须是纯的,而 getter 通常不是纯的 - 但是通常这是一个很好的方法(如果它有效,我相信它会) - 非常感谢:)
  • @AlekseyL。是的。如果你需要根据memoizeEyeSrc 参数来记忆它,当然你需要一个单独的函数。但是您仍然可以使descriptor.get = 成为memoizee 的包装器,以根据this 属性决定是否需要使其失效。
  • 另一件事 - 装饰的 getter 似乎无法访问其他实例成员 (this.*)
  • @AlekseyL。可以访问,常见情况,descriptor.get = function () { /* this is class instance here */ }
【解决方案2】:

根据@estus 的回答,这是我最终想到的:

@memoize(['this.eyes.left.clickCount'])
get leftEyeSrc() {
  return this.eyes.left.clickCount < this.maxEyeClickCount ? this.botEyesDir + '/bot-eye-tiny.png' : this.botEyesDir + '/bot-eye-black-tiny.png'
}

而 memoize 装饰器是:

// decorated method must be pure when not applied to a getter

import { get } from 'lodash'
import * as memoizee from 'memoizee'

// noinspection JSUnusedGlobalSymbols
const options = {
  normalizer(args) {
    return args[0]
  }
}

const memoizedFuncs = {}

export const memoize = (props: string[] = []): MethodDecorator => {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    props = props.map(prop => prop.replace(/^this\./, ''))
    if ('value' in descriptor) {
      const valueFunc = descriptor.value
      descriptor.value = memoizee(valueFunc)
    } else if ('get' in descriptor) {
      const getFunc = descriptor.get
      // args is used here solely for determining the memoize cache - see the options object
      memoizedFuncs[propertyKey] = memoizee((args: string[], that) => {
        const func = getFunc.bind(that)
        return func()
      }, options)
      descriptor.get = function() {
        const args: string[] = props.map(prop => get(this, prop))
        return memoizedFuncs[propertyKey](args, this)
      }
    }
    return descriptor
  }
}

这允许传递一个字符串数组,其中确定哪些属性将用于 memoize 缓存(在这种情况下,只有 1 个 prop - clickCount - 是可变的,其他 2 个是常量)。

memoizee 选项规定只有 memoizee((args: string[], that) =&gt; {...}) 的第一个数组 arg 用于记忆目的。

仍在努力让我了解这段代码有多漂亮!一定是度过了愉快的一天。感谢耶稣我的朋友和救主:)

【讨论】:

  • 使用 Lodash 进行 IMO 路径导航是多余的,它禁用类型检查是 TS 的一大缺点。如果您需要这种行为,原始方法要简单得多。 memoizeEyeSrc 的使用是合理的。它不一定是类的一部分,可能只是一个辅助函数。
猜你喜欢
  • 1970-01-01
  • 2018-06-25
  • 2011-09-18
  • 2020-12-09
  • 1970-01-01
  • 2017-06-25
  • 2018-01-11
  • 2016-02-08
  • 2015-08-03
相关资源
最近更新 更多