如何避免 vue 子组件中的数百个事件?

How to avoid hundreds of events in vue child components?

我有一个像

这样的对象
class Engine {
    id = 0;
    crankRPM: = 200;
    maxRPM = 2400;
    numCylinders = 8;
    maxOilTemp = 125;
    isRunning = false;

    start() { ... }
    stop() { ... }
}

现在我想要一个引擎组件。

Vue 不希望我在任何子组件中改变 属性 的状态,所以我现在必须这样做:

engine-ui 组件定义:

<template>
    <card> // Imagine fancy styling here
        {{ engine.id }}
        <input :value="engine.crankRPM" @input="$emit('changeCrankRPM', $event.target.value)"></input>
        <input :value="engine.maxRPM" @input="$emit('changeMaxRPM', $event.target.value)"></input>
        <input :value="engine.numCylinders" @input="$emit('changeMumCylinders', $event.target.value)"></input>
        <input :value="engine.maxOilTemp" @input="$emit('changeMaxOilTemp', $event.target.value)"></input>
        <toggle type="toggle" :value="engine.isRunning" @input="$emit('changeIsRunning', $event.target.value)  </toggle>
    </card>
</template>
<script lang="ts">
import { Engine } from "src/code/Engine";
import { defineComponent } from "vue";

export default defineComponent({
    name: "engine-ui",
    props: {
        engine: {
            type: Engine,
            required: true,
        },
    },
});
</script>

在父组件中的用法:

<template>
    ...
    <engine-ui :engine="myEngine" 
        @changeCrankRPM="myEngine.crankRPM = $event.target.value"
        @changeMAxRPM="myEngine.maxRPM = $event.target.value"
        @changeNumCylinders="myEngine.numCylinders = $event.target.value"
        @changeOilTemp="myEngine.maxOilTemp = $event.target.value"
        @changeIsRunning="myEngine.isRunning = $event.target.value"/>
    ...
</template>

这非常冗长和笨拙。如果我在 Engine class 中还有另外 100 个字段,它将在使用 engine-ui 的每个父组件中变得不可读,因为它将是一堵巨大的文本墙。

设计这个的“正确方法”是什么?

如果我现在更改引擎 class 我必须更新引擎-ui 组件以及任何父组件中的每个事件,因为发出的是字符串。

为了使代码易于维护,可以将模板逻辑减少到最少。

惯用的方法是将engine视为单个值并实现two-way绑定

对于 v-model,它将是:

<engine-ui v-model="myEngine"/>

...
<div v-for="(_, field) in modelValue">
  <input v-model="modelValue[field]"/>
...
props: {
    modelValue: Engine
},

改变 prop 被认为不是一个好的做法,因为这会使数据流更加复杂。这可以通过在每次字段更改时从 child 发出克隆的 Engine 实例来克服,这可能会造成浪费。另一种方法是在 parent 中保持更新与现在相同:

...
<engine-ui :value="myEngine" @update="onEngineUpdate" />
...
methods: {
    onEngineUpdate({ field, fieldValue }) {
         this.myEngine[field] = fieldValue;
    },
},
...

...
<div v-for="(fieldValue, field) in value">
  <input :value="fieldValue" @input="$emit('update', { field, fieldValue : $event.target.value })" />
...
props: {
    value: Engine
},
...

这是一个简单的示例,您只需列出字段类型即可。一切都通过 update 函数。但是你需要在那里小心并将值转换为正确的类型,因为原生 @input@change 事件总是 return 值作为字符串。

但是,如您所见,这消除了为每个 prop 提供更新函数的冗长。

如果需要,它可以轻松扩展为自定义输入类型的自定义组件。

const { defineComponent, createApp, toRefs, reactive } = Vue;

class Engine {
  id = 0;
  crankRPM = 200;
  description = 'Some description';
  maxRPM = 2400;
  numCylinders = 8;
  maxOilTemp = 125;
  isRunning = false;
}

const app = createApp({
  setup() {
    const state = reactive({
      engine: new Engine(),
      engineFields: [
        { key: 'crankRPM', type: 'number' },
        { key: 'maxRPM', type: 'number' },
        { key: 'description', type: 'text' },
        { key: 'numCylinders', type: 'number' },
        { key: 'maxOilTemp', type: 'number' },
        { key: 'isRunning', type: 'boolean' }
      ]
    })
    const update = ({ value, field }) => {
      state.engine[field.key] = field.type === 'number' ? +value : value;
    };
    return {
      ...toRefs(state),
      update
    }
  }
}).mount('#app')
<script src="https://unpkg.com/vue@next/dist/vue.global.prod.js"></script>
<div id="app">
    <div class="card">
        {{ engine.id }}
        <template v-for="field in engineFields" :key="field.key">
          <label v-if="field.type === 'boolean'">
            <input type="checkbox"
                   :checked="engine[field.key]"
                   @change="update({ value: $event.target.checked, field })">
            {{ field.key }}
          </label>
          <input v-else
                 :type="field.type"
                 :value="engine[field.key]"
                 @input="update({ value: $event.target.value, field })">
          
          <!-- you can have as many input types as you want,
               just replace v-else above with v-else-if and add
               another case (e.g: textarea, custom components, ...) -->
        </template>
    </div>
    <pre v-text="engine" />
</div>