9

查看DataLoader 库,它是如何缓存和批处理请求的?

说明通过以下方式指定用法:

var DataLoader = require('dataloader')

var userLoader = new DataLoader(keys => myBatchGetUsers(keys));

userLoader.load(1)
    .then(user => userLoader.load(user.invitedByID))
    .then(invitedBy => console.log(`User 1 was invited by ${invitedBy}`));

// Elsewhere in your application
userLoader.load(2)
    .then(user => userLoader.load(user.lastInvitedID))
    .then(lastInvited => console.log(`User 2 last invited ${lastInvited}`));

但我不清楚该load功能是如何工作的,以及该myBatchGetUsers功能可能是什么样子。如果可能的话,请你给我一个例子!

4

2 回答 2

4

Facebook 的 DataLoader 实用程序通过将输入请求与您必须提供的批处理功能相结合来工作。它仅适用于使用Identifiers.

分为三个阶段:

  1. 聚合阶段:对Loader对象的任何请求都会延迟到process.nextTick
  2. 批处理阶段:Loader只需调用myBatchGetUsers您提供的所有请求键的组合的功能。
  3. 分裂阶段:结果然后被“分裂”,因此输入请求得到响应的所需部分。

这就是为什么在您提供的示例中您应该只有两个请求:

  • 用户 1 和 2 一个
  • 然后是相关用户(invitedByID

例如,要使用 mongodb 实现这一点,您应该只定义 myBatchGetUsers 函数以find适当地使用该方法:

function myBatchGetUsers(keys) {
    // usersCollection is a promisified mongodb collection
    return usersCollection.find(
       {
          _id: { $in: keys }
       }
    )
}
于 2017-02-13T17:00:44.467 回答
2

我发现重新创建dataloader我使用的部分很有帮助,以查看可以实现它的一种可能方式。(就我而言,我只使用该.load()功能)

因此,创建DataLoader构造函数的新实例会为您提供两件事:

  1. 标识符列表(开头为空)
  2. 使用此标识符列表来查询数据库的函数(您提供此)。

构造函数可能看起来像这样:

function DataLoader (_batchLoadingFn) {
  this._keys = []
  this._batchLoadingFn = _batchLoadingFn
}

并且构造函数的实例DataLoader可以访问一个.load()函数,该函数需要能够访问该_keys属性。所以它是在DataLoad.prototype对象上定义的:

DataLoader.prototype.load = function(key) {
   // this._keys references the array defined in the constructor function
}

new DataLoader(fn)当通过 DataLoader 构造函数 ( )创建一个新对象时,fn您传递它需要从某个地方获取数据,将一个键数组作为参数,并返回一个解析为对应于初始键数组的值数组的 Promise .

例如,这是一个伪函数,它接受一个键数组,并将相同的数组传回,但值加倍:

const batchLoadingFn = keys => new Promise( resolve => resolve(keys.map(k => k * 2)) )
keys: [1,2,3]
vals: [2,4,6]

keys[0] corresponds to vals[0]
keys[1] corresponds to vals[1]
keys[2] corresponds to vals[2]

然后每次调用该函数时,都会向数组.load(indentifier)添加一个键,并在某个时候调用该函数,并将该数组作为参数传递。_keysbatchLoadingFn_keys

诀窍是......如何.load(id)多次调用但batchLoadingFn只执行一次?这很酷,也是我探索这个库如何工作的原因。

我发现可以通过指定batchLoadingFn在超时后执行来执行此操作,但是如果.load()在超时间隔之前再次调用,则取消超时,添加新键并batchLoadingFn重新安排调用。在代码中实现这一点如下所示:

DataLoader.prototype.load = function(key) {
  clearTimeout(this._timer)
  this._timer = setTimeout(() => this.batchLoadingFn(), 0)
}

本质上调用.load()会删除挂起的对 的调用batchLoadingFn,然后batchLoadingFn在事件循环的后面安排一个新的调用。这保证了在很短的时间内如果.load()被多次调用,batchLoadingFn只会被调用一次。这实际上与debouncing非常相似。或者,至少在构建网站并且您想在事件中做某事时它很有用mousemove,但是您得到的事件比您想要处理的要多得多。我认为这被称为去抖动。

但是调用.load(key)也需要向_keys数组推送一个键,我们可以在.load函数体中通过将key参数推送到_keys(just this._keys.push(key))。但是,该.load函数的约定是它返回与 key 参数解析的内容有关的单个值。在某些时候,batchLoadingFn将调用并获得一个结果(它必须返回一个对应于 的结果_keys)。此外,还需要batchLoadingFn实际返回该值的承诺。

我认为接下来的这一点特别聪明(非常值得查看源代码)!

dataloader_keys实际上保留了一个键列表,与对resolve函数的引用相关联,而不是在 中保留键列表,当调用该函数时,会导致将值解析为.load(). .load()返回一个promise,一个promise在它的resolve函数被调用时被解析。

所以_keys数组实际上保留了一个[key, resolve]元组列表。当您batchLoadingFn返回时,resolve会使用一个值调用该函数(希望_keys通过索引号对应于数组中的项目)。

所以.load函数看起来像这样(就将元组推[key, resolve]送到_keys数组而言):

DataLoader.prototype.load = function(key) {
  const promisedValue = new Promise ( resolve => this._keys.push({key, resolve}) )
  ...
  return promisedValue
}

剩下的就是执行batchLoadingFnwith_keys键作为参数,并resolve在返回时调用正确的函数

this._batchLoadingFn(this._keys.map(k => k.key))
  .then(values => {
    this._keys.forEach(({resolve}, i) => {
      resolve(values[i])
    })
    this._keys = [] // Reset for the next batch
  })

结合起来,实现上述的所有代码都在这里:

function DataLoader (_batchLoadingFn) {
  this._keys = []
  this._batchLoadingFn = _batchLoadingFn
}

DataLoader.prototype.load = function(key) {
  clearTimeout(this._timer)
  const promisedValue = new Promise ( resolve => this._keys.push({key, resolve}) )

  this._timer = setTimeout(() => {
    console.log('You should only see me printed once!')
    this._batchLoadingFn(this._keys.map(k => k.key))
      .then(values => {
        this._keys.forEach(({resolve}, i) => {
          resolve(values[i])
        })
        this._keys = []
      })
  }, 0)

  return promisedValue
}

// Define a batch loading function
const batchLoadingFunction = keys => new Promise( resolve => resolve(keys.map(k => k * 2)) )

// Create a new DataLoader
const loader = new DataLoader(batchLoadingFunction)

// call .load() twice in quick succession
loader.load(1).then(result => console.log('Result with key = 1', result))
loader.load(2).then(result => console.log('Result with key = 2', result))

如果我没记错的话,我认为dataloader图书馆不使用setTimeout,而是使用process.nextTick. 但我无法让它发挥作用。

于 2019-09-24T20:29:20.280 回答