-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
upcoming v3 changes #147
Comments
Should I implement |
in v3 you can just replace in v3 you could implement your own diff inside render, by storing prior state within the view closure, diffing on each redraw and returning either a fresh vtree template or the current |
That's awesome! Thanks |
to be really future-proof, i recommend using the object-return pattern (or implement the diff yourself in none of the stuff mentioned here is set in stone, yet. hold off on making any changes just yet. |
Yes, I agree. I do not like a direct assignment API. It also prevents validation, read-only values, and in general being unable to transparently introduce many changes without breaking backward-compatibility. That is, it's too brittle. |
maybe the replacement would be to provide a single setter, to prevent the vm from continuing to grow public hair: vm.config({
hooks: {},
diff: function() {},
}); then internally i can also run |
What are the goals of V3? |
"views will no longer be auto-keyed by the passed-in model" is the main goal. this has surprised many people and it's clear that this is an unneeded point of friction. in general, api refinement is always a goal when BC is involved (which mandates a major semver bump). the overview at the top is the answer to your question. the reason it's v3 and not v v2.2 is because of semver/BC. |
Good to know; I am a fan of API refinement, and I dislike when there are multiple ways of doing the same thing... unless there are defensible use-cases that warrant it. I would support removing any duplication of functionality from the API. If there are 3 ways to set hooks, let's just pick one. |
it's difficult to provide a single way that satisfies multiple styles.
while i agree that having "one way" to do things would be ideal, there really are multiple scenarios where it cannot always work. in theory, |
All good points. Sounds like it fits into "unless there are defensible use-cases that warrant it". |
And that's one of the great strengths, and weaknesses, of JavaScript, that there's no one "best" way to do things is almost ubiquitously true of everything. |
if anyone is interested in testing, all the public-facing changes listed above besides
i think the new design is more powerful, logical (it keeps the partial templates within docs still need to catch up (hooks, diff), but i've removed the autokeyed-by-model section from them (DOM Recycling). |
i actually have some serious concerns about removing the model auto-keying. it may not be such a good fit for domvm's api after all. i'd like some feedback to see if this change causes more problems than it solves. the core issue is that we have a closure that accepts some in v3, this guarantee would be weakened significantly. now you have other problems like unintentional DOM recycling, view state transfer and a stale closured model. it seems there are more caveats to the new situation than the old. we'd have to recommend to ignore the closured we can change the API to remove |
FWIW, I grew tired of constantly checking and updating my view vs render arguments. I now avoid the closured Does |
here are a few considerations handled by the current design. a passed when you realize that render is recreated for each view, for stateless but model-bound views it may make sense to share render across instances while still having access to the vm and args (thus extracting it from the closure). this is something OO and class-based inheritance offer for free and it does matter [1]. the tldr is that render is currently fully extractable/sharable across view instances. such an optimization, though, is best handled by using ES6 class views. given how trivial it is to ignore and omit everything besides the as for the stale model, it's a side-effect of being able to operate in isolation on a non-global, mutable state. in react, the parent must always pass down data (or it must be pulled from some global via mobx/redux). in the former case, refactoring a component involves updating all ancestors to pass the data down a different path (eww). in the latter, magical globals or some shared echo-chamber event bus. if we decide that an extractable render is a far-fetched case, then its sig can be modified to sorry for the brain dump. [1] https://www.reddit.com/r/javascript/comments/6enbvb/classes_vs_closures_performance/ |
model can be made into a getter that returns the ideal solution here would be for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy |
another thing you can do to keep the closured or a slightly more expensive |
Would it make sense for the model to be an object created by DOMVM with the current arg value being a property of that object?
Then the argument is always the same state object and the model data is always current. Then the "state" would be a natural place to store stuff internal to the view that needs to be retained across all calls.
…--
Lawrence Dol
http://tech.dolhub.com/
http://think.dolhub.com/
(Sent from my phone)
|
the vm is already this object |
how would you guys feel about adding another view type. it would be easy to do and caveat-free. var v2 = domvm.defineView2;
v2(View2, state);
function View2(vm) {
return function(vm, oldDiff, newDiff) {
return el("div");
};
} this view would not be auto-keyed and would not inject the initial model or anything besides the vm into the closure. you would operate only on the hardest part would be meaningfully naming each version. if you understand the caveats of the original views and are not bothered by the extra args, then the original views can cover all cases as they do today. |
Now that I am at work, I have the time & energy to more fully consider this thread. Caveat: I am looking at this from the app developer's perspective, looking for a tight delineation of what "belongs" to DOMVM and what "belongs" to me. I don't like having to screw with properties of
I can think of important use-cases for having a non-closured, shared render function with the model/state injected in. Retaining that is very important.
This is the kind of situation where I prefer to have an "opt in", with such values provided in a map/object. However, I agree that the ability to simply omit trailing arguments makes the case for reducing arguments weaker. With that said, I have found over the years that adding arguments is very much not future-proof and usually indicates that the function is too broad, or that the API has been too ad hoc over time.
I've resorted to that trick somewhere, in a Mithril app, I think. I wasn't thrilled with it, but it got the job done. I'd rather the problem didn't exist.
It is from the point-of-view of DOMVM, but less so from the point-of-view of the app. The minute I have to store something in
With render then still extractable:
Maaaaybeee. But the simpler option should be
With the above said, it seems like a big part of the confusion is that the model data is accessible via two arguments because the render function is enclosed in the view init function. This could be mitigated by encouraging a form that always has the render function as a separate function at the same level as the View function. It could also be mitigated with an Lastly, I mentally distinguish between things that DOMVM defines and supplies in Maybe "model" should be a vm function which returns the current model supplied in the argument, instead of an argument passed in; then it can't be closed-over. In this case, I'd still want to have a state object available to render to store it's specific local state of the view that isn't part of the data-model driving the view, whether that was supplied as an argument or via Also, I think that the "key" is not something that should be of interest to the parent view; it should be something that the sub-view sets using which determines whether it represents logically the "same" node or a "different" node. It would usually be derived from the model. With that said I can think of situations where the key would be determined by the parent. So perhaps that means that the key is actually two optional parts, one from the parent joined with one from the child? |
In re-reading my comment, another important point occurs to me. I said:
But that's not the entire picture. There's:
I feel like DOMVM conflates 1 & 2, while leaving the app developer to (instinctively) conflate 2 & 3 to avoid polluting 1 with stuff for 2. |
thanks for the thorough feedback. i don't have too much time to respond to each point but i've read through everything and want to refine the api.
// plain-object view
const View = {
init: function(vm) {},
hooks: {},
diff: function(vm) {},
render: function(vm, oldDiff, newDiff) {},
};
// object-returning closure
function View(vm) {
return {
init: function(vm) {}, // redundant w/closure invocation?
hooks: {},
diff: function(vm) {},
render: function(vm, oldDiff, newDiff) {},
};
}
// render-returning closure (shorthand for object-returning closure)
function View(vm) {
return function(vm, oldDiff, newDiff) {
};
}
// functional view with state
// todo: need a way to discern this from non-functional view
// todo: where do oldDiff, newDiff go?
function View(vm, arg1, arg2) {
return el("div");
}
class MyView extends View {
init() {} // redundant w/constructor?
diff() {}
render(oldDiff, newDiff) {}
} by only ever injecting function View(vm) {
var model = vm.data;
return function(vm) {
return el("div", data.text);
};
} i think this is a good balance and i can sleep at night with removing the auto-keying. also, |
Given that this is a "0.1%" optimization, I would be fine with the diffs being extractable from
I concur.
Either |
I like the explicitness of that much, much better. |
i tried dropping all closure and render args in eaef453 and the resulting ergonomics were not great IMO. needing access to not passing for me, keeping the closured model/data updated (if needed) from inside render() is the shortest (3 chars minified) and 0-overhead solution. if this method feels dirty to you, then you can always use function View(vm, data) {
function click() {
data.count++;
}
return function(vm, newData) {
data = newData;
return el("a", {href: "#", onclick: click}, data.count);
}
} when plain-object views are implemented, then a closure will not be forced on the unwilling, and you won't have any of this staleness to worry about.
|
Can the overarching problem not be resolved by not including the data in the View factory arguments, since it is a conflict with a stale value? function View(vm) { // no data arg for "initial" value of data
function click() {
let data=vm.data; // only if you actually need data during event
data.count++;
}
return function(vm, data) { // here's where you most often need data
return el("a", {href: "#", onclick: click}, data.count);
}
} Personally, I would far prefer to employ explicit helper functions than have counter-intuitive "magic" and "strangeness". For me, caveats indicate possible flawed design. That is, the "data may be stale" indicates a possible error. function View(vm) { // no data arg for "initial" value of data
return function(vm, data) { // here's where you most often need data
return el("a", {href: "#", onclick: vmHandler(vm,data,incCounter) }, data.count);
}
}
function incCounter(vm,data) {
data.count++;
}
function vmHandler(vm,data,hdl) {
return hdl.bind(null,vm,dta);
} This tends to result is looser coupling and more reusable with more explicit values and less assumptions. The code is easier to understand in context. |
right. that and event handlers (and maybe hooks). typically the data is not needed directly within the closure itself. maybe it makes sense to pass the current data as an arg to event handlers (as well as render) to prevent having to alias it there. everywhere else is much less common.
btw, in your example, you're re-creating the onclick handler every redraw, which means domvm will re-bind |
FWIW I'm happy with the route this discussion took. More important than any semantics, is a strong conviction from the main contributor to shape the vision of the project and to stay true to that focus. I think mithril lost some of that, which is why die hard fans are now looking for alternatives. It also looks like Tangents: |
Mithril's API never felt great to me. things like always-global redraw (without granular redraw) and its use of a Controller in pre-1.0 was the deal-breaker that had me look elsewhere. I found domchanger and it felt much better but ultimately pretty slow and still too restrictive. and so i decided to write a vdom lib that took some api bits i liked from both libs and here we are.
yep, i've never used key or opts in render and if i ever need them they're both available on the vm or in the closure's args since they're immutable.
ideally, yes. but not necessarily. for instance, you may want to define handlers ad-hoc via you'll definitely want to avoid
domvm builds use Bublé rather than Babel. as for typescript, i'm warming up to the syntax, albeit slowly. same for Prettier. i havent done too much testing for bundle size and resulting perf of TSC, so i'm not ready to migrate domvm to it yet. however, i'm open to someone contributing a |
I completely agree, particularly with "shape the vision". I like having input, making my case... and I'm happy for Leon to say, "Yeah... nah!". Swings designed by committee very rarely work out well. It's very hard to reason about things out of context as Leon's final comment showed, with respect to arg order, "it became obvious why the arg order for events was
Components, "controller", global redraw + redraw suppression are what disenchanted me, though the concept is great. But finally, the disengagement of Leo and the sense of "lost-ness" of the community coupled with an apparent desire to complicate the shit out of building it (even if simplicity of use was preserved). But, what actually made me most nervous with Mithril is the sheer velocity of commits; it felt like this very simple, solid core on which to build an app was becoming the next Behemoth. I very much want to see DOMVM's core stabilize and remain largely untouched, while specific things develop around it's periphery.
Neither have I.
I'm very much opposed to it; something like DOMVM should remain Vanilla JS. In no small part because "I very much want to see DOMVM's core stabilize and remain largely untouched", therefore, DOMVM should not need a meta-language on top of JS. Related is that I want to be able to debug POJS if ever there appears to be a problem in DOMVM. |
i assume you're talking about the core api. the only time when the core will remain untouched is when domvm is no longer actively maintained. i do hope that v3 will be the last api iteration for some time. domvm v1 had aspirations for being 100% build-free. however, that didnt seem to provide the ease-of-contribution benefit it was meant to encourage. people don't even help write docs, lol. in the end build-free isnt realistic in a complex, well-organized app. you'll always have at least some assembly of modules and domvm v1 left this up to the browser and acript includes, which moved the burden to the frontend/production, which was shitty. domvm's codebase is not es5 either, and will likely use more es2015 features, not just modules. so the build steps are already there. they're easy but not absent. the build process produces both minified bundles and non-minified es5 bundles with sourcemaps that anyone can debug & tweak without building anything. what language the actual source is written in is only of relevance to those who work on the code, not users of the lib (who will always get es5 bundles). therefore, the aversion to typescript by users is rather misplaced given the current state of affairs. that being said, i dont see much value that typescript would bring me, the only core dev. the project is small and digestible and i usually code in an editor that doesnt have TS support anyhow. maybe once vscode starts also working well for php, i'll consider switching. i do see a lot of value in a |
Precisely. No editing environment has lured me away from my trusty vi setup until now. @lawrence-dol One of the goals of TypeScript is to emit idiomatic, recognizable JavaScript code[1]. No source code mappings necessary. Everything is essentially opt-in, so you can use it merely as a standard ES2015 compiler, which is how I got started. I found immediate benefits from the implicit types, and eventually moved to VS Code to see the errors and warnings instantly on keystroke, instead of waiting for run time or even build time. Then came the light of inline type definitions, documentation, and source-code, and it is a whole new world. I no longer need to visit some terribly formatted website for documentation, because I can discover a library's API as I consume it, with each keystroke. If I am refactoring something, I can see right away if I forgot to rename a reference. @leeoniya One of my immediate draws to domvm was its readability. Though it lacked a large community, I felt comfortable reading the source code and confident I could reason about it if necessary. Taking this a step further, if this project was in TypeScript, I would never need to leave my editor. While upgrading versions, mismatched API arguments would be highlighted instantly. I'm totally biased, but embracing types can be a solid form of documentation, and we know everyone loves documentation =) [1] https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals |
wouldn't a |
for v3, i'm going to drop ES6 class views in favor of plain object views. the the main problem with ES6 class views is that the calling convention for all the defined methods needs to drop any |
plain object views are pretty sweet. and were basically 0 effort. https://rawgit.com/leeoniya/domvm/3.x-dev/demos/object-views.html |
what are people's thoughts on splitting the
you'd simply include domvm.config({
autoPx: (function() {
var unitless = {
animationIterationCount: true,
boxFlex: true,
boxFlexGroup: true,
boxOrdinalGroup: true,
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
flexOrder: true,
gridRow: true,
gridColumn: true,
order: true,
lineClamp: true,
borderImageOutset: true,
borderImageSlice: true,
borderImageWidth: true,
fontWeight: true,
lineHeight: true,
opacity: true,
orphans: true,
tabSize: true,
widows: true,
zIndex: true,
zoom: true,
fillOpacity: true,
floodOpacity: true,
stopOpacity: true,
strokeDasharray: true,
strokeDashoffset: true,
strokeMiterlimit: true,
strokeOpacity: true,
strokeWidth: true
};
return function(name, val) {
return !isNaN(val) && !unitless[name] ? (val + "px") : val;
};
})()
}); and the default impl would be function autoPx(name, val) {
return val;
} EDIT i did the impl on the |
If I've made any use of auto-px, it's accidental. I prefer to be explicit with this and most things. Carve it out, I say. |
ditto. |
the current |
i ended up with a compromise. i really don't want script includes and also don't want the smaller builds expanding. i moved |
while i may still do some internal cleanup, v3's features & API are now fully baked. if anyone feels adventurous, please take it for a spin and report any issues or perf regressions. i'm gonna spend some time re-testing the demos, running the full benchmarks and updating the docs. barring any serious regressions, v3 should be tagged sometime next week. |
initial round of benchmarks shows a possible minor regression (~5%) in dom creation for massive trees (30k elements, 10k subviews). everything else falls within the noise variance. mem useage shows ~15% reduction. i think we're good for perf. |
almost forgot about // old
domvm.streamCfg({
is: null,
val: null,
sub: null,
unsub: null,
});
// new
domvm.config({
stream: {
is: null,
val: null,
sub: null,
unsub: null,
}
}); |
btw, i removed the flyd adapter from domvm core and moved it into userspace in the demos: https://github.com/leeoniya/domvm/blob/3.x-dev/demos/streams.html |
SSR benchmarks [1] are in. this is for 204 views & 2007 DOM nodes vidom // 1630 ops/s
inferno // 1430 ops/s
domvm // 1246 ops/s
preact // 604 ops/s
react.with-hack // 228 ops/s
vue // 100 ops/s
react // 30 ops/s code for the curious: var domvm = require('../node_modules/domvm/dist/server/domvm.server.min.js');
var el = domvm.defineElement,
vw = domvm.defineView;
function mkArr(count, fn, frag) {
let i = 0, arr = [];
while (i < count)
frag ? arr.push.apply(arr, fn(i++)) : arr.push(fn(i++));
return arr;
}
function App(vm, data) {
return () =>
el('.app', [
vw(Header, data),
vw(Content, data),
vw(Footer, data),
])
}
function Header(vm, data) {
return () =>
el('.header', mkArr(data.childrenNum, i =>
el('div', { id : 'header-' + i })
))
}
function Content(vm, data) {
return () =>
el('.content', mkArr(data.childrenNum, i => [
el('b', 'bold' + i),
el('span.link', [
vw(Link, { href : '/', value : 'link-' + i })
]),
el('i', 'italic' + i),
el('div', [
el('div', [
el('div', [
el('div', 'div')
])
])
])
], true))
}
function Link(vm, data) {
return () =>
el('a', { href : data.href }, data.value)
}
function Footer(vm, data) {
return () =>
el('.footer', mkArr(data.childrenNum, i =>
el('div', { id : 'footer-' + i })
))
}
module.exports = childrenNum => () => domvm.createView(App, { childrenNum }).html(); [1] https://github.com/dfilatov/vidom/tree/master/benchmarks |
k guys, i have a barebones playground working. please try it. i'd like to start moving all demos into it. |
Looks good man, it'd be nice to be able to resize the panes. |
lots of stuff would be nice :) the whole thing is 216 LOC [1]; contributions always welcome! [1] https://github.com/leeoniya/domvm/blob/gh-pages/playground/index.html |
I guess what I meant was the conceptual core, which includes the API. I hope that the core of DOMVM becomes highly stable, and the out-of-the-box functionality is delivered as optional parts of an ecosystem, rather than DOMVM continually growing, in both complexity and size. You've shown every proclivity, to date, in keeping the core small and laying in additional functionality. I think that's good. |
By the way, I am using V3 for a new project. So far, so good. I quite like the ergonomics of the view returning an object, with the init and render functions outside the enclosure. |
sounds like you might as well just use plain object views: const View = {
init: (vm) => {...},
render: (vm, data) => tpl,
}; |
I completely agree having just ported Mithril |
🎉 v3.0.0 🎉 changelog: https://github.com/leeoniya/domvm/releases/tag/3.0.0 |
heads up! event handler registration for
sorry i had to do this API break post-3.0, but it was an oversight and this change makes things more uniform with other similar changes introduced in 3.0. will be tagging 3.0.1 in a bit. |
domvm.lazyList()
creator &domvm.LAZY_LIST
flag for creating deferred homogenous children that can reuse old vnodes without additional allocation.render()
will be able to returnvm.node
(the old vnode) to prevent/optimize redraw when no changes.xlink:href
support in svgvm.diff()
&vm.hook()
will be removed in favor of what is already possible:direct assignmentvm.hooks = {}
vm.config({hooks:..., diff...})
{hooks: ..., diff:..., render: ...}
{hooks: ..., diff: ...}
v3 should be ready within the next couple weeks.
The text was updated successfully, but these errors were encountered: