每当组件的内容(插槽、子组件等)发生变化时,我需要更新它的状态。对于上下文,它是一个表单组件,用于跟踪其输入的有效性状态。
下面的代码片段是以Options
API 格式编写的,但除了指定的地方外可以在 Vue2 和 Vue3 中使用。
开始
先从控制表单状态开始,根据状态修改一个类,子内容使用<slot/>
填充:
<template> <form :> <slot /> </form> </template> <script> export default { data: () => ({ isInvalid: false, }), }; </script>
为了更新isInvalid
属性,我们需要添加一个触发的事件,可以使用 sumit
事件 ,但我更喜用 input
事件。
表单不会触发 input
事件,但我们可以使用 “事件委托”。我们将监听器附加到父元素(<form>
)上,当事件发生在它的子元素(<input>
、<select>
、<textarea>
等)上时就会被触发。
任何时候在这个组件的<slot>
中触发input
事件,表单将捕获该事件。
<template> <form : @input="validate"> <slot /> </form> </template> <script> export default { data: () => ({ isInvalid: false, }), methods: { validate() { // 验证逻辑 } } }; </script>
验证逻辑可以是简单或复杂的。本文为了演示,用简单的方法,使用form.checkValidity()
API 来查看表单是否基于 HTML 验证属性而有效。
为了访问<form>
元素,可以用refs
或$el
属性。为了简单起见,本文使用$el
。
<template> <form : @input="validate"> <slot /> </form> </template> <script> export default { data: () => ({ isInvalid: false, }), methods: { validate() { this.isInvalid = !this.$el.checkValidity() } } }; </script>
问题
这里有一点问题。如果表单的内容改变了,会发生什么?如果一个<input>
在表单加载被添加到 DOM 中,会发生什么?
举个例子,我们把这个表单组件称为 "MyForm"
,在 App 中,内容如下:
// App.vue <template> <MyForm> <input v-model="showInput" id="toggle-name" name="toggle-name" type="checkbox" /> <label for="toggle-name">显示其它 input</label> <template v-if="showInput"> <label for="name">Name:</label> <input id="name" name="name" required /> </template> <button type="submit">提交</button> </MyForm> </template> <script> import Form from "./components/form.vue"; export default { name: "App", components: { MyForm: Form, }, data: () => ({ showInput: false, }), }; </script>
当App.vue
通过条件来隐藏显示某些 input
,我们的表单需要知道。在这种情况下,我们会想到在表单内容发生变化时跟踪其有效性,而不仅仅是在 input
事件或mounted
生命周期钩子上。否则,可能会显示不正确的信息。
熟悉 Vue 的生命周期钩子小伙伴,这里可能会想到使用 update
来跟踪变化。理论上,这听起来不错。在实践中,它会创造一个无限的循环,然后浏览器挂了。
解决方法
经过一番研究和测试,最佳解决方案是使用MutationObserver
API。它是浏览器内置的方法,提供了监视对 DOM 树所做更改的能力,如果节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。
它是原生的方法,所以不受限于框架。
使用时,首先使用MutationObserver
构造函数,新建一个观察器实例,同时指定这个实例的回调函数。在每次 DOM 变动后调用,这个回调都被调用。该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例,将我们的 form
组件改写成如下:
<template> <form : @input="validate"> <slot /> </form> </template> <script> export default { data: () => ({ isInvalid: false, }), mounted() { const observer = new MutationObserver(this.validate); observer.observe(this.$el, { childList: true, subtree: true, }); this.observer = observer; }, methods: { validate() { this.isInvalid = !this.$el.checkValidity(); }, }, beforeUnmount() { this.observer.disconnect(); }, }; </script> <style scoped> </style>
这里还需要使用 beforeUnmount
生命周期事件来断开observer
的连接,这会清除它所分配的任何内存。
最后,我们将isInvalid
状态传递给要访问的内容的插件槽,这也称作用域的槽,它非常有用。
<template> <form : @input="validate"> <slot v-bind="{ isInvalid }" /> </form> </template> <script> export default { data: () => ({ isInvalid: false, }), mounted() { const observer = new MutationObserver(this.validate); observer.observe(this.$el, { childList: true, subtree: true, }); this.observer = observer; }, methods: { validate() { this.isInvalid = !this.$el.checkValidity(); }, }, beforeUnmount() { this.observer.disconnect(); }, }; </script>
通过这样的设置,可以在我们的表单组件中添加任意数量的 input
,并添加任何它需要的条件渲染逻辑。只要input
使用 HTML 验证属性,表单就会跟踪它是否处于有效状态。
此外,由于使用的是作用域槽,我们将表单的状态提供给父级,所以父级可以对有效性的变化做出反应。
例如,在 App.vue
,我们想在表单无效时 “禁用” 提交按钮,可以这么来写:
<template> <MyForm> <template slot:default="form"> <label for="name">Name:</label> <input id="name" name="name" required> <button type="submit" : > Submit </button> </template> </MyForm> </template>
以上就是怎么监听 Vue 的 slot 插槽变化的解决方法,希望本文能对你未来的开发有所帮助。