Open rjgotten opened 4 years ago
This behavior is different for "magic" attributes and "magic" between-tags as shown here:
https://codepen.io/justinbmeyer/pen/wvvBGEo?editors=1111
This is due to the catching @rjgotten mentioned. This asymmetry isn't great.
Yeah, this is confusing. To summarize:
<div>{{ uuid() }}</div>
-> not cached<div data-uuid:from="uuid()"></div>
-> not cached<div data-uuid="{{ uuid() }}"></div>
-> cached@phillipskevin yeah, attributes cache the compute. This helped the performance of the "spinner" example as we wouldn't need to rebuild computes (and all the scope walking) for something like:
<div id="{{number}}" style="left: {{number}}px" alt-thing="{{number}}">
I don't remember why this wasn't also done for "magic between-tags". Maybe for a reason similar to this.
An alternate approach would be to only
cache computes when they have dependencies.
// NOT CACHED
var compute = new Observation(()=> Math.random() )
compute.hasDependencies() //-> false
// CACHED
var compute = new Observation(()=> Math.random()*obs.value )
compute.hasDependencies() //-> true
This would allow people to opt out with ObservationRecorder.ignore()
too:
var compute = new Observation( ObservationRecorder.ignore( ()=> {
return Math.random()*obs.value
}) )
An alternate approach would be to only cache computes when they have dependencies.
That would actually still break if you have something side-effectful that also observes, right? 😞
Granted; that would also break in my own proposal and yes, you could avoid it with ObservationRecorder.ignore
.
However, after having given this some more thought over the course of the day, maybe something altogether different would work to solve the general problem. I'll explain.
Currently there's a metadata.rendered
which can be set to true
to signal that the helper will return a callback function( el ) {}
expecting the rendered DOM element to do some work on. Ofcourse; that's specific to contexts where an element exists.
So...
what if you allow helpers in general to return a function for a late-evaluated effect? The rendered
flag is really just a special case - particular to needing a DOM element reference - of the general purpose of late bound evaluation, isn't it?
What if a conceptual metadata.late = true
could signal the view engine that whatever value is spit back out of a call expression evaluator consists of a function that needs to be run and its that function's output which should be used by the magic tag?
In such a case, all the parts that need to be observable can be inside the outer helper. And the parts that are side-effectful can be inside the returned function.
Quick draft for the uuid
example and how that would work out:
import { stache } from "can";
const uuid = 0;
function uuid( prefix = "", options ) {
if ( stache.isOptionsLike( prefix ) {
[ prefix, options = [ "", prefix ];
}
options.metadata.late = true;
return () => prefix + (++uuid);
}
uuid.requiresOptionsArgument = true;
export { uuid as default };
Here the observable (and bound/cached) part is in the outer function which is made observable and whose result is cached in the evaluator cache. This observable then returns a function which is uncached and unobserved and facilitates the side-effectful sequence number.
The general logic flow would still remain: pull the observable from the evaluator cache; call can.getValue
and grab the bound/cached result. But, if options.metadata.late
indicates late-evaluation and that result is a function, then call said function and return its value - instead of directly returning the observable's result.
Proposal
Extend call expressions to pass in
notWrappedInObservation
flag from individual user-supplied helpers, allowing non-observed functions to return non-cached values.Benefits
Allows users to author side-effectful helpers. One use-case is a helper that implements unique IDs as sequence-numbers; increasing with each call. E.g.
This currently doesn't work, as call expressions are always wrapped in observations which cache their result value; and the observations are in turn put into the expression evaluator cache. I.e. both
uuid
calls above will return the same result.Strip away the observation; turning it into a plain function, and things do work.
Drawbacks
None that I am aware of. The code to add this isn't even that many lines to adapt.
Migration
This can be implemented strictly as a feature toggle that requires opt-in on individual helper functions. This means it wouldn't break anything.
Implementation details
Call.prototype.value
has a small piece of code that would need to be adjusted:https://github.com/canjs/can-stache/blob/942724f6cd15b8ea5ab07f7f186ee3b47162282f/expressions/call.js#L117-L123
This would become: