Vue 反应性错误

Vue reactivity bug

先声明前端不是我的强项..

我有一个下拉组件,我想要的只是当我按下重置按钮时,它会恢复到占位符文本,而不是保留所选选项。 我不知道为什么这不起作用。我已经阅读了两天的文档并尝试了很多方法都无济于事。对我来说,它显然是反应性的,因为它在其他部分也是反应性的,但是当我将它设置为 null 时,没有反应性触发。

Dropdown.vue(主要来自 here,稍作修改)

<template>
    <div class="btn-group">

        <li @click="toggleMenu()" class="dropdown-toggle dropdown-toggle-placeholder" v-if="isPlaceholder()">
            {{placeholderText}}
            <span class="caret"></span>
        </li>
        <li @click="toggleMenu()" class="dropdown-toggle" v-else>
            {{ selectedOption }}
            <span class="caret"></span>
        </li>

        <ul class="dropdown-menu" v-if="showMenu">
            <li v-for="(option, idx) in options" :key="idx">
                <a href="javascript:void(0)" @click="updateOption(option)" draggable="false">
                    {{ option }}
                </a>
            </li>
        </ul>
    </div>
</template>

<script>
    export default {
        props: {
            options: {
                type: Array
            },
            selected: {
                type: String
            },
            placeholder: {
                type: String
            },
            closeOnOutsideClick: {
              type: Boolean,
              default: true,
            },
        },

        data() {
            return {
                selectedOption: '',
                showMenu: false,
                placeholderText: 'Please select an item',
            }
        },

        mounted() {
            this.selectedOption = this.selected;
            if (this.placeholder)
            {
                this.placeholderText = this.placeholder;
            }

            if (this.closeOnOutsideClick) {
              document.addEventListener('click', this.clickHandler);
            }
        },

        beforeUnmount() {
            document.removeEventListener('click', this.clickHandler);
        },

        methods: {
            updateOption(option) {
                this.selectedOption = option;
                this.showMenu = false;
                this.$emit('update-option', this.selectedOption);
            },

            clearOption() {
                this.selectedOption = null;
                this.showMenu = false;
                console.log("reset:", this.selectedOption);
            },

            toggleMenu() {
                this.showMenu = !this.showMenu;
            },

            clickHandler(event) {
                const { target } = event;
                const { $el } = this;

                if (!$el.contains(target)) {
                  this.showMenu = false;
                }
            },

            isPlaceholder() {
                return (this.selectedOption === undefined || this.selectedOption === null || this.selectedOption === '');
            }
        },


    }
</script>

<style>

    .btn-group {
        min-width: 160px;
        height: 40px;
        position: relative;
        margin: 10px 1px;
        display: inline-block;
        vertical-align: middle;
    }

        .btn-group a:hover {
            text-decoration: none;
        }

    .dropdown-toggle {
        color: #636b6f;
        min-width: 160px;
        padding: 10px 20px 10px 10px;
        text-transform: none;
        font-weight: 300;
        margin-bottom: 7px;
        border: 0;
        background-image: linear-gradient(#009688, #009688), linear-gradient(#D2D2D2, #D2D2D2);
        background-size: 0 2px, 100% 1px;
        background-repeat: no-repeat;
        background-position: center bottom, center calc(100% - 1px);
        background-color: transparent;
        transition: background 0s ease-out;
        float: none;
        box-shadow: none;
        border-radius: 0;
        white-space: nowrap;
        text-overflow: ellipsis;
        overflow: hidden;
        user-select: none;
    }

        .dropdown-toggle:hover {
            background: #e1e1e1;
            cursor: pointer;
        }

    .dropdown-menu {
        display: inherit !important;
        position: absolute;
        top: 100%;
        left: 0;
        z-index: 1000;
        float: left;
        min-width: 160px;
        padding: 5px 0;
        margin: 2px 0 0;
        list-style: none;
        font-size: 14px;
        text-align: left;
        background-color: #fff;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
        background-clip: padding-box;
    }

        .dropdown-menu > li > a {
            padding: 10px 30px;
            display: block;
            clear: both;
            font-weight: normal;
            line-height: 1.6;
            color: #333333;
            white-space: nowrap;
            text-decoration: none;
            user-select: none;
        }

            .dropdown-menu > li > a:hover {
                background: #efefef;
                color: #409FCB;
            }

        .dropdown-menu > li {
            overflow: hidden;
            width: 100%;
            position: relative;
            margin: 0;
        }

    .caret {
        width: 0;
        position: absolute;
        top: 19px;
        height: 0;
        margin-left: -24px;
        vertical-align: middle;
        border-top: 4px dashed;
        border-top: 4px solid ;
        border-right: 4px solid transparent;
        border-left: 4px solid transparent;
        right: 10px;
    }

    li {
        list-style: none;
    }

        li.dropdown-toggle::after {
            display: none;
        }
</style>

App.vue

<template>
    <form @reset.prevent="onReset">
        <dropdown v-if="foo.bar" class="my-dropdown"
                  :options="myOptions"
                  :selected="foo.bar.name"
                  @update-option="onSelected"
                  :placeholder="'Select a MyOption'">
        </dropdown>

        <button type="reset">Reset</button>
    </form>
</template>

<script>
    import dropdown from './components/Dropdown.vue'

    export default {
        name: 'App',
        components: {
            dropdown,
        },
        data() {
            return {
                foo: {},
                myOptions: [],
            };
        },
        methods: {
            onReset() {
                console.log("Resetting...");
                this.foo = {};
                this.foo['bar'] = {};
                this.foo['bar']['name'] = '';

                dropdown.methods.clearOption();
            },
            onSelected(selected) {
                this.foo.bar.name = selected;
                console.log(selected);
            }
        },
        created() {
            this.onReset();
        },
        mounted() {
            this.myOptions = ['A', 'B'];
        }
    }
</script>

<style scoped>
    #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
    }

    .my-dropdown {
        border-radius: 5px;

        ::v-deep(.dropdown-toggle)
        {
            color: tomato;
            font-size: 25px;
            font-weight: 800;
        }

        ::v-deep(.dropdown-toggle-placeholder) {
            color: #c4c4c4;
        }

    }
</style>

当我按下重置按钮并调用 dropdown.methods.clearOption() 时,下拉菜单将 this.selectedOption 设置为 null(我也尝试过空字符串等)。我所有的 console.logs 看起来都很好,看起来好像有效,但 v-if="isPlaceholder()"v-else 就是不起作用。所选选项保留在保管箱中,而不是恢复为占位符文本。

我做错了什么????

根据您自己的代码假设 Vue 的反应性中存在错误,甚至没有对其进行测试...您真是太大胆了。我没有深入研究,因为我发现这真的很讨厌错误在你自己的代码中。您直接在 App 的实例中调用 dropdown.methods.clearOption(); 函数。对于我来说,Vue 可以很好地解释它,但 JS 并不像您认为的那样工作。首先,使用类似的方法,您一路上丢失了 this context

要在 Vue 中解决这个问题,您需要获取对您尝试调用的组件的引用。要在 Vue 中创建引用,需要在 App 组件的模板中添加一个 ref 属性:

<dropdown
  ref="myDropDown"
  v-if="foo.bar"
  class="my-dropdown"
  :options="myOptions"
  :selected="foo.bar.name"
  @update-option="onSelected"
  :placeholder="'Select a MyOption'">
</dropdown>

然后可以通过myDropDown引用到达组件实例,例如:

onReset() {
  // the `?.` is an optional chaining operator
  // can be easily replaced with an if statement
  this.$refs.myDropDown?.clearOption();
}

此外,正如我注意到的,您在模板中使用了 v-if 语句,此时它可能会以缺少 $refs.myDropDown 引用而告终。要解决此问题,您可以通过 Vue.nextTick.

延迟调用该方法
// defers clearing the dropdown options
Vue.nextTick(() => this.$refs.myDropDown?.clearOption())

无论如何,编写这样的组件 - 与 Vue 中必须提供的反应性相反。您可以随时观察 props 的变化并使组件相应地运行,而无需获取任何引用。