I bumped into some unexpected behavior when trying to refer to a variable outside of an event handler.
Below is function called update
that joins data with div elements. In this case, there's only one data point, so only one div element is rendered. The function also takes in a number currentState
as an argument. The div element has a click event listener, which prints out the currentState
number and increments a global variable state
by 1. The original value of state
is 5. Then the click handler calls update
with the incremented state
as a parameter.
When the div is clicked, currentState
that's passed into update
increments as expected. This is evident from the output of console.log("update called. currentState: ", currentState)
.
However, inside of the 'click' event handler, currentState
remains at 5 regardless of how many times the div element is clicked.
Correct me if I'm wrong, but it seems to me that when the 'click' event listener is created in D3, it holds onto whatever context existed at that time. And because that listener is attached to the div only when it first enters the DOM, that context is never updated.
An easy fix for this issue is just not to attach the click listener on the enter selection, but rather outside of the call to join
. That way, the listener will be updated each time update
is called.
I basically have two questions. I wanted to confirm whether I'm correctly understanding what's happening. And second, is this behavior in D3 favorable? Perhaps I'm missing an advantage to it. Although this example's a bit contrived, I would have liked for the click handler to refer to the value of currentState
that gets updated with each call to update
. Or is it not good practice to attach listeners on the enter selection?
const data = ['randomString']
let state = 5
function update (currentState) {
console.log("update called. currentState: ", currentState)
d3.selectAll('div')
.data(data)
.join(enter => enter.append('div')
.style('width', '50px')
.style('height', '50px')
.style('background', 'red')
.on('click', function () {
console.log("click event. currentState: ", currentState)
state = 1
update(state)
}))
}
update(state)
CodePudding user response:
What is happening is that your initial on
binding is the only one that d3 uses in your case. Let's analyse this code:
d3.selectAll('div')
.data(data)
// ...
.join(enter => enter.append('div')
.on('click', function () {
console.log("click event. currentState: ", currentState)
state = 1
update(state)
}))
What is this doing?
- Select all
div
that exist in the DOM so far - Attach
data
to them - If there are less
div
nodes thandata
entries, create a newdiv
for each one and attach a click handler which logscurrentState
What happens if you run this code a second time? At step 3, there is already a div for each data entry and thus no new div
is created and no new click handlers are attached unless if the data
changes.
CodePudding user response:
As a complement to the other answer, which explains correctly what's happening in D3, this is not a D3 behaviour, this is how JavaScript works.
Here's a demo for showing what's happening with just JavaScript, no D3. Just as in your example when foo
is created it depends on its context and therefore on the currentState
value. For a simple comparison with your D3 code, foo
is stored in an object, so it's not created again every time update
is called.
Thus, as expected, modifying the currentState
value has no effect on foo
after it has been created: it will log 5
every time, not 5
, 6
, 7
etc...
let state = 5;
const obj = {};
function update(currentState) {
if (!obj.foo) obj.foo = () => console.log(currentState);
obj.foo();
state = 1;
};
update(state);
update(state);
update(state);