- Start Date: 2019-04-08
- Target Major Version: 3.x
- Reference Issues: N/A
- Implementation PR: N/A
-
h
is now globally imported instead of passed to render functions as argument -
render function arguments changed and made consistent between stateful and functional components
-
VNodes now have a flat props structure
// globally imported `h`
import { h } from 'vue'
export default {
render() {
return h(
'div',
// flat data structure
{
id: 'app',
onClick() {
console.log('hello')
}
},
[
h('span', 'child')
]
)
}
}
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 (h
is a conventional alias for createElement
):
// 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 (as is this.$createElement
).
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:
- More verbose to write
class
andstyle
special cases are somewhat inconsistent- More memory usage (more objects allocated)
- Slower to diff (each nested object needs its own iteration loop)
- More complex / expensive to clone / merge / spread
- Needs more special rules / implicit conversions when working with JSX
In 3.x, we are moving towards a flat VNode data structure to address these problems.
h
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 will no longer receive any arguments. In fact, in 3.0 the render
option will mostly be used as an integration point for the render functions produced by the template compiler. For manual render functions, it is recommended to return it from the setup()
function:
import { h, reactive } from 'vue'
export default {
setup(props, { slots, attrs, emit }) {
const state = reactive({
count: 0
})
function increment() {
state.count++
}
// return the render function
return () => {
return h('div', {
onClick: increment
}, state.count)
}
}
}
The render function returned from setup()
naturally has access to reactive state and functions declared in scope, plus the arguments passed to setup:
-
props
andattrs
will be equivalent tothis.$props
andthis.$attrs
- also see Optional Props Declaration and Attribute Fallthrough. -
slots
will be equivalent tothis.$slots
- also see Slots Unification. -
emit
will be equivalent tothis.$emit
.
The props
, slots
and attrs
objects here are proxies, so they will always be pointing to the latest values when used in render functions.
For details on how setup()
works, consult the Composition API RFC.
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, emit }) => {
// ...
}
The new list of arguments should provide the ability to fully replace the current functional render context:
-
props
andslots
have equivalent values; -
data
andchildren
are no longer necessary (just useprops
andslots
); -
listeners
will be included inattrs
; -
injections
can be replaced using the newinject
API (part of Composition API):import { inject } from 'vue' import { themeSymbol } from './ThemeProvider' const FunctionalComp = props => { const theme = inject(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 props are handled using the following rules:
key
andref
are reservedclass
andstyle
have the same API as 2.x- props that start with
on
are handled asv-on
bindings, with everything afteron
being converted to all-lowercase as the event name (more on this below) - for anything else:
- If the key exists as a property on the DOM node, it is set as a DOM property;
- Otherwise it is set as an attribute.
There are two globally reserved props:
key
ref
In addition, you can hook into the vnode lifecycle using reserved onVnodeXXX
prefixed hooks:
h('div', {
onVnodeMounted(vnode) {
/* ... */
},
onVnodeUpdated(vnode, prevVnode) {
/* ... */
}
})
These hooks are also how custom directives are built on top of. Since they start with on
, they can also be declared with v-on
in templates:
<div @vnodeMounted="() => { ... }">
Due to the flat structure, this.$attrs
inside a component now contains any raw props that are not explicitly declared by the component, including class
, style
, onXXX
listeners and vnodeXXX
hooks. This makes it much easier to write wrapper components - simply pass this.$attrs
down with v-bind="$attrs"
.
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, withDirectives } 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 withDirectives(
h(comp),
[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 directives and use them by value.
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 should not be bundled into the library;
- For module builds, the import should be left alone and be handled by the end user bundler;
- For UMD / browser builds, it should try the global
Vue.h
first and fallback torequire
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 similar adaptor can be provided.
With the flat VNode data structure, how each property is handled internally becomes a bit implicit. This also creates a few problems - for example, how to explicitly set a non-existent DOM property, or listen to a CAPSCase event on a custom element?
We may want to support explicit binding types via prefix:
h('div', {
'attr:id': 'foo',
'prop:__someCustomProperty__': { /*... */ },
'on:SomeEvent': e => { /* ... */ }
})
This is equivalent to 2.x's nesting via attrs
, domProps
and on
. However, this requires us to perform an extra check for every property being patched, which leads to a constant performance cost for a very niche use case. We may want to find a better way to deal with this.