grit

grit

妙用computed拦截v-model,面试官都夸我细

一方向データフローの保持#

皆さんご存知の通り、Vue は一方向データフローです。子コンポーネントは親コンポーネントから渡された props を直接変更することはできませんが、コンポーネントをラップして v-model を使用する際に、不注意で一方向データフローのルールを破ってしまうことがあります。例えば、以下のように:

<!-- 親コンポーネント -->
<my-component v-model="msg"></my-component>


<!-- 子コンポーネント -->
<template>
  <div>
    <el-input v-model="msg"></el-input>
  </div>
</template>

<script setup>
defineOptions({
  name: "my-component",
});

const props = defineProps({
  msg: {
    type: String,
    default: "",
  },
});

</script>

v-model の実装原理#

子コンポーネントで props の値を直接変更すると、一方向データフローが破られます。では、どうすればよいのでしょうか。まずは v-model の実装原理を見てみましょう:

<!-- 親コンポーネント -->
<template>
  <my-component v-model="msg"></my-component>
  <!-- 等価 -->
  <my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component>
</template>

emit で親コンポーネントに prop 値の変更を通知#

したがって、emit を通じて、子コンポーネントの値が変わったときに直接 props を変更するのではなく、親コンポーネントにその値を変更するよう通知することができます!

子コンポーネントの値が変更されると、親コンポーネントの updateイベントがトリガーされ、新しい値が渡されます。親コンポーネントは msg を新しい値に更新します。コードは以下の通りです:

<!-- 親コンポーネント -->
<template>
  <my-component v-model="msg"></my-component>
  <!-- 等価 -->
  <my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
</script>

<!-- 子コンポーネント -->
<template>
  <el-input :modelValue="modelValue" @update:modelValue="handleValueChange"></el-input>
</template>
<script setup>
const props = defineProps({
  modelValue: {
    type: String,
    default: '',
  }
});

const emit = defineEmits(['update:modelValue']);

const handleValueChange = (value) => {
    // 子コンポーネントの値が変更され、親コンポーネントのupdate:modelValueイベントがトリガーされ、新しい値が渡されます。親コンポーネントはmsgを新しい値に更新します。
    emit('update:modelValue', value)
}
</script>

これがほとんどの開発者がコンポーネントの値を変更するために使用する方法ですが、実はもう一つの方法があります。それは計算プロパティの get、set を利用することです。

computed で prop をインターセプト#

ほとんどの人は計算プロパティを使用する際に get を使いますが、一部の人は計算プロパティに set があることすら知らないかもしれません。では、実装方法を見てみましょう:

<!-- 親コンポーネント -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref } from "vue";

const msg = ref('hello')
</script>

<template>
  <div>
    <my-component v-model="msg"></my-component>
  </div>
</template>


<!-- 子コンポーネント -->
<template>
  <el-input v-model="msg"></el-input>
</template>
<script setup>
import { computed } from "vue";

const props = defineProps({
  modelValue: {
    type: String,
    default: "",
  },
});

const emit = defineEmits(["update:modelValue"]);

const msg = computed({
  // getter
  get() {
    return props.modelValue
  },
  // setter
  set(newValue) {
    emit('update:modelValue', newValue)
  },
});
</script>

v-model がオブジェクトをバインドする場合#

では、v-model がオブジェクトをバインドする場合はどうでしょうか?

以下のように、computed で複数の値をインターセプトすることができます。

<!-- 親コンポーネント -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref } from "vue";

const form = ref({
  name:'張三',
  age:18,
  sex:'man'
})
</script>

<template>
  <div>
    <my-component v-model="form"></my-component>
  </div>
</template>


<!-- 子コンポーネント -->
<template>
  <div>
    <el-input v-model="name"></el-input>
    <el-input v-model="age"></el-input>
    <el-input v-model="sex"></el-input>
  </div>
</template>
<script setup>
import { computed } from "vue";

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  },
});

const emit = defineEmits(["update:modelValue"]);

const name = computed({
  // getter
  get() {
    return props.modelValue.name;
  },
  // setter
  set(newValue) {
    emit("update:modelValue", {
      ...props.modelValue,
      name: newValue,
    });
  },
});

const age = computed({
  get() {
    return props.modelValue.age;
  },
  set(newValue) {
    emit("update:modelValue", {
      ...props.modelValue,
      age: newValue,
    });
  },
});

const sex = computed({
  get() {
    return props.modelValue.sex;
  },
  set(newValue) {
    emit("update:modelValue", {
      ...props.modelValue,
      sex: newValue,
    });
  },
});
</script>

このようにして、私たちのニーズを実現することができますが、v-model オブジェクトのプロパティ値を一つ一つ手動でインターセプトするのは非常に面倒です。もし 10 個の入力があれば、10 回インターセプトする必要がありますので、インターセプトを統合する必要があります!

オブジェクト全体を監視#

<!-- 親コンポーネント -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref } from "vue";

const form = ref({
  name:'張三',
  age:18,
  sex:'man'
})
</script>

<template>
  <div>
    <my-component v-model="form"></my-component>
  </div>
</template>


<!-- 子コンポーネント -->
<template>
  <div>
    <el-input v-model="form.name"></el-input>
    <el-input v-model="form.age"></el-input>
    <el-input v-model="form.sex"></el-input>
  </div>
</template>
<script setup>
import { computed } from "vue";

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  },
});

const emit = defineEmits(["update:modelValue"]);

const form = computed({
  get() {
    return props.modelValue;
  },
  set(newValue) {
    alert(123)
    emit("update:modelValue", newValue);
  },
});
</script>

これで完璧に見えますが、set 内で alert (123) を実行しても、実行されません!!

原因は、form.xxx = xxx の時には computed の set はトリガーされず、form = xxx の時にのみ set がトリガーされるからです。

Proxy 代理オブジェクト#

では、form の属性が変更されたときにも emit ("update", newValue); を実行できる方法を考える必要があります。この問題を解決するために、Proxy を使用します。

<!-- 親コンポーネント -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref, watch } from "vue";

const form = ref({
  name: "張三",
  age: 18,
  sex: "man",
});

watch(form, (newValue) => {
  console.log(newValue);
});
</script>

<template>
  <div>
    <my-component v-model="form"></my-component>
  </div>
</template>


<!-- 子コンポーネント -->
<template>
  <div>
    <el-input v-model="form.name"></el-input>
    <el-input v-model="form.age"></el-input>
    <el-input v-model="form.sex"></el-input>
  </div>
</template>
<script setup>
import { computed } from "vue";

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  },
});

const emit = defineEmits(["update:modelValue"]);

const form = computed({
  get() {
    return new Proxy(props.modelValue, {
      get(target, key) {
        return Reflect.get(target, key);
      },
      set(target, key, value,receiver) {
        emit("update:modelValue", {
          ...target,
          [key]: value,
        });
        return true;
      },
    });
  },
  set(newValue) {
    emit("update:modelValue", newValue);
  },
});
</script>

これにより、Proxy + computed を使用して v-model のオブジェクトを完璧にインターセプトしました!

その後、使用を簡単にするために、これをフックとしてラップします。

// useVModel.js
import { computed } from "vue";

export default function useVModle(props, propName, emit) {
    return computed({
        get() {
            return new Proxy(props[propName], {
                get(target, key) {
                    return Reflect.get(target, key)
                },
                set(target, key, newValue) {
                    emit('update:' + propName, {
                        ...target,
                        [key]: newValue
                    })
                    return true
                }
            })
        },
        set(value) {
            emit('update:' + propName, value)
        }
    })
}

<!-- 子コンポーネントの使用 -->
<template>
  <div>
    <el-input v-model="form.name"></el-input>
    <el-input v-model="form.age"></el-input>
    <el-input v-model="form.sex"></el-input>
  </div>
</template>
<script setup>
import useVModel from "../hooks/useVModel";

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  },
});

const emit = defineEmits(["update:modelValue"]);

const form = useVModel(props, "modelValue", emit);

</script>

原文アドレス: https://juejin.cn/post/7277089907974422588

この記事は Mix Space によって xLog に同期更新されました。原始リンクは https://liu-wb.com/posts/default/computed-intercepts-v-model

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。