【问题标题】:In Vue.js can a component detect when the slot content changes在 Vue.js 中,组件可以检测插槽内容何时发生变化
【发布时间】:2016-11-04 00:52:03
【问题描述】:

我们在 Vue 中有一个组件,它是一个框架,缩放到窗口大小,其中包含(在 <slot> 中)一个元素(通常是 <img><canvas>),它可以缩放以适应框架并启用平移和缩放该元素。

组件需要在元素发生变化时做出反应。我们可以看到的唯一方法是让父组件在发生这种情况时刺激组件,但是如果组件能够自动检测<slot> 元素何时发生变化并做出相应反应,那就更好了。有没有办法做到这一点?

【问题讨论】:

    标签: javascript html vue.js vue-component


    【解决方案1】:

    据我所知,Vue 没有提供这样做的方法。不过,这里有两种方法值得考虑。

    观察 Slot 的 DOM 的变化

    使用MutationObserver 来检测<slot> 中的DOM 何时发生变化。这不需要组件之间的通信。只需在组件的 mounted 回调期间设置观察者即可。

    这里有一个 sn-p 展示了这种方法的实际应用:

    Vue.component('container', {
      template: '#container',
      
      data: function() {
      	return { number: 0, observer: null }
      },
      
      mounted: function() {
        // Create the observer (and what to do on changes...)
        this.observer = new MutationObserver(function(mutations) {
          this.number++;
        }.bind(this));
        
        // Setup the observer
        this.observer.observe(
          $(this.$el).find('.content')[0],
          { attributes: true, childList: true, characterData: true, subtree: true }
        );
      },
      
      beforeDestroy: function() {
        // Clean up
        this.observer.disconnect();
      }
    });
    
    var app = new Vue({
      el: '#app',
    
      data: { number: 0 },
      
      mounted: function() {
        //Update the element in the slot every second
        setInterval(function(){ this.number++; }.bind(this), 1000);
      }
    });
    .content, .container {
      margin: 5px;
      border: 1px solid black;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
    
    <template id="container">
      <div class="container">
        I am the container, and I have detected {{ number }} updates.
        <div class="content"><slot></slot></div>
      </div>
    </template>
    
    <div id="app">
      
      <container>
        I am the content, and I have been updated {{ number }} times.
      </container>
      
    </div>

    使用发射

    如果 Vue 组件负责更改插槽,那么最好在更改发生时emit an event。这允许任何其他组件在需要时响应发出的事件。

    为此,使用一个空的 Vue 实例作为全局事件总线。任何组件都可以在事件总线上发出/监听事件。在您的情况下,父组件可以发出“更新内容”事件,子组件可以对其做出反应。

    这是一个简单的例子:

    // Use an empty Vue instance as an event bus
    var bus = new Vue()
    
    Vue.component('container', {
      template: '#container',
    
      data: function() {
        return { number: 0 }
      },
    
      methods: {
        increment: function() { this.number++; }
      },
      
      created: function() {
        // listen for the 'updated-content' event and react accordingly
        bus.$on('updated-content', this.increment);
      },
      
      beforeDestroy: function() {
        // Clean up
        bus.$off('updated-content', this.increment);
      }
    });
    
    var app = new Vue({
      el: '#app',
    
      data: { number: 0 },
    
      mounted: function() {
        //Update the element in the slot every second, 
        //  and emit an "updated-content" event
        setInterval(function(){ 
          this.number++;
          bus.$emit('updated-content');
        }.bind(this), 1000);
      }
    });
    .content, .container {
      margin: 5px;
      border: 1px solid black;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
    
    <template id="container">
      <div class="container">
        I am the container, and I have detected {{ number }} updates.
        <div class="content">
          <slot></slot>
        </div>
      </div>
    </template>
    
    <div id="app">
    
      <container>
        I am the content, and I have been updated {{ number }} times.
      </container>
      
    </div>

    【讨论】:

    • 第一个不是 DOMSubtreeModified deprecated?
    • 第二,类似地,在 Vue 2.0(我们计划在它稳定后过渡到)中也有一个计划弃用广播事件see here,尽管这并没有防止事件直接在组件上分派。
    • 好收获。我已经更新了使用MutationObservers$emit 的答案。
    • @pqvst 你是对的。旧答案是为 Vue 1.0 编写的,它使用 ready 而不是 mounted。我已编辑答案以改用mounted
    • VueJS 还没有提供将观察者绑定到$slots.default 的能力吗?这个复杂的观察者杂物还需要吗?
    【解决方案2】:

    据我了解 Vue 2+,当插槽内容发生变化时,应该重新渲染组件。在我的情况下,我有一个 error-message 组件应该隐藏,直到它有一些插槽内容要显示。起初,我将此方法附加到组件根元素的v-if 上(computed 属性不起作用,Vue 似乎对this.$slots 没有反应性)。

    checkForSlotContent() {
        let checkForContent = (hasContent, node) => {
            return hasContent || node.tag || (node.text && node.text.trim());
        }
        return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
    },
    

    当 99% 的更改发生在插槽中时,这很有效,包括任何添加或删除 DOM 元素。唯一的边缘情况是这样的用法:

    <error-message> {{someErrorStringVariable}} </error-message>
    

    这里只更新了一个文本节点,由于我仍然不清楚的原因,我的方法不会触发。我通过连接beforeUpdate()created() 解决了这个问题,留给我一个完整的解决方案:

    <script>
        export default {
            data() {
                return {
                    hasSlotContent: false,
                }
            },
            methods: {
                checkForSlotContent() {
                    let checkForContent = (hasContent, node) => {
                        return hasContent || node.tag || (node.text && node.text.trim());
                    }
                    return this.$slots.default && this.$slots.default.reduce(checkForContent, false);
                },
            },
            beforeUpdate() {
                this.hasSlotContent = this.checkForSlotContent();
            },
            created() {
                this.hasSlotContent = this.checkForSlotContent();
            }
        };
    </script>
    

    【讨论】:

    • 很好的解决方案,谢谢。您是将此作为添加到 VueJS 的功能,还是作为错误修复提出来的?
    【解决方案3】:

    还有另一种方法可以对插槽更改做出反应。老实说,我觉得它更干净,以防万一。在我看来,emit+event-bus 和突变观察都不正确。

    以下场景:

    <some-component>{{someVariable}}</some-component>
    

    在这种情况下,当 someVariable 发生变化时,一些组件应该做出反应。我在这里要做的是在组件上定义一个 :key,这会强制它在 someVariable 更改时重新呈现。

    <some-component :key="someVariable">Some text {{someVariable}}</some-component>
    

    亲切的问候 Rozbeh Chiryai Sharahi

    【讨论】:

    • 简单又好用,只要您还可以控制组件的使用位置和方式,以及是否可行。因为这不是常规使用模式,所以对于组件的使用者来说可能是意料之外的。我们有类似&lt;some-component&gt;&lt;router-view/&gt;&lt;/some-component&gt; 的东西,所以您使用的模式不起作用。
    • 我明白了。在您给定的情况下,&lt;router-view&gt; 将是像MyView.vue 这样的渲染组件。如果可能的话,应该更喜欢在MyView.vue 中发出一个事件并对父母做出反应。示例:&lt;some-component ref="someComponent"&gt;&lt;router-view @someEventYouThrowInside="$refs.someComponent.someMethod()" /&gt;&lt;/some-component&gt;。虽然,我完全理解,在一些复杂的情况下,比如你的情况,没有办法绕过像 EventBus 或 MutationObservers 这样的锤子,只是把这个放在这里,对于那些有更简单情况的人。亲切的问候
    【解决方案4】:

    我建议你考虑一下这个技巧:https://codesandbox.io/s/1yn7nn72rl,我过去常常观察变化并对插槽内容做任何事情。

    vuetifyVIcon 组件 的工作原理启发,这个想法是使用一个函数式组件,我们在其render 函数中实现逻辑。 context 对象作为render 函数的第二个参数传递。特别是,context 对象有一个data 属性(您可以在其中找到属性,attrs)和一个children 属性,对应于插槽(您可以使用事件调用context.slot() 函数结果相同)。

    最好的问候

    【讨论】:

    • 在我看来,您正在制作依赖于内容的内容 - 在这种情况下,如果插槽内容是“foo”或“bar”,则显示 1 或 2。我们需要的是稍有不同 - 对更改的图形元素、img 标签或 canvas 元素做出反应。当情况发生变化时,我们需要做一些事情,而不是做一些依赖于内容的事情。为此,您基本上需要在 html 上创建一个内容监视机制。对我们来说,我认为上述选项可能更简单。不过感谢您的想法并努力分享代码。
    猜你喜欢
    • 2021-02-27
    • 2021-01-15
    • 1970-01-01
    • 1970-01-01
    • 2017-11-22
    • 2020-12-27
    • 2016-12-28
    • 2017-11-16
    • 1970-01-01
    相关资源
    最近更新 更多