vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
46.74k stars 8.19k forks source link

Initial proposal of breaking changes in 3.0 #2

Closed yyx990803 closed 5 years ago

yyx990803 commented 5 years ago

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 and nativeOn*) will be available in this.$props and also as the first argument of the render function. This eliminates the need for this.$attrs and this.$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 the props 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 to false. The component will be responsible for merging the props as attrs onto the desired element.

VNodes

Flat Data Format

// before
{
  attrs: { id: 'foo' },
  domProps: { innerHTML: '' },
  on: { click: foo },
  key: 'foo',
  ref: 'bar'
}

// after (consistent with JSX usage)
{
  id: 'foo',
  domPropsInnerHTML: '',
  onClick: foo,
  key: 'foo',
  ref: ref => {
    this.$refs.bar = ref
  }
}

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 access vnode.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 to h.

import { resolveComponent } from 'vue'

render (h) {
  // only necessary when you are trying to access a registered component instead
  // of an imported one
  const Comp = resolveComponent(this, 'foo')
  return h(Comp)
}

In templates, components should be uppercase to differentiate from normal elements.

NOTE: how to tell in browser templates? In compiler, use the following intuitions:

  1. If uppercase -> Component
  2. If known HTML elements -> element
  3. Treat as unknown component - at runtime, try resolving as a component first, if not found, render as element. (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

// before
h(Comp, [
  h('div', { slot: 'foo' }, 'foo')
  h('div', { slot: 'bar' }, 'bar')
])

// after
h(Comp, () => h('div', 'default slot'))

// or
import { childFlags } from 'vue/flags'

h(Comp, null, {
  slots: {
    foo: () => h('div', 'foo'),
    bar: () => h('div', 'bar')
  }
}, childFlags.COMPILED_SLOTS)

// also works
h(Comp, null, {
  foo: () => h('div', 'foo'),
  bar: () => h('div', 'bar')
})

Functional Component

Functional components can now really be just functions.

// before
const Func = {
  functional: true,
  render (h, ctx) {
    return h('div')
  }
}

// Now can also be:
const Func = (h, props, slots, ctx) => h('div')
Func.pure = true

Async Component

Async components now must be explicitly created.

import { createAsyncComponent } from 'vue'

const AsyncFoo = createAsyncComponent(() => import('./Foo.vue'))

Directives

import { applyDirective, resolveDirective } from 'vue'

render (h) {
  // equivalent for v-my-dir
  const myDir = resolveDirective(this, 'my-dir')
  return applyDirective(h('div', 'hello'), [[myDir, this.someValue]])
}

Styles

No longer performs auto-prefixing.

Attributes

Filters

Filters are gone for good (or can it?)

Refs

yyx990803 commented 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.

Justineo commented 5 years ago

Hi Evan,

Nice work! Here are some of my early thoughts:

Props

However, the behavior of undeclared props falling through as attrs will be removed; it's as if inheritAttr now defaults to false. 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.

VNodes

VNodes are now context-free

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.

Component in Render Functions

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?

Slots

Unifying Normnal Slots and Scoped Slots

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.

Directives

  • Custom directives are now applied via a helper:

Does this only affects render functions? Is there any difference for templates?

Attributes

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)?

KaelWD commented 5 years ago

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.

znck commented 5 years ago

However, the behavior of undeclared props falling through as attrs will be removed; it's as if inheritAttr now defaults to false.

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?

znck commented 5 years ago

VNodes are now context-free

I guess this would affect devtools. /cc @Akryum

znck commented 5 years ago
 // 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.

Kingwl commented 5 years ago

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

eddyerburgh commented 5 years ago

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.

phanan commented 5 years ago

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 :)

znck commented 5 years ago

What would be the prop merging behavior for class and event props (onClick)?

HerringtonDarkholme commented 5 years ago

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

Kingwl commented 5 years ago

@phanan oh, i forget the pipeline operator syntax :rofl: you are right, drop it Although pipeline is still stage 1

Leopoldthecoder commented 5 years ago

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.

Hanks10100 commented 5 years ago

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.

LinusBorg commented 5 years ago

This looks a amazing, I finally feel like this is something I can navigate and undestand :-P

Points I want to comment on:

Props

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?

Filters

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.

  1. this creates the risk of name conflicts. 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.
  2. if people decide to collect their filters in one object with which they expend the prototype to minimize the riks of the previous point - what to call it? And what about conflicts with filters added by plugins?

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.

Slots

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?)

Vnodes

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.

Mixins

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

KaelWD commented 5 years ago

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) {...}
octref commented 5 years ago

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:

yyx990803 commented 5 years ago

Answering a few concerns:

$attrs

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:

  1. The addition of the .attr modifier. This allows you to be explicit and not relying on anything implicit.
  2. inheritAttrs: false affecting style and class: probably a good idea to make it more consistent.
  3. Exposing parent class/style/attrs on $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?

Accessing component instance in directives

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.

yyx990803 commented 5 years ago

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.

yyx990803 commented 5 years ago

@KaelWD that mixins helper is great, I was actually still thinking about how to deal with it 😂

znck commented 5 years ago

Adding .attr make me feel Vue templates are not HTML anymore.

octref commented 5 years ago

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.

Justineo commented 5 years ago
<!-- 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.

Akryum commented 5 years ago

Will there be an easier way for devtools to access to functional components if we cannot access context of vnodes anymore?

yyx990803 commented 5 years ago

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.

rigor789 commented 5 years ago

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!

Jinjiang commented 5 years ago

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.

DanielRosenwasser commented 5 years ago

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 and nativeOn*) will be available in this.$props and also as the first argument of the render function. This eliminates the need for this.$attrs and this.$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 the props 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:

  1. props always will appear on this.$props or props in a functional component.
  2. If a user specifies props for a new component's options, props will also be available on this.
  3. 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?

yyx990803 commented 5 years ago

@DanielRosenwasser it's definitely not complete yet.

  1. In a functional component there's no this, so the only way to access its props is via the argument.

  2. Yes.

  3. 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:

  1. 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.

  2. 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
}
KaelWD commented 5 years ago

@prop bar: number = 'baz'

TS2322: Type '"baz"' is not assignable to type 'number'.

yyx990803 commented 5 years ago

@KaelWD fixed ;)

HerringtonDarkholme commented 5 years ago

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`
  }
}
octref commented 5 years ago

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.

KaelWD commented 5 years ago

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.

octref commented 5 years ago

@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" 😛

chrisvfritz commented 5 years ago

Handling multiple listeners to the same event

@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.

chrisvfritz commented 5 years ago

Events interpretation

@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"?

chrisvfritz commented 5 years ago

Emitting events

@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?

chrisvfritz commented 5 years ago

Functional component function syntax

@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.

chrisvfritz commented 5 years ago

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
})
chrisvfritz commented 5 years ago

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?

chrisvfritz commented 5 years ago

Concerns about class components being the default

Would 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. 🙂

znck commented 5 years ago

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.

yyx990803 commented 5 years ago

@chrisvfritz

High level notes: anything not specifically mentioned is in principle unchanged.

Handling multiple listeners to the same event

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.

Events Interpretation

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 as on-click="foo"/onClick="foo"?

No. Template syntax is irrelevant and does not change.

Emitting Events

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.

Functional Syntax

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.

Async Components

Does that mean we'd have this kind of API for advanced async components?

Yes (slightly different), see implementation

resolveComponent

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

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:

  1. Plain ES2015 without build step / stage-x features: that's the default.

  2. 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.

  3. 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.

chrisvfritz commented 5 years ago

Deprecating 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.

chrisvfritz commented 5 years ago

@yyx990803

Events Interpretation

So in a template, does this mean @click="foo" would do the same thing as on-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?

Emitting events (and getting rid of $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.

Async Components

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 components

Class 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. 😕

yyx990803 commented 5 years ago

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 newed.

octref commented 5 years ago

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.

DanielRosenwasser commented 5 years ago

@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. computeds 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:

  1. The types are hard to reason about.
  2. The UX isn't ideal - inference is hard, loopiness makes the experience unpredictable, and scary-looking intersection types end up leaking out to the user.

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

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. 🙂

yyx990803 commented 5 years ago

I was thinking a bit more about the render function signature and would like some feedback.

Should we remove 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.

Pros for removing it:

Cons for removing it: