问题

封装一个自定义组件,实现的这种嵌套子组件的写法。

<tab-container v-model="activeTab"
    @tab-click="tabClick"
    @tab-remove="tabRemove">
    <tab-panel label="Tab-1">
        // Tab-1
    </tab-panel >
    <tab-panel label="Tab-2">
        // Tab-2
    </tab-panel >
</tab-container>

数据更新是通过$children直接读取子组件数据进行更新

//TabContainer.vue
//template...
<div class="tab-items">
    <div class="tab-item" ref="tabBar"
        :class="{ active : activeTab === item.value}"
        v-for="( item, index ) in tabs" 
        :key="item.value"
        @click="clickHandler(item, $event)">
        <div class="info">
            <span class="info-label">{{item.label}}</span>
            <span v-if="item.simpleTab === undefined"
                class="info-id">
                # {{item.value}}
            </span>
        </div>
        <div v-if="item.closable !== undefined && tabs.length > 1" class="close"
            @click.stop="removeHandler(item, index)">
            <i class="el-icon-close"></i>
        </div>
    </div>
</div>
//...
//js...
mounted (){
    this.children = this.$children
},
updated(){
    this.children = this.$children
},
computed: {
    tabs (){
      return this.children.map(item => ({ label: item.label, value: item.value }))
    },
    ...
}
...

但这里如果换成将tab-item进行for循环的写法,
会出现更新异常的情况 (距离我写此文的时间太久远,忘了具体是啥报错...)

<tab-container v-model="activeTab"
    @tab-click="tabClick"
    @tab-remove="tabRemove">
    <tab-panel v-for="item in tabs"
        :key="item.id"
        :label="item.label" 
        :value="item.value">
        ...
    </tab-panel>
</tab-container>

解决

经过一番谷歌并没找着什么想要的,但最后在element-ui源码中找到了类似的实现方式

结论:只需要将$children替换成$slots来实现同样效果即可。

**原因是$children并不能够访问足够多的有效数据来跟踪组件更新,
导致v-for循环操作时会产生子组件乱序。但使用$slots能有效解决这个问题**

在Github Vue仓库的issues #6702中有相同问题的讨论

最后贴一下改动后的代码

//TabContainer.vue
<template>
    <div class="tabs-container">
        <div class="tabs">
            <div class="tab-items">
                <div class="tab-item" ref="tabBar"
                    :class="{ active : activeTab === item.value}"
                    v-for="( item, index ) in tabs" 
                    :key="item.value"
                    @click="clickHandler(item, $event)">
                    <div class="info">
                        <span class="info-label">{{item.label}}</span>
                        <span v-if="item.simpleTab === undefined"
                            class="info-id">
                            # {{item.value}}
                        </span>
                    </div>
                    <div v-if="item.closable !== undefined && tabs.length > 1" class="close"
                        @click.stop="removeHandler(item, index)">
                        <i class="el-icon-close"></i>
                    </div>
                </div>
            </div>
            <div class="tab-scroll tab-scroll-prev"
                v-if="scrollable">
                <i class="el-icon-arrow-left"></i>
            </div>
            <div class="tab-scroll tab-scroll-next"
                v-if="scrollable">
                <i class="el-icon-arrow-right"></i>
            </div>
        </div>
        <div class="content">
            <slot />
        </div>
    </div>
</template>
<script>
export default {
    name: 'TabContainer',
    props:{
        value: Number
    },
    data(){
        return{
            panes: [],
            scrollable: false
        }
    },
    computed:{
        tabs (){
            return this.panes.map(item => ({ 
                label: item.label, 
                value: item.value,
                closable: item.closable,
                simpleTab: item.simpleTab
            }))
        },
        activeTab: {
            get(){
                return this.value
            },
            set(val){
                return this.$emit('input', val)
            }
        }
    },
    methods:{
        // 修改部分
        calcPaneInstances(){
            let _this = this
            var isForceUpdate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;

            if(this.$slots.default){
                let paneSlots = this.$slots.default.filter(vnode => {
                    return vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'TabPanel'
                })
                let panes = paneSlots.map(ref => {
                    return ref.componentOptions.propsData
                })
                var panesChanged = !(panes.length === this.panes.length && panes.every(function (pane, index) {
                    return pane === _this.panes[index];
                }));
                if (isForceUpdate || panesChanged) {
                    this.panes = panes;
                }
            }else if(this.panes.length !== 0){
                this.panes = []
            }
        },
        clickHandler(tab, e){
            this.activeTab = tab.value
            this.$emit('tab-click', tab, e)
        },
        removeHandler(tab, index){
            let currentTabFlg = this.panes[index].value === tab.value 
            this.panes.splice( this.panes.findIndex( item => item.value === tab.value ), 1)

            if(currentTabFlg){
                index = index > 0 ? index - 1 : 0
                this.activeTab = this.panes[index].value
            }

            this.$emit('tab-remove', tab.value, index)
        }
    },
    created(){
        this.calcPaneInstances.bind(null, true)
    },
    mounted(){
        this.calcPaneInstances()
    },
    updated(){
        this.calcPaneInstances()
    },
}
</script>
//TabPanel.vue
<template>
    <div class="tab-panel"
        v-if="$parent.activeTab === value">
        <slot />
    </div>
</template>
<script>
export default {
    name: 'TabPanel',
    props:{
        label: String,
        value: Number,
        closable: Boolean,
        simpleTab: Boolean
    }
}
</script>

添加新评论