Vue 3 通过双向绑定将响应式对象传递给组件

Pet*_*len 14 vue.js vue-component vuejs3

我在使用组合 API 在 vue 3 中双向绑定反应式组件时遇到问题。

设置:

父调用代码是:

<template>
  <h1>{{ message.test }}</h1>
  <Message v-model="message" />
</template>

<script>
import Message from '@/components/Message.vue';
import { reactive } from 'vue';

export default {
  name: 'Home',
  components: { Message },

  setup() {
    const message = reactive({ test: '123' });

    return {
      message
    };
  }
};
</script>
Run Code Online (Sandbox Code Playgroud)

子组件代码为:

<template>
  <label>
    <input v-model="message" type="text" />
  </label>
</template>

<script>
import { computed } from 'vue';

export default {
  props: {
    messageObj: {
      type: Object,
      default: () => {},
    },
  },

  emits: ['update:messageObj'],

  setup(props, { emit }) {
    const message = computed({
      get: () => props.messageObj.test,
      set: (value) => emit('update:messageObj', value),
    });

    return {
      message,
    };
  },
};
</script>
Run Code Online (Sandbox Code Playgroud)

问题:

加载组件后,对象的默认值将显示在输入字段中。这是应该的,但是,当我更新输入框中的值时,父视图中的 H1 不会使用新的输入框值进行更新。

我已经通过 stackoverflow board 和 google 进行了搜索,但没有找到任何关于需要做什么才能使对象具有反应性的提示。

我通读了反应性文档,但仍然没有找到解决我的问题的任何方法。

为了进行测试,我已将消息更改为引用,并使用此单个引用值,数据保持反应状态,并且一切都按预期工作。

关于反应式对象不更新可能出现什么问题的任何指示?

Mat*_*att 13

这里

<div id="app">
    <h1>{{ message.test }}</h1>
    <child v-model="message"></child>
</div>
Run Code Online (Sandbox Code Playgroud)
const { createApp, reactive, computed } = Vue;


// -------------------------------------------------------------- child
const child = {
    template: `<input v-model="message.test" type="text" />`,
    
    props: {
        modelValue: {
            type: Object,
            default: () => ({}),
        },
    },

    emits: ['update:modelValue'],

    setup(props, { emit }) {
        const message = computed({
            get: () => props.modelValue,
            set: (val) => emit('update:modelValue', val),
        });

        return { message };
    }
};


// ------------------------------------------------------------- parent
createApp({
    components: { child },

    setup() {
        const message = reactive({ test: 'Karamazov' });

        return { message };
    }
}).mount('#app');
Run Code Online (Sandbox Code Playgroud)

  • 你好,Matt,这提供了一个解决方案,但它将我希望在调用 vue 时使用的一些逻辑移至组件中。我的设置的目的是,我最终会在调用 vue 时拥有逻辑,该逻辑被传递到添加到该页面的组件上,因此实际上是“哑”组件,仅从父级接收数据并发回所做的更新以组件的形式。经过进一步测试,我找到了解决方案。我将在我对问题的回答中发表我的观察。 (3认同)

Pet*_*len 7

解决方案和观察结果:

在调用组件的父视图中,如果您只需要传递对象中的值之一,则可以使用 v-model 并向该 v-model 添加参数。

<template>
  <h1>{{ message.test }}</h1>
  <!-- <h1>{{ message }}</h1> -->
  <Message v-model:test="message" />
</template>

<script>
import Message from '@/components/Message.vue';
import { reactive } from 'vue';

export default {
  name: 'Home',
  components: { Message },

  setup() {
    const message = reactive({ test: '123' });

    return {
      message
    };
  }
};
</script>
Run Code Online (Sandbox Code Playgroud)

然后,在接收组件中,您将在 props 中传递的对象参数注册为对象。

<template>
  <label>
    <input v-model="message.test" type="text" />
  </label>
</template>

<script>
import { computed } from 'vue';

export default {
  props: {
    test: {
      type: Object,
      default: () => {}
    },
  },

  emits: ['update:test'],

  setup(props, { emit }) {
    const message = computed({
      get: () => props.test,
      set: (value) => emit('update:test', value),
    });

    return {
      message,
    };
  },
};
</script>
Run Code Online (Sandbox Code Playgroud)

如果需要传递整个对象,则需要在组件中使用名称 modelValue 作为道具。

与之前的代码相比,父级发生了变化:

<template>
  <h1>{{ message.test }}</h1>
  <!-- <h1>{{ message }}</h1> -->
  <Message v-model="message" />
</template>
Run Code Online (Sandbox Code Playgroud)

组件代码:

<template>
  <label>
    <input v-model="message.test" type="text" />
  </label>
</template>

<script>
import { computed } from 'vue';

export default {
  props: {
    modelValue: {
      type: Object,
      default: () => {}
    },
  },

  emits: ['update:modelValue'],

  setup(props, { emit }) {
    const message = computed({
      get: () => props.modelValue,
      set: (value) => emit('update:modelValue', value),
    });

    return {
      message,
    };
  },
};
</script>
Run Code Online (Sandbox Code Playgroud)


gwi*_*llz 6

你最初的问题很简单。在 Vue 3 中v-model,默认有一个名为 的 prop modelValue,并且发出来自update:modelValue。这里的其他答案在他们的解决方案中假设了这一点,但没有直接解决它。

您可以重命名messageObjprop 以使用默认 prop,或者使用 Vue 3 中的多模型功能:

<Message v-model:messageObj="message" />
Run Code Online (Sandbox Code Playgroud)

然而我们的问题还更深层次。

所有(当前)答案都有效,但并不完全正确。它们都不符合惯用的“单向数据流”规则。

考虑这个 JSFiddle,修改自这个答案

const child = {
    template: `<input v-model="message.test" type="text" />`,
    setup(props, { emit }) {
        const message = computed({
            get: () => props.modelValue,
            // No set() ?
        });

        return { message };
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例中,子组件永远不会“发出” - 但父组件中的数据仍在更新。这违反了“单向”规则。数据必须仅使用发出而不是通过 prop 代理从子组件传播。

这里的问题是props.modelValue当到达子组件时它是反应性的。可以向isReactive()助手验证这一点。当它通过时,computed()它会保留这种反应性,并将继续通过自身将更新代理到父组件中。

一个办法:

JSF 在这里

const { createApp, ref, computed } = Vue;

const child = {
    template: `<input v-model="message" type="text" />`,
    
    props: {
        modelValue: {
            type: Object,
            default: () => ({}),
        },
    },

    emits: ['update:modelValue'],

    setup(props, { emit }) {
        const message = computed({
           get: () => props.modelValue.test,
           set: (test) => emit('update:modelValue', ({...props.modelValue, test })),
        });
        
        return { message };
    }
};

createApp({
    components: { child },

    setup() {
        const message = ref({ test: 'Karamazov' });

        return { message };
    }
}).mount('#app');
Run Code Online (Sandbox Code Playgroud)

解决方案分为三部分:

  1. 计算的 getter 不得从父组件返回代理对象。一旦发生这种情况,您就有违反“单向”规则的危险[注 1]。在这个例子中props.modelValue.test是一个字符串,所以我们是安全的。

  2. 计算设置器必须发出整个对象,但同样它不能是反应类型。因此,我们克隆modelValue使用传播并包含在更新的test字段中。Object.assign({}, props.modelValue, {test})这也可以通过[注2]来实现。

  3. message父组件中的变量不能areactive()并且必须是 a ref()。当v-model接收到新发出的对象时message,变量被破坏并且不再具有反应性[注3]。即使有引用,props.modelValue当它到达子组件时,它仍然会完全反应,因此克隆步骤仍然很重要。

或者:

我还应该提到的是,来自的值computed()并不具有深度反应性。如图所示,在计算对象上设置值不会触发计算设置器。

将整个对象传递到模板的替代解决方案:

setup(props, { emit }) {
    const message = reactive({...props.modelValue});
    watch(message, message => emit('update:modelValue', ({...message})));
    return { message };
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,每当字段更新时,整个message对象就会发出。.test例如<input v-model="message.test" />。这仍然遵循“单向”数据规则,因为发出是向父组件提供数据的唯一方式。

推理:

“单向”数据流很重要[4]。考虑一下:

<child :modelValue="message"></child>
Run Code Online (Sandbox Code Playgroud)

乍一看(也是明智的),这似乎将数据传递“child”中,但没有从“child”中传递出去。但是,如果子级未正确处理反应性对象,这会将更改发送到我自己的组件中。

观察这段代码,我并不期望出现这种行为,因此子组件正确执行非常重要。

笔记:

[1]:测试是否违反“单向”规则非常简单。删除任何emit内容,如果父级收到更新 - 你就破坏了它。或者替换v-modelv-bind也可以。

[2]:Object.assign(){...}传播确实不同。但应该不影响我们在这里的使用。

[3]:我还没有找到任何关于reactive()和的这种行为的明确文档v-model。如果有人愿意插话,那就太好了。

[4]:Vue 文档强调了单向绑定的重要性。Evan 本人(Vue 的创建者)甚至提供了有关如何使用对象的示例v-model(在 Vue 2 中,但原则仍然适用)。

我觉得稍后在同一个线程中值得注意的是,Evan 建议嵌套超过 1 层的对象被视为滥用v-model.