5 ms·
Can someone explain the benefits of React's DOM diffing model? Rather than try to diff two DOM trees and optimize reconciliation, why not use one-way data bind
by jiaweihli 10y ago
Can someone explain the benefits of React's DOM diffing model?
Rather than try to diff two DOM trees and optimize reconciliation, why not use one-way data binding and update exactly what has changed, with 0 reconciliation cost?
Either way, the upfront work - linking DOM elements with model attributes - is the same. In JSX this is done by interpolating variables into the template, and in one-way data binding this is done via data attributes.
Anecdotally, I've found one-way data binding (using Rivets[1]) to be very fast in practice for a view hierarchy ~10 layers deep.
[1] http://rivetsjs.com/ http://rivetsjs.com/
- spankalee 10y agoI'm with you. All this complexity is baffling, when there's a very optimized rendering engine sitting there in the form of the browser, and soon a suitable component model. If you wire up your components so that you just re-render the components that have data changes, you get the benefits of dom diffing, without having to diff. You can skip all this framework JS, and just directly write an async-rendering Web Component. It's pretty compact and easy to understand. I just wrote up a sketch that would work with one of the incremental-dom-based template libraries: class AsyncWebComponent extends HTMLElement { constructor() { super(); this.isLayoutValid = true; this.attachShadow({mode: 'open'}); this._foo = 'bar'; } // Accessors could be generated with decorators or a helper library set foo(value) { this._foo = value; // You could check for deep equality before invalidating... this.invalidate(); } get foo() { return this._foo; } connectedCallback() { this.invalidate(); // trigger the initial render } render() { // Call into template library to re-render component. // If the template is incrementally updated (say with incremental-dom), // then only child components with data that changes will be updated. templateLib.render(this.template, this.shadowRoot); } invalidate() { if (this.isLayoutValid) { this.isLayoutValid = false; // scheduleRenderTask enqueues tasks, so they'll be run in order down the component tree // A simple and correct scheduler is to just enqueue a microtask with Promise.resolve().then(task) scheduleRenderTask(() => { this.render(); this.isLayoutValid = true; }); } } } edit: example uses. In a template: <async-web-component foo="[[foo]]"></async-web-component> When the parent component renders and sets `foo`, the child will schedule a task to re-render. In JS: let e = document.querySelector('async-web-component'); e.foo = 'baz'; // e schedules a task to re-render e.foo = 'qux'; // e doesn't schedule a task, because one is pending
- kensign 10y agothank you, thank you, thank you. Aurelia is an excellent system for this exact use of a web component.
- spankalee 10y agoMy example was "plain" custom elements, nothing to do with Aurelia. Last I checked Aurelia used a lot of the Web Components terminology, but didn't actually create or use custom elements. Did that change?
- kensign 10y agoyes
- spankalee 10y agoSo Aurelia components are web components? I can create one with document.createElement('my-aurelia-component'), and container.appendChild(), and the element will fully work and have it's lifecycle callbacks called? I tried to look in the documentation, but couldn't see this clearly stated. It looked like Aurelia would have to be in charge of lifecycle to get dependency injection to work. Specifically, custom element constructors are called by the browser with no arguments: how does this work with Aurelia components that expect to receive constructor arguments via dependency injection?
- kensign 10y agothat work is handled by Aurelia itself. You don't explicitly need to define the element. http://aurelia.io/hub.html#/doc/article/aurelia/framework/latest/creating-components/1 http://aurelia.io/hub.html#/doc/article/aurelia/framework/la...
- spankalee 10y agoSorry, I still don't get it. Take the CustomerDetail example in the "Component Instantiation Through Dependency Injection" section: @inject(HttpClient) export class CustomerDetail { constructor(http) { this.http = http; } } If this really creates a Web Component, how does `this.http` ever get set? The browser will call the constructor with no arguments. What exactly gets passed to `document.registerElement()` (for the v0 Custom Elements API) or `customElements.define()` (for the v1 Custom Elements API)? Are you saying that Aurelia generates and registers a separate custom element class? That's not in the docs. If so, how does that element find the instances to inject into the user-defined constructors? The main issue that talks about custom element support is still open, and the answer really does seem to be that Aurelia can't create custom elements: https://github.com/aurelia/framework/issues/7 https://github.com/aurelia/framework/issues/7 This unfortunately seems to confirm that Aurelia uses the terminology of Web Components, but doesn't actually use or create real Web Components.
- Silhouette 10y agoRather than try to diff two DOM trees and optimize reconciliation, why not use one-way data binding and update exactly what has changed, with 0 reconciliation cost? If all of your rendered HTML corresponds 1:1 with the underlying data model, there's no particular advantage to the declarative approach used by React. However, once you start to have relationships in the underlying data, so DOM events in one place can affect the desired DOM content somewhere else, simple one-way data binding is not sufficient. For example, suppose we have an HTML table that needs to be rendered from some underlying data structure in our JS. How do you do that with simple one-way data binding -- what are you binding to what, exactly? If you're managing the HTML directly, you need to allow for things like rows being added or removed, not just updating the content of an individual cell. Now suppose that table also allows you to sort by different columns and filter so only certain rows are shown. Rendering the correct HTML now requires keeping track of the underlying data model and all of the UI-related state, with the actual rows to be shown affected by several different factors. Now suppose the table is going to have many rows, and pagination is required in the markup and controls. You certainly can handle all of the related events manually, but in order to keep the rendered HTML correct you now need to write code that understands each possible change in the underlying data and UI in every possible context, and the combinatorics can get quite unpleasant here. The alternative that declarative rendering tools like React offer is that you can specify what your rendered table should look like in absolute terms, using whatever inputs need to be considered but ignoring the existing state of the DOM. You still need to consider however many factors you have that affect the rendering, which might still be somewhat complicated in a case like this, but at least you only need to consider them from a single, neutral starting point, not relative to every possible starting point.
- spankalee 10y agoI don't get how your table example justifies the need for such a complex rendering pipeline. There are plenty of template libraries that can optimally re-render a list/table after row insert/remove/move operations. Events generated by elements in rows can easily be associated with the proper data item. React doesn't own declarative templating, and if you only use one-way databinding, and re-render only what changes (again, plenty of template libraries do this), you get `v=f(d)` and efficient updates. Why the extra complexity?
- underwater 10y agoIt's component diffing, not DOM diffing. I agree that the later is unnecessary. omponent diffing is an implementation detail that enables the declarations that makes React worth using. Imagine a parent component that conditionally composes a Button that is either red or blue. You could track that button instance and flip the property if necessary. But that requires a lot of error prone boilerplate, as most real applications might have many states depending on many inputs. With React you just define which variant you want in the render function of the parent. No state transitions necessary. React then does the hard work of understanding that you rendered a red button then rendered a blue button, so it should reconcile the two.
- kentor 10y agoData-binding is crap for many reasons. Here's a big one: inability to use immutable data types, except for the trivial case of a static component. When you're diffing the vdom, the system doesn't care where the data comes from, i.e. the data can come from a plain js array or an Immutable.js list. Second reason: When you have to worry about how to get your model to play nice with the view, that's a leak in the abstraction. This applies to ALL data-binding systems.