diff --git a/active-rfcs/0008-render-function-api-change.md b/active-rfcs/0008-render-function-api-change.md new file mode 100644 index 00000000..6a3e4b59 --- /dev/null +++ b/active-rfcs/0008-render-function-api-change.md @@ -0,0 +1,322 @@ +- Start Date: 2019-04-08 +- Target Major Version: 3.x +- Reference Issues: N/A +- Implementation PR: N/A + +# Summary + +- `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 + +# Basic example + +``` js +// 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') + ] + ) + } +} +``` + +# Motivation + +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`): + +``` js +// 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: + +``` js +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: + +``` js +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` and `style` 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. + +# Detailed design + +## Globally imported `h` function + +`h` is now globally imported: + +``` js +import { h } from 'vue' + +export default { + render() { + return h('div') + } +} +``` + +## Render Function Signature Change + +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: + +``` js +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` 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). + +- `slots` will be equivalent to `this.$slots` - also see [Slots Unification](https://github.com/vuejs/rfcs/pull/20). + +- `emit` will be equivalent to `this.$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](https://vue-composition-api-rfc.netlify.com/api.html#setup). + +## Functional Component Signature + +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: + +``` js +const FunctionalComp = (props, { slots, attrs, emit }) => { + // ... +} +``` + +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` are no longer necessary (just use `props` and `slots`); +- `listeners` will be included in `attrs`; +- `injections` can be replaced using the new `inject` API (part of [Composition API](https://vue-composition-api-rfc.netlify.com/api.html#provide-inject)): + + ``` js + 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. + +## Flat VNode Props Format + +``` js +// 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` and `ref` are reserved +- `class` and `style` have the same API as 2.x +- 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) +- 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. + +### Special "Reserved" Props + +There are two globally reserved props: + +- `key` +- `ref` + +In addition, you can hook into the vnode lifecycle using reserved `onVnodeXXX` prefixed hooks: + +``` js +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: + +``` html +