Programming/JavaScript

[EloquentJS] Ch15. Handling Events

dododoo 2020. 4. 26. 14:05

Handling Events

  • Some programs work with direct user input, such as mouse and keyboard actions. That kind of input isn’t available as a well-organized data structure—it comes in piece by piece, in real time, and the program is expected to respond to it as it happens.

    마우스나 키보드의 액션같은 유저 입력은 실시간으로 조각조각 들어오고 바로 처리되길 기대한다.

Event handlers

  • Imagine an interface where the only way to find out whether a key on the keyboard is being pressed is to read the current state of that key. To be able to react to keypresses, you would have to constantly read the key’s state so that you’d catch it before it’s released again. It would be dangerous to perform other time-intensive computations since you might miss a keypress.

    키의 상태를 직접 확인해야 입력을 알 수 있다면 이를 끊임없이 확인해야 할 것이다.

  • Some primitive machines do handle input like that. A step up from this would be for the hardware or operating system to notice the keypress and put it in a queue. A program can then periodically check the queue for new events and react to what it finds there.

    한 발짝 더 나아간 형태로, 하드웨어나 운영체제가 입력을 알아차리고 큐에 넣는 방법이 있다. 이 방법 역시 프로그램은 매번 큐를 주기적으로 체크해야 한다.

  • Of course, it has to remember to look at the queue, and to do it often, because any time between the key being pressed and the program noticing the event will cause the software to feel unresponsive. This approach is called polling. Most programmers prefer to avoid it.

    큐를 자주 체크하지 않으면 프로그램이 응답하지 않는 것처럼 느끼게 될 수 있다.

  • A better mechanism is for the system to actively notify our code when an event occurs. Browsers do this by allowing us to register functions as handlers for specific events.

    더 나은 방법으로 시스템이 이벤트가 발생했을 때 코드로 알리는 방법이 있다. 브라우저는 특정 이벤트의 핸들러를 등록하게 해서 이를 가능하게 한다.

  • <p>Click this document to activate the handler.</p>
    <script>
        window.addEventListener("click", () => {
            console.log("You knocked?");
        });
    </script>
  • The window binding refers to a built-in object provided by the browser. It represents the browser window that contains the document. Calling its addEventListener method registers the second argument to be called whenever the event described by its first argument occurs.

    window, addEventLister

Events and DOM nodes

  • Each browser event handler is registered in a context. In the previous example we called addEventListener on the window object to register a handler for the whole window. Such a method can also be found on DOM elements and some other types of objects. Event listeners are called only when the event happens in the context of the object they are registered on.

    특정 문맥에서의 이벤트 핸들러를 정의할 수 있다.

  • <button>Click me</button>
    <p>No handler here.</p>
    <script>
        let button = document.querySelector("button");
        button.addEventListener("click", () => {
            console.log("Button clicked.");
        });
    </script>
  • Giving a node an onclick attribute has a similar effect. This works for most types of events—you can attach a handler through the attribute whose name is the event name with on in front of it.

    비슷하게, on- 어트리뷰트를 이용해서 대부분의 이벤트 핸들러를 등록할 수 있다.

  • But a node can have only one onclick attribute, so you can register only one handler per node that way. The addEventListener method allows you to add any number of handlers so that it is safe to add handlers even if there is already another handler on the element.

    onclick은 하나의 핸들러만 가질 수 있는데 addEventListener는 여러 핸들러를 부여할 수 있다. (같은 이벤트에 대해서 말하는 걸까? 여러 개 등록할 수 있으면 뭐가 안전하다는 걸까?)

  • The removeEventListener method, called with arguments similar to addEventListener, removes a handler.

    removeEventListener

  • <button>Act-once button</button>
    <script>
        let button = document.querySelector("button");
        function once() {
            console.log("Done.");
            button.removeEventListener("click", once);
        }
        button.addEventListener("click", once);
    </script>
  • The function given to removeEventListener has to be the same function value that was given to addEventListener. So, to unregister a handler, you’ll want to give the function a name (once, in the example).

    핸들러를 지울 때는 등록할 때와 같은 함수 값을 써야한다.

Event objects

  • Though we have ignored it so far, event handler functions are passed an argument: the event object. This object holds additional information about the event. For example, if we want to know which mouse button was pressed, we can look at the event object’s button property.

    핸들러에는 이벤트 객체가 인자로 전달된다. button

  • <button>Click me any way you want</button>
    <script>
        let button = document.querySelector("button");
        button.addEventListener("mousedown", event => {
            if (event.button == 0) {
                console.log("Left button");
            } else if (event.button == 1) {
                console.log("Middle button");
            } else if (event.button == 2) {
                console.log("Right button");
            }
        });
    </script>
  • The information stored in an event object differs per type of event. The object’s type property always holds a string identifying the event (such as "click" or "mousedown").

    이벤트마다 이벤트 객체가 갖는 정보는 다 다르다. type 프로퍼티는 언제나 이벤트의 종류를 나타내는 문자열을 갖고 있다.

Propagation

  • For most event types, handlers registered on nodes with children will also receive events that happen in the children. If a button inside a paragraph is clicked, event handlers on the paragraph will also see the click event.

    대부분의 이벤트에서, 자식 노드가 있는 노드에 등록된 핸들러는 자식 노드에서 발생한 이벤트 역시 받게 된다.

  • But if both the paragraph and the button have a handler, the more specific handler—the one on the button—gets to go first. The event is said to propagate outward, from the node where it happened to that node’s parent node and on to the root of the document. Finally, after all handlers registered on a specific node have had their turn, handlers registered on the whole window get a chance to respond to the event.

    이벤트는 바깥쪽으로 전파된다.

  • At any point, an event handler can call the stopPropagation method on the event object to prevent handlers further up from receiving the event. This can be useful when, for example, you have a button inside another clickable element and you don’t want clicks on the button to activate the outer element’s click behavior.

    이벤트 객체의 stopPropagation 메소드를 호출하여 전파를 막을 수 있다.

  • <p>A paragraph with a <button>button</button>.</p>
    <script>
        let para = document.querySelector("p");
        let button = document.querySelector("button");
        para.addEventListener("mousedown", event => {
            console.log("Handler for paragraph.");
        });
        button.addEventListener("mousedown", event => {
            console.log("Handler for button.");
            if (event.button == 2) event.stopPropagation();
        });
    </script>
  • Most event objects have a target property that refers to the node where they originated. You can use this property to ensure that you’re not accidentally handling something that propagated up from a node you do not want to handle.

    대부분의 이벤트 객체가 자신이 처음 발생한 노드를 가리키는 target 프로퍼티를 갖는다. 이를 이용하면 처리하고 싶지 않은 노드에서 전파된 것을 처리하지 않을 수 있다.

  • It is also possible to use the target property to cast a wide net for a specific type of event. For example, if you have a node containing a long list of buttons, it may be more convenient to register a single click handler on the outer node and have it use the target property to figure out whether a button was clicked, rather than register individual handlers on all of the buttons.

    target 프로퍼티를 활용하면 마치 넓은 그물을 치는 것처럼 특정 타입의 이벤트를 처리할 수 있다. (후손 노드에서 발생한 특정 타입의 이벤트를 전부 처리)

  • <button>A</button>
    <button>B</button>
    <button>C</button>
    <script>
        document.body.addEventListener("click", event => {
            if (event.target.nodeName == "BUTTON") {
                // *** Node.textContent
                // https://developer.mozilla.org/ko/docs/Web/API/Node/textContent
                // 노드와 그 자손의 텍스트 콘텐츠를 표현합니다.
                console.log("Clicked", event.target.textContent);
            }
        });
    </script>

Default actions

  • Many events have a default action associated with them. If you click a link, you will be taken to the link’s target. If you press the down arrow, the browser will scroll the page down. If you right-click, you’ll get a context menu. And so on.

    이런 것들이 다 이벤트의 기본 액션이었다.

  • For most types of events, the JavaScript event handlers are called before the default behavior takes place. If the handler doesn’t want this normal behavior to happen, typically because it has already taken care of handling the event, it can call the preventDefault method on the event object.

    일반적으로 기본 액션보다 이벤트 핸들러들이 먼저 실행된다. 이벤트 객체에 preventDefault 메소드를 호출하여 기본 액션을 수행하지 않게 할 수 있다.

  • This can be used to implement your own keyboard shortcuts or context menu. It can also be used to obnoxiously interfere with the behavior that users expect.

    이를 활용해 나만의 키보드 단축키 등을 만들 수도 있다.

  • <a href="https://developer.mozilla.org/">MDN</a>
    <script>
        let link = document.querySelector("a");
        link.addEventListener("click", event => {
            console.log("Nope.");
            event.preventDefault();
        });
    </script>
  • Try not to do such things unless you have a really good reason to.
  • Depending on the browser, some events can’t be intercepted at all. On Chrome, for example, the keyboard shortcut to close the current tab (control-W or command-W) cannot be handled by JavaScript.

    기본 액션을 제어하지 못하는 이벤트들도 있다.

Key events

  • When a key on the keyboard is pressed, your browser fires a "keydown" event. When it is released, you get a "keyup" event.

    keydown, keyup

  • <p>This page turns violet when you hold the V key.</p>
    <script>
        window.addEventListener("keydown", event => {
            if (event.key == "v") {
                document.body.style.background = "violet";
            }
        });
        window.addEventListener("keyup", event => {
            if (event.key == "v") {
                document.body.style.backgroung = "";
            }
        });
    </script>
  • Despite its name, "keydown" fires not only when the key is physically pushed down. When a key is pressed and held, the event fires again every time the key repeats. Sometimes you have to be careful about this.

    keydown 핸들러는 누르고 있으면 계속 호출된다.

  • The example looked at the key property of the event object to see which key the event is about. This property holds a string that, for most keys, corresponds to the thing that pressing that key would type. For special keys such as enter, it holds a string that names the key ("Enter", in this case). If you hold shift while pressing a key, that might also influence the name of the key—"v" becomes "V", and "1" may become "!", if that is what pressing shift-1 produces on your keyboard.

    이벤트 객체의 key 프로퍼티는 해당 이벤트가 어떤 키 때문에 발생했는지 알려준다. 특수 키가 아닌 대부분의 키의 경우 키를 눌렀을 때 타이핑되는 값을 나타내는 문자열을 key 프로퍼티로 갖는다.

  • Modifier keys such as shift, control, alt, and meta (command on Mac) generate key events just like normal keys. But when looking for key combinations, you can also find out whether these keys are held down by looking at the shiftKey, ctrlKey, altKey, and metaKey properties of keyboard and mouse events.

    키보드, 마우스 이벤트에서 shiftKey, ctrlKey, altKey, metaKey로 해당 특수 키가 눌렸는지 확인할 수 있다.

  • <p>Press Control-Space to continue.</p>
    <script>
        window.addEventListener("keydown", event => {
            if (event.key == " " && event.ctrlKey) {
                console.log("Continuing!");
            }
        });
    </script>
  • The DOM node where a key event originates depends on the element that has focus when the key is pressed. Most nodes cannot have focus unless you give them a tabindex attribute, but things like links, buttons, and form fields can. When nothing in particular has focus, document.body acts as the target node of key events.

    키가 눌렸을 때 포커스를 가진 DOM 노드가 키 이벤트가 시작된 노드이다. 그런데 링크, 버튼, 폼과 같은 엘리먼트가 아닌 대부분의 엘리먼트들은 tabindex 어트리뷰트를 주지 않는 이상 포커스를 갖지 못한다. 특별히 포커스를 가진 노드가 없으면 document.body가 타겟이 된다.

  • When the user is typing text, using key events to figure out what is being typed is problematic. Some platforms, most notably the virtual keyboard on Android phones, don’t fire key events. But even when you have an old-fashioned keyboard, some types of text input don’t match key presses in a straightforward way, such as input method editor (IME) software used by people whose scripts don’t fit on a keyboard, where multiple key strokes are combined to create characters.
  • To notice when something was typed, elements that you can type into, such as the <input> and <textarea> tags, fire "input" events whenever the user changes their content. To get the actual content that was typed, it is best to directly read it from the focused field.

    유저가 타이핑할 때 무엇이 입력되었는지 알기 위해 키 이벤트를 사용하는 것은 문제를 일으킬 가능성이 크다. 무엇을 써 넣을 수 있는 <input>이나 <textarea> 태그 같은 것들은 사용자가 그 내용을 바꿀 때마다 "input" 이벤트를 발생시킨다. 타이핑 된 실제 내용을 얻기 위해서는 포커스를 가진 필드에서 직접 읽는 것이 최선이다.

Pointer events

  • There are currently two widely used ways to point at things on a screen: mice (including devices that act like mice, such as touchpads and trackballs) and touchscreens. These produce different kinds of events.

    화면의 무언가를 가리키는 메이저한 방법으로 마우스(트랙패드, 트랙볼 포함)와 터치패드가 있는데 다른 종류의 이벤트를 발생시킨다.

Mouse clicks

  • Pressing a mouse button causes a number of events to fire. The "mousedown" and "mouseup" events are similar to "keydown" and "keyup" and fire when the button is pressed and released. These happen on the DOM nodes that are immediately below the mouse pointer when the event occurs.

    'mousedown", "mouseup"이벤트는 "keydown", "keyup"과 비슷하다. (그런데 키 이벤트와 다르게 이벤트 발생 시 마우스의 밑에 있는 노드가 target이 되는 것 같다. 키 이벤트는 포커스를 가질 수 있는 곳에서...)

  • After the "mouseup" event, a "click" event fires on the most specific node that contained both the press and the release of the button. For example, if I press down the mouse button on one paragraph and then move the pointer to another paragraph and release the button, the "click" event will happen on the element that contains both those paragraphs.

    "click" 이벤트는 "mouseup"이벤트가 끝난 후에 발생하는데, 버튼을 누르고 떼는 동작을 모두 포함한 노드에서 발생한다. 예를 들어 한 문단에서 마우스 버튼을 누르고 다른 문단에서 뗀다면 이 두 문단을 포함한 노드에서 "click" 이벤트가 발생하는 것이다.

  • If two clicks happen close together, a "dblclick" (double-click) event also fires, after the second click event.

    "dblclick" 이벤트는 두 번째 클릭 이벤트 후에 발생한다.

  • To get precise information about the place where a mouse event happened, you can look at its clientX and clientY properties, which contain the event’s coordinates (in pixels) relative to the top-left corner of the window, or pageX and pageY, which are relative to the top-left corner of the whole document.

    clientX, clientY: 윈도우의 왼쪽 위 기준; pageX, pageY: 전체 문서의 왼쪽 위 기준

  • The following implements a primitive drawing program. Every time you click the document, it adds a dot under your mouse pointer.
  • <style>
        body {
            height: 200px;
            background: beige;
        }
        .dot {
            height: 8px; width: 8px;
            border-radius: 4px; /* rounds corners */
            background: blue;
            position: absolute;
        }
    </style>
    <script>
        window.addEventListener("click", event => {
            let dot = document.createElement("div");
            dot.className = "dot";
            dot.style.left = (event.pageX - 4) + "px";
            dot.style.top = (event.pageY - 4) + "px";
            document.body.appendChild(dot);
        });
    </script>

Mouse motion

  • Every time the mouse pointer moves, a "mousemove" event is fired. This event can be used to track the position of the mouse. A common situation in which this is useful is when implementing some form of mouse-dragging functionality.

    마우스 포인터가 움직일 때 "mousemove" 이벤트가 계속 발생한다. 이는 마우스 드래그 기능을 구현할 때 유용하다.

  • <p>Drag the bar to change its width:</p>
    <div style="background: orange; width: 60px; height: 20px">
    </div>
    <script>
        // 잘 읽어보면 도움이 많이 되는 예제
        let lastX;
        let bar = document.querySelector("div");
        bar.addEventListener("mousedown", event => {
            if (event.button == 0) {
                lastX = event.clientX;
                window.addEventListener("mousemove", moved);
                event.preventDefault(); 
                // Prevent selection (안 하면 <p>가 드래그 됨)
            }
        });
        function moved(event) {
            if (event.buttons == 0) { // button's'
                window.removeEventListener("mousemove", moved);
            } else {
                let dist = event.clientX - lastX;
                let newWidth = Math.max(10, bar.offsetWidth + dist);
                bar.style.width = newWidth + "px";
                lastX = event.clientX;
            }
        }
    </script>
  • Note that the "mousemove" handler is registered on the whole window. Even if the mouse goes outside of the bar during resizing, as long as the button is held we still want to update its size.

    시작은 막대 위에서만 가능하지만 조정 중에는 벗어나도 가능하도록!

  • We must stop resizing the bar when the mouse button is released. For that, we can use the buttons property (note the plural), which tells us about the buttons that are currently held down. When this is zero, no buttons are down. When buttons are held, its value is the sum of the codes for those buttons—the left button has code 1, the right button 2, and the middle one 4.

    buttons(복수형임에 주의) 프로퍼티로 어떤 버튼들이 눌러지고 있는지 알 수 있다.

  • Note that the order of these codes is different from the one used by button, where the middle button came before the right one.

    button 프로퍼티와 버튼 코드의 순서가 다르다.

Touch events

  • ... Browsers for those devices pretended, to a certain extent, that touch events were mouse events. But this illusion isn’t very robust. A touchscreen works differently from a mouse: it doesn’t have multiple buttons, you can’t track the finger when it isn’t on the screen (to simulate "mousemove"), and it allows multiple fingers to be on the screen at the same time.

    이전에는 터치 이벤트를 마우스 이벤트인 것처럼 따라했는데, 사실 이건 그렇게 견고하지 않다.

  • ... Something like the resizeable bar in the previous example does not work on a touchscreen.
  • There are specific event types fired by touch interaction. When a finger starts touching the screen, you get a "touchstart" event. When it is moved while touching, "touchmove" events fire. Finally, when it stops touching the screen, you’ll see a "touchend" event.

    "touchstart", "touchmove", "touchend"

  • Because many touchscreens can detect multiple fingers at the same time, these events don’t have a single set of coordinates associated with them. Rather, their event objects have a touches property, which holds an array-like object of points, each of which has its own clientX, clientY, pageX, and pageY properties.

    많은 터치스크린에서 멀티 터치가 가능하므로, 이벤트 객체는 touches라는 array-like 프로퍼티로 각 요소마다 clientX, clientY, pageX, pageY 프로퍼티를 제공한다.

  • <style>
        dot { position: absolute; display: block; /* 왜 block? */
              border: 2px solid red; border-radius: 50px;
              height: 100px; width: 100px; }
    </style>
    <p>Touch this page</p>
    <script>
        function update(event) {
            for (let dot; dot = querySelector("dot")) { // ***
                dot.remove();
            }
            for (let i = 0; i < event.touches.length; i++) {
                let {pageX, pageY} = event.touches[i];
                let dot = document.createElement("dot");
                dot.style.left = (pageX - 50) + "px";
                dot.style.top = (pageY - 50) + "px";
                document.body.appendChild(dot);
            }
        }
        window.addEventListener("touchstart", update);
        window.addEventListener("touchmove", update);
        window.addEventListener("touchend", update);
    </script>
  • You’ll often want to call preventDefault in touch event handlers to override the browser’s default behavior (which may include scrolling the page on swiping) and to prevent the mouse events from being fired, for which you may also have a handler.

Scroll events

  • Whenever an element is scrolled, a "scroll" event is fired on it. This has various uses, such as knowing what the user is currently looking at or showing some indication of progress.

    한 엘리먼트가 스크롤되면 "scroll"이벤트가 그 엘리먼트에서 수행된다.

  • The following example draws a progress bar above the document and updates it to fill up as you scroll down:

  • <style>
        #progress {
            border-bottom: 2px solid blue;
            width: 0;
            position: fixed;
            left: 0; top: 0;
        }
    </style>
    <div id="progress"></div>
    <script>
        document.body.appendChild(document.createTextNode(
            "supercalifragilisticexpialidocious ".repeat(1000)));
    
        let bar = document.querySelector("#progress");
        window.addEventListener("scroll", () => {
            let max = document.body.scrollHeight - innerHeight;
            bar.style.width = `${(pageYOffset / max) * 100}%`;
        });
    </script>
  • Giving an element a position of fixed acts much like an absolute position but also prevents it from scrolling along with the rest of the document. The effect is to make our progress bar stay at the top. Its width is changed to indicate the current progress. We use %, rather than px, as a unit when setting the width so that the element is sized relative to the page width.

    positionfixed로 주면 absolute와 비슷하지만 스크롤되지 않는다는 차이점이 있다. 또한 페이지 너비에 상대적인 값을 주기위해 px대신 %를 사용할 수 있다.

  • The global innerHeight binding gives us the height of the window, which we have to subtract from the total scrollable height—you can’t keep scrolling when you hit the bottom of the document. There’s also an innerWidth for the window width. By dividing pageYOffset, the current scroll position, by the maximum scroll position and multiplying by 100, we get the percentage for the progress bar.

    innerHeight, innerWidth

  • https://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively
  • Calling preventDefault on a scroll event does not prevent the scrolling from happening. In fact, the event handler is called only after the scrolling takes place.

Focus events

  • When an element gains focus, the browser fires a "focus" event on it. When it loses focus, the element gets a "blur" event.

    어떤 엘리먼트가 포커스를 얻으면 "focus", 잃으면 "blur" 이벤트를 발생시킨다.

  • Unlike the events discussed earlier, these two events do not propagate.

    지금까지 알아본 이벤트들과 다르게 이 두 이벤트는 전파되지 않는다. (생각해보면 전파되는게 이상함)

  • <p>Name: <input type="text" data-help="Your full name"></p>
    <p>Age: <input type="text" data-help="Your age in years"></p>
    <p id="help"></p>
    
    <script>
        let help = document.querySelector("#help");
        let fields = document.querySelectorAll("input");
        for (let field of Array.from(fields)) {
            field.addEventListener("focus", event => {
                let text = event.target.getAttribute("data-help");
                help.textContent = text;
            });
            field.addEventListener("blur", event => {
                help.textContent = "";
            });
        }
    </script>
  • The window object will receive "focus" and "blur" events when the user moves from or to the browser tab or window in which the document is shown.

Load event

  • When a page finishes loading, the "load" event fires on the window and the document body objects. This is often used to schedule initialization actions that require the whole document to have been built. Remember that the content of <script> tags is run immediately when the tag is encountered. This may be too soon, for example when the script needs to do something with parts of the document that appear after the <script> tag.

    페이지 로드가 끝나면 "load"이벤트가 윈도우와 도큐먼트 바디 객체들에 발생한다. 이는 주로 전체 문서가 만들어진 후 초기화가 필요할 때 사용되곤 한다. (<scirpt> 태그의 내용은 만나자 마자 즉시 실행되기 때문)

  • Elements such as images and script tags that load an external file also have a "load" event that indicates the files they reference were loaded. Like the focus-related events, loading events do not propagate.
  • 이미지나 외부 파일을 불러오는 script 태그 역시 "load" 이벤트가 발생한다. focus같은 이벤트처럼 이 역시 전파되지 않는다.
  • When a page is closed or navigated away from, a "beforeunload" event fires. The main use of this event is to prevent the user from accidentally losing work by closing a document. If you prevent the default behavior on this event and set the returnValue property on the event object to a string, the browser will show the user a dialog asking if they really want to leave the page. That dialog might include your string, but because some malicious sites try to use these dialogs to confuse people into staying on their page to look at dodgy weight loss ads, most browsers no longer display them.

    페이지를 닫거나 떠날 때 "beforeunload" 이벤트가 발생한다. 이는 주로 사용자가 뜻하지 않게 정보를 잃는 것을 막기 위해 사용한다. 이벤트 객체의 preventDefault 메서드를 호출하고 returnValue를 문자열로 세팅하면 페이지를 떠날 시 다이얼로그 박스를 띄운다.

Events and the event loop

  • In the context of the event loop, as discussed in Chapter 11, browser event handlers behave like other asynchronous notifications. They are scheduled when the event occurs but must wait for other scripts that are running to finish before they get a chance to run.

    이벤트 핸들러는 이벤트가 발생했을 때 스케쥴되지만 실행되기 위해 현재 실행 중인 다른 스크립트들이 끝나기를 기다려야 한다.

  • The fact that events can be processed only when nothing else is running means that, if the event loop is tied up with other work, any interaction with the page (which happens through events) will be delayed until there’s time to process it. So if you schedule too much work, either with long-running event handlers or with lots of short-running ones, the page will become slow and cumbersome to use.

    https://meetup.toast.com/posts/89

  • For cases where you really do want to do some time-consuming thing in the background without freezing the page, browsers provide something called web workers. A worker is a JavaScript process that runs alongside the main script, on its own timeline.

    시간을 잡아먹는 작업을 백그라운드에서 실행하여 페이지가 프리징되지 않게 하기 위해, 브라우저는 web worker라는 것을 제공한다. (Workers really are multi-threaded.)

  • Imagine that squaring a number is a heavy, long-running computation that we want to perform in a separate thread. We could write a file called code/squareworker.js that responds to messages by computing a square and sending a message back.
  • // code/squareworker.js
    addEventListener("message", event => {
        postMessage(event.data * event.data);
    });
  • To avoid the problems of having multiple threads touching the same data, workers do not share their global scope or any other data with the main script’s environment. Instead, you have to communicate with them by sending messages back and forth.

    worker들은 글로벌 스코프나 메인 스크립트 환경의 다른 데이터를 공유하지 않는다. 대신 메시지를 주고 받는다.

  • This code spawns a worker running that script, sends it a few messages, and outputs the responses.
  • let squareWorker = new Worker("code/squareworker.js");
    squareWorker.addEventListener("message", event => {
        console.log("The worker responded:", event.data);
    });
    squareWorker.postMessage(10);
    squareWorker.postMessage(24);
  • The postMessage function sends a message, which will cause a "message" event to fire in the receiver. The script that created the worker sends and receives messages through the Worker object, whereas the worker talks to the script that created it by sending and listening directly on its global scope.

    postMessage 함수는 메시지를 보내고, 수신 측에 "message"이벤트를 발생시킨다. worker를 만드는 스크립트는 Worker객체를 통해 메시지를 주고 받는 반면 worker는 전역 스코프에서 메시지를 주고 받는다.

  • Only values that can be represented as JSON can be sent as messages—the other side will receive a copy of them, rather than the value itself.

    JSON으로 표현 가능한 값만이 메시지로 전달될 수 있으며 전달 되는 값은 값 그 자체가 아니라 복사본이다.

Timers

  • Sometimes you need to cancel a function you have scheduled. This is done by storing the value returned by setTimeout and calling clearTimeout on it.

    setTimeout - clearTimeout

  • let bombTimer = setTimeout(() => {
        console.log("BOOM!");
    }, 500);
    
    if (Math.random() < 0.5) {
        console.log("Defused.");
        clearTimeout(bombTimer);
    }
  • The cancelAnimationFrame function works in the same way as clearTimeout—calling it on a value returned by requestAnimationFrame will cancel that frame (assuming it hasn’t already been called).

    requestAnimationFrame - cancelAnimationFrame (Timeout과 같은 방식으로 작동)

  • A similar set of functions, setInterval and clearInterval, are used to set timers that should repeat every X milliseconds.

    setInterval, clearInterval

  • let ticks = 0;
    let clock = setInterval(() => {
        console.log("tick", ticks++);
        if (ticks == 10) {
            clearInterval(clock);
            console.log("stop.");
        }
    }, 200);

Debouncing

  • Some types of events have the potential to fire rapidly, many times in a row (the "mousemove" and "scroll" events, for example). When handling such events, you must be careful not to do anything too time-consuming or your handler will take up so much time that interaction with the document starts to feel slow.

    몇몇 이벤트들은 연속적으로 빠르게 많이 발생할 가능성이 있다. 이런 이벤트를 다룰 때 시간이 많이 드는 작업을 수행하면 핸들러가 시간을 너무 많이 써서 도큐먼트와의 상호작용이 느리게 느껴지게 될 수도 있다.

  • If you do need to do something nontrivial in such a handler, you can use setTimeout to make sure you are not doing it too often. This is usually called debouncing the event. There are several slightly different approaches to this.

    이런 핸들러에서 간단하지 않은 작업을 꼭 해야 한다면, setTimeout을 사용하여 자주 하지 않게끔 할 수 있는데, 여러가지 방법이 있다.

  • In the first example, we want to react when the user has typed something, but we don’t want to do it immediately for every input event. When they are typing quickly, we just want to wait until a pause occurs. Instead of immediately performing an action in the event handler, we set a timeout. We also clear the previous timeout (if any) so that when events occur close together (closer than our timeout delay), the timeout from the previous event will be canceled.

    첫 번째 경우: 모든 입력을 즉각적으로 처리하는 것이 아니라 입력이 잠시 멈췄을 때 처리

  • <textarea>Type something here...</textarea>
    <script>
        let textarea = document.querySelector("textarea");
        let timeout;
        textarea.addEventListener("input", () => {
            clearTimeout(timeout);
            timeout = setTimeout(() => console.log("Typed!"), 500);
        });
    </script>
  • Giving an undefined value to clearTimeout or calling it on a timeout that has already fired has no effect. Thus, we don’t have to be careful about when to call it, and we simply do so for every event.

    undefined value나 이미 실행된 timeout으로 clearTimeout을 호출하더라도 어떤 효과가 발생하지 않으므로 문제가 없다.

  • We can use a slightly different pattern if we want to space responses so that they’re separated by at least a certain length of time but want to fire them during a series of events, not just afterward. For example, we might want to respond to "mousemove" events by showing the current coordinates of the mouse but only every 250 milliseconds.

    두 번째 경우: 응답에 적어도 특정한 시간의 간격을 둬서 분리하되 미루지 않고 이벤트의 연속적인 흐름 내에서 처리

  • <script>
        let scheduled = null;
        window.addEventListener("mousemove", event => {
            if (!scheduled) {
                setTimeout(() => {
                    document.body.textContent = 
                        `Mouse at ${scheduled.pageX}, ${scheduled.pageY}`;
                    scheduled = null;
                }, 250);
            }
            scheduled = event;
        });
    </script>

Exercises

Balloon

  • <p>🎈</p>
    <script>
        let balloon = document.querySelector("p");
        let threshold = 50;
        function flate(event) {
            let [_, size, unit] = /^(\d+|\d+\.\d+)(\D+)$/
                              .exec(balloon.style.fontSize);
              // console.log(size, unit);
            if (event.key === "ArrowUp") size *= 1.1;
            else if (event.key === "ArrowDown") size *= 0.9;
    
            if (size > threshold) {
                balloon.textContent = "💥";
                window.removeEventListener("keydown", flate);
            } else {
                balloon.style.fontSize = size + unit;
            }
            event.preventDefault();
        }
        window.addEventListener("keydown", flate);
    </script>
  • <!-- solution -->
    <script>
        let p = document.querySelector("p");
        let size;
        function setSize(newSize) {
            size = newSize;
            p.style.fontSize = size + "px";
        }
        setSize(20);
    
        function handleArrow(event) {
            if (event.key == "ArrowUp") {
                if (size > 70) {
                    p.textContent = "💥";
                    document.body.removeEventListener("keydown", handleArrow);
                } else {
                    setSize(size * 1.1);
                    event.preventDefault();
                }
            } else if (event.key == "ArrowDown") {
                setSize(size * 0.9);
                event.preventDefault();
            }
        }
        document.body.addEventListener("keydown", handleArrow);
    </script>

Mouse trail

  •   let trails = [];
    document.body.addEventListener("mousemove", event => {
        if (trails.length == 0) {
            for (let i = 0; i < 12; ++i) {
                let trail = document.createElement("div");
                trail.className = "trail";
                trail.style.left = (event.pageX - 3) + "px";
                trail.style.top = (event.pageY - 3) + "px";
                trails.push(trail);
            }
        } else {
            let trailsInDOM = document.querySelectorAll(".trail");
            for (let trail of Array.from(trailsInDOM)) {
                trail.remove();
            }
            let trail = trails.pop();
            trail.style.left = (event.pageX - 3) + "px";
            trail.style.top = (event.pageY -3) + "px";
            trails.unshift(trail);
            for (let trail of trails) {
                document.body.appendChild(trail);
            }
        }
    });
  • // solution
    let dots = [];
    for (let i = 0; i < 12; i++) {
        let node = document.createElement("div");
        node.className = "trail";
        document.body.appendChild(node);
        dots.push(node);
    }
    let currentDot = 0;
    
    window.addEventListener("mousemove", event => {
        let dot = dots[currentDot];
        dot.style.left = (event.pageX - 3) + "px";
        dot.style.top = (event.pageY - 3) + "px";
        currentDot = (currentDot + 1) % dots.length;
    });

Tabs

  • let current = null;
    function asTabs(node) {
        for (let child of Array.from(node.children)) {
            let button = document.createElement("button");
            button.textContent = child.getAttribute("data-tabname");
            node.parentNode.insertBefore(button, node);
            child.style.display = "none";
    
            button.addEventListener("click", event => {
                if (current) {
                    current.node.style.display = "none";
                    current.button.style.background = "";
                }
                child.style.display = "block";
                button.style.background = "gray";
                current = {button, node: child};
            });
        }
    }
    
    asTabs(document.querySelector("tab-panel"));
  • // solution
    function asTabs(node) {
        let tabs = Array.from(node.children).map(node => {
            let button = document.createElement("button");
            button.textContent = node.getAttribute("data-tabname");
            let tab = {node, button};
            button.addEventListener("click", () => selectTab(tab));
            return tab;
        });
    
        let tabList = document.createElement("div");
        for (let {button} of tabs) tabList.appendChild(button);
        node.insertBefore(tabList, node.firstChild);
    
        function selectTab(selectedTab) {
            for (let tab of tabs) {
                let selected = tab == selectedTab;
                tab.node.style.display = selected ? "" : "none";
                tab.button.style.color = selected ? "red" : "";
            }
        }
        selectTab(tabs[0]);
    }
    
    asTabs(document.querySelector("tab-panel"));