表单封装后表单项的事件回调中为什么需要加上nexttick才可以正常改变其他项的值?

对element-plus的表单封装,但是封装后发现在表单项的onchane事件中修改其他表单项的值页面没有重新渲染,必须使用nexttick才会生效,请问需要如何解决?

我希望在父组件中不使用nexttick能够修改其他表单项的值。
36fcd581f7b7330b11fb6e58d28b7c8.png

父组件使用

<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue'
import HForm from '@/components/HForm/index.vue'
const dataFormRef = ref()
const dataForm = reactive({
  select1Value: '',
  select2Value: '选项2-2',
})

// 配置表单
const formOptions = reactive({
  formItems: [
    {
      type: 'select',
      label: '选项1',
      span: 12,
      name: 'select1Value',
      required: true,
      options: [
        { text: '选项1', value: '选项1' },
        { text: '选项2', value: '选项2' },
      ],
      props: {
        onChange: () => {
          // nextTick(() => {
          dataForm.select2Value = ''
          // })
        },
      },
    },
    {
      type: 'select',
      label: '选项2',
      span: 12,
      name: 'select2Value',
      required: true,
      options: [
        { text: '选项2-1', value: '选项2-1' },
        { text: '选项2-2', value: '选项2-2' },
        { text: '选项2-3', value: '选项2-3' },
        { text: '选项2-4', value: '选项2-4' },
      ],
      props: {},
    },
  ],
})
</script>

<template>
  <HForm v-model="dataForm" v-bind="formOptions" ref="dataFormRef" />
</template>

封装的表单组件

<template>
  <div class="h-from-container">
    <el-form
      :model="dataForm"
      label-width="auto"
      v-bind="$attrs"
      ref="formRef"
      :rules="formRules"
    >
      <el-row :gutter="itemGutter">
        <template v-for="item in formItems" :key="item.name">
          <el-col :span="item.span" v-if="!item.hidden">
            <el-form-item
              :label="item.label"
              :label-width="item.labelWidth"
              :required="item.required"
              :prop="item.name"
            >
              <template v-if="item.type === 'select'">
                <el-select
                  v-model="dataForm[item.name]"
                  v-bind="item.props"
                  style="width: 100%"
                >
                  <el-option
                    label="请选择"
                    value=""
                    v-if="item.props?.showNull"
                  ></el-option>
                  <el-option
                    :label="item.optionsKey ? o[item.optionsKey.text] : o.text"
                    :value="
                      item.optionsKey ? o[item.optionsKey.value] : o.value
                    "
                    v-for="o in item.options"
                    :key="o.value"
                  ></el-option>
                </el-select>
              </template>
            </el-form-item>
          </el-col>
        </template>
      </el-row>
    </el-form>
  </div>
</template>

<script setup lang="ts" name="HForm">
import { ref, onMounted, watch } from 'vue'
import type { FormRules } from 'element-plus'
import { type FormItemIntf } from './types'

const emits = defineEmits(['update:modelValue'])
const dataForm = ref<Record<string, any>>({})
const formRef = ref()
const props = withDefaults(
  defineProps<{
    modelValue: Record<string, any>
    itemGutter?: number
    formItems: FormItemIntf[]
  }>(),
  { modelValue: () => ({}), itemGutter: 10, formItems: () => [] },
)

function initForm() {
  initFormRules()
  initFormData()
}

// 生成表单数据
function initFormData() {
  dataForm.value = JSON.parse(JSON.stringify(props.modelValue))
}

const formRules = ref<FormRules>({})
// 生成表单验证规则
function initFormRules() {
  const requiredItems = props.formItems.filter(e => e.required)
  if (requiredItems && requiredItems.length) {
    requiredItems.forEach(e => {
      if (e.rules) formRules.value[e.name] = e.rules
      else {
        formRules.value[e.name] = [
          { required: true, message: `请填写${e.label}!` },
        ]
      }
    })
  }
}

async function validate() {
  return await formRef.value.validate()
}
function resetFields() {
  formRef.value.resetFields()
}
function clearValidate() {
  formRef.value.clearValidate()
}

// 监听当前表单数据变化,更新父组件的值
watch(
  () => dataForm,
  () => {
    emits('update:modelValue', Object.assign(props.modelValue, dataForm.value))
  },
  {
    deep: true,
  },
)

// 监听父组件的值变化,更新当前表单数据
watch(
  () => props.modelValue,
  newval => {
    dataForm.value = Object.assign(dataForm.value, newval)
  },
  {
    deep: true,
  },
)

onMounted(() => {
  initForm()
})

defineExpose({
  validate,
  resetFields,
  clearValidate,
})
</script>

<style scoped>
.h-form-container {
}
</style>
阅读 442
avatarAI BotBETA

回答

在 Vue.js 中,当你直接修改响应式对象的属性时,虽然数据已经更新,但 DOM 的重新渲染是异步进行的。Vue 使用一个异步队列来批量处理 DOM 更新以提高性能。这就是为什么你在 onChange 回调中直接修改 dataForm.select2Value 后,页面没有立即重新渲染的原因。

nextTick 是 Vue 提供的一个方法,它允许你在 DOM 更新循环之后执行代码。当你使用 nextTick 包裹修改其他表单项的代码时,你实际上是在等待当前的 DOM 更新循环结束后再执行修改,这时修改后的数据会触发新的 DOM 更新。

解决方案

  1. 使用 watch 监听表单项的变化

    在你的封装表单组件中,你可以使用 watch 来监听 dataForm 中每个表单项的变化,并在变化时执行相应的逻辑。这样,你不需要在 onChange 中使用 nextTick

    修改封装表单组件中的 watch 部分,为每个表单项添加单独的监听器:

    <script setup lang="ts" name="HForm">
    // ... 其他代码
    
    props.formItems.forEach(item => {
      watch(
        () => dataForm.value[item.name],
        (newVal, oldVal) => {
          if (newVal !== oldVal && item.props?.onChange) {
            item.props.onChange(newVal, oldVal);
    
            // 可以在这里直接修改其他表单项,Vue 会自动处理 DOM 更新
            // 例如,如果需要根据 select1Value 清空 select2Value:
            if (item.name === 'select1Value') {
              dataForm.value.select2Value = '';
            }
          }
        },
        { immediate: true, deep: true }
      );
    });
    
    // ... 其他代码
    </script>

注意:这种方法要求你在 formItems 中为每个需要特殊处理的表单项定义 onChange 回调,并在回调中处理其他表单项的更新。

  1. 使用计算属性或组合式 API

    另一个解决方案是使用计算属性或 Vue 3 的组合式 API 来管理表单状态,这样你可以在状态变化时自动执行逻辑,而不需要依赖 nextTick

    这种方法通常涉及将表单状态逻辑抽象到可重用的组合式函数或计算属性中,并在这些函数或属性中处理依赖关系。

选择哪种方法取决于你的具体需求和代码结构。通常,使用 watch 监听表单项的变化是一种简单且有效的方法,可以确保在数据变化时执行必要的逻辑。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏