Closed yyx990803 closed 5 years ago
/cc @vuejs/collaborators @octref @Atinux @DanielRosenwasser @alexchopin @clarkdo @pi0 @chenjiahan @johnleider @Leopoldthecoder @KaelWD @icarusion @rstoenescu @rigor789 @Hanks10100
You have been added to this repo either because you are a core team member or as maintainer of notable projects that builds on top of Vue. I'm giving you early access to the WIP repo to provide early feedback on proposed breaking changes to Vue internals. What I am most interested in is how much would the above changes affect your project - for each change, how difficult would it be to adapt to it? Would any of these be a deal breaker? Any of these would really help? Any additional ideas? Any type of feedback is welcome - also keep in mind that this is very early stage and nothing is set in stone yet.
Hi Evan,
Nice work! Here are some of my early thoughts:
However, the behavior of undeclared props falling through as attrs will be removed; it's as if
inheritAttr
now defaults tofalse
. The component will be responsible for merging the props as attrs onto the desired element.
Does this mean component users cannot output arbitrary attributes onto the root element unless the component authors explicitly allow this? I think that might cause some troubles because component authors usually cannot know in advance what attributes are necessary in certain use cases (mostly interoperability issues). eg. A11Y related stuff aria-*
/role
or Microdata's itemprop
/itemtype
/... or some necessary attributes when leveraging some existing frontend libraries that depend them.
This seem that we cannot access components inside directives anymore (which we do quite a lot currently in our projects). I personally prefer directives over components on certain use cases and I haven't think of a way to migrate without drastically breaking our current API ATM.
In templates, components should be uppercase to differentiate from normal elements.
Does this mean we no longer allow kebab-casing for Vue components in templates?
NOTE: how to tell in browser templates?
Just skip step 1 for compiler intuitions seems fine?
Great. Though this might hurt those components providing slots and scoped slots with the same name (but for different purposes). It already doesn't work as expected when using template, while those who are using render functions for this might gonna change slot names.
- Custom directives are now applied via a helper:
Does this only affects render
functions? Is there any difference for templates?
No longer removes attribute if value is boolean false. Instead, it's set as attr="false" instead. To remove the attribute, use null.
Do we still have predefined boolean attributes (true boolean attrs like disabled
/checked
, not boolean-ish strings like draggable
)?
might gonna change slot names
You could probably use slot.length
to differentiate between scoped and not.
seem that we cannot access components inside directives anymore
I think we use this in a few places too.
However, the behavior of undeclared props falling through as
attrs
will be removed; it's as ifinheritAttr
now defaults tofalse
.
What I get here is: this.$props
would have all the props and there is no this.$attrs
which means if I have to forward unhandled attributes to an element I have to calculate this.$attrs
equivalent. Right?
VNodes are now context-free
I guess this would affect devtools. /cc @Akryum
// after (consistent with JSX usage)
{
type: [Function MyComponent], // type of vnode, some sort of reference to component.
id: 'foo',
domPropsInnerHTML: '',
onClick: foo,
key: 'foo',
ref: ref => {
this.$refs.bar = ref
}
}
Maybe we can add type to vnode so that it's easy do the check vnode.type === MyComponent
.
Flat Data Format
seems friendly to tsx, should we add event(emit) declaration into component?
similar to props declaration, you can access the declaration of the event in component instance, or $emit the dynamic event
// Now can also be: const Func = (h, props, slots, ctx) => h('div') Func.pure = true
why we need the pure
props?
Filters are gone for good (or can it?)
a nice sugar for template
why we need the pure props?
I think it's to identify functional components from components created with Vue.extend
/ async components. But I think we could add a flag to extended components, and omit pure
from functional components.
Filters are gone for good (or can it?)
a nice sugar for template
Oh man, I can still recall the chaos it created when the same proposal was raised for v2. Too bad it's been a while and pipe operator is still not a thing yet. FWIW, all the arguments (for both sides) in the linked issue should still be valid.
Edit: Yes, they can be gone now that users are more used to a v2 world with limited support for filters :)
What would be the prop merging behavior for class
and event props (onClick
)?
I share the same concern with @Justineo . Other than ariia/itemprop like attributes, class
is a common usage of prop falling. Migration tool might help but code modification seems to be unavoidable and tedious.
why we need the pure props?
@Kingwl I guess it might be used to differentiate between class component or
Vue.extend
from pure functional component.pure
seems to be optimization hint.
https://github.com/vuejs/vue-next/blob/8de1c484ff2c9bab81f1a93fcb58f53859ff0227/packages/core/src/createRenderer.ts#L558-L560
@phanan oh, i forget the pipeline operator syntax :rofl:
you are right, drop it
Although pipeline is still stage 1
I also share some of the concerns @Justineo has.
component authors usually cannot know in advance what attributes are necessary in certain use cases
Without $attrs
, we may have to manually filter out all explicit component APIs from props
and attach what's left of it to the desired element.
we cannot access components inside directives anymore
This seems to be a fairly common use case in our projects as well.
Filters are gone for good (or can it?)
I personally have never used filters in my projects since Vue 2.0, so for me removing it doesn't hurt.
Really be excited with Vue 3, and looking forward to it.
Weex is built on bottom of Vue.js, so the breaking changes of syntax will not affect Weex actually. But some compatibility work still can't be omitted, mostly for the new package structure and VNode, not syntax.
VNodes are now context-free
By the way, I think it is a good idea and should be insisted. I wish the Component and VNode could be separate, the interactive API between them could be explicit and minimal.
If be more radical, the vdom (or VNode) may not be needed for native-rendering and server-side-rendering scenarios, at least it should not be handled by javascript. For Weex, it's feasible to implement the vdom (VNode) natively by C++, and expose the corresponding APIs to the running context. Moreover, the create/diff process of vdom can also be compiled to WebAssembly, although it may not certainly improve the performance since WebAssembly can't assess DOM API yet, it can be used to generate HTML strings in the server side. However, if component and vnode have so many coupled properties or features, it would be very hard to make the rendering process to take advantage of native platform abilities.
So, I think separate template, component, and vdom is good for long-term evolving, even if it hurts.
This looks a amazing, I finally feel like this is something I can navigate and undestand :-P
Points I want to comment on:
Like others I'm not too sure about the whole droppinf of $attrs and $listeners, and especially automatic interance of attributes.
While $attrs and $listeners can probably be re-implemented in userland pretty easily, the last point could be a point of great pain for people - unless we find a way to easily allow for that to be done in userland as well without requiring to touch every single template in your project?
I'm torn. Filters are usually formatters that people use appliction-wide. When we drop them, people will be forced to implement them as methods, possibly via extensions to the Vue prototype.
currency
is a create name for a filter or a data/prop. Sure, naming rules can help here, but the current implementation doesn't have this problems as filters live in there own namespace.I'm not too attached to them, but if we drop them, we need a good guide about how to replace them in a maintainable way.
Awesome, will be very good for performance to make them lazy I imagine. Even though that change requires changes to manually-written render function, the changes are small and easy (and maybe possible to be automated?)
The new API seems great, slim and easy to parse. and I see how attrs
and listeners
don't fint in there, but could be re-implemented on the instance as mentioned above.
Of course this means, similar to slots, that manually written render components have to be updated, but unlike slots, the change is a little more work. I could imagine that we provide a little helper method that people can wrap their manually written VNodeData
objects in to be converted to the new format. That would allow for a quick fix, and can be cleaned manually later.
This is a point that wasn't mentioned at all in the OP, and I can't find anything about them in the source either.
I hope they're not killed like React did when they switched to a class-based syntax. So much of the ecosystem relies on them (most of Vuetify is implemented as mixins I think), so I can imagine it would be a big problem.
Would it be possible to make them a static property on the class that is used by the renderer to apply the mixin after creating the instance? what about beforeCreate
mixins, then? How do we make them works with types? I have no idea. :/
And on a lighter note: Not sure how to feel about the fact that Vue 3 will have Portals, which kills the need for my only popular OS library :D :D
How do we make them works with types?
I wrote a little helper for that, could probably be included with vue depending on what the API ends up looking like.
export default mixins(A, B, C).extend({...})
// OR
export default class MyComponent extends mixins(A, B, C) {...}
The only thing affecting Vetur is removal of filter. This is great because I can treat interpolations as JS statements without any custom syntax handling. But also +1 to what @LinusBorg said:
I'm not too attached to them, but if we drop them, we need a good guide about how to replace them in a maintainable way.
I might be able to provide editor support that:
Answering a few concerns:
First, class
and style
merging behavior remains the same (and now applies to single-root functional components as well).
Second, I think the ability to render arbitrary attributes on the root of a child component is a useful one, but currently it's a very implicit behavior.
The problem I see with the implicit fall-through is that you read the code of a component being passed props, you won't know which ones will be treated as props and which ones will be treated as attributes without knowing what props the component declares:
<!-- is label a prop or an attribute? -->
<Foo label="123" />
In addition, because props declarations are now optional, we actually don't have a way to implicitly extract attributes for component that does not declare props.
Maybe we can differentiate the two (component props vs. native attributes) similar to how we differentiate component events vs. native events with the .attr
modifier:
<!-- this is always a prop, although the component *may* explicitly render it as an attribute, there's no guarantee -->
<Foo label="123" />
<!-- this is always an attribute on the component root -->
<Foo label.attr="123" />
In the compiled code, props with .attr
modifiers are extracted as:
h(Comp, {
// explicitly merged on to child component root, like `class` and `style`.
attrs: { /* ...* / }
})
Then, we need to consider the case where the component may return a Fragment, or may have a wrapper element and want to take full control of the full-through. In such case, inheritAttrs: false
will disable merging for class
, style
and attrs
, and the component author will be able to spread these onto desired element via this.$props.[class | style | attrs]
. (this avoids the runtime cost of extracting and allocating memory for $attrs
.)
Or, maybe I'm overthinking all this and implicit fall-through is fine (and actually useful). Although, note that the following are orthogonal to whether we keep implicit fall-through or not:
.attr
modifier. This allows you to be explicit and not relying on anything implicit.inheritAttrs: false
affecting style
and class
: probably a good idea to make it more consistent.$props
when inheritAttrs: false
: this allows components to achieve the equivalent of {...props}
with v-bind="$props"
.The only difference is that with implicit fall-through, any props not declared (plus ones with .attr
modifier) will be grouped under $props.attrs
. Otherwise, only ones with .attr
modifier will be grouped in there.
Thoughts?
This is still possible. The vnodes are context-free, but directives are applied in render functions with the component this
passed in.
Also re @Justineo : the directive change only affects render functions. Template syntax remains the same.
.pure
Functional components will always update by default due to possible props mutations in a nested property, so using shallow compare by default will be unsafe. Explicitly marking a functional component with .pure = true
essentially enables automatic shallow equal shouldUpdate checks.
Btw @octref - I'd like the 3.0 compiler to provide infrastructure for even better IDE support - e.g. template type checking / type-aware completions. Maybe even keep the language service in the repo.
@KaelWD that mixins helper is great, I was actually still thinking about how to deal with it 😂
Adding .attr
make me feel Vue templates are not HTML anymore.
Btw @octref - I'd like the 3.0 compiler to provide infrastructure for even better IDE support - e.g. template type checking / type-aware completions. Maybe even keep the language service in the repo.
That's great. I'll take a look of the parser / compiler and let you know what change would it take. If we have Error Tolerance in the core parser I should be able to use it in Vetur.
<!-- is label a prop or an attribute? --> <Foo label="123" />
I think it's implemention detail and should be encapsulated by component authors. As I understand, the separation of content attributes (attrs) and IDL attributes (props) only makes sense for native elements, as we can only specify string values in HTML. While Vue templates can specify any data type with v-bind
, we don't need to separate them at least for components API. Component users shouldn't care if a documented prop serves as an attribute or not.
In addition, because props declarations are now optional, we actually don't have a way to implicitly extract attributes for component that does not declare props.
Actually because props declarations are optional so we cannot tell the true semantics of a prop. It feels like you define a function without defining parameter types, but pass in types at each time you call a function and this is kinda weird. I'd rather explicitly define props by component authors instead of let users dig into details.
Will there be an easier way for devtools to access to functional components if we cannot access context of vnodes anymore?
Update: attribute fallthrough will be preserved when the component has declared props (the behavior will be the same as before). In addition, all parent class
, style
and nativeOn
bindings will be in $attrs
, so when the child component returns a Fragment, or has inheritAttrs: false
, simply doing v-bind="$attrs"
or {...this.$attrs}
in JSX will place all non-props bindings from parent root node on that node instead.
Thanks for inviting me to this early (and exciting) preview of Vue 3.0!
VNodes
Does the change in VNodes affect the template compiler modules? In NativeScript-Vue we have some syntactic sugar that is handled by the template compiler through modules, I'm guessing these will need to be updated (not a deal breaker, just wondering)
Component in Render Functions
No longer resolves component by string names
This is not a deal breaker, but currently we use render: h => h('frame', [h(App)])
in the default nativescript-vue template (frame
is a component), as some IDE's complain about the <Frame>
element, and mess up autocompletion (phpstorm/webstorm I'm looking at you...)
In templates, components should be uppercase to differentiate from normal elements.
Does uppercase mean <FOOCOMPONENT>
or can we still use PascalCase <FooComponent>
?
Slots
Scoped slots and normal slots are now unified. There's no more difference between the two. Inside a component, all slots on
this.$slots
will be functions and all them can be passed arguments.
Will the template syntax for scoped slots change due to this, or is this just an implementation detail/render function specific change?
Filters
I'm not attached to them, but I agree with @LinusBorg about the potential risks of name conflicts.
I haven't had the time to dig through the codebase entirely but just glancing at some parts of it, I'm really liking the new structure, seems a lot easier to follow / contribute to!
Maybe we can differentiate the two (component props vs. native attributes) similar to how we differentiate component events vs. native events with the .attr modifier:
<!-- this is always a prop, although the component *may* explicitly render it as an attribute, there's no guarantee --> <Foo label="123" /> <!-- this is always an attribute on the component root --> <Foo label.attr="123" />
How about <Foo :attrs="{ label: '123'}">
which is more friendly if you want to put a set of aria-*
attributes in. And it's not necessary to be converted into { attrs }
for render function again.
Component no longer need to delcare props in order to receive props. Everything passed from parent vnode's
data
(with the exception of internal properties, i,e.key
,ref
,slots
andnativeOn*
) will be available inthis.$props
and also as the first argument of the render function. This eliminates the need forthis.$attrs
andthis.$listeners
.When no props are declared on a component, props will not be proxied on the component instance and can only be accessed via
this.$props
or theprops
argument in render functions.
I've read the source but I'm not sure if things are complete yet, but here's my understanding of that:
props
always will appear on this.$props
or props
in a functional component.props
for a new component's options, props will also be available on this
.props
that get transferred onto this
are all reactive and can be mutated, but are otherwise considered immutable.Is that correct? If not, can you give an example of the two scenarios in action just so I can get an idea of what the workflow is?
@DanielRosenwasser it's definitely not complete yet.
In a functional component there's no this
, so the only way to access its props is via the argument.
Yes.
No, props on this
are readonly (both type-wise and implementation-wise).
Here's how a user would specify props types:
interface Data {
foo: string
}
interface Props {
bar: number
}
class Foo extends Component<Data, Props> {
static options = {
props: {
bar: { type: Number, default: 123 }
}
}
data () {
return {
foo: 'hello' // will be an error if type doesn't match interface
}
}
render (props) {
// accessing data
this.foo
this.$data.foo
// accessing props
this.bar
props.bar
this.$props.bar
}
}
A few obvious things to improve here:
If the user provided a Props interface but didn't specify the static props options, the props will still be merged onto this
in types but not in implementation.
User has to specify the Props interface for compile-time type checking AND the props options for runtime type checking. It'd be nice if the compile-time types can be inferred from the props options like it does for 2.x, although I haven't figured out how to do that yet.
Note the reason we are designing it like this is because we want to make plain ES usage and TS usage as close as possible. As you can see, an ES2015 version of the above would simply be removing the interfaces and replacing the static options with an assignment.
If we can get a decorator implementation that matches the new stage-2/3 proposal, we can provide decorators that make it simpler:
class Foo extends Component {
@data foo: string = 'hello'
@prop bar: number = 123
}
@prop bar: number = 'baz'
TS2322: Type '"baz"' is not assignable to type 'number'.
@KaelWD fixed ;)
I've come up an alternative component definition. Make Component
take two arguments which define data
and prop
. It would look like:
const propDef = {
bar: { type: Number, default: 123 }
}
const dataDef = (prop) => ({
myData: 'bar',
baz: prop.bar + 1
})
class Foo extends Component(propDef, dataDef) {
// more definition
}
The good part is that this method doesn't require users to duplicate prop/data definition for type and for runtime. But the bad part is also evident that it breaks component definition into several distinct blocks.
Ideally, if we can instruct TypeScript compiler to "inject" some properties to class instance via static field, this might be more idiomatic. By "injection", I mean something like this:
class A extends Component {
static prop = { bar: Number }
method() {
this.bar // property bar is injected to class instance via the static field `prop`
}
}
I like interface and implementation for props separated. This makes it possible to specify compound types as props, and it makes clear that anything you put to options
are runtime behaviors. The typings could be simplified a lot as well.
makes it possible to specify compound types as props
Compound types can already be inferred in 2.x, it's non-primitive types that have to be annotated manually.
type: [String, Number]
works correctly.
@KaelWD You are right, but afaik it doesn't support some of the more advanced union types, such as string literal unions. Also as you mentioned, it doesn't work for non-primitives, especially object literal / classes.
I guess I meant "complex types" 😛
@yyx990803 Will we be able to assign multiple simultaneous listeners without patterns like prop getters, which are necessary in React? Currently, child components don't have to worry about whether the parent might be listening to the same events, like in this example.
Also, what would you think about removing nativeOn
? The issue is that it requires knowledge of the root element of a child component, which is really an implementation detail. If it's a BaseInput
component for example, the root element could become a <label>
, which then breaks silently breaks all components that were relying on nativeOn
.
@yyx990803 With the flat structure, does this mean any prop/attr that starts with on
will be interpreted as an event? So in a template, does this mean @click="foo"
would do the same thing as on-click="foo"
/onClick="foo"
?
@yyx990803 With listeners being props, does this mean a listener would have to be declared as a prop to be used? (I'm OK with this.) And if so, would we still need $emit
or would we just call the function passed to the prop?
@yyx990803 Regarding:
const Func = (h, props, slots, ctx) => h('div')
I'm wondering if we should keep everything in an object in the 2nd argument, as we do now, so that users don't have to worry about argument order and can just pull out what they need with destructuring.
createAsyncComponent
with advanced async components@yyx990803 Does that mean we'd have this kind of API for advanced async components?
createAsyncComponent(() => import('./MyComponent.vue', {
loading: require('./Loading.vue').default
})
resolveComponent
questions@yyx990803 So this means globally registered components have no effect on render functions? I'm curious what's the reason for this change, as it seems much less convenient.
If we do need resolveComponent
though, should it actually be asynchronous in case the component being resolved is async?
class
components being the defaultWould class
components be the recommended default for build-step apps? Would there be any cases where users would have to use a class component, instead of the object syntax? If the answer to any of these is yes, I have strong reservations.
Since many features of class
are still in flux in the spec, we're opening up the possibility for users to take advantage of these stage-x features, even if we don't rely on them ourselves. For example, React has faced issues on several occasions when something changes in the spec, and so changes in Babel, and everyone's apps are suddenly broken. In the next version of React, I've even heard from Dan and Andrew that they're moving completely away from class
components, partly due to these chronic problems (and the awkwardness of JavaScript's classes in many cases).
I also have some other thoughts on the many advantages of the object syntax, particularly from a learning and organization perspective, but I'll hold those for now. 🙂
If we do need resolveComponent though, should it actually be asynchronous in case the component being resolved is async?
AFAIK async component triggers rerender when component is resolved.
@chrisvfritz
High level notes: anything not specifically mentioned is in principle unchanged.
h('button', {
onClick: [handlerA, handlerB]
})
Also when you cloneVNode(vnode, { onClick: foo })
, the listeners is merged with existing ones instead of overriding. Same for nativeOn
. A component that does not want nativeOn
to be placed on its root node should specify inheritAttrs: false
and then spread $attrs
on to desired root node, which includes all nativeOn
listeners.
With the flat structure, does this mean any prop/attr that starts with on will be interpreted as an event?
Inside render functions, yes
So in a template, does this mean
@click="foo"
would do the same thing ason-click="foo"/onClick="foo"
?
No. Template syntax is irrelevant and does not change.
With listeners being props, does this mean a listener would have to be declared as a prop to be used?
No.
Would we still need
$emit
or would we just call the function passed to the prop?
You can still use $emit
.
I'm wondering if we should keep everything in an object in the 2nd argument, as we do now, so that users don't have to worry about argument order and can just pull out what they need with destructuring.
Yeah, probably good for normal render fns too.
Does that mean we'd have this kind of API for advanced async components?
Yes (slightly different), see implementation
So this means globally registered components have no effect on render functions?
No. resolveComponent
still checks for globally registered components. The change simply moves the part that is coupled to this
out of the vdom implementation itself (into user render functions) so that VNodes can be context-free.
If we do need resolveComponent though, should it actually be asynchronous in case the component being resolved is async?
Async components are created inside a wrapper HOC so there's no need for async resolving.
Class will be the new recommended API for any setup. It's designed specifically to be usable in native ES2015 environments without a build step AND without any reliance on stage-x features. The reason is because it serves well consistently for all major setup types:
Plain ES2015 without build step / stage-x features: that's the default.
Babel: same API, but can optionally use class fields or (new) decorators. Stage-x features may change, so we don't encourage it, but you also don't want to prevent users from willingly opt-in.
TS: same API + optional class fields / decorators usage + type inference (important).
Right now, projects not using TS and projects using TS look completely different, and with TS becoming more prevalent, it's going to make it difficult for TS and non-TS users to cross-contribute.
That said, object syntax will still be supported and the user will never have to use classes. (And the compat code is quite simple. Right now in 2.x, what we are doing is actually converting an object component into a constructor internally. 3.0 simply exposes the ability to directly author this constructor using classes.
nativeOn
(and maybe inheritAttrs
altogether?)@yyx990803 I worry that nativeOn
is actually an anti-pattern though. I personally teach people to never use it, since you can accidentally break any component by changing its root node. And unlike attributes passed to a component, it actually requires a refactor to remove the .native
in the parent after using v-bind="$attrs"
/{...this.$attrs}
and inheritAttrs: false
in the parent.
In general, maybe it would be best to force the explicitness, rather than having parent and child components coupled by default, with the parent making assumptions about the root node of the child? When we first shipped Vue 2.0, it wasn't easy to choose an element to pass all attrs/listeners to, which is why implicit attribute passing and .native
were useful. Now we've solved that problem and moving to explicitness, even when you want to pass to the root node, is a really quick refactor, so I feel like those features are no longer necessary.
@yyx990803
So in a template, does this mean
@click="foo"
would do the same thing ason-click="foo"
/onClick="foo"
?No. Template syntax is irrelevant and does not change.
How would on-click="foo"
be interpreted in a template then? Would it simply be ignored? Translated to a prop also called onClick
?
$emit
maybe?)What I like about the props/attrs behavior you suggested is that if you define props, then anything that's not defined there will be in attrs
, creating a nice split between the API of this component and API that should just be transparently passed through to a child element. If v-bind="$attrs"
will also bind listeners, should we recommend declaring those listeners as props, so that the same clean separation exists for listeners as well?
And if that is the best practice, I'm wondering if it might be best to get rid of $emit
and just recommend calling those listeners directly, thus reducing our API surface area.
That API looks great. 🙂 I have slight concerns about renaming the component
attribute to factory
, but I understand why you did it. What would you think about something like asyncComponent
or componentFunction
instead though? That way, we still communicate that they can't just use a raw component definition, but it can still be more meaningful than factory
to a lot of people, particularly beginners.
class
componentsClass will be the new recommended API for any setup.
OK, then my concern still remains actually. I do like this a lot:
It's designed specifically to be usable in native ES2015 environments without a build step AND without any reliance on stage-x features.
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
And even if we only use features in native class, when many users pull themselves back to the Babel implementation to get newer stage-x features, we end up with 3 different implementations of class (the Babel one, the native one, and the TypeScript one) being used in the wild, with many, subtle or not-so-subtle behavior differences.
And finally a (perhaps unjustified) fear. The ES class
implementation is particlarly hacky within engines and a relatively new feature, so I worry there might be many subtle behavior differences and optimization choices between browser engines that we'll keep discovering and have to deal with.
Right now, projects not using TS and projects using TS look completely different, and with TS becoming more prevalent, it's going to make it difficult for TS and non-TS users to cross-contribute.
I agree this is a problem, but I worry we might be fixing a fragmented ecosystem by creating a frequently broken one. I'm definitely not a TS expert, so I really don't know what other kinds of options we might have, but if there's literally any other way to improve support - even relying on an as-yet-unreleased feature of TypeScript - I feel like we should strongly consider it.
Finally, this also reminds me a little of what Angular went through. They wanted first-class TypeScript support, while also allowing anyone to use Babel just as easily. Keeping both happy turned out to be so difficult that at this point, they're not even pretending to support or recommend JavaScript anymore. I'm not suggesting it's impossible to offer a consistent, first-class experience for both. I just don't know where all the landmines are, so worry about falling into the same traps. 😕
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
Honestly, it's like throwing the baby out with the water if we don't use classes because of this. It's a valid concern, but only for the now. Note 3.0 is designed for the future where these proposals are likely more stable than they are now and breaking changes becoming less frequent (not like they are actually frequent now). What's more important is we make it clear in our docs that we are aware of these stage-x features that can be used, but there's risk and the user should be responsible for opting into such risk.
And even if we only use features in native class, when many users pull themselves back to the Babel implementation to get newer stage-x features, we end up with 3 different implementations of class (the Babel one, the native one, and the TypeScript one) being used in the wild, with many, subtle or not-so-subtle behavior differences.
Unless the user wants to support IE11, their Babel / TS setup will emit native classes so this is not really a problem. Also see implementation notes below.
And finally a (perhaps unjustified) fear. The ES class implementation is particlarly hacky within engines and a relatively new feature, so I worry there might be many subtle behavior differences and optimization choices between browser engines that we'll keep discovering and have to deal with.
I think this is unfounded. Classes isn't really that new - earliest browser support shipped in mid 2015 (Edge 12) and it became available in stable versions of all major evergreen browsers in March 2016. So that's two and half years since they've been supported in these major browsers and I can't really recall any "subtle behavior differences" that would affect us. In fact, our implementation doesn't even care if it's a native class or not. The code treats a component as a good old constructor function with a prototype
and can be new
ed.
However, some library authors in the community will inevitably want to take advantage of stage-x features, thus encouraging their users to, and sometimes they'll be so useful that they eventually become widespread enough that most apps just break sometimes, when part of the spec changes.
I think the worry is unwarranted. Lib authors can use any setup they want and compile down to ES2015. Users can use any setup to consume the distribution. Many packages in npm today are written in TS but compiled down to JS. They don't force the user into using TS but even gives them correct auto-completion and error checking in IDE.
most apps just break sometimes, when part of the spec changes.
Not really. Let's say you use a babel plugin for a stage 1/2 proposal. The version is locked and your code compiles. When the spec change, you either do not upgrade your babel plugin or you upgrade both your babel plugin and your code. It's an acknowledged price you pay for using stage 1 proposal. And this is a babel/TS-experimental-feature issue, not a Vue issue.
Also given the timeline, I feel the decorator spec will be stable enough when Vue 3.0 is out.
@chrisvfritz I work on TypeScript, but I hope you can trust this is in good faith. 🙂
I really don't want to push the Vue community into anything that would negatively affect it for the sake of TypeScript support, so I have some of the same concerns you have on the Vue community's behalf; however, the truth of it is that so many of the things Vue's API does today is to just construct a class without using any language syntax. It feels more comfortable from a pure ES5 world, but as time goes on the concept will look stranger to newer JS users who come from other languages or who already know about classes. Anecdotally, I have friends who've used some frameworks that started in the ES3-era whose reactivity model was based around calling .get()
and .set()
. They now find it strange in a world with getters and setters.
In fact, most of the basic concepts boil down to something simpler when you use classes. methods
are now just instance methods. computed
s are just get
-ers and set
-ers. The name
field can just be the class name. As a small bonus, these things don't need to be separated by commas. 😉
And while I get the concern over ECMA 262 proposal churn, as an occasional TC39 attendee, I think you'll see less of this over time as these features stabilize - something @yyx990803 and @octref partially alluded to.
I've even heard from Dan and Andrew that they're moving completely away from class components, partly due to these chronic problems (and the awkwardness of JavaScript's classes in many cases
Admittedly I'm not the expert on component models here; I'd be curious to see what they have as an alternative, but I'm very surprised given that the community very quickly switched over from createClass
even without things like auto-binding this
on methods.
if there's literally any other way to improve support - even relying on an as-yet-unreleased feature of TypeScript - I feel like we should strongly consider it.
This is tough - I have spent a lot of hours trying to make things work better between Vue and TypeScript with the goal of making it so that the Vue experience is 1:1 between JavaScript and TypeScript users. I don't have any ideas that would significantly improve the experience around the current API. In short, my goal was always to make it so the Vue ecosystem wouldn't have to accommodate TypeScript, but rather that the TypeScript language would find a way to accommodate Vue. We have that in some capacity, and it's been helpful for tooling in Vetur. But:
We're working on driving UX improvements from our side. But on the whole, if there's an opportunity to use modern features that's largely backwards compatible, and it opens up the chance for things like
this
in classes (e.g. Flow).then I feel like this is a reasonable direction for Vue to take.
The ES class implementation is particularly hacky within engines
I can't speak to that (I'm not an engine person) but I'm wondering what you mean. Maybe something we can chat about that elsewhere. 🙂
I was thinking a bit more about the render function signature and would like some feedback.
h
as the first argument?Please vote with thumbs up for removing it, and thumbs down for keeping it.
Since in 3.0 VNodes are context free, it's no longer necessary to use the instance-bound h
function in render functions. Instead, we can just use the global h
imported from Vue.
Relying on instance specific h
makes it necessary to always pass along the h
when you want to split part of the render function into another function, which can be really annoying. Importing the global h
allows you to just do it once and forget about it.
h
injection has been annoying to deal with in JSX. We have some pretty hacky logic for automatic h
injection in our current JSX implementation. Importing instead of passing argument allows us to get rid of that problem entirely.
Assuming we will be importing h
in a reasonable amount of cases, we are essentially wasting one argument position in those cases. Removing the argument and enforcing importing h
everywhere makes usage consistent.
Incompatibility with 2.x API. Although this can easily be made compatible via an runtime adaptor or automatic codemods.
Always need to import h
when using render functions. For end users, this isn't that bad and is balanced by the benefit mentioned above.
This has another implication for library authors, because they will be importing h
from Vue as a peer dependency, this requires them to use a correct externals configuration when distributing their libraries. However, externals configuration will likely become a necessity for 3.0 compatible libs because we will have a lot of the framework become tree-shakable, and for libs to be able to use these tree-shakable features, they will always need correct externals setup (e.g. import { h } from 'vue'
should be preserved in ESM builds but converted to const { h } = Vue
in global builds.) The configuration can be standardized via Vue CLI or a library boilerplate.
Changes
Props
Component no longer need to delcare props in order to receive props. Everything passed from parent vnode's
data
(with the exception of internal properties, i,e.key
,ref
,slots
andnativeOn*
) will be available inthis.$props
and also as the first argument of the render function. This eliminates the need forthis.$attrs
andthis.$listeners
.When no props are declared on a component, props will not be proxied on the component instance and can only be accessed via
this.$props
or theprops
argument in render functions.You still can delcare props in order to specify default values and perform runtime type checking, and it works just like before. Declared props will also be proxied on the component instance. However, the behavior of undeclared props falling through as attrs will be removed; it's as if
inheritAttr
now defaults tofalse
. The component will be responsible for merging the props as attrs onto the desired element.VNodes
Flat Data Format
VNodes are now context-free
h
can now be globally imported and is no longer bound to component instacnes. VNodes created are also no longer bound to compomnent instances (this means you can no longer accessvnode.context
to get the component instance that created it)Component in Render Functions
No longer resolves component by string names; Any
h
call with a string is considered an element. Components must be resolved before being passed toh
.In templates, components should be uppercase to differentiate from normal elements.
NOTE: how to tell in browser templates? In compiler, use the following intuitions:
resolveComponent
returns name string if component is not found)Slots
Unifying Normnal Slots and Scoped Slots
Scoped slots and normal slots are now unified. There's no more difference between the two. Inside a component, all slots on
this.$slots
will be functions and all them can be passed arguments.Usage Syntax Change
Functional Component
Functional components can now really be just functions.
Async Component
Async components now must be explicitly created.
Directives
Now are internally on-vnode hooks with the exact same lifecycle as components.
Custom directives are now applied via a helper:
Styles
No longer performs auto-prefixing.
Attributes
false
. Instead, it's set asattr="false"
instead. To remove the attribute, usenull
.Filters
Filters are gone for good (or can it?)
Refs
v-for
. Instead, use something like:ref="'foo' + key"
or function refs.