Массив компонентов Vue.js неправильно отображается при замене более коротким массивом

Я знакомлюсь с Vue.js (версия 2.5.16), но столкнулся с проблемой понимания того, что выглядит как проблема с реактивностью:

У меня есть два списка компонентов с циклами v-for, которые большую часть времени отображаются корректно — я могу взаимодействовать с компонентами и добавлять новые компоненты. Компоненты взаимодействуют с REST API, представляющим «зоны», и чья функция, как я надеюсь, довольно очевидна — в основном она возвращает JSON-представления либо одной, либо всех «зон», например:

{
    "state": "off",
    "pin": 24,
    "uri": "/zones/6",
    "name": "extra"
}

Всякий раз, когда зона добавляется или удаляется, приложение перезагружает весь список зон. Однако по какой-то причине, когда я удаляю зону и запускаю перезагрузку зон, список отображается с неправильными данными! Независимо от того, какую зону я удаляю, последний элемент списка кажется «выскочившим», а не ожидаемой зоной, которая не отображается в новом списке! Когда я проверяю данные app.zones, в данных Javascript все выглядит правильно, но они просто не отображаются прямо в браузере.

Вот соответствующий код:

...
<div class="tab-content">
    <div id="control" class="tab-pane fade in active">
        <ul>
            <zone-control
                v-for="zone in zones"
                v-bind:init-zone="zone"
                v-bind:key="zone.id">
            </zone-control>
        </ul>
    </div>
    <div id="setup" class="tab-pane fade">
        <table class="table table-hover">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Pin</th>
                    <th></th>
                </tr>
            </thead>
            <tbody>
                <tr is="zone-setup"
                    v-for="zone in zones"
                    v-bind:init-zone="zone"
                    v-bind:key="zone.id">
                </tr>
                <tr is="zone-add"></tr>
            </tbody>
        </table>
    </div>
</div>
...
<script>
    Vue.component('zone-control', {
        props: ['initZone'],
        template:
            `<li>
                <div class="btn btn-default" v-on:click="toggleState">
                    <span v-if="zone.state == 'on'" class="glyphicon glyphicon-ok-circle text-success"></span>
                    <span v-else class="glyphicon glyphicon-remove-sign text-danger"></span>
                    Zone: {{ zone.name }}
                </div>
            </li>`,
        data: function() {
            return {
                zone: this.initZone
            };
        },
        methods: {
            toggleState: function() {
                var state = (this.zone.state == 'on' ? 'off' : 'on');
                console.log('Turning zone ' + this.zone.name + ' ' + state);
                var comp = this
                fetch(
                    this.zone.uri,
                    {method: 'PUT',
                    headers: new Headers({
                        'Content-Type': 'application/json'
                    }),
                    body: JSON.stringify({state: state})
                }).then( function(result) {
                    return result.json()
                }).then( function(data) {
                    comp.zone = data;
                });
            }
        }
    })

    Vue.component('zone-setup', {
        props: ['initZone'],
        template:
            `<tr>
                <td>{{ zone.name }}</td>
                <td>{{ zone.pin }}</td>
                <td><div v-on:click="deleteZone" class="btn btn-danger btn-small"></div></td>
            </tr>`,
        data: function() {
            return {
                zone: this.initZone
            };
        },
        methods: {
            deleteZone: function() {
                fetch(
                    this.zone.uri,
                    {method: 'DELETE'}
                ).then( function(result) {
                    app.load_zones();
                });
            }
        }
    })

    Vue.component('zone-add', {
        template:
            `<tr>
                <td><input v-model="zone.name" type="text" class="form-control"></input></td>
                <td><input v-model="zone.pin" type="text" class="form-control"></input></td>
                <td><div v-on:click="addZone" class="btn btn-success btn-small"></div></td>
            </tr>`,
        data: function() {
            return {
                zone: {
                    name: '',
                    pin: ''
                }
            };
        },
        methods: {
            addZone: function() {
                console.log('Adding zone ' + this.zone.name);
                var comp = this
                fetch(
                    "/zones",
                    {
                        method: 'POST',
                        headers: new Headers({
                            'Content-Type': 'application/json'
                        }),
                        body: JSON.stringify({
                            name: comp.zone.name,
                            pin: comp.zone.pin})
                    }
                ).then( function(result) {
                    app.load_zones()
                    comp.zone = {}
                });
            }
        }
    })

    var app = new Vue({
        el: '#app',
        data: {
            zones: []
        },
        methods: {
            load_zones: function() {
                fetch("zones")
                .then(function(result){
                    return result.json()
                }).then(function(data){
                    app.zones = data;
                });
            }
        },
        created: function() {
            this.load_zones();
        }
    })
</script>

Я прочитал несколько ссылок, и, похоже, есть некоторые пограничные случаи или другие «ошибки», но это, похоже, не попадает ни в одно из них. Например, другой вопрос, по-видимому, согласен с тем, что замена Весь массив — довольно надежный способ изменения массивов в рамках ограничений системы реактивности Vue.js.

Некоторые другие ссылки на документацию Vue.js, которые я проверил и которые имеют отношение к моей проблеме:

Реактивность в деталях

Визуализация списка

Общие ошибки для начинающих

Полный код в контексте см. в репозитории github здесь.

ОБНОВИТЬ

Согласно комментариям, я воспроизвел симптомы в упрощенном JSBin. Нажмите на любую кнопку, чтобы увидеть, что при удалении второго элемента вместо него удаляется последний!


person Justin Frahm    schedule 19.04.2018    source источник
comment
Вы пробовали: Vue.set(app, zones, data); вместо app.zones = data ?   -  person Randy Casburn    schedule 19.04.2018
comment
Сделал сейчас. Я изменил строку на Vue.set(app, 'zones', data);. Никаких изменений в поведении.   -  person Justin Frahm    schedule 19.04.2018
comment
вам нужно будет продублировать проблему в Plunker или JSBin или что-то в этом роде. Тогда мы можем взглянуть.   -  person Randy Casburn    schedule 19.04.2018
comment
Ok. Я попробую...   -  person Justin Frahm    schedule 19.04.2018


Ответы (1)


Хорошо, я понял: все дело в v-bind:key. Я понял, что привязываюсь к атрибуту, который не предоставляется API, из которого я извлекаю данные. Ссылайтесь на (реальный и) уникальный атрибут для каждого компонента, и проблема исчезнет.

Я заметил, что документация по отображению списка упоминает, что

В версии 2.2.0+ при использовании v-for с компонентом теперь требуется ключ.

Интересно, что JSBin, на который я ссылался выше, в частности, использует Vue.js версии 2.0.3, но проблема все еще существует и устраняется правильным использованием атрибута key в цикле v-for.

Если кто-то еще хочет это увидеть, посмотрите на ссылку JSBin в вопросе и добавьте v-bind:key="zone.id" к элементу <tr>. После этого проблема решается.

person Justin Frahm    schedule 19.04.2018