【问题标题】:How to add Vue lifecycle listener dynamically如何动态添加Vue生命周期监听器
【发布时间】:2018-08-02 13:41:21
【问题描述】:

我有一些函数需要在 vue 组件被销毁时调用,但在事件创建之前我不一定知道它们是什么。

有没有办法为 vue 生命周期事件动态添加监听器?

我想要达到的目标:

...methods: {
    logOnDestroy(txt) {
        this.$on('beforeDestroy', () => {
            console.log(txt)
        }
    }
}

但目前没有调用它。是否有其他方法可以在运行时以编程方式将侦听器绑定到组件生命周期事件?

【问题讨论】:

    标签: javascript vuejs2


    【解决方案1】:

    Eric99 的答案非常好,但它不适用于 beforeRouteLeavebeforeRouteUpdatebeforeRouteEnter 等组件内导航防护 - 它们由 VueRouter 维护(并在编译相应组件时收集) )。 Vue-Router 缓存了组件的构造函数并使用它而不是实际的组件实例,这让我们的生活变得更加困难。

    为了解决这个问题,我不得不深入研究 VueRouter 的内部结构,但我想出了以下代码(_Ctor 是缓存的构造函数 - 我没有检查是否只使用它就足够了,所以为了安全起见,我同时使用了这两个组件定义和构造函数):

    const routeComponent = this.$route.component;
    injectHook([
      routeComponent, 
      routeComponent._Ctor && routeComponent._Ctor[0] 
        ? routeComponent._Ctor[0].options 
        : null
    ], 'beforeRouteLeave', this.hookFunction);
    
    function injectHook(routeComponentInstance, hookName, hookFunction)
    {
      (Array.isArray(routeComponentInstance) 
        ? routeComponentInstance 
        : [routeComponentInstance]
      ).forEach(instance =>
      {
        if (instance)
        {
          const existing = instance[hookName];
          if (existing && Array.isArray(existing))
          {
            const index = existing.findIndex(item => item === hookFunction);
            if (index < 0) return;
          }
          instance[hookName] = existing
            ? Array.isArray(existing) ? existing.concat(hookFunction) : [existing, hookFunction]
            : [hookFunction];
        }
      });
    }
    
    function removeHook(routeComponentInstance, hookName, hookFunction)
    {
      (Array.isArray(routeComponentInstance) 
        ? routeComponentInstance 
        : [routeComponentInstance]
      ).forEach(instance =>
      {
        if (instance)
        {
          const existing = instance[hookName];
          if (existing && Array.isArray(existing))
          {
            const index = existing.findIndex(item => item === hookFunction);
            if (index !== -1) existing.splice(index, 1);
          }
        }
      });
    }
    

    【讨论】:

      【解决方案2】:

      你可能会问,能不能简单点?

      来自Vue.js Component Hooks as Events,这是您要查找的语法

      this.$once('hook:beforeDestroy', () => {
      

      我不确定您打算如何使其动态化,但这是 Vue CLI 的默认 HelloWorld 应用程序中您的 logOnDestroy() 方法的改编,

      演示

      Vue.component('helloworld', {
        template: '<h1>{{ msg }}</h1>',
        name: 'helloworld',
        props: { msg: String },
        mounted() {
          this.logOnDestroy('Goodbye HelloWorld')
        },
        methods: {
          logOnDestroy(txt) {
            this.$once('hook:beforeDestroy', () => {
              console.log(txt)
            })
          }    
        }
      });
      
      new Vue({
        el: '#app',
        data: {
          showHello: true
        },
        mounted() {
          setTimeout(() => {
            this.showHello = false
          }, 3000)
        }
      });
      Vue.config.productionTip = false
      Vue.config.devtools = false
      <script src="https://unpkg.com/vue"></script>
      <div id="app">
        <img alt="Vue logo" src="https://upload.wikimedia.org/wikipedia/commons/thumb/9/95/Vue.js_Logo_2.svg/277px-Vue.js_Logo_2.svg.png" style="height: 50px;">
        <helloworld v-if="showHello" msg="Welcome to Your Vue.js App"/>
      </div>

      【讨论】:

      • 嘿,感谢$once 的提示!我不知道它存在,为什么在 vuejs 文档中甚至没有提到它。
      • 如何在 vue3 中使用 this.$once('hook:beforeDestroy'?
      【解决方案3】:

      Eric99 的回答很有启发性,但我觉得有比摆弄 Vue 内部更容易实现相同目标的方法:在组件上使用事件。更易读,写更短。

      Vue.component('foo', {
        methods {
          logOnDestroy (txt) {
            this.$on('beforeDestroy', () => {
              console.log(txt)
            })
          }
        },
        beforeDestroy () {
          this.$emit('beforeDestroy');
        }
      });
      

      您可能会问:我是否需要在发出事件后调用this.$off() 以防止内存泄漏?答案是否定的,Vue 会自动为你做这个,所以上面的代码就可以了。

      【讨论】:

        【解决方案4】:

        2019 年 1 月 - 关于此问题答案的警告 - 我在使用此代码添加动态侦听器时发现了一个微妙的问题。

        this.$options.beforeDestroy.push(() => {
          console.log(txt)
        });
        

        当没有定义静态beforeDestroy 时,它可以正常工作。在这种情况下,处理程序数组是$options 的直接属性。

        但是如果你在组件上定义一个静态的beforeDestroy钩子,handlers数组是$options.__proto__的属性,这意味着组件的多个实例继承之前实例的动态处理程序(实际上,上面的代码修改了使用的模板创建连续的实例)。

        这是一个多大的实际问题,我不确定。它看起来很糟糕,因为当您浏览应用程序时,处理程序数组会变得更大(例如,每次切换页面都会添加一个新函数)。


        添加动态处理程序的一种更安全的方法是使用此 injectHook 代码,Vue 使用该代码进行热模块重新加载(您可以在正在运行的 Vue 应用程序的 index.js 中找到它)。注意,我使用的是 Vue CLI 3。

        function injectHook(options, name, hook) {
          var existing = options[name]
          options[name] = existing
            ? Array.isArray(existing) ? existing.concat(hook) : [existing, hook]
            : [hook]
        }
        ...
        injectHook(this.$options, 'beforeDestroy', myHandler)
        

        这里发生的是在实例上创建一个新数组,其中包含来自__proto__ 的所有处理程序以及新的处理程序。旧数组仍然存在(未修改),并且新数组与实例一起被销毁,因此__proto__ 处理程序数组中没有处理程序的构建。

        【讨论】:

          【解决方案5】:

          每个生命周期事件的处理程序数组存储在this.$options 对象中。您可以通过推送到相应的数组来添加处理程序(如果尚未设置处理程序,则需要先创建数组):

          new Vue({
            el: '#app',
            created() {
              if (!this.$options.mounted) {
                this.$options.mounted = [];
              }
            
              this.$options.mounted.push(() => {
                console.log('mounted')
              });
              
              this.$options.mounted.push(() => {
                console.log('also mounted')
              });
            }
          })
          <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
          
          <div id="app">
            <div></div>
          </div>

          所以在你的情况下:

          methods: {
            logOnDestroy(txt) {
              if (!this.$options.beforeDestroy) {
                this.$options.beforeDestroy = [];
              }
          
              this.$options.beforeDestroy.push(() => {
                console.log(txt)
              });
            }
          }
          

          【讨论】:

          • 这是我用例的完美解决方案!我真的很感激,这非常有帮助
          • Eric99 的回答是关于这个回答的一个重要警告。而且我自己的答案更简单,不依赖于 Vue 的内部结构,它可能会在没有通知的情况下发生变化。
          【解决方案6】:

          一个简单的解决方案是简单地跟踪您想要在组件中添加的所有动态处理程序:

          Vue.component('foo', {
            template: '<div>foo component</div>',
            data() {
              return {
                beforeDestroyHandlers: []
              }
            },
            created() {
              this.beforeDestroyHandlers.push(() => {
                console.log('new handler called');
              });
            },
            beforeDestroy() {
              this.beforeDestroyHandlers.forEach(handler => handler());
            }
          });
          
          new Vue({
            el: '#app',
            data: {
              includeComponent: false
            }
          });
          <script src="https://unpkg.com/vue"></script>
          
          <div id="app">
            <button v-on:click="includeComponent = true">Add component</button>
            <button v-on:click="includeComponent = false">Destroy component</button>
            <foo v-if="includeComponent"></foo>
          </div>

          【讨论】:

            猜你喜欢
            • 2015-02-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2022-12-21
            • 2012-01-27
            • 1970-01-01
            • 2013-04-09
            • 1970-01-01
            相关资源
            最近更新 更多