dalgard / meteor-viewmodel

Minimalist VM for Meteor
24 stars 2 forks source link

Binding twice after reconnect #20

Open KoenLav opened 8 years ago

KoenLav commented 8 years ago

Hi Kristian,

We are happily using your package for a while now. But since a while it seems that events are binded to HTML nodes twice after Meteor reconnects.

Have you seen this problem before?

Where is the code located which binds the event (within your package)? Maybe I can take a look ;)

wvanooijen92 commented 8 years ago

I am running into the same problem...

KoenLav commented 8 years ago

More specifically: the click binding will get executed twice (or thrice, etc.) when the connection is lost.

I think the package expects the unbind function to be called when the elements are removed as a result of the data being unavailable, but we use GroundDB to ensure the data is available even when offline.

It seems the bind function is called again on reconnect, but the previous bind is not removed. (unbind is not called)

The strange this is that this seems to happen on some templates, but not others...

KoenLav commented 8 years ago

Note: I added a custom mousedown binding and the same occurs.

Note: when I console.log(this) within the viewmodel I can see the functions get executed after reconnection and a second nexus is added, but the first one is not removed.

Some pointers would be greatly appreciated :)

dalgard commented 8 years ago

Thanks for the debugging effort. I'll look into it first thing, probably later today.

dalgard commented 8 years ago

@wvanooijen92 Do you use GroundDB as well?

KoenLav commented 8 years ago

Basically the onReady gets executed, but the onInvalidate does not.

Is it possible that in

const computation = Tracker.currentComputation;

if (computation)
  computation.onInvalidate(callback);

}

Tracker.currentComputation is undefined?

KoenLav commented 8 years ago

I'm pretty sure it is (null, not undefined), but now on to the why...

Could it be that because the datasource is GroundDB the computation is different?

But then why doesn't this happen on some of our other pages (where the datasource is also GroundDB, I verified it doesn't happen)...

KoenLav commented 8 years ago

I can verify that the unbind function is actually called on these elements after Meteor.reconnect() and ends up all the way at elem.removeEventListener with elem, type and listener properly defined (as far as I can tell).

So maybe it's not the unbinding going wrong, but the binding executing twice? Looking into that now.

KoenLav commented 8 years ago

Nope, bind gets called only once after reconnect, so I'm back to looking at the removeEventListener and why it's not working...

KoenLav commented 8 years ago
  console.log(this.view)

  this.view[ViewModel.nexusesKey].remove(this);

  console.log(this.view)

Both view objects are identical and have identical contents (so no nexus is removed).

Is it possible that the listener has changed, which is why removeEventListener is not working and which would also explain by .remove(this) is not working (if it removes based on matching the object in an array)?

KoenLav commented 8 years ago

I also noticed that the vm-bind-id of all elements changes every time after calling meteor.reconnect() (this also happens on elements where we do not experience the multiple bindings problem).

dalgard commented 8 years ago

If you set a custom attribute on one of those elements in dev tools, does the attribute disappear after the reconnect, meaning that the element is re-rendered?

dalgard commented 8 years ago

I swapped jQuery for vanilla removeEventListener in 1.0.0 – would it be possible for you to downgrade to 0.9.4 and check if the problem is there, too? I suspect that it's not.

KoenLav commented 8 years ago

I tried downgrading to 0.9.4 indeed, the problem persisted.

dalgard commented 8 years ago

Hum...

dalgard commented 8 years ago

Let me know about the custom attributes. I definitely think you've found the correct part of the code to look at, but I'm having trouble reproducing the bug simply with Meteor.disconnect(), Meteor.reconnect(). The ids don't change in my page.

Something to do with computations is a good bet...

KoenLav commented 8 years ago

Yeah, the thing is it only happens on one set of elements in our code, all other sets are unaffected, but I don't see anything in those elements which is 'strange'. Would it help you to have a description of the element object?

Checking the custom attribute now (also re-added jQuery as a dependency and manually change the code back to jQuery in your package to test).

The IDs do change on all elements in our app, so it seems this might be caused by one 'bug' in combination with another...

dalgard commented 8 years ago

Good point, but in that case @wvanooijen92 has the same combination.

KoenLav commented 8 years ago

wvanooijen92 is working on the same project... Don't know why he hasn't responded, he was also troubleshooting this.

I checked: when adding a custom attribute the attribute remains (indicating the element is not rerendered), but the vm-bind-id does change.

KoenLav commented 8 years ago

By the way: the vm-bind-id of ALL elements changes (not just those who depend on a GroundDB collection, also those who don't depend on a collection at all).

And damn, I forgot, but we are using GroundDB at ground:db@1.0.0-alpha.3

KoenLav commented 8 years ago

Apologies for the inconenience :(

KoenLav commented 8 years ago

I just also tried switching back to jQuery (re-adding it as a dependency and using the .on and .off methods rather than the eventListener methods in plain Javascript but the problem persists.

I think the the problem of multiple bindings is VERY specific and only happens when elements are somehow 're-evaluated' by your package, causing the vm-bind-id to change. I am however yet to identify the difference between most elements in our project and these specific elements.

KoenLav commented 8 years ago

Also I'm using Meteor 1.2.1

dalgard commented 8 years ago

No reason for apology. It's definitely caused by some detail that has to do with the computation – what you call re-evaluation.

dalgard commented 8 years ago

The hooks below should unbind the element, before it is bound anew.

    this.onRefreshed(this.unbind);
    this.onDestroyed(this.unbind);
    this.onInvalidate(() => this.unbind(true));

Maybe one of these hooks doesn't run. Would you mind checking onRefreshed with a breakpoint on base.js#L102?

If the unbind function is called – please check which hook calls it by looking one level up in the call stack with a breakpoint on nexus.js#L237 – maybe the do_unbind parameter doesn't evaluate to true for some reason?

KoenLav commented 8 years ago

I can do you one better: the onInvalidate hook actually runs, the do_unbind value is true and it even ends up all the way at removeEventListener (with the (seemingly) correct element, binding type and listener, but it does not get removed. Op 15 mrt. 2016 10:23 a.m. schreef "Kristian Dalgård" < notifications@github.com>:

The hooks below (declared in the abstract Base class) should unbind the element, before it is bound anew. Maybe one of these hooks doesn't run. Would you mind checking that with a breakpoint on base.js#L102 https://github.com/dalgard/meteor-viewmodel/blob/master/packages/dalgard_viewmodel/lib/base.js#L102 ?

this.onRefreshed(this.unbind);
this.onDestroyed(this.unbind);
this.onInvalidate(() => this.unbind(true));

If unbind is called (breakpoint on nexus.js#L237 https://github.com/dalgard/meteor-viewmodel/blob/master/packages/dalgard_viewmodel/lib/nexus.js#L237), maybe its do_unbind parameter doesn't evaluate to true for some reason.

— You are receiving this because you authored the thread. Reply to this email directly or view it on GitHub:

https://github.com/dalgard/meteor-viewmodel/issues/20#issuecomment-196737617

dalgard commented 8 years ago

Oh, right – you already said that...

dalgard commented 8 years ago

Maybe a new bind is triggered prematurely so that this.listener gets overwritten with a new listener, before unbind get a chance to unregister the old this.listener?

You might be able to debug that if you set a breakpoint where the listener is created (inside bind) and then right click the listener and select Store as global variable. Then you can compare that variable (temp1) with the listener that is unregistered in unbind.

(I have a tendency to edit my posts right after sending them, would it be possible for you to read directly on GitHub?)

dalgard commented 8 years ago

I guess it's simple – a second call to bind should never occur unless unbind has run successfully.

KoenLav commented 8 years ago

Just the one I read on my phone (currently doing research for my thesis, so have to multitask a little).

I guess that makes sense (not calling a second bind on an element with the same type and same listener), do you still need me to check which method calls the unbind?

dalgard commented 8 years ago

I don't think that's necessary, but it would be great if you could check whether bind is called the second time without unbind having been called successfully first – if you find the time.

KoenLav commented 8 years ago

It seems unbind is called before bind is called, re-checking at the point in time where the removeEventListener and addEventListener are actually called.

KoenLav commented 8 years ago

Ok, what happens for one specific element:

  1. bind is called
  2. unbind is called (the element, at this point, already has a new vm-bind-id)
  3. bind is called again

Afterwards: both binds are still functional (even though unbind is called and removeEventListener is reached, shame removeEventListener does not return true or false, but I think it is safe to say that one of the input variables for this function is not as it should be).

KoenLav commented 8 years ago

The guid of the listener in the 1st bind function is the same as the guid of the listener in the unbind function though...

KoenLav commented 8 years ago

I collected the Nexuses at 1 (bind called), 2 (unbind) and 3 (bind called again). (Right before addEventListener and removeEventListener.)

http://imgur.com/iEvmTW7

Is there anything you would like me to take a look at?

dalgard commented 8 years ago

I wonder whether it would be possible for the listener to print its own guid when it fires? It would be interesting to see exactly what listeners are being called.

KoenLav commented 8 years ago

Ok, as I mentioned earlier I re-implemented jQuery in your latest version (just api.use('jquery') and added on and off instead of addEventListener and removeEventListener.

What I did now was remove the listener part of the function call ($(elem).off(type) instead of $(elem).off(type, listener) but this doesn't work either).

Trying $(elem).off() now (removing all event handlers).

KoenLav commented 8 years ago

Strange things are happening... When I call $(elem).off() (wihout type and listener) the bind event does not get fired a second time.

The result is obviously that the button no longer does anything.

Don't get me wrong: it's not that bind is called once, then bind is called and unbind removes both binds. It's that when unbind is called and .off() (without parameters) is used the second bind is never called...

KoenLav commented 8 years ago

Ok, using jQuery I was able to find out that the listeners actually do get removed (when I console.log jQuery._data(elem, "events") right after the .off call there are not click handlers attached.

I now think the Nexus is not properly removed and each nexus is re-bound when binding.

dalgard commented 8 years ago

That must be due to some Blaze internals that aren't relevant to this issue.

dalgard commented 8 years ago

I think that's much more likely. Glad we've solved the mystery around removeEventListener, at least.

dalgard commented 8 years ago

Are there duplicate nexuses on the list if you inspect ViewModel.Nexus.find()?

KoenLav commented 8 years ago

Looking at the amout of Nexuses listed in the global list I don't think there are duplicates, but there are multiple Nexuses attached to a single element, that's for sure.

I just checked, there are not duplicates.

dalgard commented 8 years ago

Somewhere there's a reference from the element to the nexus or vice versa that should be removed and garbage collected.

dalgard commented 8 years ago

So. There are duplicate nexuses that involve the same binding-element pair, and each time Meteor reconnects, one more pops up. If you set a breakpoint inside bind, is it triggered twice, three times etc.? What about unbind, is it triggered the same number of times?

KoenLav commented 8 years ago

As far as I have been able to tell unbind is just triggered once, I'm looking at bind now.

dalgard commented 8 years ago

Btw, you can filter the global nexus-list by passing in the element in question:

// $0 refers to the currently selected element in dev tools
ViewModel.Nexus.find($0);
dalgard commented 8 years ago

How about this?

temp1 = some_superfluous nexus;
temp1.view.nexuses.indexOf(temp1) >= 0;
KoenLav commented 8 years ago

This indeed returns two Nexuses, so it seems I was wrong before (and you were right, I was looking through the entire list, rather than filtering them).

dalgard commented 8 years ago

Right, so it simply isn't unbound – back to square one ;)