Skip to content

Batched bindings #760

@Rich-Harris

Description

@Rich-Harris

Bindings on components are implemented with component.observe. That's convenient enough, but it has an awkward consequence — if you have two bindings, and they both change in the same set operation, you end up with two distinct updates. That's obviously somewhat wasteful.

Consider this example:

<!-- App.html -->
<Foo bind:bar bind:baz/>
<p>bar: {{bar}}</p>
<p>baz: {{baz}}</p>

<script>
  import Foo from './Foo.html';
  
  export default {
    components: {
      Foo
    }
  };
</script>

<!-- Foo.html -->
<p>bar in Foo: {{bar}}</p>
<p>baz in Foo: {{baz}}</p>
<button on:click='double()'>double</button>

<script>
  export default {
    data() {
      return {
        bar: 1,
        baz: 2
      }
    },
    
    methods: {
      double() {
        const { bar, baz } = this.get();
        this.set({ bar: bar * 2, baz: baz * 2 });
      }
    }
  };
</script>

Every time you hit the 'double' button, the app has to update twice.

We could fix this by implementing bindings differently. This is some untested code that probably misses certain important scenarios, but I think something like this is probably on the right lines:

import Foo from './Foo.html';

function create_main_fragment ( state, component ) {
-  var foo_updating = false, foo_updating_1 = false, text, p, text_1, text_2_value, text_2, text_3, p_1, text_4, text_5_value, text_5;
+  var foo_updating = false, text, p, text_1, text_2_value, text_2, text_3, p_1, text_4, text_5_value, text_5;

  var foo_initial_data = {};
  if ( 'bar' in state ) foo_initial_data.bar = state.bar ;
  if ( 'baz' in state ) foo_initial_data.baz = state.baz;
  var foo = new Foo({
    _root: component._root,
+    _bind: function( changed ) {
+      var data = {}, state = component.get(), dirty = false;
+      if ( 'bar' in changed ) { data.bar = changed.bar; dirty = true; }
+      if ( 'baz' in changed ) { data.baz = changed.baz; dirty = true; }
+      if ( dirty ) {
+        foo_updating = true;
+        component._set( data );
+        foo_updating = false;
+      }
+    },
    data: foo_initial_data
  });

-  function observer ( value ) {
-    if ( foo_updating ) return;
-    foo_updating = true;
-    component.set({ bar: value });
-    foo_updating = false;
-  }

-  foo.observe( 'bar', observer, { init: false });

-  component._root._beforecreate.push( function () {
-    var value = foo.get( 'bar' );
-    if ( differs( value, state.bar  ) ) {
-      observer.call( foo, value );
-    }
-  });

-  function observer_1 ( value ) {
-    if ( foo_updating_1 ) return;
-    foo_updating_1 = true;
-    component.set({ baz: value });
-    foo_updating_1 = false;
-  }

-  foo.observe( 'baz', observer_1, { init: false });

-  component._root._beforecreate.push( function () {
-    var value = foo.get( 'baz' );
-    if ( differs( value, state.baz ) ) {
-      observer_1.call( foo, value );
-    }
-  });

  foo._context = {
    state: state
  };

  return {
    create: function () {
      foo._fragment.create();
      text = createText( "\n" );
      p = createElement( 'p' );
      text_1 = createText( "bar: " );
      text_2 = createText( text_2_value = state.bar );
      text_3 = createText( "\n" );
      p_1 = createElement( 'p' );
      text_4 = createText( "baz: " );
      text_5 = createText( text_5_value = state.baz );
    },

    mount: function ( target, anchor ) {
      foo._fragment.mount( target, anchor );
      insertNode( text, target, anchor );
      insertNode( p, target, anchor );
      appendNode( text_1, p );
      appendNode( text_2, p );
      insertNode( text_3, target, anchor );
      insertNode( p_1, target, anchor );
      appendNode( text_4, p_1 );
      appendNode( text_5, p_1 );
    },

    update: function ( changed, state ) {
+      if ( !foo_updating ) {
+        var data = {}, dirty = false;
+        if ( 'bar' in changed ) { data.bar = changed.bar; dirty = true; }
+        if ( 'baz' in changed ) { data.baz = changed.baz; dirty = true; }
+        if ( dirty ) {
+          foo_updating = true;
+          component._set( data );
+          foo_updating = false;
+        }
+      }
      
-      if ( !foo_updating && 'bar' in changed ) {
-        foo_updating = true;
-        foo._set({ bar: state.bar  });
-        foo_updating = false;
-      }

-      if ( !foo_updating_1 && 'baz' in changed ) {
-        foo_updating_1 = true;
-        foo._set({ baz: state.baz });
-        foo_updating_1 = false;
-      }

      foo._context.state = state;

      if ( text_2_value !== ( text_2_value = state.bar ) ) {
        text_2.data = text_2_value;
      }

      if ( text_5_value !== ( text_5_value = state.baz ) ) {
        text_5.data = text_5_value;
      }
    },

    unmount: function () {
      foo._fragment.unmount();
      detachNode( text );
      detachNode( p );
      detachNode( text_3 );
      detachNode( p_1 );
    },

    destroy: function () {
      foo.destroy( false );
    }
  };
}

Then, we call this._bind inside _set. Which itself could arguably be improved by checking that values were in fact changed:

App.prototype._set = function _set ( newState ) {
  var oldState = this._state;
+  var changed = {}, dirty = false;
+  for ( var key in newState ) {
+    if ( differs( newState[key], oldState[key] ) ) { changed[key] = newState[key]; dirty = true; }
+  }
+  if ( !dirty ) return;
-  this._state = assign( {}, oldState, newState );
-  dispatchObservers( this, this._observers.pre, newState, oldState );
-  this._fragment.update( newState, this._state );
-  dispatchObservers( this, this._observers.post, newState, oldState );
+  this._state = assign( {}, oldState, changed );
+  if ( this._bind ) this._bind( changed );
+  dispatchObservers( this, this._observers.pre, changed, oldState );
+  this._fragment.update( changed, this._state );
+  dispatchObservers( this, this._observers.post, changed, oldState );
};

(Though perhaps the dirty check belongs in set and not _set? Not actually sure.)

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