jridgewell / babel-plugin-transform-incremental-dom

Turn JSX into IncrementalDOM
MIT License
144 stars 12 forks source link

Glimmer #11

Open jridgewell opened 9 years ago

jridgewell commented 9 years ago

After https://github.com/babel-plugins/babel-plugin-incremental-dom/pull/10 lands, we can shamelessly rip off Glimmer's rendering engine.

First, the naive approach (that we're currently doing) is to diff everything:

function render() {
 return <div>
    <span>{this.props.content}</span>
    <ul>
      { this.props.li.map((li) => { <li>{li}</li> }) }
    </ul>
  </div>;
}

// - - -
function render() {
  elementOpen("div");
  elementOpen("span");

  _renderArbitrary(this.props.content);

  elementClose("span");
  elementOpen("ul");

  this.props.li.map(function (li) {
    elementOpen("li");

    _renderArbitrary(li);

    elementClose("li");
  });

  elementClose("ul");
  return elementClose("div");
}

In essence, we'll diff the <div>, the <span>, the span's content, the <ul> and the ul's content. But, we can do a bit better using Glimmer's approach.

Essentially, we only ever need to patch the <span> and <ul> on second renders:

// First render
function render() {
  render.div = elementOpen("div");
  render.span = elementOpen("span");

  _renderArbitrary(this.props.content);

  elementClose("span");
  render.ul = elementOpen("ul");

  this.props.li.map(function (li) {
    elementOpen("li");

    _renderArbitrary(li);

    elementClose("li");
  });

  elementClose("ul");
  return elementClose("div");
}

// Second render
function secondRender() {
  patch(render.span, _renderArbitrary, this.props.content);
  patch(render.ul, function() {
    elementOpen("li");

    _renderArbitrary(li);

    elementClose("li");
  });
  return render.div;
}

This can pay off immensely for static renders or deeply nested views:

function render() {
  return <div>
    <div>
      <span>Lot</span>
    </div>
    <div>
      <span>o'</span>
    </div>
    <div>
      <span>Content</span>
    </div>
  </div>;
}

// - - -
function secondRender() {
  return render.div;
}
function render() {
  return <div>
    <div>
      <div>
        <div>
          <span>{this.props.content}</span>
        </div>
      </div>
    </div>
  </div>;
}

// - - -
function secondRender() {
  patch(render.span, _renderArbitrary, this.props.content);
  return render.div;
}
jamiebuilds commented 9 years ago

Isn't the element returned on close not open?

jamiebuilds commented 9 years ago

I like the basic concept here, it'd be cool to explore

jridgewell commented 9 years ago

Isn't the element returned on close not open?

The Element is returned from both.

jamiebuilds commented 9 years ago

How far is this from reality?

jridgewell commented 9 years ago

Maybe a weekend's worth of hacking. We already have a lot of information about what is constant and what changes from JSX's own semantics.

I'm working on static hosting first, though.

jridgewell commented 9 years ago

Edit: see https://github.com/babel-plugins/babel-plugin-incremental-dom/issues/11#issuecomment-155489607.


Hmm, blocked until https://github.com/google/incremental-dom/pull/132 is merged. If any element in the top-level of the render isn't constant, I won't have a reference node to patch up:

function render(data) {
  return <div id={data.id} />;
};

// - - -
// Would have to translate to something like

function render(data) {
  var elements = render.elements = [];
  elements.push(elementVoid("div", null, null, "id", data.id));
  return elements[0];
}
render.secondRender = function(data) {
  // We can't already be in a `patch`, since that would wipe out everything.
  // So we have to call patch a bunch of times.
  patch(getCurrentElement(), render, data);
  return render.elements[0];
};

Specifically with top-level elements, I won't have the current container (or be able to generate one with an elementOpen), so I'll need iDOM to give it to me.

jridgewell commented 9 years ago

iDOM just added skip, which allows you to skip the clearing of unvisited child nodes. Tied together with getCurrentElement, this works pretty handily to solve a second problem with my example. Namely, even if I were to get the top-level container element and patch it, it would still have it's children cleared by the original patch.

// Top level change
function render(data) {
  return <div id={data.id} />;
};

// - - -
function render(data) {
  return elementVoid("div", null, null, "id", data.id);
}
render.secondRender = function(data) {
  return render(data);
};

patch(container, render, data);
// Later, repatch
patch(container, render.secondRender, data);

Patching a subelement:

function render(data) {
  return <div>
    <div id={data.id} />
  </div>;
};

// - - -
// Translates to something like

function render(data) {
  var elements = render.elements = [];
  elements.push(elementOpen("div"));
  elementVoid("div", null, null, "id", data.id);
  elementClose('div');
  return elements[0];
}
render.secondRender = function(data) {
  patch(render.elements[0], (data) => {
    elementVoid("div", null, null, "id", data.id);
  }, data);

  // Use skip to prevent clearing from outer patch context,
  // since `secondRender` was called like so:
  //     patch(container, render.secondRender, data)
  skip();
  return render.elements[0];
};

patch(container, render, data);
// Later, repatch
patch(container, render.secondRender, data);
justinfagnani commented 7 years ago

Is this still in the plans?

justinfagnani commented 7 years ago

I so... I've been experimenting with this transform with web components, and this kind of optimization would be very useful where shadow roots often have large <style> tags amongst other static beginning and end sections, as well as often having nested static sections around <slot>s.

jridgewell commented 7 years ago

Still stuck waiting for better iDOM support. I had a working implementation of this, but it was actually slower than doing the total diff due to multiple patch calls.

justinfagnani commented 7 years ago

ok, thanks for the update!