Vue 3.x $attrs 用法的改变
Summary
- Make the attrs fallthrough behavior more consistent;
- Make it easier to pass all extraneous attrs to child elements / components.
Motivation
In Vue 2.x, components have an implicit attrs fallthrough behavior. Any prop passed to a component, but is not declared as a prop by the component, is considered an "extraneous attribute". In 2.x, these extraneous attributes are exposed in this.$attrs
and implicitly applied to the component's root node. This behavior can be disabled with inheritAttrs: false
, where the user expects to explicit control where the attrs should be applied.
There are a number of inconsistencies and issues in the 2.x behavior:
inheritAttrs: false
does not affectclass
andstyle
.class
,style
,v-on
listeners and custom directives are not included in$attrs
, making it cumbersome for a higher-order component (HOC) to properly pass everything down to a nested child component.- Functional components have no implicit attrs fallthrough at all.
In 3.x, the need for "spreading extraneous attrs" also becomes more prominent due to the ability for components to render multiple root nodes (fragments). This RFC seeks to address these problems.
Detailed design
this.$attrs
now contains everything passed to the component except those that are declared as props. This includes class
, style
, v-on
listeners (as onXXX
props), and custom directives (as onVnodeXXX
props) . (This is based on flat props structure as proposed in Render Function API Change ). As a result of this:
.native
modifier forv-on
will be removed.this.$listeners
will be removed.
When the component returns a single root node, this.$attrs
will be implicitly merged into the root node's props. This is the same as 2.x, except it will now include all the props that were not previously in this.$attrs
, as discussed above.
If the component receives extraneous attrs, but returns multiple root nodes (a fragment), an automatic merge cannot be performed. If the user did not perform an explicit spread (checked by access to this.$attrs
during render), a runtime warning will be emitted. The component should either pick an element to apply the attrs to (via v-bind="$attrs"
), or explicitly suppress the warning with inheritAttrs: false
.
inheritAttrs: false
With inheritAttrs: false
, the component can either choose to intentionally ignore all extraneous attrs, or explicitly control where the attrs should be applied via v-bind="$attrs"
:
<div class="wrapper">
<!-- apply attrs to an inner element instead of root -->
<input v-bind="$attrs">
</div>
In 2.x, this option does not affect class
and style
- they will be implicitly merged on root in all cases for stateful components - but in 3.0 this special case is removed: class
and style
will be part of $attrs
just like everything else.
Merging Attrs in Render Functions(mergeProps)
In manual render functions, it may seem convenient to just use a spread:
export default {
props: { /* ... */ },
inheritAttrs: false,
render() {
return h('div', { class: 'foo', ...this.$attrs })
}
}
However, this will cause attrs to overwrite whatever existing props of the same name. For example, there the local class
may be overwritten when we probably want to merge the classes instead. Vue provides a mergeProps
helper that handles the merging of class
, style
and onXXX
listeners:
import { mergeProps } from 'vue'
export default {
props: { /* ... */ },
inheritAttrs: false,
render() {
return h('div', mergeProps({ class: 'foo' }, this.$attrs))
}
}
This is also what v-bind
uses internally.
Consistency between Functional and Stateful Components
Functional components will now share the exact same behavior with Stateful components. The extraneous attrs is passed via the second context argument (as specified in Render Function API Change ):
const Func = (props, { attrs }) => {
return h('div', mergeProps({ id: 'x' }, attrs), props.msg)
}
Func.props = { /* ... */ }
Components with no Props Declaration
Note that for components without props declaration (see Optional Props Declaration ), there will be no implicit attrs handling of any kind, because everything passed in is considered a prop and there will be no "extraneous" attrs. A component without props declaration (mostly functional components) is responsible for explicitly passing down necessary props. This can be easily done with object rest spread:
const Func = ({ msg, ...rest }) => {
return h('div', mergeProps({ id: 'x' }, rest), [
h('span', msg)
])
}
Unresolved questions
Removing Unwanted Listeners
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
.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
下一篇: TypeScript 常见问题
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论