callbacks.md 9.65 KB
Newer Older
Caleb C. Sander's avatar
Caleb C. Sander committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Asynchronous idioms: event handlers and callbacks

## An example in the browser

JavaScript was designed as a programming language to run on webpages so that users can interact with the content on the page.
Some examples of things webpages often do in JavaScript:
- Show/hide part of the page when the user clicks on a button
- When the user enters some text in a form, check that it is a valid email address
- Update the state of a game when the user presses a key
- Upload data to a server
- Periodically request updated data from a server

[Here](click.html) is a simple example of JS code (between the `<script>` and `</script>` tags) that displays a message when the user clicks on a button:
```html
<html>
  <body>
    <button id='alert-button'>Click me!</button>
    <script>
      // We use the `id` property of the button to access it from JS
      const button = document.getElementById('alert-button')
21
      button.addEventListener('click', () => {
Caleb C. Sander's avatar
Caleb C. Sander committed
22
23
24
        // You could put any other code you wanted here
        // and it would run each time the button is clicked
        alert('Button was clicked')
25
      })
Caleb C. Sander's avatar
Caleb C. Sander committed
26
27
28
29
30
31
32
33
34
35
36
37
38
    </script>
  </body>
</html>
```

If you open this HTML file in the browser, it will show a button on the screen.
Each time the button is clicked, it displays a message saying that the button was clicked.

## How does this work?

Let's tease apart the code responsible for making button clicks display the message.
First, note that `() => alert('Button was clicked')` is a *function* in JS.
When this function is called, it causes the message `Button was clicked` to appear.
39
40
Then `button.addEventListener('click', ...)` tells the browser "I want the click action on `button` to call this function".
When a user interacts with the webpage, the browser figures out whether the button was clicked, and if so, it runs the "click handler" function.
Caleb C. Sander's avatar
Caleb C. Sander committed
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

## How doesn't this work?

Note that nowhere does our code say something like "wait until `button` is clicked" or "was `button` clicked yet?".
If you've dealt with user input in another language, you might have expected our JS code to handle button clicks to look more like:
```js
// 1.
while (true) {
  button.waitForClick()
  alert('Button was clicked')
}

// or
// 2.
while (true) {
  if (button.wasClicked()) {
    alert('Button was clicked')
  }
}
```
So why doesn't JS use an interface like one of these?
1. We call a function `button.waitForClick()` that asks the browser to pause our code until the user clicks the button.
  Although this could work in our simple example, what if the user might click either of two buttons and we want to respond to both?
  If we call `button1.waitForClick()` and they click `button2` instead, we will fail to detect their click.
2. This approach solves the issue of not being able to respond to multiple events: we could just add an `if (button2.wasClicked())` inside the loop.
  However, it has a more subtle problem.
  The loop may run billions of times before the user actually clicks the button.
68
  Since our program won't actually do anything until the user finally clicks the button, this is a huge waste of the processor's time.
Caleb C. Sander's avatar
Caleb C. Sander committed
69
70
71
72
73
74
75
76
77
78
79
80
81

We see that the "event handler" approach used by JS has two main benefits: it can easily set up handlers for thousands of events at once, and no JS code runs while we wait for an event to occur.

## Combining asynchronous events in parallel

Imagine that we would like to wait until several events have all happened before running some code.
In [this example](lock1.html), we make a combination lock with 4 digits and want to take an action when all the digits are correct:
```js
// Adds a digit to the lock. When the digit changes,
// `changeCallback()` will be called with the new value.
function makeLockDigit(changeCallback) {
  // ... (the full code is at the link above)

82
83
84
  digit.addEventListener('change', () => {
    changeCallback(Number(digit.value))
  })
Caleb C. Sander's avatar
Caleb C. Sander committed
85
86
87
88
89
90
}

// The super secret combination
const COMBO = [1, 2, 3, 4]
// Whether each digit is correct
const digitCorrect = [false, false, false, false]
91
for (let i = 0; i < 4; i++) {
Caleb C. Sander's avatar
Caleb C. Sander committed
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
  makeLockDigit(value => {
    // Digit i has changed, so store whether it is correct
    digitCorrect[i] = (value === COMBO[i])
    // If all digits are now correct, show a message
    if (digitCorrect.every(correct => correct)) {
      alert('You got the combination')
    }
  })
}
```

Since any of the 4 digits might be the last one to become correct, we check whether all of the digits are correct whenever each one changes.
This is generally the case when we don't know what order some asynchronous events will finish in: whenever each one finishes, it needs to check if it was the last one to finish.
There are other ways to store the state of the digits; for example, we could instead track how many are currently correct.
[This version](lock2.html) of the code implements that approach:
```js
// The number of correct digits
let correctCount = 0
110
for (let i = 0; i < 4; i++) {
Caleb C. Sander's avatar
Caleb C. Sander committed
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
  // Whether this digit was previously correct
  let wasCorrect = false
  makeLockDigit(value => {
    // Compare the digit's actual value to the correct value
    const isCorrect = (value === COMBO[i])
    // Update the number of correct digits if this one changed
    if (!wasCorrect && isCorrect) correctCount++
    else if (wasCorrect && !isCorrect) correctCount--
    wasCorrect = isCorrect
    // If all digits are now correct, show a message
    if (correctCount === COMBO.length) {
      alert('You got the combination')
    }
  })
}
```

## `setTimeout()`

Another asynchronous JS API is the function `setTimeout()`, which is used to wait for time to pass.
For example, to display a message after 3 seconds (3000 ms), you would write
```js
setTimeout(() => {
  console.log('3 seconds have passed')
}, 3000)
```
The fact that `setTimeout()` is asynchronous means it returns immediately, without witing for the time interval.
For example, the following code displays `Timeout started` immediately and `Timeout ended` after 1 second:
```js
setTimeout(() => {
  console.log('Timeout ended')
}, 1000)
console.log('Timeout started')
```

As with event handlers, because this API is asynchronous, you can easily wait for multiple intervals of time to pass at the same time.
For example, to create events that will finish 1, 2, ..., 10 seconds from now:
```js
for (let i = 1; i <= 10; i++) {
  setTimeout(() => {
    console.log(`${i} seconds passed`)
  }, i * 1000)
}
```

## Combining asynchronous events in sequence

It is also common to have one asynchronous event that depends on the result of another.
For example, we might want to wait for the user to enter some text, then send it to a server and wait for the server to respond.
In general, this requires us to create the next asynchronous event *inside* the handler for the previous one.

As a concrete example, let's simulate a "random walk" on a grid.
At each step, we randomly move left, right, up, or down, and we count the total number of times we have visited each grid square.
164
To run an asynchronous action after another one finshes, we *nest it inside the previous callback*.
Caleb C. Sander's avatar
Caleb C. Sander committed
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
Here is a [first attempt](walk1.html) that only moves 3 times.
```js
// ... (the full code is at the link above)

// Visit a given grid position and
// display the number of times it has been visited
function visit(position) {
  // Don't do anything if we're off the grid
  const row = grid[position.row]
  if (row === undefined) return
  const cell = row[position.col]
  if (cell === undefined) return

  cell.count++
  cell.element.innerText = String(cell.count)
}
// Compute the next position by randomly going left, right, up, or down
function nextPosition(position) {
  const {row, col} = position
  const random = Math.random()
  if (random < 0.25) return {row: row + 1, col}
  else if (random < 0.5) return {row: row - 1, col}
  else if (random < 0.75) return {row, col: col + 1}
  else return {row, col: col - 1}
}

// Start in the middle of the grid and jump every DELAY ms
let position = {row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)}
visit(position)
setTimeout(() => {
  // First movement
  position = nextPosition(position)
  visit(position)
  setTimeout(() => {
    // Second movement
    position = nextPosition(position)
    visit(position)
    setTimeout(() => {
      // Third movement
      position = nextPosition(position)
      visit(position)
    }, DELAY)
  }, DELAY)
}, DELAY)
```

You can see that each sequential delay is represented by nesting another `setTimeout()` inside the last one.
If we want to keep jumping to new squares forever, we need to [rewrite this program using a recursive function](walk2.html):
```js
function walkFrom(position) {
  // To perform the walk from the given starting position,
  // visit it and then start from the next position after a delay
  visit(position)
  setTimeout(() => walkFrom(nextPosition(position)), DELAY)
}

// Start in the middle of the grid
walkFrom({row: Math.floor(SIZE / 2), col: Math.floor(SIZE / 2)})
```

## Conclusion

We have seen two examples of asynchronous standard library functions in JavaScript: handling user input and waiting for time.
With both of them, we define a function that we want to get called when something happens.
This structure is common to any asynchronous function; the function passed in to run when it finishes is refered to as the "callback function".
It is up to the browser (or Node.js) to ensure that the callbacks we provide get called at the right times.

We have also looked at some of the primary ways to combine asynchronous actions.
Most programs that interact with a user, read/write files, make web requests, etc. can be broken into actions that run sequentially and in parallel.
Asynchronous programming makes it simple to run actions in parallel or in sequence, although running an action sequentially in a "loop" requires recursion.