adamhaile / surplus

High performance JSX web views for S.js applications
639 stars 26 forks source link

Basics of Surplus Array rendering #83

Closed mrjjwright closed 5 years ago

mrjjwright commented 5 years ago

I have yet another question about fundamentals that my slow brain is not quite getting. I come from the React virtual dom world where I always put a key on every item in a dynamic list so that React's virtual dom implementation can safely do a keyed list reconciliation. I notice no use of keys here in Surplus. So in a list situation on the first render, Surplus is going to emit a set of real DOM nodes, and on subsequent renders it will do the same and reconcile them. I see from the source that reconciliation is done by being equable. 2 nodes are the same if they are strictly === or if they are string or Text nodes that can be reused. Now I also notice that in most of the examples that typically SArray maps are being used, which I believe memoize computations in items in the underlying array that haven't changed. Is that the trick to doing the reconciliation without keys since the SArray map will produce a node list that is a blend of old and new nodes? Should Surplus then only be used with SArray?

mrjjwright commented 5 years ago

I think this also jives with what @ryansolid says here https://github.com/adamhaile/S/issues/21#issuecomment-471271882

ryansolid commented 5 years ago

Short answer is yes.

It is common for these types of libraries to special methods/types for arrays. One of the biggest gripes pre MobX 5 (which switched to proxies) was that ObservableArrays weren't actual arrays.

That being said is if you deconstruct it a bit it makes sense why there are special helpers. Unlike VDom which makes faux virtual nodes a library like this makes actual nodes. So if you write list().map in an expression it has no choice but to do all the work since accessing list means any change to it will cause the whole thing to refire. Whereas VDom nodes describe the intended state pretty cheaply, Surplus would have to create the DOM nodes and nested computations. This is not cheap.

The next idea to cache values still has this problem since you'd want to hoist those outside of the computation but accessing list() will still cause the whole thing to reevaluate. The expression computation itself can't be smart enough to handle this as it won't know its content until it runs.

At minimum you need to use some sort of helper to make sure list isn't accessed directly in the expression computation and instead creates a nested computation so we can keep the cached values around. So either you extend the signal prototype with a list.map(notice we don't access the value with ()) or write one that takes the list signal as an argument like, map(list, item => ....). So at minimum you need a signal aware helper, meaning a generic map method found elsewhere will not do.

At that point understanding what happens in that helper is related to the other conversation since as soon as are hoisting cached mapped values you have to be aware of the computation lifetime.

mrjjwright commented 5 years ago

Wow intriguing answer as to what goes into that decision, thanks!.

mrjjwright commented 5 years ago

I guess looking back on your answer, the question that comes to mind, is how does Surplus do it then without such a helper? This sounds like an explanation of how your lib does it, but curious how Surplus gets away without it.

mrjjwright commented 5 years ago

Oh I get it, "extending the Signal prototype" is is what SArray essentially does.