Re: [WICG/webcomponents] Idea: A tree-aware task scheduler (Issue #1055)

I think talking about techniques like scheduling microtasks and how a queueDOMUpdate() method might integrate with Signals can be a bit abstract, so I made some code examples. These presume a particular API, but just for example's sake. I know it's too early to get this concrete in the solution space before people agree on the problem and use cases.

This is an example of an element that reads two numbers, `x` and `y` and displays them and their sum. We want to batch the updates so that there's only one if both `x` and `y` change.

Note: These examples _does not_ fully show the problem with microtasks though: to do that we would need both shared signals and data passed down the tree to show the situation where the children update before the parent, then update again after the parent passes new data down.

These examples just demonstrate the techniques I'm referring to. I sketched up examples with no scheduler (microtasks), one with a hypothetical DOM scheduler, and one with hypothetical DOM scheduler and signals.

<details>
  <summary><h2>With Microtasks</h2></summary>

```ts
class MyElement extends HTMLElement {
  #isUpdatePending = false;
  
  #x: string;
  set x(v) {
    this.#x = v;
    this.#requestUpdate();
  }
  get x() { return this.#x; }

  #y: string;
  set x(v) {
    this.#y = v;
    this.#requestUpdate();
  }
  get y() { return this.#y; }

  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <span id="x"></span> + <span id="y"></span> = <span id="sum"></span>;
    `;
  }
  
  async #requestUpdate() {
    if (this.#isUpdatePending === true) {
      return;
    }
    await 0;
    this.#isUpdatePending = false;
    this.shadowRoot.getElementById('x').textContent = this.x;
    this.shadowRoot.getElementById('y').textContent = this.y;
    this.shadowRoot.getElementById('sum').textContent = this.x + this.y;
  }
}
```
</details>

<details>
  <summary><h2>With queueDOMUpdate()</h2></summary>

```ts
class MyElement extends HTMLElement {
  #x: string;
  set x(v) {
    this.#x = v;
    this.queueDomUpdate(this.#update);
  }
  get x() { return this.#x; }
  
  #y: string;
  set x(v) {
    this.#y = v;
    this.queueDomUpdate(this.#update);
  }
  get y() { return this.#y; }

  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <span id="x"></span> + <span id="y"></span> = <span id="sum"></span>;
    `;
  }
  
  #update() {
    this.shadowRoot.getElementById('x').textContent = this.x;
    this.shadowRoot.getElementById('y').textContent = this.y;
    this.shadowRoot.getElementById('sum').textContent = this.x + this.y;
  }
}
```

</details>

<details>
  <summary><h2>With Signals and queueDOMUpdate()</h2></summary>

To keep this example simpler, I replaced the instance fields with shared Signals. A real element might be more complicated that this - accepting values as fields or signals, or allowing passing signals via fields.

This code is likely a bit buggy, since I'm just typing it out. I left out fine-grained DOM updates.

```ts
const x = new Signal.State(2);
const y = new Signal.State(3);
const sum = new Signal.Computed(() => x.get() + y.get());

class MyElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({mode: 'open'});
    this.shadowRoot.innerHTML = `
      <span id="x"></span> + <span id="y"></span> = <span id="sum"></span>;
    `;
    
    // With Signals you want to separate reading your data from side-effects, so
    // this computed just reads all the data that is used. A template system might
    // do this separation for you, and/or a fine-grained approach might use a separate watcher
    // and computed per "binding", or use a proposed `notifiedBy` feature for watchers.
    const viewData = new Signal.Computed(() => ({
      x: x.get(),
      y: y.get(),
      sum: sum.get(),
    }));

    const watcher = new Signal.subtle.Watcher(() => {
      this.queueDOMUpdate(() => {
        // The actual side-effects
        const {x, y, sum} = viewData.get();
        this.shadowRoot.getElementById('x').textContent = x;
        this.shadowRoot.getElementById('y').textContent = y;
        this.shadowRoot.getElementById('sum').textContent = sum;
      });
    });
    watcher.watch(viewData);
    viewData.get();
  }  
}
```

</details>


-- 
Reply to this email directly or view it on GitHub:
https://github.com/WICG/webcomponents/issues/1055#issuecomment-2077760741
You are receiving this because you are subscribed to this thread.

Message ID: <WICG/webcomponents/issues/1055/2077760741@github.com>

Received on Thursday, 25 April 2024 17:05:54 UTC