最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to add Vue lifecycle listener dynamically - Stack Overflow

programmeradmin1浏览0评论

I have certain functions that I need to be able to call when a vue component is destroyed, but I don't necessarily know what they are before the event is created.

Is there a way to dynamically add a listener to the vue lifecycle events?

What I'm trying to achieve:

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

But that is not currently being called. Is there a different way to programatically bind listeners to component lifecycle events at runtime?

I have certain functions that I need to be able to call when a vue component is destroyed, but I don't necessarily know what they are before the event is created.

Is there a way to dynamically add a listener to the vue lifecycle events?

What I'm trying to achieve:

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

But that is not currently being called. Is there a different way to programatically bind listeners to component lifecycle events at runtime?

Share Improve this question asked Feb 22, 2018 at 16:03 Rafael KennedyRafael Kennedy 1,0047 silver badges18 bronze badges
Add a comment  | 

6 Answers 6

Reset to default 10

You might ask, can it be simpler?

From Vue.js Component Hooks as Events, this is the syntax you are looking for

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

I'm not sure how exactly you intend to make it dynamic, but here is an adaption of your logOnDestroy() method in Vue CLI's default HelloWorld app,

Demo

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>

An array of handlers for each life-cycle event is stored in the this.$options object. You can add a handler by pushing to the corresponding array (you'll need to create the array first if there are no handlers set already):

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>

So in your case:

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

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

Jan 2019 - A warning about the answer to this question - I found a subtle problem when using this code to add dynamic listeners.

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

It works ok when there is no static beforeDestroy defined. In this case the handlers array is a direct property of $options.

But if you define a static beforeDestroy hook on the component, the handlers array is property of $options.__proto__, which means multiple instances of the component inherit dynamic handlers of previous instances (effectively, the above code modifies the template used to create successive instances).

How much of a practical problem this is, I'm not sure. It looks bad because the handler array gets bigger as you navigate the app (e.g switching pages adds a new function each time).


A safer way of adding dynamic handlers is to use this injectHook code, which is used by Vue for hot module reload (you can find it in index.js of a running Vue app). Note, I am using 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)

What happens here is a new array is created on the instance which contains all the handlers from __proto__, plus the new one. The old array still exists (unmodified), and the new array is destroyed along with the instance, so there is no build-up of handlers in __proto__ handler array.

One simple solution would be to simply track all dynamic handlers that you want to add inside the component:

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>

Eric99's answer is instructive, but I feel there is something easier than fiddling with Vue's internals to do to achieve the same goal: use an event on the component. Easier to read, shorter to write.

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

You might ask: do I need to call this.$off() after I emit my event to prevent memory leaks? The answer is no, Vue does this for you automatically, so the above code is fine.

The Eric99's answer is excellent but it does not work for in-component navigation guards like beforeRouteLeave, beforeRouteUpdate, beforeRouteEnter - they are maintained by VueRouter (and collected at the time of the compilation of the corresponding component). Our life is made even more difficult by the fact that Vue-Router caches the constructor of the component - and uses it instead of the actual component instance.

To solve the issue I had to dig into the VueRouter's internals but I came up with the following code ( _Ctor is the cached constructor - I have not checked whether using just it is enough, so to be safe I use both the component definition and the constructor):

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);
      }
    }
  });
}
发布评论

评论列表(0)

  1. 暂无评论