onUpdate(
    itemCount: number,
    scrollOffset: number,
    viewportHeight: number,
    getFrameMetrics: (index: number) => ?{length: number, offset: number},
    createViewToken: (index: number, isViewable: boolean) => ViewToken,
    onViewableItemsChanged: ({
      viewableItems: Array<ViewToken>,
      changed: Array<ViewToken>,
    }) => void,
    renderRange?: {first: number, last: number}, // Optional optimization to reduce the scan size
  ): void {
    if (
      (this._config.waitForInteraction && !this._hasInteracted) ||
      itemCount === 0 ||
      !getFrameMetrics(0)
    ) {
      return;
    }
    let viewableIndices = [];
    if (itemCount) {
      viewableIndices = this.computeViewableItems(
        itemCount,
        scrollOffset,
        viewportHeight,
        getFrameMetrics,
        renderRange,
      );
    }
    if (
      this._viewableIndices.length === viewableIndices.length &&
      this._viewableIndices.every((v, ii) => v === viewableIndices[ii])
    ) {
      // We might get a lot of scroll events where visibility doesn't change and we don't want to do
      // extra work in those cases.
      return;
    }
    this._viewableIndices = viewableIndices;
    if (this._config.minimumViewTime) {
      const handle = setTimeout(() => {
        this._timers.delete(handle);
        this._onUpdateSync(
          viewableIndices,
          onViewableItemsChanged,
          createViewToken,
        );
      }, this._config.minimumViewTime);
      this._timers.add(handle);
    } else {
      this._onUpdateSync(
        viewableIndices,
        onViewableItemsChanged,
        createViewToken,
      );
    }
  }
Figures out which items are viewable and how that has changed from before and calls
`onViewableItemsChanged` as appropriate.