Skip to content

Commit 5fb76aa

Browse files
authored
Render function API change (#28)
1 parent b53b29b commit 5fb76aa

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
- Start Date: 2019-04-08
2+
- Target Major Version: 3.x
3+
- Reference Issues: N/A
4+
- Implementation PR: N/A
5+
6+
# Summary
7+
8+
- `h` is now globally imported instead of passed to render functions as argument
9+
10+
- render function arguments changed and made consistent between stateful and functional components
11+
12+
- VNodes now have a flat props structure
13+
14+
# Basic example
15+
16+
``` js
17+
// globally imported `h`
18+
import { h } from 'vue'
19+
20+
export default {
21+
render() {
22+
return h(
23+
'div',
24+
// flat data structure
25+
{
26+
id: 'app',
27+
onClick() {
28+
console.log('hello')
29+
}
30+
},
31+
[
32+
h('span', 'child')
33+
]
34+
)
35+
}
36+
}
37+
```
38+
39+
# Motivation
40+
41+
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`):
42+
43+
``` js
44+
// looking up a component based on a string ID
45+
h('some-component')
46+
47+
h('div', {
48+
directives: [
49+
{
50+
name: 'foo', // looking up a directive by string ID
51+
// ...
52+
}
53+
]
54+
})
55+
```
56+
57+
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`).
58+
59+
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:
60+
61+
``` js
62+
function renderSomething(h) {
63+
return h('div')
64+
}
65+
66+
export default {
67+
render(h) {
68+
return renderSomething(h)
69+
}
70+
}
71+
```
72+
73+
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.
74+
75+
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.
76+
77+
---
78+
79+
Another issue with 2.x's render function API is the nested VNode data structure:
80+
81+
``` js
82+
h('div', {
83+
class: ['foo', 'bar'],
84+
style: { }
85+
attrs: { id: 'foo' },
86+
domProps: { innerHTML: '' },
87+
on: { click: foo }
88+
})
89+
```
90+
91+
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.
92+
93+
However, over time we have noticed there are a number of drawbacks of the nested structure compared to a flat structure:
94+
95+
- More verbose to write
96+
- `class` and `style` special cases are somewhat inconsistent
97+
- More memory usage (more objects allocated)
98+
- Slower to diff (each nested object needs its own iteration loop)
99+
- More complex / expensive to clone / merge / spread
100+
- Needs more special rules / implicit conversions when working with JSX
101+
102+
In 3.x, we are moving towards a flat VNode data structure to address these problems.
103+
104+
# Detailed design
105+
106+
## Globally imported `h` function
107+
108+
`h` is now globally imported:
109+
110+
``` js
111+
import { h } from 'vue'
112+
113+
export default {
114+
render() {
115+
return h('div')
116+
}
117+
}
118+
```
119+
120+
## Render Function Signature Change
121+
122+
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:
123+
124+
``` js
125+
import { h, reactive } from 'vue'
126+
127+
export default {
128+
setup(props, { slots, attrs, emit }) {
129+
const state = reactive({
130+
count: 0
131+
})
132+
133+
function increment() {
134+
state.count++
135+
}
136+
137+
// return the render function
138+
return () => {
139+
return h('div', {
140+
onClick: increment
141+
}, state.count)
142+
}
143+
}
144+
}
145+
```
146+
147+
The render function returned from `setup()` naturally has access to reactive state and functions declared in scope, plus the arguments passed to setup:
148+
149+
- `props` and `attrs` will be equivalent to `this.$props` and `this.$attrs` - also see [Optional Props Declaration](https://github.com/vuejs/rfcs/pull/25) and [Attribute Fallthrough](https://github.com/vuejs/rfcs/pull/92).
150+
151+
- `slots` will be equivalent to `this.$slots` - also see [Slots Unification](https://github.com/vuejs/rfcs/pull/20).
152+
153+
- `emit` will be equivalent to `this.$emit`.
154+
155+
The `props`, `slots` and `attrs` objects here are proxies, so they will always be pointing to the latest values when used in render functions.
156+
157+
For details on how `setup()` works, consult the [Composition API RFC](https://vue-composition-api-rfc.netlify.com/api.html#setup).
158+
159+
## Functional Component Signature
160+
161+
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:
162+
163+
``` js
164+
const FunctionalComp = (props, { slots, attrs, emit }) => {
165+
// ...
166+
}
167+
```
168+
169+
The new list of arguments should provide the ability to fully replace the current functional render context:
170+
171+
- `props` and `slots` have equivalent values;
172+
- `data` and `children` are no longer necessary (just use `props` and `slots`);
173+
- `listeners` will be included in `attrs`;
174+
- `injections` can be replaced using the new `inject` API (part of [Composition API](https://vue-composition-api-rfc.netlify.com/api.html#provide-inject)):
175+
176+
``` js
177+
import { inject } from 'vue'
178+
import { themeSymbol } from './ThemeProvider'
179+
180+
const FunctionalComp = props => {
181+
const theme = inject(themeSymbol)
182+
return h('div', `Using theme ${theme}`)
183+
}
184+
```
185+
186+
- `parent` access will be removed. This was an escape hatch for some internal use cases - in userland code, props and injections should be preferred.
187+
188+
## Flat VNode Props Format
189+
190+
``` js
191+
// before
192+
{
193+
class: ['foo', 'bar'],
194+
style: { color: 'red' },
195+
attrs: { id: 'foo' },
196+
domProps: { innerHTML: '' },
197+
on: { click: foo },
198+
key: 'foo'
199+
}
200+
201+
// after
202+
{
203+
class: ['foo', 'bar'],
204+
style: { color: 'red' },
205+
id: 'foo',
206+
innerHTML: '',
207+
onClick: foo,
208+
key: 'foo'
209+
}
210+
```
211+
212+
With the flat structure, the VNode props are handled using the following rules:
213+
214+
- `key` and `ref` are reserved
215+
- `class` and `style` have the same API as 2.x
216+
- props that start with `on` are handled as `v-on` bindings, with everything after `on` being converted to all-lowercase as the event name (more on this below)
217+
- for anything else:
218+
- If the key exists as a property on the DOM node, it is set as a DOM property;
219+
- Otherwise it is set as an attribute.
220+
221+
### Special "Reserved" Props
222+
223+
There are two globally reserved props:
224+
225+
- `key`
226+
- `ref`
227+
228+
In addition, you can hook into the vnode lifecycle using reserved `onVnodeXXX` prefixed hooks:
229+
230+
``` js
231+
h('div', {
232+
onVnodeMounted(vnode) {
233+
/* ... */
234+
},
235+
onVnodeUpdated(vnode, prevVnode) {
236+
/* ... */
237+
}
238+
})
239+
```
240+
241+
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:
242+
243+
``` html
244+
<div @vnodeMounted="() => { ... }">
245+
```
246+
247+
---
248+
249+
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"`.
250+
251+
## Context-free VNodes
252+
253+
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:
254+
255+
``` js
256+
import { h, resolveComponent, resolveDirective, withDirectives } from 'vue'
257+
258+
export default {
259+
render() {
260+
const comp = resolveComponent('some-global-comp')
261+
const fooDir = resolveDirective('foo')
262+
const barDir = resolveDirective('bar')
263+
264+
// <some-global-comp v-foo="x" v-bar="y" />
265+
return withDirectives(
266+
h(comp),
267+
[fooDir, this.x],
268+
[barDir, this.y]
269+
)
270+
}
271+
}
272+
```
273+
274+
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.
275+
276+
# Drawbacks
277+
278+
## Reliance on Vue Core
279+
280+
`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:
281+
282+
- Vue should not be bundled into the library;
283+
- For module builds, the import should be left alone and be handled by the end user bundler;
284+
- For UMD / browser builds, it should try the global `Vue.h` first and fallback to `require` calls.
285+
286+
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.
287+
288+
# Alternatives
289+
290+
N/A
291+
292+
# Adoption strategy
293+
294+
- For template users this will not affect them at all.
295+
296+
- For JSX users the impact will also be minimal, but we do need to rewrite our JSX plugin.
297+
298+
- 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.
299+
300+
- 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.
301+
302+
- 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.
303+
304+
- Functional components using context will likely have to be manually migrated, but a similar adaptor can be provided.
305+
306+
# Unresolved Questions
307+
308+
## Escape Hatches for Explicit Binding Types
309+
310+
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?
311+
312+
We may want to support explicit binding types via prefix:
313+
314+
``` js
315+
h('div', {
316+
'attr:id': 'foo',
317+
'prop:__someCustomProperty__': { /*... */ },
318+
'on:SomeEvent': e => { /* ... */ }
319+
})
320+
```
321+
322+
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.

0 commit comments

Comments
 (0)