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 的對象!

然後,為了後面使用方便,我們直接將其封裝成 hook

// 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


載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。