Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render Function API Change #29

Closed
yyx990803 opened this issue Mar 21, 2019 · 6 comments
Closed

Render Function API Change #29

yyx990803 opened this issue Mar 21, 2019 · 6 comments

Comments

@yyx990803
Copy link
Member

yyx990803 commented Mar 21, 2019

  • Start Date: 2019-03-12
  • 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 data structure

Basic example

// 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()
    )
  }
}

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:

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

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

import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}

Render Function Arguments Change

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.

Flat VNode Data Format

// 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 properties
  • class and style have the same API as 2.x
  • props that start with on are handled as v-on bindings
  • 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.

Due 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).

Context-free VNodes

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.

Drawbacks

Reliance on Vue Core

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

Alternatives

N/A

Adoption strategy

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

@Akryum
Copy link
Member

Akryum commented Mar 21, 2019

Context-free VNodes

What impact can this have on the devtools?

  • Will we still be able to access a component from DOM elements?
  • Will instance.$vnode and vnode.componentInstance sill work?
  • Functional components will have a context so they can be displayed in the devtools?

@yyx990803
Copy link
Member Author

@Akryum

Will we still be able to access a component from DOM elements?

We can expose anything the devtool needs, since it doesn't have to be part of the public API.

Will instance.$vnode and vnode.componentInstance sill work?

Yes. vnode.componentInstance will probably have a different name but still exposed.

Functional components will have a context so they can be displayed in the devtools?

Yes.

@posva
Copy link
Member

posva commented Mar 25, 2019

I enjoy destructuring arguments in the render function, it makes it easier to ignore unused arguments. Is it really better to have multiple parameters? It's to avoid allocating an extra object every time we call render, isn't it?

@LinusBorg
Copy link
Member

Concerning Event liisteners:

  • we use the on* prefix to differentiate between events and props/attributes, but
  • all of them will end up in $attrs so we can do v-bind="$attrs"

So what does that mean for events in templates on their own? Will all of these work?

<button v-on:click="handleClick">
<button v-bind:on-click="handleClick">
<button v-bind:onClick="handleClick">
  • If yes: I would worry that this leads to inconsistent templates
  • If no: Then it will be confusing why v-bind="$attrs" does bind events, while the individual bindings won't work.

@yyx990803
Copy link
Member Author

v-bind:onClick does work, but is not recommended. I think it's ok since most users would just @click shorthand.

@yyx990803
Copy link
Member Author

Published: vuejs/rfcs#28

@github-actions github-actions bot locked and limited conversation to collaborators Nov 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants