Matt-Esch / virtual-dom

A Virtual DOM and diffing algorithm
MIT License
11.67k stars 777 forks source link

Efficient Subtree Patching Proposal #343

Open Zolmeister opened 8 years ago

Zolmeister commented 8 years ago

Edit: After considering the problem further, it may be that it's already possible to do this without any changes (as a hack) and to do properly would only require adding hooks to Thunks (nothing else).

// tl;dr
Hook.prototype.hook = function(node) {
  this.state.subscribe(function(){
    oldTree = Thunk.tree
    newTree = Thunk.render()
    patches = diff oldTree, newTree
    patch node, patches
  })
}
Thunk.prototype.render = function() {
   return this.tree = h('div', 'Hello World')
}

// more detailed
$component = createComponent function() {
  var state = new Rx.BehaviorSubject('test')
  setTimeout(function() { state.onNext('change!') })
  return {
    state: state
    render: function() {
      currentState = state.getValue()
      return h('div', currentState)
    }
  }
}

render($component, DOM)

// kind of, you actually need a singleton-ish object from a custom 'h()' function / constructor
createComponent = function (init) {
  {state, render} = init()
  var thunk = new Thunk()
  thunk.render = function() { return thunk.tree = render() }
  thunk.hook = new Hook()
  var subscription = null
  thunk.hook.hook = function(node) {
    subscription = state.subscribe(function(){
      oldTree = thunk.tree
      newTree = thunk.render()
      patches = diff oldTree, newTree
      patch node, patches
    })
  }
  thunk.hook.unhook = function() {
    if (subscription) {
      subscription.dispose()
      subscription = null
   }
  }
  return thunk
}

Old:

Stemming from https://github.com/Matt-Esch/virtual-dom/issues/304, having an efficient way to update components in-place. This solution is designed as an extension of hooks on top of thunks.

var Hook = function(state, render){
  this.state = state
  this.render = render
}
// The new render method here is what's important
// It updates the root v-tree (by mutation... or something efficient)
Hook.prototype.hook = function(node, propertyName, previousValue, render) { // render
  this.disposable = this.state.subscribe(function() {
    render(this.render()) // render v-tree, patch contents of thunk, render to DOM
  }
}
Hook.prototype.unhook = function() {
  this.disposable.dispose()
}
var Thunk = function() {
  this.state = Rx.BehaviorSubject()
  this.hook = new Hook(this.state, this.render)
}
Thunk.prototype.type = 'thunk'
Thunk.prototype.render = function(previous) {
   return h('div', 'Hello World')
}
createElement(new Thunk(){})
serapath commented 8 years ago

I guess this issue is meant for internal discussion about how to eventually add a feature to virtual-dom or not.

I'm still having a hard time to wrap my head around how to use thunks and hooks. The above seems to use Rxjs - is it an example? Otherwise - could you give an example how to just "use" the Thunk Efficient Subtree Patching thing to write a pseudo-status-update-component where all the generic Hook/Thunk stuff is hidden behind a helper and instead of Rxjs it might use some kind of plain old callback or something? That would really help me a lot.

Zolmeister commented 8 years ago

@serapath The Thunk is the component. Here is a naive implementation which doesn't allow for lazy state.

var Hook = function(){}
Hook.prototype.hook = function(node, propertyName, previousValue, render) {
  this.render = render
}
Hook.prototype.unhook = function() {
  this.render = null
}
var Thunk = function(){
  this.hook = new Hook()
  this.state = {}
  setTimeout(function() {
    this.state = {updated: 'state'}
    this.hook.render(this.render())
  }.bind(this))
}
Thunk.prototype.type = 'thunk'
Thunk.prototype.render = function(previous) {
   return h('div', 'Hello World' + this.state.updated)
}
createElement(new Thunk(){})
kuraga commented 8 years ago

So, what's the proposal?

Zolmeister commented 8 years ago

@kuraga 2-fold.

  1. Adding hooks to Thunks (which I don't think exist right now)
  2. extending the hook function signature with a new 'render' function which updates the tree in-place
serapath commented 8 years ago

@Zolmeister proposal sounds good to me. One question - should the "extended hook function" provide a 'render' function, that "updates the tree in-place" or should it instead generate a PATCH directly? A bunch of patches can then be applied to the previous version of the vtree - no diffing required.

Zolmeister commented 8 years ago

@serapath Good idea, in fact the new solution is much more elegant. See the edit on the proposal

serapath commented 8 years ago

Oh wow, that looks so much nicer and I actually feel I kind of get how it's supposed to work :-)

One thing I was thinking is, that there is the Big VTree that represent's the whole page. Each "component" updates it's own vtree and node, but all the Patches produced should maybe be "piped to the engine" which applies them at once in each requestAnimationFrame.

I don't know how the patches currently look like and how they get combined into an optimized DOM update, but if the component patches it's own DOM node, the benefit of that might be lost...

Zolmeister commented 8 years ago

@serapath originally the proposal would have meant mutating Big VTree, and it would continue to be the 'single point of truth' about the DOM. However the trade-off didn't seem worth it

As for updating, requestAnimationFrame fires (I think) for all listeners on the same frame/tick, so there should be no need to synchronize updates per-node, it should just work.

serapath commented 8 years ago

I tried to create an example using: virtual-dom, memdb & level-tracker Do you think this requirebin makes sense?

Zolmeister commented 8 years ago

@serapath it does for the most part. init returning a div wrapper illustrates the need for hooks on thunks.

serapath commented 8 years ago

I'm still not happy with the "API".

With normal dominic tarr's hyperscript, i saw the option to pass in "observables" in order to update itself without the need to re-render a hyperscript template. See: https://www.npmjs.com/package/hyperscript#cleaning-up

Does that work for virtual-hyperscript too?

Basically, the hook in the "requirebin" (line27 - line 50) is just used to set up the a subscription to "data updates" and if there is new data, the template "re-renders" ... I imagine that something like that text reference in dominic tarr's example could have a similar effect.

Zolmeister commented 8 years ago

@serapath That approach is similar to what Zorium achieves with streams, though not as fine-grained (e.g. a single text element).

One of the biggest problems I've faced while implementing streaming components is the need to guarantee server-side rendering success withing a given time constraint. Having a 'snapshot' (non-stream) way to capture the server-side DOM is essential for efficient server-side rendering.

Not that it couldn't be done in the way you describe, but it's not an easy problem to solve.

serapath commented 8 years ago

But you use Rxjs and not node streams.

Zolmeister commented 8 years ago

@serapath it doesn't matter, it's a question of dealing with 'observables' which are 'lazy' (e.g. API requests) and contain no value until resolved

calebmer commented 8 years ago

Have hooks been added to thunks? This would be an amazing feature to implement the ideas discussed here.

kuraga commented 8 years ago

Returning to this... Sorry, @Zolmeister, can you make your first-message example more precise, please? What's Thunk.tree? Why Thunk.tree and Thunk.render are class-level not instance level?

Zolmeister commented 8 years ago

@kuraga updated to be more clear, the shorthand was simply to avoid writing out full notation

kuraga commented 8 years ago

@Zolmeister thanks! That's what I expected - creation a hook in thunk. In // tl;dr example I see else - creation a thunk in hook... And Thunk.tree and Thunk.render are now thunk.tree and thunk.render (not singleton) - as I expected :smile: