vuejs / core Goto Github PK
View Code? Open in Web Editor NEW🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
Home Page: https://vuejs.org/
License: MIT License
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
Home Page: https://vuejs.org/
License: MIT License
I'd like to keep the other breaking changes feedback thread scoped to breaking changes. Here's some TypeScript-specific feedback which doesn't need to be addressed immediately, but which stood out to me while exploring the package.
P
) should come before data (D
)This was the biggest footgun I made when I worked on the Vue .d.ts
files the first time. I specified Props
first rather than Data
and I believe that Vue 3.0 shouldn't make the same mistake. When it comes to components, almost all of them will use props and have a harder time specifying them than data.
P
has defaultsThere are a lot of instances of P = Data
(which is really just P = Record<string, any>
). Is there any reason that's necessary? I feel like the class-based API doesn't really need this since, on the whole, it's mostly inferred, though I could be missing something.
Vue
is currently non-genericVue
(from packages/vue
) currently has a different API than Component
(from packages/core
). Specifically, Vue
is generic and seems to be the "full-featured" version of the component. You already mentioned that things are in a state of progress, but is the difference mostly about mount points depending on target runtimes (i.e. DOM vs. native vs. ...)?
.d.ts
files are produced using dts-bundledts-bundle throws all of your modules into a single global file with several ambient module declarations. The problem with ambient module declarations is that they are in the global scope in the first place. If you have multiple versions of Vue loaded up by a project (via a separate dependency probably), then two ambient modules for the path @vue/renderer-dom
can conflict, which can cause problems.
We should consider looking into API Extractor to produce .d.ts
files that describe top-level APIs for Vue.
Right now it appears that there's a custom TypeScript build that happens, and it seems like that's an all-or-nothing approach. Depending on whether build times are an issue, we could potentially leverage project references. If there's a reason you can't use them, it'd be helpful for our team to get an idea of the challenges. 😃
3.0.0-alpha.1
2.x - https://jsfiddle.net/natuqebm/
3.0.0-alpha.1 - https://jsfiddle.net/u7pt6gjo/
Just open these above reproductions links
There is an inconsistency between Vue 2.x and the latest alpha. 3.x should not render a boolean values in the render function.
Renders 0falsetrueNaN
instead of 0NaN
h
is now globally imported instead of passed to render functions as argument// globally imported `h`
import { h } from 'vue'
export default {
// adjusted render function arguments
render(props, slots) {
return h(
'div',
// flat data structure
{ id: props.id },
slots.default()
)
}
}
In 2.x, VNodes are context-specific - which means every VNode created is bound to the component instance that created it (the "context"). This is because we need to support the following use cases:
// looking up a component based on a string ID
h('some-component')
h('div', {
directives: [
{
name: 'foo', // looking up a directive by string ID
// ...
}
]
})
In order to look up locally/globally registered components and directives, we need to know the context component instance that "owns" the VNode. This is why in 2.x h
is passed in as an argument, because the h
passed into each render function is a curried version that is pre-bound to the context instance.
This has created a number of inconveniences, for example when trying to extract part of the render logic into a separate function, h
needs to be passed along:
function renderSomething(h) {
return h('div')
}
export default {
render(h) {
return renderSomething(h)
}
}
When using JSX, this is especially cumbersome since h
is used implicitly and isn't needed in user code. Our JSX plugin has to perform automatic h
injection in order to alleviate this, but the logic is complex and fragile.
In 3.0 we have found ways to make VNodes context-free. They can now be created anywhere using the globally imported h
function, so it only needs to be imported once in any file.
Another issue with 2.x's render function API is the nested VNode data structure:
h('div', {
class: ['foo', 'bar'],
style: { }
attrs: { id: 'foo' },
domProps: { innerHTML: '' },
on: { click: foo }
})
This structure was inherited from Snabbdom, the original virtual dom implementation Vue 2.x was based on. The reason for this design was so that the diffing logic can be modular: an individual module (e.g. the class
module) would only need to work on the class
property. It is also more explicit what each binding will be processed as.
However, over time we have noticed there are a number of drawbacks of the nested structure compared to a flat structure:
class
and style
special cases are somewhat inconsistentIn 3.x, we are moving towards a flat VNode data structure to address these problems.
h
functionh
is now globally imported:
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
With h
no longer needed as an argument, the render
function now receives a new set of arguments:
// MyComponent.js
export default {
render(
// declared props
props,
// resolved slots
slots,
// fallthrough attributes
attrs,
// the raw vnode in parent scope representing this component
vnode
) {
}
}
props
and attrs
will be equivalent to this.$props
and this.$attrs
- also see Optional Props Declaration and Attribute Fallthrough
slots
will be equivalent to this.$slots
- also see Slots Unification
vnode
will be equivalent to this.$vnode
, which is the raw vnode that represents this component in parent scope, i.e. the return value of h(MyComponent, { ... })
.
Note that the render function for a functional component will now also have the same signature, which makes it consistent in both stateful and functional components:
const FunctionalComp = (props, slots, attrs, vnode) => {
// ...
}
The new list of arguments should provide the ability to fully replace the current functional render context:
props
and slots
have equivalent values
data
and children
can be accessed directly on vnode
listeners
will be included in attrs
injections
will have a dedicated new API:
import { resolveInjection } from 'vue'
import { themeSymbol } from './ThemeProvider'
const FunctionalComp = props => {
const theme = resolveInjection(themeSymbol)
return h('div', `Using theme ${theme}`)
}
parent
access will be removed. This was an escape hatch for some internal use cases - in userland code, props and injections should be preferred.
// before
{
class: ['foo', 'bar'],
style: { color: 'red' },
attrs: { id: 'foo' },
domProps: { innerHTML: '' },
on: { click: foo },
key: 'foo'
}
// after
{
class: ['foo', 'bar'],
style: { color: 'red' },
id: 'foo',
innerHTML: '',
onClick: foo,
key: 'foo'
}
With the flat structure, the VNode data props are handled using the following rules:
key
, ref
and slots
are reserved special propertiesclass
and style
have the same API as 2.xon
are handled as v-on
bindingsDue to the flat structure, this.$attrs
inside a component now also contains any raw props that are not explicitly declared by the component, including onXXX
listeners. This makes it much easier to write wrapper components - simply pass this.$attrs
down with v-bind="$attrs"
(as a result, this.$listeners
will also be removed).
With VNodes being context-free, we can no longer use a string ID (e.g. h('some-component')
) to implicitly lookup globally registered components. Same for looking up directives. Instead, we need to use an imported API:
import { h, resolveComponent, resolveDirective, applyDirectives } from 'vue'
export default {
render() {
const comp = resolveComponent('some-global-comp')
const fooDir = resolveDirective('foo')
const barDir = resolveDirective('bar')
// <some-global-comp v-foo="x" v-bar="y" />
return applyDirectives(
h(comp),
this,
[fooDir, this.x],
[barDir, this.y]
)
}
}
This will mostly be used in compiler-generated output, since manually written render function code typically directly import the components and use them by value, and use rarely have to use directives.
h
being globally imported means any library that contains Vue components will include import { h } from 'vue'
somewhere (this is implicitly included in render functions compiled from templates as well). This creates a bit of overhead since it requires library authors to properly configure the externalization of Vue in their build setup:
Vue.h
first and fallback to require
calls.This is common practice for React libs and possible with both webpack and Rollup. A decent number of Vue libs also already does this. We just need to provide proper documentation and tooling support.
N/A
For template users this will not affect them at all.
For JSX users the impact will also be minimal, but we do need to rewrite our JSX plugin.
Users who manually write render functions using h
will be subject to major migration cost. This should be a very small percentage of our user base, but we do need to provide a decent migration path.
It's possible to provide a compat plugin that patches render functions and make them expose a 2.x compatible arguments, and can be turned off in each component for a one-at-a-time migration process.
It's also possible to provide a codemod that auto-converts h
calls to use the new VNode data format, since the mapping is pretty mechanical.
Functional components using context will likely have to be manually migrated, but a smilar adaptor can be provided.
Introduce built-in support for authoring components as native ES2015 classes.
import Vue from 'vue'
export default class App extends Vue {
// options declared via static properties (stage 3)
// more details below
static template = `
<div>{{ count }}</div>
`
// reactive data declared via class fields (stage 3)
// more details below
count = 0
// lifecycle
created() {
console.log(this.count)
}
// getters are converted to computed properties
get plusOne() {
return this.count + 1
}
// a method
increment() {
this.count++
}
}
Vue's current object-based component API has created some challenges when it comes to type inference. As a result, most users opting into using Vue with TypeScript end up using vue-class-component. This approach works, but with some drawbacks:
Internally, Vue 2.x already represents each component instance with an underlying "class". We are using quotes here because it's not using the native ES2015 syntax but the ES5-style constructor/prototype function. Nevertheless, conceptually components are already handled as classes internally.
vue-class-component
had to implement some inefficient workarounds in order to provide the desired API without altering Vue internals.
vue-class-component
has to maintain typing compatibility with Vue core, and the maintenance overhead can be eliminated by exposing the class directly from Vue core.
The primary motivation of native class support is to provide a built-in and more efficient replacement for vue-class-component
. The affected target audience are most likely also TypeScript users.
The API is also designed to not rely on anything TypeScript specific: it should work equally well in plain ES, for users who prefer using native ES classes.
Note we are not pushing this as a replacement for the existing object-based API - the object-based API will continue to work in 3.0.
A component can be declared by extending the base Vue
class provided by Vue core:
import Vue from 'vue'
class MyComponent extends Vue {}
Reactive instance data properties can be declared using class fields syntax (stage 3):
class MyComponent extends Vue {
count = 0
}
This is currently supported in Chrome stable 72+ and TypeScript. It can also be transpiled using Babel. If using native ES classes without any transpilation, it's also possible to manually set this.count = 0
in constructor
, which would in turn require a super()
call:
// NOT recommended.
class MyComponent extends Vue {
constructor() {
super()
this.count = 0
}
}
This is verbose and also has incorrect semantics (see below). A less verbose alternative is using the special data()
method, which works the same as in the object-based syntax:
class MyComponent extends Vue {
data() {
return {
count: 0
}
}
}
[[Set]]
vs [[Define]]
The class field syntax uses [[Define]]
semantics in both native and transpiled implementations (Babel already conforms to the latest spec and TS will have to follow suite). This means count = 0
in the class body is executed with the semantics of Object.defineProperty
and will always overwrite a property of the same name inherited from a parent class, regardless of whether it has a setter or not.
In comparison, this.count = 0
in constructor is using [[Set]]
semantics - if the parent class has a defined setter named count
, the operation will trigger the setter instead of overwriting the definition.
For Vue's API, [[Define]]
is the correct semantics, since an extended class declaring a data property should overwrite a property with the same name on the parent class.
This should be a very rare edge case since most users will likely be using the class field syntax either natively or via a transpiler with correct semantics, or using the data()
alternative.
Built-in lifecycle hooks should be declared directly as methods, and works largely the same with their object-based counterparts:
class MyComponent extends Vue {
created() {
console.log('created')
}
}
In v3, props declarations can be optional. The behavior will be different based on whether props are declared.
Props can be declared using the props
static property (static properties are used for all component options that do not have implicit mapping). When props are declared, they can be accessed directly on this
:
class MyComponent extends Vue {
// props declarations are fully compatible with v2 options
static props = {
msg: String
}
created() {
// available on `this`
console.log(this.msg)
// also available on `this.$props`
console.log(this.$props.msg)
}
}
Similar to v2, any attributes passed to the component but is not declared as a prop will be exposed as this.$attrs
. Note that the non-props attribute fallthrough behavior will also be adjusted - it is discussed in more details in a separate RFC.
It is possible to omit props declarations in v3. When there is no explicit props declaration, props will NOT be exposed on this
- they will only be available on this.$props
:
class MyComponent extends Vue {
created() {
console.log(this.$props.msg)
}
}
Inside templates, the prop also must be accessed with the $props
prefix, .e.g. {{ $props.msg }}
.
Any attribute passed to this component will be exposed in this.$props
. In addition, this.$attrs
will be simply pointing to this.$props
since they are equivalent in this case.
Computed properties are declared as getter methods:
class MyComponent extends Vue {
count = 0
get doubleCount() {
return this.count * 2
}
}
Note although we are using the getter syntax, these functions are not used a literal getters - they are converted into Vue computed properties internally with dependency-tracking-based caching.
Do we need a way to opt-out? It can probably be done via decorators.
Any method that is not a reserved lifecycle hook is considered a normal instance method:
class MyComponent extends Vue {
count = 0
created() {
this.logCount()
}
logCount() {
console.log(this.count)
}
}
When methods are accessed from this
, they are automatically bound to the instance. This means there is no need to worry about calling this.foo = this.foo.bind(this)
.
Other options that do not have implicit mapping in the class syntax should be declared as static class properties:
class MyComponent extends Vue {
static template = `
<div>hello</div>
`
}
The above syntax requires static class fields (stage 3). In non-supporting environment, manual attaching is required:
class MyComponent extends Vue {}
MyComponent.template = `
<div>hello</div>
`
Or:
class MyComponent extends Vue {}
Object.assign(MyComponent, {
template: `
<div>hello</div>
`
})
In TypeScript, since data
properties are declared using class fields, the type inference just works:
class MyComponent extends Vue {
count: number = 1
created() {
this.count // number
}
}
For props, we intend to provide a decorator that internally transforms decorated fields in to corresponding runtime options (similar to the @Prop
decorator in vue-property-decorators
):
import { prop } from '@vue/decorators'
class MyComponent extends Vue {
@prop count: number
created() {
this.count // number
}
}
This is equivalent to the following in terms of runtime behavior (only static type checking, no runtime checks):
class MyComponent extends Vue {
static props = ['count']
created() {
this.count
}
}
The decorator can also be called with additional options for more specific runtime behavior:
import { prop } from '@vue/decorators'
class MyComponent extends Vue {
@prop({
validator: val => {
// custom runtime validation logic
}
})
msg: string = 'hello'
created() {
this.count // number
}
}
Note that due to the limitations of the TypeScript decorator implementation, we cannot use the following to declare default value for a prop:
class MyComponent extends Vue {
@prop count: number = 1
}
The culprit is the following case:
class MyComponent extends Vue {
@prop foo: number = 1
bar = this.foo + 1
}
If the parent component passes in the foo
prop, the default value of 1
should be overwritten. However, the way TypeScript transpiles the code places the two lines together in the constructor of the class, giving Vue no chance to overwrite the default value properly. Vue will throw a warning when such usage is detected.
Instead, use the decorator option to declare default values:
class MyComponent extends Vue {
@prop({ default: 1 }) foo: number
bar = this.foo + 1
}
This restriction can be lifted in the future when the ES decorators proposal has been finalized and TS has been updated to match the spec, assuming the final spec does not deviate too much from how it works now.
$props
and $data
To access this.$props
or this.$data
in TypeScript, the base Vue
class accepts generic arguments:
interface MyProps {
msg: string
}
interface MyData {
count: number
}
class MyComponent extends Vue<MyProps, MyData> {
count: number = 1
created() {
this.$props.msg
this.$data.count
}
}
Mixins work a bit differently with classes, primarily to ensure proper type inference:
If type inference is needed, mixins must be declared as classes extending the base Vue
class (otherwise, the object format also works).
To use mixins, the final component should extend a class created from the mixins
method instead of the base Vue
class.
import Vue, { mixins } from 'vue'
class MixinA extends Vue {
// class-style mixin
}
const MixinB = {
// object-style mixin
}
class MyComponent extends mixins(MixinA, MixinB) {
// ...
}
The class returned from mixins
also accepts the same generics arguments as the base Vue
class.
One major difference between 3.0 classes and the 2.x constructors is that they are not meant to be instantiated directly. i.e. you will no longer be able to do new MyComponent({ el: '#app' })
to mount it - instead, the instantiation/mounting process will be handled by separate, dedicated APIs. In cases where a component needs to be instantiated for testing purposes, corresponding APIs will also be provided. This is largely due to the internal changes where we are moving the mounting logic out of the component class itself for better decoupling, and also has to do our plan to redesign the global API for bootstrapping an app.
The proposed syntax relies on two currently stage-3 proposals related to class fields:
These are required to achieve the ideal usage. Although there are workarounds in cases where they are not available, the workarounds result in sub-optimal authoring experience.
If the user uses Babel or TypeScript, these can be covered. Luckily these two combined should cover a pretty decent percentage of all users. For learning / prototyping usage without compile steps, browsers with native support (e.g. Chrome Canary) can also be used.
There is a small risk since these proposals are just stage 3, and are still being actively debated on - technically, there are still chances that they get further revised or even dropped. The good news is that the parts that are relevant here doesn't seem likely to change. There was a somewhat related debate regarding the semantics of class fields being [[Set]]
vs [[Define]]
, and it has been settled as [[Define]]
which in my opinion is the preferred semantics for this API.
The TypeScript usage relies on decorators. The decorators proposal for JavaScript is still stage 2 and undergoing major revisions - it's also completely different from how it is implemented in TS today (although TS is expected to match the proposal once it is finalized). Its latest form just got rejected from advancing to stage 3 at TC39 due to concerns from JavaScript engine implementors. It is thus still quite risky to design the API around decorators at this point.
Before ES decorators are finalized, we only recommend using decorators in TypeScript.
The decision to go with decorators for props in TypeScript is due to the following:
Decorators is the only option that allows us to express both static and runtime behavior in the same syntax, without the need for double declaration. This is discussed in more details in the Alternatives section.
Both the current TS implementation and the current stage 2 proposal can support the desired usage.
It's also highly likely that the finalized proposal is going to support the usage as well. So even after the proposal finalizes and TS' implementation has been updated to match the proposal, the API can continue to work without syntax changes.
The decorator-based usage is opt-in and built on top of the static props
based usage. So even if the proposal changes drastically or gets abandoned we still have something to fallback to.
If users are using TypeScript, they already have decorators available to them via TypeScript's tool chain so unlike vanilla JavaScript there's no need for additional tooling.
this
Identity in constructor
In Vue 3 component classes, the this
context in all lifecycle hooks and methods are in fact a Proxy to the actual underlying instance. This Proxy is responsible for returning proper values for the data, props and computed properties defined on the current component, and provides runtime warning checks. It is important for performance reasons as it avoids many expensive Object.defineProperty
calls when instantiating components.
In practice, your code will work exactly the same - the only cases where you need to pay attention is if you are using this
inside the native constructor
- this is the only place where Vue cannot swap the identity of this
so it will not be equal to the this
exposed everywhere else:
let instance
class MyComponent extends Vue {
constructor() {
super()
instance = this // actual instance
}
created() {
console.log(this === instance) // false, `this` here is the Proxy
}
}
In practice, there shouldn't be cases where you must use the constructor
, so the best practice is to simply avoid it and always use component lifecycle hooks.
This may cause beginners to face a choice early on: to go with the object syntax, or the class syntax?
For users who already have a preference, it is not really an issue. The real issue is that for beginners who are not familiar with classes, the syntax raises the learning barrier. In the long run, as ES classes stabilize and get more widely used, it may eventually become a basic pre-requisite for all JavaScript users, but now is probably not the time yet.
One way to deal with it is providing examples for both syntaxes in the new docs and allow switching between them. This allows users to pick a preferred syntax during the learning process.
@Component({
template: `...`
})
class MyComponent extends Vue {}
This is similar to vue-class-component
but it requires decorators - and as mentioned, it is only stage 2 and risky to rely on. We are using decorators for props, but it's primarily for better type-inference and only recommended in TypeScript. For now we should avoid decorators in plain ES as much as possible.
For declaring prop types in TypeScript, we considered avoiding decorators by merging the props interface passed to the class as a generic argument on to the class instance:
interface MyProps {
msg: string
}
class MyComponent extends Vue<MyProps> {
created() {
this.msg // this becomes available
}
}
However, this creates a mismatch between the typing and the runtime behavior. Because there is no runtime declaration for the msg
prop, it will not be exposed on this
. To make the types and runtime consistent, we end up with a double-declaration:
interface MyProps {
msg: string
}
class MyComponent extends Vue<MyProps> {
static props = ['msg']
created() {
this.msg
}
}
We also considered eliminating the need for double-declaration via tooling - e.g. Vetur can pre-transform the interface into equivalent runtime declaration, or vice-versa, so that only the interface or the static props
declaration is needed. However, both have drawbacks:
The interface cannot enforce runtime type checking or custom validation;
The static props
runtime declaration cannot facilitate type inference for advanced type shapes.
Decorators is the only option the can unify both in the same syntax:
class MyComponent extends Vue {
@prop({
validator: value => {
// custom runtime validation logic
}
})
msg: SomeAdvancedType = 'hello'
created() {
this.msg
}
}
This does not break existing usage, but rather introduces an alternative way of authoring components. TypeScript users, especially those already using vue-class-component
should have no issue grasping it. For beginners, we should probably avoid using it as the default syntax in docs, but we should provide the option to switching to it in code examples.
For existing users using TypeScript and vue-class-component
, a simple migration strategy would be shipping a build of vue-class-component
that provides a @Component
decorator that simply spreads the options on to the class. Since the required change is pretty mechanical, a code mod can also be provided.
Expose logic-related component options via function-based APIs instead.
import { value, computed, watch, onMounted } from 'vue'
const App = {
template: `
<div>
<span>count is {{ count }}</span>
<span>plusOne is {{ plusOne }}</span>
<button @click="increment">count++</button>
</div>
`,
setup() {
// reactive state
const count = value(0)
// computed state
const plusOne = computed(() => count.value + 1)
// method
const increment = () => { count.value++ }
// watch
watch(() => count.value * 2, val => {
console.log(`count * 2 is ${val}`)
})
// lifecycle
onMounted(() => {
console.log(`mounted`)
})
// expose bindings on render context
return {
count,
plusOne,
increment
}
}
}
One of the key aspects of the component API is how to encapsulate and reuse logic across multiple components. With Vue 2.x's current API, there are a number of common patterns we've seen in the past, each with its own drawbacks. These include:
mixins
option)These patterns are discussed in more details in the appendix - but in general, they all suffer from one or more of the drawbacks below:
Unclear sources for properties exposed on the render context. For example, when reading the template of a component using multiple mixins, it can be difficult to tell from which mixin a specific property was injected from.
Namespace clashing. Mixins can potentially clash on property and method names, while HOCs can clash on expected prop names.
Performance. HOCs and renderless components require extra stateful component instances that come at a performance cost.
The function based API, inspired by React Hooks, presents a clean and flexible way to compose logic inside and between components without any of these drawbacks. This can be achieved by extracting code related to a piece of logic into what we call a "composition function" and returning reactive state. Here is an example of using a composition function to extract the logic of listening to the mouse position:
function useMouse() {
const x = value(0)
const y = value(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// in consuming component
const Component = {
setup() {
const { x, y } = useMouse()
const { z } = useOtherLogic()
return { x, y, z }
},
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
Note in the example above:
See also:
One of the major goals of 3.0 is to provide better built-in TypeScript type inference support. Originally we tried to address this problem with the now-abandoned Class API RFC, but after discussion and prototyping we discovered that using Classes doesn't fully address the typing issue.
The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs.
See also:
Function-based APIs are exposed as named ES exports and imported on demand. This makes them tree-shakable, and leaves more room for future API additions. Code written with function-based APIs also compresses better than object-or-class-based code, since (with standard minification) function and variable names can be shortened while object/class methods and properties cannot.
setup
functionA new component option, setup()
is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component. setup()
is called when an instance of the component is created, after props resolution. The function receives the resolved props as its argument:
const MyComponent = {
props: {
name: String
},
setup(props) {
console.log(props.name)
}
}
Note this props
object is reactive - i.e. it is updated when new props are passed in, and can be observed and reacted upon using the watch
function introduced later in this RFC. However, for userland code, it is immutable during development (will emit warning if user code attempts to mutate it).
Similar to data()
, setup()
can return an object containing properties to be exposed to the template's render context:
const MyComponent = {
props: {
name: String
},
setup(props) {
return {
msg: `hello ${props.name}!`
}
},
template: `<div>{{ msg }}</div>`
}
This works exactly like data()
- msg
becomes a reactive and mutable property, but only on the render context. In order to expose a reactive value that can be mutated by a function declared inside setup()
, we can use the value
API:
import { value } from 'vue'
const MyComponent = {
setup(props) {
const msg = value('hello')
const appendName = () => {
msg.value = `hello ${props.name}`
}
return {
msg,
appendName
}
},
template: `<div @click="appendName">{{ msg }}</div>`
}
Calling value()
returns a value wrapper object that contains a single reactive property: .value
. This property points to the actual value the wrapper is holding - in the example above, a string. The value can be mutated:
// read the value
console.log(msg.value) // 'hello'
// mutate the value
msg.value = 'bye'
Primitive values in JavaScript like numbers and strings are not passed by reference. Returning a primitive value from a function means the receiving function will not be able to read the latest value when the original is mutated or replaced.
Value wrappers are important because they provide a way to pass around mutable and reactive references for arbitrary value types. This is what enables composition functions to encapsulate the logic that manages the state while passing the state back to the components as a trackable reference:
setup() {
const valueA = useLogicA() // logic inside useLogicA may mutate valueA
const valueB = useLogicB()
return {
valueA,
valueB
}
}
Value wrappers can also hold non-primitive values and will make all nested properties reactive. Holding non-primitive values like objects and arrays inside a value wrapper provides the ability to entirely replace the value with a fresh one:
const numbers = value([1, 2, 3])
// replace the array with a filtered copy
numbers.value = numbers.value.filter(n => n > 1)
If you want to create a non-wrapped reactive object, use observable
(which is an exact equivalent of 2.x Vue.observable
API):
import { observable } from 'vue'
const object = observable({
count: 0
})
object.count++
Note in the last example we are using {{ msg }}
in the template without the .value
property access. This is because value wrappers get "unwrapped" when they are accessed on the render context or as a nested property inside a reactive object.
You can mutate an unwrapped value binding in inline handlers:
const MyComponent = {
setup() {
return {
count: value(0)
}
},
template: `<button @click="count++">{{ count }}</button>`
}
Value wrappers are also automatically unwrapped when accessed as a nested property inside a reactive object:
const count = value(0)
const obj = observable({
count
})
console.log(obj.count) // 0
obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1
count.value++
console.log(obj.count) // 2
console.log(count.value) // 2
As a rule of thumb, the only occasions where you need to use .value
is when directly accessing value wrappers as variables.
In addition to plain value wrappers, we can also create computed values:
import { value, computed } from 'vue'
const count = value(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2
A computed value behaves just like a 2.x computed property: it tracks its dependencies and only re-evaluates when dependencies have changed.
Computed values can also be returned from setup()
and will get unwrapped just like normal value wrappers. The main difference is that they are read-only by default - assigning to a computed value's .value
property or attempting to mutate a computed value binding on the render context will be a no-op and result in a warning.
To create a writable computed value, provide a setter via the second argument:
const count = value(0)
const writableComputed = computed(
// read
() => count.value + 1,
// write
val => {
count.value = val - 1
}
)
All .value
access are reactive, and can be tracked with the standalone watch
API, which behaves like the 2.x vm.$watch
API but with important differences.
The first argument passed to watch
can be either a getter function or a value wrapper. The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed:
watch(
// getter
() => count.value + 1,
// callback
(value, oldValue) => {
console.log('count + 1 is: ', value)
}
)
// -> count + 1 is: 1
count.value++
// -> count + 1 is: 2
Unlike 2.x $watch
, the callback will be called once when the watcher is first created. This is similar to 2.x watchers with immediate: true
, but with a slight difference. By default, the callback is called after current renderer flush. In other words, the callback is always called when the DOM has already been updated. This behavior can be configured.
In 2.x we often notice code that performs the same logic in mounted
and in a watcher callback - e.g. fetching data based on a prop. The new watch
behavior makes it achievable with a single statement.
As mentioned previously, the props
object passed to the setup()
function is reactive and can be used to watch for props changes:
const MyComponent = {
props: {
id: number
},
setup(props) {
const data = value(null)
watch(() => props.id, async (id) => {
data.value = await fetchData(id)
})
}
}
// double is a computed value
const double = computed(() => count.value * 2)
// watch a value directly
watch(double, value => {
console.log('double the count is: ', value)
}) // -> double the count is: 0
count.value++ // -> double the count is: 2
A watch
call returns a stop handle:
const stop = watch(...)
// stop watching
stop()
If watch
is called inside setup()
or lifecycle hooks of a component instance, it will automatically be stopped when the associated component instance is unmounted:
export default {
setup() {
// stopped automatically when the component unmounts
watch(/* ... */)
}
}
Sometimes the watcher callback will perform async side effects that need to be invalidated when the watched value changes. The watcher callback receives a 3rd argument that can be used to register a cleanup function. The cleanup function is called when:
watch
is used inside setup()
)watch(idValue, (id, oldId, onCleanup) => {
const token = performAsyncOperation(id)
onCleanup(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
We are registering cleanup via a passed-in function instead of returning it from the callback (like React useEffect
) because the return value is important for async error handling. It is very common for the watcher callback to be an async function when performing data fetching:
const data = value(null)
watch(getId, async (id) => {
data.value = await fetchData(id)
})
An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves. In addition, Vue relies on the returned Promise to automatically handle potential errors in the Promise chain.
By default, all watcher callbacks are fired after current renderer flush. This ensures that when callbacks are fired, the DOM will be in already-updated state. If you want a watcher callback to fire before flush or synchronously, you can use the flush
option:
watch(
() => count.value + 1,
() => console.log(`count changed`),
{
flush: 'post', // default, fire after renderer flush
flush: 'pre', // fire right before renderer flush
flush: 'sync' // fire synchronously
}
)
watch
Optionsinterface WatchOptions {
lazy?: boolean
deep?: boolean
flush?: 'pre' | 'post' | 'sync'
onTrack?: (e: DebuggerEvent) => void
onTrigger?: (e: DebuggerEvent) => void
}
All current lifecycle hooks will have an equivalent onXXX
function that can be used inside setup()
:
import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
}
}
import { provide, inject } from 'vue'
const CountSymbol = Symbol()
const Ancestor = {
setup() {
// providing a value can make it reactive
const count = value(0)
provide({
[CountSymbol]: count
})
}
}
const Descendent = {
setup() {
const count = inject(CountSymbol)
return {
count
}
}
}
If provided key contains a value wrapper, inject
will also return a value wrapper and the binding will be reactive (i.e. the child will update if ancestor mutates the provided value).
Makes it more difficult to reflect and manipulate component definitions. (Maybe that's a good thing?)
The proposed APIs are all new additions and can theoretically be introduced in a completely backwards compatible way. However, the new APIs can replace many of the existing options and makes them unnecessary in the long run. Being able to drop some of these old options will result in considerably smaller bundle size and better performance.
Therefore we are planning to provide two builds for 3.0:
Compatibility build: supports both the new function-based APIs AND all the 2.x options.
Standard build: supports the new function-based APIs and only a subset of 2.x options.
Current 2.x users can start with the compatibility build and progressively migrate away from deprecated options, until eventually switching to the standard build.
Preserved options work the same as 2.x and are available in both the compatibility and standard builds of 3.0. Options marked with * may receive further adjustments before 3.0 official release.
name
props
template
render
components
directives
filters
*delimiters
*comments
*These options will only be available in the compatibility build of 3.0.
data
(replaced by value
and value.raw
returned from setup()
)computed
(replaced by computed
returned from setup()
)methods
(replaced by plain functions returned from setup()
)watch
(replaced by watch
)provide/inject
(replaced by provide
and inject
)mixins
(replaced by function composition)extends
(replaced by function composition)onXXX
functions)These options will only be available in the compatibility build of 3.0.
el
Components are no longer mounted by instantiating a constructor with new
, Instead, a root app instance is created and explicitly mounted. See RFC#29.
propsData
Props for root component can be passed via app instance's mount
method. See RFC#29.
functional
Functional components are now declared as plain functions. See RFC#27.
model
No longer necessary with v-model
arguments. See RFC#31.
inheritAttrs
Deperecated by RFC#26.
The function based API provides the same level of logic composition capabilities as React Hooks, but with some important differences. Unlike React hooks, the setup()
function is called only once. This means code using Vue's function APIs are:
useEffect
callback may capture stale variables if the user forgets to pass the correct dependency array;useMemo
is almost always needed in order to prevent inline handlers causing over-re-rendering of child components;The primary goal of introducing the Class API was to provide an alternative API that comes with better TypeScript inference support. However, the fact that Vue components need to merge properties declared from multiple sources onto a single this
context creates a bit of a challenge even with a Class-based API.
One example is the typing of props. In order to merge props onto this
, we have to either use a generic argument to the component class, or use a decorator.
Here's an example using generic arguments:
interface Props {
message: string
}
class App extends Component<Props> {
static props = {
message: String
}
}
Since the interface passed to the generic argument is in type-land only, the user still needs to provide a runtime props declaration for the props proxying behavior on this
. This double-declaration is redundant and awkward.
We've considered using decorators as an alternative:
class App extends Component<Props> {
@prop message: string
}
Using decorators creates a reliance on a stage-2 spec with a lot of uncertainties, especially when TypeScript's current implementation is completely out of sync with the TC39 proposal. In addition, there is no way to expose the types of props declared with decorators on this.$props
, which breaks TSX support. Users may also assume they can declare a default value for the prop with @prop message: string = 'foo'
when technically it just can't be made to work as expected.
In addition, currently there is no way to leverage contextual typing for the arguments of class methods - which means the arguments passed to a Class' render
function cannot have inferred types based on the Class' other properties.
3.0.0-alpha.0
https://jsfiddle.net/bn08c9rL/2/
The item should be deleted from the set.
The item is not deleted from the set.
It seems like this still needs to be implemented, in v3 because currently it passes slots
as component's prop instead of its children.
<child v-bind="{ slots: $slots }"></child>
Right now, the pattern that's been called "renderless components" doesn't make complete sense, because technically, those components still need a render function that just returns null
:
render: () => null
What are thoughts on removing the need for either a template or render function in components, by default rendering null
?
Since data
in components, state
in Vuex, and Vue.observable
are all used to register observable state objects, I wonder if we should standardize the language we use to reduce the number of concepts people have to learn. My personal preference would be to settle on state
, since this is also the word generally preferred in the React and Angular ecosystems, thus easing migration. So the total renames would be:
data
/$data
-> state
/$state
Vue.observable
-> Vue.state
The obvious downside is the adjustment pain for current users, but I think that could be mostly alleviated by a period of allowing the old names to work as aliases, while emitting a warning if they're actually used.
Review note: I've split this part out of the React hooks like composition API because it is not strictly coupled to that proposal.
Provide standalone APIs for creating and observing reactive state.
import { state, value, computed, watch } from '@vue/observer'
// reactive object
// equivalent of 2.x Vue.observable()
const obj = state({ a: 1 })
// watch with a getter function
watch(() => obj.a, value => {
console.log(`obj.a is: ${value}`)
})
// a "ref" object that has a .value property
const count = value(0)
// computed "ref" with a read-only .value property
const plusOne = computed(() => count.value + 1)
// refs can be watched directly
watch(count, (count, oldCount) => {
console.log(`count is: ${count}`)
})
watch(plusOne, countPlusOne => {
console.log(`count plus one is: ${countPlusOne}`)
})
Vue's reactivity system powers a few aspects of Vue:
Tracking dependencies used during a component's render for automatic component re-render
Tracking dependencies of computed properties to only re-compute values when necessary
Expose this.$watch
API for users to perform custom side effects in response to state changes
Until 2.6, the reactivity system has largely been considered an internal implementation, and there is no dedicated API for creating / watching reactive state without doing it inside a component instance.
However, such coupling isn't technically necessary. In 3.x we've already split the reactivity system into its own package (@vue/observer
) with dedicated APIs, so it makes sense to also expose these APIs to enable more advanced use cases.
With these APIs it becomes possible to encapsulate stateful logic and side effects without components involved. In addition, with proper ability to "connect" the created state back into component instances, they also unlock a powerful component logic reuse mechanism.
In 2.6 we introduced the observable
API for creating reactive objects. We've noticed the naming causes confusion for some users who are familiar with RxJS or reactive programming where the term "observable" is commonly used to denote event streams. So here we intend to rename it to simply state
:
import { state } from 'vue'
const object = state({
count: 0
})
This works exactly like 2.6 Vue.observable
. The returned object behaves just like a normal object, and when its properties are accessed in reactive computations (render functions, computed property getters and watcher getters), they are tracked as dependencies. Mutation to these properties will cause corresponding computations to re-run.
The state
API cannot be used for primitive values because:
Vue tracks dependencies by intercepting property accesses. Usage of primitive values in reactive computations cannot be tracked.
JavaScript values are not passed by reference. Passing a value directly means the receiving function will not be able to read the latest value when the original is mutated.
The simple solution is wrapping the value in an object wrapper that can be passed around by reference. This is exactly what the value
API does:
import { value } from 'vue'
const countRef = value(0)
The value
API creates a wrapper object for a value, called a ref. A ref is a reactive object with a single property: .value
. The property points to the actual value being held and is writable:
// read the value
console.log(countRef.value) // 0
// mutate the value
countRef.value++
Refs are primarily used for holding primitive values, but it can also hold any other values including deeply nested objects and arrays. Non-primitive values held inside a ref behave like normal reactive objects created via state
.
In addition to plain value refs, we can also create computed refs:
import { value, computed } from 'vue'
const count = value(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2
Computed refs are readonly by default - assigning to its value
property will result in an error.
Computed refs can be made writable by passing a write callback as the 2nd argument:
const writableRef = computed(
// read
() => count.value + 1,
// write
val => {
count.value = val - 1
}
)
Computed refs behaves like computed properties in a component: it tracks its dependencies and only re-evaluates when dependencies have changed.
All .value
access are reactive, and can be tracked with the standalone watch
API.
NOTE: unlike 2.x, the watch
API is immediate by default.
watch
can be called with a single function. The function will be called immediately, and will be called again whenever dependencies change:
import { value, watch } from 'vue'
const count = value(0)
// watch and re-run the effect
watch(() => {
console.log('count is: ', count.value)
})
// -> count is: 0
count.value++
// -> count is: 1
When using a single function, any reactive properties accessed during its execution are tracked as dependencies. The computation and the side effect are performed together. To separate the two, we can pass two functions instead:
watch(
// 1st argument (the "computation", or getter) should return a value
() => count.value + 1,
// 2nd argument (the "effect", or callback) only fires when value returned
// from the getter changes
value => {
console.log('count + 1 is: ', value)
}
)
// -> count + 1 is: 1
count.value++
// -> count + 1 is: 2
The 1st argument can also be a ref:
// double is a computed ref
const double = computed(() => count.value * 2)
// watch a ref directly
watch(double, value => {
console.log('double the count is: ', value)
})
// -> double the count is: 0
count.value++
// -> double the count is: 2
A watch
call returns a stop handle:
const stop = watch(...)
// stop watching
stop()
If watch
is called inside lifecycle hooks or data()
of a component instance, it will automatically be stopped when the associated component instance is unmounted:
export default {
created() {
// stopped automatically when the component unmounts
watch(() => this.id, id => {
// ...
})
}
}
The effect callback can also return a cleanup function which gets called every time when:
watch(idRef, id => {
const token = performAsyncOperation(id)
return () => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
}
})
To make watchers non-immediate like 2.x, pass additional options via the 3rd argument:
watch(
() => count.value + 1,
() => {
console.log(`count changed`)
},
{ immediate: false }
)
While this proposal is focused on working with reactive state outside of components, such state should also be usable inside components as well.
Refs can be returned in a component's data()
function:
import { value } from 'vue'
export default {
data() {
return {
count: value(0)
}
}
}
When a ref
is returned as a root-level property in data()
, it is bound to the component instance as a direct property. This means there's no need to access the value via .value
- the value can be accessed and mutated directly as this.count
, and directly as count
inside templates:
<div @click="count++">
{{ count }}
</div>
The APIs proposed here are just low-level building blocks. Technically, they provide everything we need for global state management, so Vuex can be rewritten as a very thin layer on top of these APIs. In addition, when combined with the ability to programmatically hook into the component lifecycle, we can offer a logic reuse mechanism with capabilities similar to React hooks.
N/A
This is mostly new APIs that expose existing internal capabilities. Users familiar with Vue's existing reactivity system should be able to grasp the concept fairly quickly. It should have a dedicated chapter in the official guide, and we also need to revise the Reactivity in Depth section of the current docs.
watch
API overlaps with existing this.$watch
API and watch
component option. In fact, the standalone watch
API provides a superset of existing APIs. This makes the existence of all three redundant and inconsistent.
Should we deprecate this.$watch
and watch
component option?
Sidenote: removing this.$watch
and the watch
option also makes the entire watch
API completely tree-shakable.
We probably need to also expose a isRef
method to check whether an object is a value/computed ref.
Currently, we provide warnings like this one in several places of the docs. However, I think it might make more sense to automatically add unique keys to unkeyed conditional elements, since it's very rare to need elements with the same tag name to be reused.
The only potential disadvantages that come to mind are:
key
to each conditional element.3.0.0-alpha.1
Create a component with a simple <div>
.
createVNode
is not imported,_ctx
is not declared.Currently, when component contains only static HTML, createVNode
is imported even though it is not needed, additionally _ctx
variable is created which is also not used.
So I was wondering about this ... and it's more a question of curiosity than me spotting a problem but ...
We will provide a compatibility version of the observer package that will implement the 2.0 Reactivity system (getters&setters) that people can use for browsers that don't support Proxy
(read: IE).
However, also heavily rely on Proxy
in the runtime-core (componentProxy.ts)
With this implementation, we don't have to actually add properties to the component instance for each prop and $data
property, computed property and so on. We can just catch get and set access in the component's proxy.
But for a compatibility build, we would have to replace this behaviour with code that dpoes add getters & setters to the component instance, right?
In the current implementtion I don't see a clean / easy way to exchange this like we plan to have it for the observer package, do we (@evan) have a plan for how to approach it?
This is a reference of the current implemented version and open to discussion.
Update: don't know since when but latest Chrome Canary now enables class fields by default, so we can already play with the following API without a transpiler.
Plain ES usage:
import { h, Component } from '@vue/renderer-dom'
class App extends Component {
static props = {
msg: String
}
// data
count = 0
// lifecycle
created() {
console.log(this.count)
}
// getters are converted to computed properties
get plusOne() {
return this.count + 1
}
// a method
increment() {
this.count++
}
render(props) {
return h('div', [
h('span', this.count),
h('span', props.msg,
h('button', {
// methods are auto-bound when they are accessed via the render proxy
onClick: this.increment
}, 'increment')
])
}
}
TS Usage:
import { h, Component, ComponentWatchOptions } from '@vue/renderer-dom'
interface Props {
msg: string
}
interface Data {
count: number
}
class App extends Component<Props, Data> {
static props = {
msg: String
}
// data fields
count: number = 0
// ComponentWatchOptions type is only needed if `this` inference
// is needed inside options, e.g. in watch callbacks
static watch: ComponentWatchOptions<App> {
count(value) {
console.log(value)
}
}
created() {
console.log(this.count)
}
get plusOne() {
return this.count + 1
}
increment() {
this.count++
}
render(props) {
// ...
}
}
I think it would be nice to have an official way of "pre-rendering" the components tree to gather prefetching data when doing SSR. Ideally much lighter than a real render, with ways to mock global/local properties and methods to speed it up, maybe with an API libraries can use to mock themselves.
This would hugely improve the SSR story because currently we are limited to the routes components. I recently made some experimentation with this in the vue-apollo SSR API that allows the user to prefetch all the GraphQL queries in his app without manually adding them if they are in rotue sub-components (or even outside of router views). However, I think it would be better as an official API so it's less exposed to breaking due to Vue internal changes.
The vue-apollo implementation also recognizes a special attribute like no-prefetch
which skips a components sub-tree to optimize the tree walking if it is known that no queries will be found there.
https://twitter.com/youyuxi/status/1056673771376050176
Hooks provides the ability to:
However, it is quite different from the intuitions of idiomatic JS, and has a number of issues that can be confusing to beginners. This is why we should integrate it in a way that complements Vue's existing API, and primarily use it as a composition mechanism (replacement of mixins, HOCs and scoped-slot components).
Directly usable inside class render functions (can be mixed with normal class usage):
class Counter extends Component {
foo = 'hello'
render() {
const [count, setCount] = useState(0)
return h(
'div',
{
onClick: () => {
setCount(count + 1)
}
},
this.foo + ' ' + count
)
}
}
For template usage:
class Counter extends Component {
static template = `
<div @click="setCount(count + 1)">
{{ count }}
</div>
`
hooks() {
const [count, setCount] = useState(0)
// fields returned here will become available in templates
return {
count,
setCount
}
}
}
In SFC w/ object syntax:
<template>
<div @click="setCount(count + 1)">
{{ count }}
</div>
</template>
<script>
import { useState } from 'vue'
export default {
hooks() {
const [count, setCount] = useState(0)
return {
count,
setCount
}
}
}
</script>
Note: counter is a super contrived example mainly to illustrate how the API works. A more practical example would be this useAPI
custom hook, which is similar to libs like vue-promised
.
Proposed usage for useState
and useEffect
are already implemented.
To ease the learning curve for Vue users, we can implement hooks that mimic Vue's current API:
export default {
render() {
const data = useData({
count: 0
})
useWatch(() => data.count, (val, prevVal) => {
console.log(`count is: ${val}`)
})
const double = useComputed(() => data.count * 2)
useMounted(() => {
console.log('mounted!')
})
useUnmounted(() => {
console.log('unmounted!')
})
useUpdated(() => {
console.log('updated!')
})
return [
h('div', `count is ${data.count}`),
h('div', `double count is ${double}`),
h('button', { onClick: () => {
// still got that direct mutation!
data.count++
}}, 'count++')
]
}
}
Currently, when component contains only static HTML, createVNode
is imported even though it is not needed, additionally _ctx
variable is created which is also not used.
Draft based on #5
Disable implicit attribute fall-through to child component root element
Remove inheritAttrs
option
To replicate 2.x behavior in templates:
<div v-bind="$attrs">hi</div>
In render function:
import { h } from 'vue'
export default {
render() {
return h('div', this.$attrs, 'hi')
}
}
In 2.x, the current attribute fallthrough behavior is quite implicit:
class
and style
used on a child component are implicitly applied to the component's root element. It is also automatically merged with class
and style
bindings on that element in the child component template.
However, this behavior is not consistent in functional components because functional components may return multiple root nodes.
With 3.0 supporting fragments and therefore multiple root nodes for all components, this becomes even more problematic. The implicit behavior can suddenly fail when the child component changes from single-root to multi-root.
attributes passed to a component that are not declared by the component as props are also implicitly applied to the component root element.
Again, in functional components this needs explicit application, and would be inconsistent for 3.0 components with multiple root nodes.
this.$attrs
only contains attributes, but excludes class
and style
; v-on
listeners are contained in a separate this.$listeners
object. There is also the .native
modifier. The combination of inheritAttrs
, .native
, $attrs
and $listeners
makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs means "pass everything that I don't care about down to this element/component".
class
and style
are always automatically merged, and are not affected by inheritAttrs
.
The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments (the ability for a component to have multiple root nodes) in 3.0, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't.
In 3.0, we are planning to make attribute fallthrough an explicit decision of component authors. Whether a component accepts additional attributes becomes part of the component's API contract. We believe overall this should result in a simpler, more explicit and more consistent API.
inheritAttrs
option will be removed.
.native
modifier will be removed.
Non-prop attributes no longer automatically fallthrough to the root element of the child component (including class
and style
). This is the same for both stateful and functional components.
This means that with the following usage:
const Child = {
props: ['foo'],
template: `<div>{{ foo }}</div>`
}
const Parent = {
components: { Child },
template: `<child foo="1" bar="2" class="bar"/>`
}
Both bar="2"
AND class="bar"
on <child>
will be ignored.
this.$attrs
now contains everything passed to the component except those that are declared as props or custom events. This includes class
, style
, v-on
listeners (as onXXX
properties). The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in Render Function API Change).
To explicitly inherit additional attributes passed by the parent, the child component should apply it with v-bind
:
const Child = {
props: ['foo'],
template: `<div v-bind="$attrs">{{ foo }}</div>`
}
This also applies when the child component needs to apply $attrs
to a non-root element, or has multiple root nodes:
const ChildWithNestedRoot = {
props: ['foo'],
template: `
<label>
{{ foo }}
<input v-bind="$attrs">
</label>
`
}
const ChildWithMultipleRoot = {
props: ['foo'],
template: `
<label :for="$attrs.id">{{ foo }}</label>
<input v-bind="$attrs">
`
}
In render functions, if simple overwrite is acceptable, $attrs
can be merged using object spread. But in most cases, special handling is required (e.g. for class
, style
and onXXX
listeners). Therefore a cloneVNode
helper will be provided. It handles the proper merging of VNode data:
import { h, cloneVNode } from 'vue'
const Child = {
render() {
const inner = h(InnerComponent, {
foo: 'bar'
})
return cloneVNode(inner, this.$attrs)
}
}
The 2nd argument to cloneVNode
is optional. It means "clone the vnode and add these additional props". The cloneVNode
helper serves two purposes:
class
, style
and event listenersInside render functions, the user also has the full flexibility to pluck / omit any props from $attrs
using 3rd party helpers, e.g. lodash.
With flat VNode data and the removal of .native
modifier, all listeners are passed down to the child component as onXXX
functions:
<foo @click="foo" @custom="bar" />
compiles to:
h(foo, {
onClick: foo,
onCustom: bar
})
When spreading $attrs
with v-bind
, all parent listeners are applied to the target element as native DOM listeners. The problem is that these same listeners can also be triggered by custom events - in the above example, both a native click event and a custom one emitted by this.$emit('click')
in the child will trigger the parent's foo
handler. This may lead to unwanted behavior.
Props do not suffer from this problem because declared props are removed from $attrs
. Therefore we should have a similar way to "declare" emitted events from a component. There is currently an open RFC for it by @niko278.
Event listeners for explicitly declared events will be removed from $attrs
and can only be triggered by custom events emitted by the component via this.$emit
.
Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, especially when using class
and style
for styling purposes, but it is the more "correct" behavior when it comes to component responsibilities and boundaries. Styling use cases can be easily worked around with by wrapping the component in a wrapper element. In fact, this should be the best practice in 3.0 because the child component may or may not have multiple root nodes.
For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread $attrs
so that any aria-x
attributes can fallthrough. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels.
N/A
This RFC discusses the problem by starting with the 2.x implementation details with a lot of history baggage so it can seem a bit complex. However if we were to document the behavior for a new user, the concept is much simpler in comparison:
For a component without explicit props and events declarations, everything passed to it from the parent ends up in $attrs
.
If a component declares explicit props, they are removed from $attrs
.
If a component declares explicit events, corresponding onXXX
listeners are removed from $attrs
.
$attrs
essentially means extraneous attributes,, or "any attributes passed to the component that hasn't been explicitly handled by the component".
This will be one of the changes that will have a bigger impact on existing code and would likely require manual migration.
We will provide a warning when a component has unused extraneous attributes (i.e. non-empty $attrs
that is never used during render).
For application code that adds class
/ style
to child components for styling purposes: the child component should be wrapped with a wrapper element.
For higher-order components or reusable components that allow the consumer to apply arbitrary attributes / listeners to an inner element (e.g. custom form components that wrap <input>
):
Declare props and events that are consumed by the HOC itself (thus removing them from $attrs
)
Refactor the component and explicitly add v-bind="$attrs"
to the target inner component or element. For render functions, apply $attrs
with the cloneVNode
helper.
If a component is already using inheritAttrs: false
, the migration should be relatively straightforward.
We will need more dogfooding (migrating actual apps to 3.0) to provide more detailed migration guidance for this one, since the migration cost heavily depends on usage.
@yyx990803
Hi Evan, the new diff algorithm is really amazing, thank you for your efforts, but I have a question about this, assuming the following code is generated by the compiler:
(
openBlock(),
createBlock('div', null, [ // block_one
h('p', 123),
h('p', this.name),
this.isTruth
? (
openBlock(),
createBlock('div', null, [ // block_two
h('p', 456),
h('p', this.text1)
])
)
: (
openBlock(),
createBlock('div', null, [ // block_three
h('p', this.text2)
])
)
])
)
When isTruth
is true
then:
block_one.dynamicChildren = [ // oldBlockTree
h('p', this.name),
block_two
]
When isTruth
becomes false
:
block_one.dynamicChildren = [ // newBlockTree
h('p', this.name),
block_three
]
When diff oldBlockTree
and newBlockTree
, since block_two
and block_three
are different blocks, their children may have different structures. If we only diff their dynamic children, it should be incorrect. I just checked the code here https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/createRenderer.ts#L367-L380 What important information did I miss? I am not sure if the above example is reasonable, If not, then simply close the issue.
Make component props
declaration optional.
<!-- valid SFC -->
<template>
<div>{{ $props.foo }}</div>
</template>
In simple use cases where there is no need for runtime props type checking (especially in functional components), making props optional could result in simpler code.
When a component has no props
declarations, all attributes passed by the parent are exposed in this.$props
. Unlike declared props, they will NOT be exposed directly on this
. In addition, in this case this.$attrs
and this.$props
will be pointing to the same object.
Nice thing about this is you can omit the <script>
block altogether in a simple SFC:
<template>
<div>{{ $props.foo }}</div>
</template>
This is based on plain-function functional components proposed in Functional and Async Component API Change.
const FunctionalComp = props => {
return h('div', props.foo)
}
To declare props for plain-function functional components, attach it to the function itself:
FunctionalComp.props = {
foo: Number
}
Similar to stateful components, when props are declared, the props
arguments will only contain the declared props - attributes received but not declared as props will be in the 3rd argument (attrs
):
const FunctionalComp = (props, slots, attrs) => {
// `attrs` contains all received attributes except declared `foo`
}
FunctionalComp.props = {
foo: Number
}
For mode details on the new functional component signature, see Render Function API Change.
N/A
N/A
The behavior is fully backwards compatible.
In a component like this:
class Counter extends Component {
static count = 0
count = 0
increment() {
this.count++
Counter.count++
}
}
I'm wondering if users might expect Counter.count
to be reactive. In this case, the static count
might exist to track activity between all instances of the component. The only cases I can think of when users would expect static properties not to be reactive, is when they're part of an API for Vue or a plugin. For example, with vuefire:
static firebase = {
anArray: db.ref('url/to/my/collection')
}
So this means plugins would be forced to register all the static properties it wants to control, but I think this is actually a good thing. It would allow us to provide explicit warnings in the rare cases when two plugins conflict - or when a plugin tries to use a property that Vue itself has already claimed.
Another caveat is that for templates, which is the only place where these static properties aren't currently available, I think we'd currently have to do something like this:
{{ $options.count }}
But this feels very strange to me, since count
isn't really an "option". So at some point, we'll probably get a requested change in scope for templates to:
with ({ [Component.name]: Component }) {
with (this) {
// compiled template
}
}
so that users could access the static count
in templates just like in JavaScript, with:
{{ Counter.count }}
That actually sounds like a good idea to me too, making the experience between JSX and templates a little more universal.
And finally, I do want to acknowledge that there is technically a way to achieve the desired behavior:
const globalData = { totalCount = 0 }
class Counter extends Component {
globalData = globalData
Counter = globalData
increment() {
this.count++
globalData.totalCount++
}
}
but that seems pretty hacky to me. 😅
So right now, in Vue 2, we have 14 different builds
Why? Because we have 4 different dimensions that define a build:
In Vue 3, we get 2 additional dimensions to the existing ones through compatibility builds:
If we want to keep all permutations supported, we end up with:
That's a lot of builds 👀 - and certainly too many to leave for the user to pick from.
import { createVue } from '@vue/core'
import apifrom '@vue/api-compat'
import ractivity from '@vue/reactivity-compat'
export default createVue({
reactivity,
api,
})
Seems cumbersome, especially since this combo will be the default starting point for migrating existing projects.
So... Toughts?
Re-design custom directive API so that it better aligns with component lifecycle
Custom directives usage on components will follow the same rules as discussed in the Attribute Fallthrough Behavior RFC. It will be controlled by the child component via v-bind="$attrs"
.
const MyDirective = {
bind(el, binding, vnode, prevVnode) {},
inserted() {},
update() {},
componentUpdated() {},
unbind() {}
}
const MyDirective = {
beforeMount(el, binding, vnode, prevVnode) {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {}, // new
unmounted() {}
}
Make custom directive hook names more consistent with the component lifecycle.
Existing hooks are renamed to map better to the component lifecycle, with some timing adjustments. Arguments passed to the hooks remain unchanged.
bind
-> beforeMount
inserted
-> mounted
beforeUpdate
new, called before the element itself is updatedupdate
updated
insteadcomponentUpdated
-> updated
beforeUnmount
newunbind
-> unmounted
In 3.0, with fragments support, components can potentially have more than one root nodes. This creates an issue when a custom directive is used on a component with multiple root nodes.
To explain the details of how custom directives will work on components in 3.0, we need to first understand how custom directives are compiled in 3.0. For a directive like this:
<div v-foo="bar"></div>
Will roughly compile into this:
const vFoo = resolveDirective('foo')
return applyDirectives(h('div'), this, [
[vFoo, bar]
])
Where vFoo
will be the directive object written by the user, which contains hooks like mounted
and updated
.
applyDirective
returns a cloned VNode with the user hooks wrapped and injected as vnode lifecycle hooks (see Render Function API Changes for more details):
{
vnodeMounted(vnode, prevVNode) {
// call vFoo.mounted(...)
}
}
As a result, custom directives are fully included as part of a VNode's data. When a custom directive is used on a component, these vnodeXXX
hooks are passed down to the component as extraneous props and end up in this.$attrs
.
This is consistent with the attribute fallthrough behavior discussed in vuejs/rfcs#26. So, the rule for custom directives on a component will be the same as other extraneous attributes: it is up to the child component to decide where and whether to apply it. When the child component uses v-bind="$attrs"
on an inner element, it will apply any custom directives used on it as well.
N/A
N/A
$attrs
as discussed in Attribute Fallthrough Behavior should apply as well.N/A
inhertiAttrs
optionnativeOn
$attrs
on desired target element
<div v-bind="$attrs">
return cloneVNode(h('div'), this.$attrs)
$attrs
will be empty and everything will be in $props
, and it should just spread / pass on $props
instead.$attrs
will just point to $props
which includes everything passed to the component.The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't. By making this an explicit decision of component authors, whether a component accepts additional attributes becomes part of the component's API contract, overall resulting in more consistency.
The combination of inheritAttrs
, nativeOn
, $attrs
and $listeners
makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs
means "pass everything that I don't care about down to this element/component".
Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, but can be easily worked around with by wrapping the component in a wrapper element.
For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread $attrs
. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels.
Not spreading $attrs
actually improves performance.
const Vue2ReactWrapper = (vueComponentFactory, componentName) => {
const ReactPage: React.FC = () => {
const [loading, setLoading] = useState(true);
const containerRef = useRef(null);
useEffect(() => {
let Vue = require('vue');
Vue = Vue.default || Vue;
if (containerRef.current) {
let vueInstance;
(async () => {
let vueApp = await vueComponentFactory();
vueApp = vueApp.default || vueApp;
const { createApp } = Vue;
vueInstance = createApp().mount(vueApp, containerRef.current);
// vueInstance = new Vue({
// render: h => h(vueApp),
// }).$mount(containerRef.current);
setLoading(false);
})();
return () => {
if(!vueInstance) return;
if (vueInstance.$destroy) {
vueInstance.$destroy();
} else if (vueInstance.sink) {
vueInstance.sink.renderer.unmount(vueInstance.$root.vnode);
}
}
}
}, []);
ReactPage.displayName = componentName;
return (
<div>
<div ref={containerRef} />
{loading && <span> 页面加载中 </span>}
</div>
);
}
return ReactPage;
}
我希望在 React 元素 unmount 时 ,Vue 元素也能够 unmount。
I hope in the React unmount to elements, also able to unmount to Vue elements.
but now vueInstance.$destroy is undefined...
vueInstance.$destroy()
release resources,call beforeUnMount、
Many on the team would like the functions API to become the new recommended way of writing components in Vue 3. And even if we did not officially recommend it as the standard, many users would still gravitate toward it for its organizational and compositional advantages. That means a schism in the community is inevitable with the current API.
I've also been experimenting with how we might document Vue 3 and have been really struggling. I think I finally have to admit that with the current, planned API, making Vue feel as simple and elegant as it does now is simply beyond my abilities.
I've been experimenting with potential changes to the API, outlining affected examples to create a gentler and more intuitive learning path. My goals are to:
Finally, I think have a potential learning path that is worth attention from the team, though you may want to read my explanations for the proposed API changes before checking it out.
setup
to create
Vue.component('button-counter', {
props: ['initialCount'],
create(props) {
return {
count: props.initialCount
}
},
template: `
<button v-on:click="count++">
You clicked me {{ count }} times.
</button>
`
})
Avoid introducing a new concept with setup
, since we already have a concept of instance creation with the beforeCreate
and created
lifecycle functions.
With create
, it's more obvious and easier to remember when in the lifecycle the function is called.
Since this
is first available in created
, it will make more intuitive sense that it's not yet available in the create
function.
on
new Vue({
el: '#app',
onCreated() {
console.log(`I've been created!`)
}
})
Removes any confusion or autocomplete conflicts if setup
is renamed to create
(see proposal 1).
Removes the only naming inconsistency between the option and function versions of an API. For example, computed
and watch
correspond to Vue.computed
and Vue.watch
, but created
and mounted
correspond to Vue.onCreated
and Vue.onMounted
.
Users only have to make the context switch once, when going from Vue 2 to Vue 3, rather than every time they move between options and functions.
Better intellisense for lifecycle functions, because when users type the on
in import { on } from 'vue'
, they'll see a list of all available lifecycle functions.
data
/computed
/methods
into create
and allow its value to be an object just like data
currentlyconst app = new Vue({
el: '#app',
create: {
count: 0,
doubleCount: Vue.computed(() =>
return app.count * 2
),
incrementCount() {
app.count++
}
}
})
Vue.component('button-counter', {
props: ['initialCount'],
create(props) {
const state = {
count: props.initialCount,
countIncrease: Vue.computed(
() => state.count - props.initialCount
),
incrementCount() {
state.count++
}
}
return state
},
template: `
<button v-on:click="incrementCount">
You clicked me {{ count }}
({{ initialCount }} + {{ countIncrease }})
times.
</button>
`
})
It's easier for users to remember which options add properties to the instance, since there would only be one: create
.
Users don't need to be more advanced to better organize their properties. This one change provides the vast majority of the organizational benefit, without the complexity that can arise once you get into advanced composition.
New users won't have to learn methods as a separate concept - they're just functions.
It's even less code and fewer concepts than the current status quo.
Prevents the larger rift of people using create
vs data
/computed
/methods
, by having everyone start with create
from the beginning. With everyone already familiar with and using the create
function, sometimes moving more options there for organization purposes (e.g. watch
, onMounted
, etc) will be a dramatically smaller leap.
Makes the transition to a create
function feel more natural, both for current users ("oh, it's just like data
- when it's a function, I just return the object") and new users ("oh, this is just like what I was doing before, except I return the object").
Although Vue.computed
will return a binding, users won't have to worry about learning the concept of bindings for the entirety of Essentials. Only once we get to advanced composition that splits features into reusable functions will it become relevant, because then you have to worry about whether you're passing a value or binding.
If users decided to log a computed property (e.g. console.log(state.countIncrease)
) inside the create
function, they would see an object with a value
rather than the value directly. They won't understand exactly why Vue.computed
returns this until they're introduced to bindings, but I don't see it as a significant problem because it won't stop them from getting their work done.
When doing something very strange, like trying to immediately use the setter on a computed property inside create
, the abstraction of a binding would leak. However, if we think this is likely to actually happen, I believe it could be resolved by emitting a warning on the setter of bindings, since I believe that's always likely to be a mistake.
const state = {
count: 0,
doubleCount: Vue.computed(
() => state.count * 2,
newValue => {
state.count = newValue / 2
}
)
}
// This will not work, because `doubleCount` has not yet
// been normalized to a reactive property on `state`.
state.doubleCount = 2
return state
context
the same exact object as this
in other optionsVue.component('button-counter', {
create(props, context) {
return {
map: context.$parent.map
}
},
onCreated() {
console.log(this.$parent.map)
},
template: '...'
})
Vue.component('username-input', {
create(props, context) {
return {
focus() {
context.$refs.input.focus()
}
}
},
onMounted() {
console.log(this.$refs.input)
},
template: '...'
})
Avoids a context switch (no pun intended 😄) when moving between options and the create
function, because properties are accessed under the same name (e.g. this.$refs
/context.$refs
instead of this.$refs
/context.refs
).
When users/plugins add properties to the prototype or to this
in onBeforeCreate
, users can rely on those properties being available on the context
object in create
.
Requires extra documentation to help people understand that this
=== context
, and that properties are added/populated at different points in the lifecycle (e.g. props and state added in onCreated
, $refs
populated in onMounted
, etc). We'll probably need a more detailed version of the lifecycle diagram with these details (or whatever the reality ends up being).
For TypeScript users, every plugin that adds properties to the prototype (e.g. $router
, $vuex
) would require extending the interface of the Vue instance. I think they probably want to do this already for render
functions though, right? Don't they still have access to this
?
This one is more of a question than an argument. We have a lot of inconsistencies between the object-based and function-based APIs. For example, when a computed property takes a setter, it's a second argument:
Vue.computed(
() => {}, // getter
() => {} // setter
)
rather than providing an object with get
and set
, like this:
Vue.computed({
get() {},
set() {}
})
It's my understanding that these changes were made for the performance benefits of monomorphism. However, they have some significant disadvantages from a human perspective:
They force users to learn two versions of every API, rather than being able to mostly copy/paste when refactoring between options and functions, creating more work and making them feel like a significant context switch.
They create code that's less explicit and less readable, since either intellisense or comments are necessary to provide more information on what these arguments actually do.
As a starting place, could we create some benchmarks from realistic use cases so we can see exactly how much extra performance we're getting from monomorphism? That could make it easier to judge the pros and cons.
@vuejs/collaborators What does everyone think about these? They include some big changes, but I think they would vastly simplify the experience of learning and using Vue. I'm also very open to alternatives I may have missed!
Re-design app bootstrapping and global configuration API.
import Vue from 'vue'
import App from './App.vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
new Vue({
render: h => h(App)
}).$mount('#app')
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp()
app.config.ignoredElements = [/^app-/]
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.mount(App, '#app')
Vue's current global API and configurations permanently mutate global state. This leads to a few problems:
Global configuration makes it easy to accidentally pollute other test cases during testing. Users need to carefully store original global configuration and restore it after each test (e.g. resetting Vue.config.errorHandler
). Some APIs (e.g. Vue.use
, Vue.mixin
) don't even have a way to revert their effects. This makes tests involving plugins particularly tricky.
vue-test-utils
has to implement a special API createLocalVue
to deal with thisThis also makes it difficult to share the same copy of Vue
between multiple "apps" on the same page, but with different global configurations:
// this affects both root instances
Vue.mixin({ /* ... */ })
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })
Technically, Vue 2 doesn't have the concept of an "app". What we define as an app is simply a root Vue instance created via new Vue()
. Every root instance created from the same Vue
constructor shares the same global configuration.
In this proposal we introduce a new global API, createApp
:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
Calling createApp
with a root component returns an app instance. An app instance provides an app context. The entire component tree formed by the root instance and its descendent components share the same app context, which provides the configurations that were previously "global" in Vue 2.x.
An app instance exposes a subset of the current global APIs. The rule of thumb is any APIs that globally mutate Vue's behavior are now moved to the app instance. These include:
Vue.config
-> app.config
Vue.config.productionTip
Vue.component
-> app.component
Vue.directive
-> app.directive
Vue.filter
-> app.filter
Vue.mixin
-> app.mixin
Vue.use
-> app.use
Global APIs that are idempotent (i.e. do not globally mutate behavior) are now named exports as proposed in Global API Treeshaking.
The app instance can be mounted with the mount
method. It works the same as the existing vm.$mount()
component instance method and returns the mounted root component instance:
const rootInstance = app.mount('#app')
rootInstance instanceof Vue // true
An app instance can also provide dependencies that can be injected by any component inside the app:
// in the entry
app.provide({
[ThemeSymbol]: theme
})
// in a child component
export default {
inject: {
theme: {
from: ThemeSymbol
}
},
template: `<div :style="{ color: theme.textColor }" />`
}
This is similar to using the provide
option in a 2.x root instance.
Global APIs are now split between app instance methods and global named imports, instead of a single namespace. However the split makes sense because:
App instance methods are configuration APIs that globally mutate an app's behavior. They are also almost always used together only in the entry file of a project.
Global named imports are idempotent helper methods that are typically imported and used across the entire codebase.
N/A
Vue.config.productionTip
is left out because it is indeed "global". Maybe it should be moved to a global method?
import { suppressProductionTip } from 'vue'
suppressProductionTip()
3.0.0-alpha.1
I accidentally noticed that if I write setup
function like this, I will not receive the context
parameter.
export default {
setup(...args) {
console.log(args[1]) // null
}
}
args[1] is context
args[1] is null
I found why:
https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts#L293
I know most people won't write like this, but it's a little weird for me.
Just for discuss.
Not sure if this is intended but this code won't work:
watch(reactive({"test": "test"}), () => console.log("foo"))
but for normal refs, this works perfectly fine:
watch(ref(0), () => console.log("foo"))
will reactives also be supported?
Note: this has been split into two separate RFCs: #24 & #25
A set of React-hooks-inspired APIs for composing and reusing stateful logic across components.
import { value, computed, watch, useMounted, useDestroyed } from 'vue'
export default {
created() {
// value returns a value "ref" that has a .value property
const count = value(0)
// computed returns a computed "ref" with a read-only .value property
const plusOne = computed(() => count.value + 1)
// refs can be watched directly (or explicitly with `() => count.value`)
watch(count, (count, oldCount) => {
console.log(`count changed to: ${count}`)
})
watch(plusOne, countPlusOne => {
console.log(`count plus one changed to: ${countPlusOne}`)
})
useMounted(() => {
console.log('mounted')
})
useDestroyed(() => {
console.log('destroyed')
})
// bind refs as properties to the instance. This exposes
// this.count as a writable data property and this.plusOne as a read-only
// computed property
return {
count,
plusOne
}
}
}
Usage in a Class-based API would be exactly the same:
import Vue, { useMounted } from 'vue'
export default class MyComponent extends Vue {
created() {
useMounted(() => {
console.log('mounted')
})
}
}
This proposal consists of three parts:
A set of APIs centered around reactivity, e.g. value
, computed
and watch
. These will be part of the @vue/observer
package, and re-exported in the main vue
package. These APIs can be used anywhere and isn't particularly bound to the usage outlined in this proposal, however they are quintessential in making this proposal work.
A set of call-site constrained APIs that registers additional lifecycle hooks for the "current component", e.g. useMounted
. These functions can only be called inside the created()
lifecycle hook of a component.
The ability for the created()
lifecycle hook to return an object of additional properties to expose on the component instance.
In Vue 2.x, we already have the observable
API for creating standalone reactive objects. Assuming we can return an object of additional properties to expose on this
from created()
, we can achieve the following:
import { observable } from 'vue'
const App = {
created() {
const state = observable({
count: 0
})
const increment = () => {
state.count++
}
// exposed on `this`
return {
state,
increment
}
},
template: `
<button @click="increment">{{ state.count }}</button>
`
}
The above is a contrived example just to demonstrate how it could work. In practice, this is intended mainly for encapsulating and reusing logic much more complex than a simple counter.
Note in the above example we had to expose an object (so that Vue can register the dependency via the property access during render) even though what we are really exposing is just a number. We can use the value
API to create a container object for a single value, called a "ref". A ref is simply a reactive object with a writable value
property that holds the actual value:
import { value } from 'vue'
const countRef = value(0)
// read the value
console.log(countRef.value) // 0
// mutate the value
countRef.value++
The reason for using a container object is so that our code can have a persistent reference to a value that may be mutated over time.
A value ref is very similar to a plain reactive object with only the .value
property. It is primarily used for holding primitive values, but the value can also be a deeply nested object, array or anything that Vue can observe. Deep access are tracked just like typical reactive objects. The main difference is that when a ref is returned as part of the return object in created()
, it is bound as a direct property on the component instance:
import { value } from 'vue'
const App = {
created() {
return {
count: value(0)
}
},
template: `
<button @click="count++">{{ count }}</button>
`,
}
A ref binding exposes the value directly, so the template can reference it directly as count
. It is also writable - note that the click handler can directly do count++
. (In comparison, non-ref bindings returned from created()
are readonly).
In addition to writable value refs, we can also create standalone computed refs:
import { value, computed } from 'vue'
const count = value(0)
const countPlusOne = computed(() => count.value + 1)
console.log(countPlusOne.value) // 1
count.value++
console.log(countPlusOne.value) // 2
Computed refs are readonly. Assigning to its value
property will result in an error.
All .value
access are reactive, and can be tracked with the standalone watch
API.
import { value, computed, watch } from 'vue'
const count = value(0)
const double = computed(() => count.value * 2)
// watch and re-run the effect
watch(() => {
console.log('count is: ', count.value)
})
// -> count is: 0
// 1st argument (the getter) can return a value, and the 2nd
// argument (the callback) only fires when returned value changes
watch(() => count.value + 1, value => {
console.log('count + 1 is: ', value)
})
// -> count + 1 is: 1
// can also watch a ref directly
watch(double, value => {
console.log('double the count is: ', value)
})
// -> double the count is: 0
count.value++
// -> count is: 1
// -> count + 1 is: 2
// -> double the count is: 2
Note that unlike this.$watch
in 2.x, watch
are immediate by default (defaults to { immediate: true }
) unless a 3rd options argument with { lazy: true }
is passed:
watch(
() => count.value + 1,
() => {
console.log(`count changed`)
},
{ lazy: true }
)
The callback can also return a cleanup function which gets called every time when the watcher is about to re-run, or when the watcher is stopped:
watch(someRef, value => {
const token = performAsyncOperation(value)
return () => {
token.cancel()
}
})
A watch returns a stop handle:
const stop = watch(...)
// stop watching
stop()
Watchers created inside a component's created()
hook are automatically stopped when the owner component is destroyed.
For each existing lifecycle hook (except beforeCreate
and created
), there will be an equivalent useXXX
API. These APIs can only be called inside the created()
hook of a component. The prefix use
is an indication of the call-site constraint.
import { useMounted, useUpdated, useDestroyed } from 'vue'
export default {
created() {
useMounted(() => {
console.log('mounted')
})
useUpdated(() => {
console.log('updated')
})
useDestroyed(() => {
console.log('destroyed')
})
}
}
Unlike React Hooks, created
is called only once, so these calls are not subject to call order and can be conditional.
useXXX
methods automatically detects the current component whose setup()
is being called. The instance is also passed into the registered lifecycle hook as the argument. This means they can easily be extracted and reused across multiple components:
import { useMounted } from 'vue'
const useSharedLogic = () => {
useMounted(vm => {
console.log(`hello from component ${vm.$options.name}`)
})
}
const CompA = {
name: 'CompA',
created() {
useSharedLogic()
}
}
const CompB = {
name: 'CompB',
created() {
useSharedLogic()
}
}
Let's take the example from React Hooks Documentation. Here's the equivalent in Vue's idiomatic API:
export default {
props: ['id'],
data() {
return {
isOnline: null
}
},
created() {
ChatAPI.subscribeToFriendStatus(this.id, this.handleStatusChange)
},
destroyed() {
ChatAPI.unsubscribeFromFriendStatus(this.id, this.handleStatusChange)
},
watch: {
id: (newId, oldId) => {
ChatAPI.unsubscribeFromFriendStatus(oldId, this.handleStatusChange)
ChatAPI.subscribeToFriendStatus(newId, this.handleStatusChange)
}
},
methods: {
handleStatusChange(status) {
this.isOnline = status
}
}
}
And here's the equivalent using the APIs introduced in this proposal:
import { value, computed, watch } from 'vue'
export default {
props: ['id'],
created() {
const isOnline = value(null)
function handleStatusChange(status) {
isOnline.value = status
}
watch(() => this.id, id => {
// this is called immediately and then very time id changes
ChatAPI.subscribeToFriendStatus(id, handleStatusChange)
return () => {
// this is called every time id changes and when the component
// is unmounted (which causes the watcher to be stopped)
ChatAPI.unsubscribeFromFriendStatus(id, handleStatusChange)
}
})
return {
isOnline
}
}
}
Note that because the watch
function is immediate by default and also auto-stopped on component unmount, it achieves the effect of watch
, created
and destroyed
options in one call (similar to useEffect
in React Hooks).
The logic can also be extracted into a reusable function (like a custom hook):
import { value, computed, watch } from 'vue'
function useFriendStatus(idRef) {
const isOnline = value(null)
function handleStatusChange(status) {
isOnline.value = status
}
watch(idRef, id => {
ChatAPI.subscribeToFriendStatus(id, handleStatusChange)
return () => {
ChatAPI.unsubscribeFromFriendStatus(id, handleStatusChange)
}
})
return isOnline
}
export default {
props: ['id'],
created() {
return {
// to pass watch-able state, make sure to pass a value
// or computed ref
isOnline: useFriendStatus(computed(() => this.id))
}
}
}
Note that even after logic extraction, the component is still responsible for:
Even with multiple extracted custom logic functions, there will be no confusion regarding where a prop or a data property comes from. This is a major advantage over mixins.
Type inference of returned values. This actually works better in the object-based API because we can reverse infer this
based on the return value of created()
, but it's harder to do so in a class. The user will likely have to explicitly annotate these properties on the class.
To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, this isn't intended as a replacement for the base API - it is positioned as a advanced mechanism to encapsulate and reuse logic across components.
watch
has automatic dependency tracking, no need to worry about exhaustive depsTODO
We can also use data()
instead of created()
, since data()
is already used for exposing properties to the template. But it feels a bit weird to perform side effects like watch
or useMounted
in data()
.
state()
? (replaces data()
and has special handling for refs in returned value)We probably need to also expose a isRef
method to check whether an object is a value/computed ref.
This is the equivalent of what we can currently achieve via scoped slots with libs like vue-promised. This is just an example showing that this new set of API is capable of achieving similar results.
function useFetch(endpointRef) {
const res = value({
status: 'pending',
data: null,
error: null
})
// watch can directly take a computed ref
watch(endpointRef, endpoint => {
let aborted = false
fetch(endpoint)
.then(res => res.json())
.then(data => {
if (aborted) {
return
}
res.value = {
status: 'success',
data,
error: null
}
}).catch(error => {
if (aborted) {
return
}
res.value = {
status: 'error',
data: null,
error
}
})
return () => {
aborted = true
}
})
return res
}
// usage
const App = {
created(props) {
return {
postData: useFetch(computed(() => `/api/posts/${props.id}`))
}
},
template: `
<div>
<div v-if="postData.status === 'pending'">
Loading...
</div>
<div v-else-if="postData.status === 'success'">
{{ postData.data }}
</div>
<div v-else>
{{ postData.error }}
</div>
</div>
`
}
Hypothetical examples of exposing state to the component from external sources. This is more explicit than using mixins regarding where the state comes from.
export default {
created() {
const { x, y } = useMousePosition()
const orientation = useDeviceOrientation()
return {
x,
y,
orientation
}
}
}
3.0.0-alpha.1
https://guides.github.com/features/mastering-markdown/
no
no
no
3.0.0-alpha.1
(None)
Attempt to compile this template:
<button @click="foo(); bar()">Click</button>
It should compile without any errors or warnings and when the button is clicked it should call the foo
and bar
functions sequentially.
The generated code is invalid.
The following template fragment works in 2.x:
<button @click="foo(); bar()">Click</button>
The click
event handler gets compiled into something like this:
click: $event => { foo(); bar() }
But in 3.0.0-alpha.1
the compiled code is this:
click: _cache[0] || (_cache[0] = $event => (foo(); bar()))
which is invalid JS syntax.
It can be fixed on the user's end by:
The transparent wrapper pattern is very convenient and increasingly common, but still a little clunky in Vue 2.x. For example, to get a BaseInput
component that can be used exactly like a normal input
element, we need to do this:
<script>
export default {
props: {
value: {
type: String
}
},
computed: {
listeners() {
return {
...this.$listeners,
input: event => {
this.$emit('input', event.target.value)
}
}
}
}
}
</script>
<template>
<input
:value="value"
v-on="listeners"
>
</template>
In Vue 3, that will currently translate to:
<script>
class InputText extends Component {
// Currently, `v-model` on a component assumes `value` is a
// prop, so it will never be included in $attrs, even if no
// `value` prop is defined. This forces users to always define
// `value` as a prop, even if it's never needed by the component.
static props = {
value: String
}
get attrs () {
return {
...this.$attrs,
// We have to manually include the `value` prop to `attrs`,
// due to `v-model`'s assumption that `value` is a prop.
value: this.value,
// Since `v-model` listens to the `input` event by default,
// the user is forced to override it when using binding
// $attrs in order to prevent the unwanted behavior of the
// `event` object being emitted as the new value.
onInput: event => {
this.$emit('input', event.target.value)
}
}
}
}
</script>
<template>
<input v-bind="attrs">
</template>
Still quite a bit of work. 😕
However, if we remove v-model
's assumption that value
is a prop, then it no longer has to be added separately. And if we also make v-model
listen for an update:value
event by default (as @posva suggested here), instead of input
, then we no longer have to override input
and can just add a new listener.
With these 2 changes, our BaseInput
component would simplify to:
<template>
<input
v-bind="$attrs"
@input="$emit('update:value', $event.target.value)"
>
</template>
To me, this is much more convenient and intuitive. 🙂
@yyx990803 @posva @johnleider What do you think?
This would allow for automatic data prefetching (for example with apollo) on the server.
Ideas:
waitCounter = 0
renderToString
with this.waitForPrefetch()
(waitCounter++
)waitCounter
is 0
, resolve renderToString
this.prefetchDone()
after fetching data
this.waitForPrefetch()
againwaitCounter--
waitCounter
is 0
we resolve renderToString
this.waitForPrefetch()
(maybe make this parametrable, like maxReRenders
)<NoSSR>
component prevents both server-side render and async prefetchingskipPrefetch () { return this.myProp }
option?3.0.0-alpha.1
https://jsfiddle.net/fva48tsr/
I tried out the basic store pattern example from the docs, but it doesn't ever update.
<template>
<div>{{ sharedState.counter }}</div>
</template>
<script>
var store = {
state: {
counter: 0
},
increment: function() {
this.state.counter ;
}
};
export default {
data: function() {
return {
sharedState: store.state
};
},
mounted: function() {
setInterval(() => {
store.increment();
}, 1000);
}
}
</script>
The counter goes up
The counter doesn't change
3.0.0-alpha.1
https://github.com/chz/vue-next-test
Clone repository and start Vue
Click "Toggle Visibility" 3-4 times
Div should be inserted to the correct dom.
The node before which the new node is to be inserted is not a child of this node.
Adjust v-model
API when used on custom components for more flexible usage, and adjust compilation output on native elements for more succinct compiler output.
This builds on top of vuejs/rfcs#8 (Replace v-bind
's .sync
with a v-model
argument).
Previously, v-model="foo"
on components roughly compiles to the following:
h(Comp, {
value: foo,
onInput: value => {
foo = value
}
})
However, this requires the component to always use the value
prop for binding with v-model
when the component may want to expose the value
prop for a different purpose.
In 2.2 we introduced the model
component option that allows the component to customize the prop and event to use for v-model
. However, this still only allows one v-model
to be used on the component. In practice we are seeing some components that need to sync multiple values, and the other values have to use v-bind.sync
. We noticed that v-model
and v-bind.sync
are fundamentally doing the same thing and can be combined into a single construct by allowing v-model
to accept arguments (as proposed in vuejs/rfcs#8).
In 3.0, the model
option will be removed. v-model="foo"
(without argument) on a component compiles to the following instead:
h(Comp, {
modelValue: foo,
'onUpdate:modelValue': value => {
foo = value
}
})
If the component wants to support v-model
without an argument, it should expect a prop named modelValue
. To sync its value back to the parent, the child should emit an event named "update:modelValue"
(see Render Function API change for details on the new VNode data structure).
The default compilation output uses the prop name modelValue
so that it is clear this prop is compiled from v-model
. This will be useful to differentiate it from the original value
prop which could've been created manually by the user (especially in the native element case detailed in a later section).
RFC #8 proposes the ability for v-model
to accept arguments. The argument can be used to denote the prop v-model
should bind to. v-model:value="foo"
compiles to:
h(Comp, {
value: foo,
'onUpdate:value': value => {
foo = value
}
})
In this case, the child component expects a value
prop and emits "update:value"
to sync.
Note that this enables multiple v-model
bindings on the same component, each syncing a different prop, without the need for extra options in the component:
<InviteeForm
v-model:name="inviteeName"
v-model:email="inviteeEmail"
/>
Another aspect of the v-model
usage is on native elements. In 2.x, the compiler produces different code based on the element type v-model
is used on. For example, it outputs different prop/event combinations for <input type="text">
and <input type="checkbox">
. However, this strategy does not handle dynamic element or input types very well:
<input :type="dynamicType" v-model="foo">
The compiler has no way to guess the correct prop/event combination at compile time, so it has to produce very verbose code to cover possible cases.
In 3.0, v-model
on native elements produces the exact same output as when used on components. For example, <input v-model="foo">
compiles to:
h('input', {
modelValue: foo,
'onUpdate:modelValue': value => {
foo = value
}
})
The idea is to move element/input type specific handling to the runtime. For this reason, the v-model
output must be something special (modelValue
) for the runtime to pick up and transform. If we use the default value and input, the runtime won't know if it's created by v-model
or manually by the user.
The module responsible for patching element props for the web platform will dynamically determine what actual prop/event to bind. For example, on <input type="checkbox">
, modelValue
will be mapped to checked
and "update:modelValue"
will be mapped to "change"
. Moving the logic to runtime allows the framework to handle dynamic cases better, and enables the compiler to output less verbose code.
TODO
N/A
TODO
It is still difficult to use v-model
on native custom elements, since 3rd party custom elements have unknown prop/event combinations and do not necessarily follow Vue's sync event naming conventions. For example:
<custom-input v-model="foo"></custom-input>
Vue has no information on the property to bind to or the event to listen to. One possible way to deal with this is to use the type
attribute as a hint:
<custom-input v-model="foo" type="checkbox"></custom-input>
This would tell Vue to bind v-model
using the same logic for <input type="checkbox">
, using checked
as the prop and change
as the event.
If the custom element doesn't behave like any existing input type, then it's probably better off to use explicit v-bind
and v-on
bindings.
Introduce APIs for dynamically injecting component lifecycle hooks.
import { onMounted, onUnmounted } from 'vue'
export default {
beforeCreate() {
onMounted(() => {
console.log('mounted')
})
onUnmounted(() => {
console.log('unmounted')
})
}
}
In advanced use cases, we sometimes need to dynamically hook into a component's lifecycle events after the component instance has been created. In Vue 2.x there is an undocumented API via custom events:
export default {
created() {
this.$on('hook:mounted', () => {
console.log('mounted!')
})
}
}
This API has some drawbacks because it relies on the event emitter API with string event names and a reference of the target component instance:
Event emitter APIs with string event names are prone to typos and is hard to notice when a typo is made because it fails silently.
If we were to extract complex logic into external functions, the target instance has to be passed to it via an argument. This can get cumbersome when there are additional arguments, and when trying to further split the function into smaller functions. When called inside a component's data()
or lifecycle hooks, the target instance can already be inferred by the framework, so ideally the instance reference should be made optional.
This proposal addresses both problems.
For each existing lifecycle hook (except beforeCreate
), there will be an equivalent onXXX
API:
import { onMounted, onUpdated, onDestroyed } from 'vue'
export default {
created() {
onMounted(() => {
console.log('mounted')
})
onUpdated(() => {
console.log('updated')
})
onDestroyed(() => {
console.log('destroyed')
})
}
}
When called inside a component's data()
or lifecycle hooks, the current instance is automatically inferred. The instance is also passed into the callback as the argument:
onMounted(instance => {
console.log(instance.$options.name)
})
When used outside lifecycle hooks, the target instance can be explicitly passed in via the second argument:
onMounted(() => { /* ... */ }, targetInstance)
If the target instance cannot be inferred and no explicit target instance is passed, an error will be thrown.
onXXX
calls return a removal function that removes the injected hook:
// an updated hook that fires only once
const remove = onUpdated(() => {
remove()
})
Pre-requisite: please read the Advanced Reactivity API RFC first.
When combined with the ability to create and observe state via standalone APIs, it's possible to encapsulate arbitrarily complex logic in an external function, (with capabilities similar to React hooks):
This is the equivalent of what we can currently achieve via scoped slots with libs like vue-promised. This is just an example showing that this new set of API is capable of achieving similar results.
import { value, computed, watch } from 'vue'
function useFetch(endpointRef) {
const res = value({
status: 'pending',
data: null,
error: null
})
// watch can directly take a computed ref
watch(endpointRef, endpoint => {
let aborted = false
fetch(endpoint)
.then(res => res.json())
.then(data => {
if (aborted) {
return
}
res.value = {
status: 'success',
data,
error: null
}
}).catch(error => {
if (aborted) {
return
}
res.value = {
status: 'error',
data: null,
error
}
})
return () => {
aborted = true
}
})
return res
}
// usage
const App = {
props: ['id'],
data() {
return {
postData: useFetch(computed(() => `/api/posts/${this.id}`))
}
},
template: `
<div>
<div v-if="postData.status === 'pending'">
Loading...
</div>
<div v-else-if="postData.status === 'success'">
{{ postData.data }}
</div>
<div v-else>
{{ postData.error }}
</div>
</div>
`
}
Hypothetical examples of exposing state to the component from external sources. This is more explicit than using mixins regarding where the state comes from.
import { value, onMounted, onDestroyed } from 'vue'
function useMousePosition() {
const x = value(0)
const y = value(0)
const onMouseMove = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', onMouseMove)
})
onDestroyed(() => {
window.removeEventListener('mousemove', onMouseMove)
})
return { x, y }
}
export default {
data() {
const { x, y } = useMousePosition()
return {
x,
y,
// ... other data
}
}
}
value.raw
? Custom Renderer API - createRenderer
Component
Portal
Fragments
ref
Directives
Options
Global API
createApp
app.config
app.use
app.mixin
(only for legacy mode)app.directive
app.component
Lifecycle hooks
Instance properties
Instance methods
<component is="">
createAsyncComponent
<transition>
<transition-group>
<keep-alive>
<await>
<script>
$isServer
instance propertyFor Reference: https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/optional/context.ts
So I took a look at the new implementation and I have some reservations about it.
vue-next
As of this writing, vue-next will implement Context (or as we call it: provide/Inject) as a pair of components:
<!-- Parent.vue -->
<Provide id="someName" :value="{ message: 'Hello Vue!' }">
<Child />
</Provide>
<!-- Child-vue -->
<Inject id="someName">
<!-- side note: I couldn't determine if we will still require `slot-scope`
to be set on the root slot element, or if we provide a way to define it
on the component providing the slot itself.
-->
<div slot-scope="{ message }">
{{ message }}
<div>
</inject>
My reservations mostly originate from the discussions in the vuex repository about the proposed removal of mapXXX
helpers in the next major of vuex here:
https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/optional/context.ts
The important point is this one:
vuex state/getters/actions are only conveniently available in templates and render functions. That means the convenience is unavailable for computed properties that rely on Vuex state and methods that dispatch Vuex actions, which is a big limitation.
I think <Inject>
suffers from similar problems. A component that uses <Inject>
can only access the provided data within the template, so computed properties and watch callbacks have no way of accessing that data.
It works for React because React has neither of those things, it only has state, props and methods. So you can easily pass the injected data as an argument to a component method. But a similar thing is not possible for computed
or watch
functions since those don't take arguments.
Maybe the new hot kid in town, Hooks
, can solve this problem?
import { useInject } from 'vue'
export default {
hooks() {
const [injectedData] = useInject('someName')
return {
injectedData
}
}
}
That doesn't mean we have to drop <Inject>
, I think it's still convenient for situations where accessing the injected Data in the template / render function is enough. But we need a way to allow users to actually inject this data as a property on the component instance.
The issue with a hook would be that we make this dependent on an experimental feature.
We sold provide/inject
as a feature primarily targeted at library authors. Dropping the old syntax from Vue 2 would be another breaking change that may be a roadblock for people updating their projects, as libs using this feature would have to find a way to make it work with the new component-based approach.
So this is another feature I think we should provide compat version for...
I accidentally noticed that if I write setup
function like this, I will not receive the context
parameter.
export default {
setup(...args) {
console.log(args[1]) // null
}
}
And I found why:
https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts#L293
I know most people won't write like this, but it's a little weird for me.
Just for discuss.
{ functional: true }
option removed<template functional>
no longer supportedcreateAsyncComponent
API methodimport { h } from 'vue'
const FunctionalComp = props => {
return h('div', `Hello! ${props.name}`)
}
import { createAsyncComponent } from 'vue'
const AsyncComp = createAsyncComponent(() => import('./Foo.vue'))
In 2.x, functional components must be created using the following format:
const FunctionalComp = {
functional: true,
render(h) {
return h('div', `Hello! ${props.name}`)
}
}
This has the following issues:
Even when the component needs nothing but the render function, it still needs to use the object with functional: true
.
Some options are supported (e.g. props
and inject
) but others are not (e.g. components
). However, users often expect all options to be supported because it looks so similar to a normal stateful component (especially when they use SFC with <template functional>
).
Another aspect of the problem is that we've noticed some users are using functional components solely for performance reasons, e.g. in SFCs with <template functional>
, and are requesting us to support more stateful component options in functional components. However, I don't think this is something we should invest more time in.
In v3, the performance difference between stateful and functional components has been drastically reduced and will be insignificant in most use cases. As a result there is no longer a strong incentive to use functional components just for performance, which also no longer justifies the maintenance cost of supporting <template functional>
. Functional components in v3 should be used primarily for simplicity, not performance.
In 3.x, we intend to support functional components only as plain functions:
import { h } from 'vue'
const FunctionalComp = (props, slots) => {
return h('div', `Hello! ${props.name}`)
}
The functional
option is removed, and object format with { functional: true }
is no longer supported.
SFCs will no longer support <template functional>
- if you need anything more than a function, just use a normal component.
The function signature has also changed - h
is now imported globally. Instead of a render context, props and slots and other values are passed in. For more details on how the new arguments can replace 2.x functional render context, see the Render Function API Change RFC.
Props declaration is now optional (only necessary when runtime validation is needed). To add runtime validation or default values, attach props
to the function itself:
const FunctionalComp = props => {
return h('div', `Hello! ${props.name}`)
}
FunctionalComp.props = {
name: String
}
With the functional component change, Vue's runtime won't be able to tell whether a function is being provided as a functional component or an async component factory. So in v3 async components must now be created via a new API method:
import { createAsyncComponent } from 'vue'
const AsyncComp = createAsyncComponent(() => import('./Foo.vue'))
The method also supports advanced options:
const AsyncComp = createAsyncComponent({
factory: () => import('./Foo.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
})
This will make async component creation a little more verbose, but async component creation is typically a low-frequency use case, and are often grouped in the same file (the routing configuration).
N/A
For functional components, a compatibility mode can be provided for one-at-a-time migration.
For async components, the migration is straightforward and we can emit warnings when function components return Promise instead of VNodes.
SFCs using <template functional>
should be converted to normal SFCs.
@yyx990803 I've seen v-bind.sync
cause quite a bit of confusion in Vue 2, as users expect it to be able to use expressions like with v-bind
(despite whatever we put in the docs). The explanation I've had the best success with is:
Thinking about
:title.sync="title"
like a normal binding with extra behavior is really the wrong way to think about it, because two-way bindings are fundamentally different. The.sync
modifier works essentially like v-model, which is Vue's other syntax sugar for creating a two-way binding. The main difference is that it expands to a slightly different pattern that allows you to have multiple two-way bindings on a single component, rather than being limited to just one.
Which brings me to the question: if it helps to tell users not to think of v-bind.sync
like v-bind
, but rather to think about it like v-model
, should it be part of the v-model
API instead? For example, instead of:
<MyComponent v-bind:title.sync="title" />
Perhaps a more intuitive syntax would be:
<MyComponent v-model:title="title" />
As a bonus, a change like this would be very easy to flag with the migration helper and fix with find-and-replace.
Thoughts?
3.0.0-alpha.1
https://codesandbox.io/s/zen-field-ewni3
vue 2 for comparison - https://jsfiddle.net/jfrhgbma/
import { h, createApp } from "vue";
function Button(props, { attrs }) {
console.log({
props,
attrs
});
}
const App = {
setup() {
return () => [
h(Button, {
"data-id": 1,
"aria-label": "Close",
})
];
}
};
createApp().mount(App, "#root");
Button's props should be in camelCase and attrs in kebab-case
Without explicit props declaration Button attrs
keys are converted to camelCase.
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.
// 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
}
}
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)
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:
resolveComponent
returns name string if component is not found)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.
// 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 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 components now must be explicitly created.
import { createAsyncComponent } from 'vue'
const AsyncFoo = createAsyncComponent(() => import('./Foo.vue'))
Now are internally on-vnode hooks with the exact same lifecycle as components.
Custom directives are now applied via a helper:
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]])
}
No longer performs auto-prefixing.
false
. Instead, it's set as attr="false"
instead. To remove the attribute, use null
.Filters are gone for good (or can it?)
v-for
. Instead, use something like :ref="'foo' + key"
or function refs.3.0.0-alpha.1
https://jsfiddle.net/v27hqo1a/
Not sure if this is intended but this code won't work:
watch(reactive({"test": "test"}), () => console.log("foo"))
but for normal refs, this works perfectly fine:
watch(ref(0), () => console.log("foo"))
will reactives also be supported?
reactive object should be watched
it will not be watched
to see the error within the fiddle just open the console.
I'm using @vue/reactivity as a low level data service, and there are some heavy computing case in my app. I want to manually check if a reactive object is being used in computed values, so I can do lazy computing when it is not.
export function isTracked(target) {
return targetMap.has(target)
}
export function onTrack(target, callback) {
// when a target is being tracked, call callback function.
}
3.0.0-alpha.1
https://github.com/chz/vue-next-test
Clone repository and start Vue
Click "Toggle Visibility" 3-4 times
Div should be inserted to the correct dom.
The node before which the new node is to be inserted is not a child of this node.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.