这是indexloc提供的服务,不要输入任何密码
Skip to content

Observable#from() @@iterator side effects #127

@domfarolino

Description

@domfarolino

Imagine the following:

const iterable = {
  get [Symbol.iterator]() {
    console.log('Symbol.iterator getter');
    return function () {
      console.log('Symbol.iterator implementation');
      return {
        next () {
          console.log('next() being called');
          return {value: undefined, done: true};
        }
      };
    }
  }
};

const source = Observable.from(iterable);
source.subscribe();
source.subscribe();

What would you expect the console to look like? I think we have two options:

First option

Symbol.iterator getter
Symbol.iterator implementation
next() being called
Symbol.iterator implementation
next() being called

This option basically means that:

  1. Observable.from() calls the [Symbol.iterator] getter initially to make sure iterable is exactly that
  2. The [Symbol.iterator] function is stored in the Observable
  3. It is called on each subscribe() invocation

Further this means that if [Symbol.iterator] is re-assigned in between from() and subscribe(), the old value will be called on subsequent subscriptions:

const iterable = {
  [Symbol.iterator]() {
    console.log('Symbol.iterator implementation');
    return {
      next () {
        console.log('next() being called');
        return {value: undefined, done: true};
      }
    };
  }
};

const source = Observable.from(iterable);
iterable[Symbol.iterator] = () => {
  throw new Error('custom error');
};

source.subscribe();
source.subscribe();

would output:

Symbol.iterator implementation
next() being called
Symbol.iterator implementation
next() being called

The new overridden [Symbol.iterator] implementation never gets called for the Observable created before the assignment.

Second option

Symbol.iterator getter
Symbol.iterator getter
Symbol.iterator implementation
next() being called
Symbol.iterator getter
Symbol.iterator implementation
next() being called

This option basically means that:

  • Observable.from transiently tests the existence of the Symbol.iterator function (invoking the getter), ensuring the input object is indeed an iterable
  • Stores a reference to the input object in the Observable returned by from(), such that a fresh acquisition of the iterator function is grabbed on every single subscription

This allows the [Symbol.iterator] method that gets invoked, to change (via reassignment, for example) across subscriptions to the same Observable.

I slightly prefer the first option above, as I think it is less prone to error and change, but I am unsure which is more purely idiomatic. For example, is it "bad" for the web to keep alive a [Symbol.iterator] implementation that a web author as re-assigned, just because it was captured by an old-enough Observable? I'm curious if @bakkot has any thoughts on this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions