6. JavaScript Asynchronous Programming

김관주·2023년 10월 11일
0

웹시스템

목록 보기
6/22

Asynchronous Programming

Asynchronous… Events

  • User interaction is naturally asynchronous

    • you can’t control when a user clicks, touches, speaks, or types (i.e., events).
  • JavaScript application runs on a single thread and JavaScript has had a mechanism for asynchronous execution.

    • The one main thread manages JS codes and handles all necessary tasks.
    • To overcome the limitation with utilization of asynchronicity and callbacks.
    • The asynchronous execution doesn’t block anything.
      • Additional threads as workers will be involved in the execution.
      • 다음 작업으로 넘어가기 전에 작업이 완료되기를 기다리는 대신, JavaScript는 작업을 시작하고, 첫 번째 작업이 백그라운드에서 처리되는 동안 다른 작업으로 이동한 다음 작업이 준비되면 다시 돌아와 결과를 처리할 수 있습니다

Event Model

  • The event handler code doesn’t execute until the event fires
  • In the following, a user click, console.log(“Clicked”) will not be executed until button is clicked.
let button = document.getElementById("btn-hello");
button.onclick = function(event) {
    console.log("Clicked");
};
  • Chaining multiple separate asynchronous calls together is more complicated because you must keep track of the event target (button) for each event

Asynchronous Programming

  • There are paradigms in JavaScript for asynchronous programming: the callback, the promise, and the generator ,and Async-Await (ES2017)

    • However, generators by themselves do not provide any sort of asynchronous support: they rely on either promises or a special type of callback to provide asynchronous behavior
    • Promises are relied on callbacks
  • Three primary things that you will be using asynchronous techniques

    • Network requests (Ajax calls, for instance)
    • Filesystem operations (reading/writing files, etc.)
    • Intentionally time-delayed functionality (an alarm, for example)

Callback (pattern)

  • A callback is simply a function that you write that will be invoked at some point in the future. It’s just a regular JavaScript function.
  • Typically, you provide these callback functions to other functions, or set them as properties on objects.
    • Callbacks are very often (but not always) anonymous functions
console.log("Before timeout: " + new Date()); // 1st console.log
function f() {
    console.log("After timeout: " + new Date());
}
setTimeout(f, 60*1000); // one minute
console.log("I happen after setTimeout!"); // 2nd console.log
console.log("Me too!"); // 3rd console.log
  • setTimeout() function returns (i.e., control return) immediately.
  • 2nd and 3rd console.log are output immediately after setTimeout is called.
  • When setTimeout finishes, it adds a new job to the end of the job queue.

Callback and Scope

function countdown() {
    let i;
    console.log("Countdown:");
    for(i=5; i>=0; i--) {
        setTimeout(function() {
            console.log(i===0 ? "GO!" : i);
        }, (5-i)*1000);
    }
}
countdown();

for 루프에서 사용된 i 변수가 하나의 스코프에만 존재하며, setTimeout의 콜백 함수들이 모두 같은 i 변수에 대한 참조를 유지한다는 점입니다. 따라서 setTimeout이 실행될 때까지 for 루프가 이미 완료되고, i 변수의 값은 -1이 됩니다.

새로운 스코프를 만들어 각 setTimeout 콜백 함수에 대해 고유한 i 값을 갖도록 하는 것입니다. 이를 위해 함수를 호출하는 IIFE를 사용할 수 있습니다.

function countdown() {
    let i;
    console.log("Countdown:");
    for(i=5; i>=0; i--) {
        (function(i) {
            setTimeout(function() {
                console.log(i===0 ? "GO!" : i);
            }, (5-i)*1000);
        })(i);
    }
}
countdown();

IIFE(Immediately Invoked Function Expression)의 사용은 주로 스코프와 변수의 유효범위와 관련이 있습니다. 몇 가지 장점은 다음과 같습니다:

  1. 스코프 격리 (Scope Isolation):
  2. 프라이버시 (Privacy):
  3. 변수 이름 충돌 방지 (Avoiding Variable Name Collisions):

예를 들어, 앞서 언급한 코드에서의 IIFE는 i라는 매개변수를 갖고, 각각의 콜백 함수에 대해 새로운 스코프를 만들어 주기 때문에 스코프를 격리하고 변수 충돌을 방지하는 역할을 합니다.

Error-first Callback in Node

  • The convention: use the first argument to a callback to receive an error object to communicate a failure to the callback.
    • callbacks make exception handling difficult
    • If that error is null or undefined, there was no error.
    • No need to process data if there is no need to even reference it
const fs = require('fs');
const fname = 'may_or_may_not_exist.txt';
fs.readFile(fname, function(err, data) {
    if(err) return console.error('error reading file ' + fname + ': ' + err.message);
    console.log(fname + ' contents: ' + data);
});
  • callback_function: It is called after reading of file.
  • First, CHECK if err is truthy. If it is, we report that to the console and immediately return
  • The most overlooked mistake: the programmer will remember to check it, and perhaps log the error, but not return.
    (가장 많이 하는 실수가 error문은 발생시키는데 return을 하지 않는 경우가 많다는 뜻임)
  • Error-first callbacks are the de facto standard in Node development (when promises aren’t being used)

Callback Hell

  • The app needs to get the contents of three different files
  • Then wait 60 seconds before combining the contents of those files and writing to the fourth file
const fs = require('fs');
fs.readFile('a.txt', function(err, dataA) {
    if(err) console.error(err);
    fs.readFile('b.txt', function(err, dataB) {
        if(err) console.error(err);
        fs.readFile('c.txt', function(err, dataC) {
            if(err) console.error(err);
            setTimeout(function() {
                fs.writeFile('d.txt', dataA+dataB+dataC, function(err) {
                    if(err) console.error(err);
                });
            }, 60*1000);
        });
    });
});

Callback Hell with Error Handling

  • The following code seems reasonable, but it won’t work.
    • try...catch blocks only work within the same function.
    • try...catch 블록은 readSketchyFile에 있지만 fs.readFile이 콜백으로 호출하는 익명 함수 내부에 오류가 발생합니다.
const fs = require('fs');
function readSketchyFile() {
    try {
        fs.readFile('does_not_exist.txt', function(err, data) {
            if(err) throw err;
        });
    } catch(err) {
        console.log('warning: minor issue occurred, program continuing');
    }
}
readSketchyFile();

너무 복잡해.. Promise로 해결하자

const fs = require('fs');

function readFileAsync(filename) {
    return new Promise((resolve, reject) => {
        fs.readFile(filename, 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

function writeFileAsync(filename, data) {
    return new Promise((resolve, reject) => {
        fs.writeFile(filename, data, 'utf8', (err) => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

Promise.all([
    readFileAsync('a.txt'),
    readFileAsync('b.txt'),
    readFileAsync('c.txt')
])
.then(dataArray => {
    const [dataA, dataB, dataC] = dataArray;
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const combinedData = dataA + dataB + dataC;
            resolve(combinedData);
        }, 60000); // 60-second delay
    });
})
.then(combinedData => {
    return writeFileAsync('d.txt', combinedData);
})
.then(() => {
    console.log('Files a.txt, b.txt, and c.txt were combined and saved as d.txt');
})
.catch(err => {
    console.error(err);
});

Promise

  • Promises are a more abstract pattern of working with asynchronous code in JavaScript.
    • It is implemented in Node.js (after 0.11.13) as well as ES6
  • Promises don’t eliminate callbacks (you still have to use callbacks with promises).
    • However, it is safer and easier to maintain code => with the predictable manner.
  • A Promise is a placeholder for the result of an asynchronous operation.
    • Instead of subscribing to an event or passing a callback to a function, the function can return a promise
  • Another convenient advantage of promises: they’re just objects, they can be passed around.
  • When you call a promise-based asynchronous function, it returns a Promise instance.
    • The instance starts its life cycle with “pending state” (reading file), i.e. unsettled.
    • Once the asynchronous operation completes, the promise is considered to be settled(종료된, 해결된) and enters(돌아가다) one of two possible states: fulfilled (success) or rejected (failure).
  • So, the internal property is set to pending, fulfilled, or rejected.
    • The result will happen only once (if it’s fulfilled, it’ll only be fulfilled once; if it’s rejected, it’ll only be rejected once).
  • We can take a specific action when a promise changes state by using the then().
  • Two arguments of then() method
    • 1st arg: a function to call when the promise is fulfilled
    • 2nd arg: a function to call when the promise is rejected

Countdown Example (create a promise instance) - Unsettled

function countdown(seconds) {
    return new Promise(function(resolve, reject) { // Anonymous Function (executor)
        for(let i=seconds; i>=0; i--) {
            setTimeout(function() {
                if(i>0) console.log(i + '...');
                else resolve(console.log("GO!"));
            }, (seconds-i)*1000);
        }
    });
}
  • Note that resolve and reject are functions.
    • You may call resolve or reject multiple times or mix them up, but only the first call will count.
    • Promise 객체 생성자(constructor)는 매개변수로 "실행 함수(executor)"를 받습니다. 이 실행함수는 매개 변수로 두 함수를 가져야 하는데, 첫 번째 함수(resolve)는 비동기 작업이 성공적으로 완료되어 결과를 값으로 반환하면 호출되고, 두 번째 함수(reject)는 작업이 실패하여 오류의 원인을 반환하면 호출됩니다. 두 번째 함수는 주로 오류 객체를 받습니다. (from MDN)

여기서 resolve 대신 reject를 호출해도 되고 error 메시지는 다음과 같이 보낸다.

reject(new Error("message"))

Countdown Example (use the promise instance)

countdown(5).then(
    function() {
        console.log("countdown completed successfully");
    },
    function(err) {
        console.log("countdown experienced an error: " + err.message);
    }
);
  • then handler takes two callbacks: the first one is the fulfilled callback, and the second is the error callback. At most, only one of these functions will get called.

Countdown Example (use the promise instance with catch)

const p = countdown(5);
p.then(function() {
    console.log("countdown completed successfully");
}).catch(function(err) {
    console.log("countdown experienced an error: " + err.message);
});
  • Promises also support a catch handler so you can split up the two handlers (we’ll also store the promise in a variable to demonstrate that):

Custom Promise

  • Since Promise is the built-in class, you have to define the custom class to extend Promise.
  • Ex) a conventional structure of custom Promise
class MyPromise {
    constructor() {
        this.promise = new Promise();
    }
    then(onFulfilled, onRejected) {}
    catch(onRejected) {}
}

Generator

  • Generators are functions that use an iterator to control their execution.

    • A regular function takes arguments and returns a value, but otherwise the caller has no control of it.
    • Generators allow you to control the execution of the function
  • Generators are capable of

    • Controlling the execution of a function, having it execute in discrete steps.
    • Communicating with the function as it executes.
  • Generators are like regular functions with two exceptions:

    • The function can yield control back to the caller at any point.(언제든 caller에게 제어권을 돌려줄수 있다.)
    • When you call a generator, it doesn’t run right away. Instead, you get back an iterator. The function runs as you call the iterator’s next method.

Generator (ECMA 6)

  • function* 선언 (끝에 별표가 있는 function keyword) 은 generator function 을
    정의하며, 이 함수는 Generator 객체를 반환합니다.
const gee = function* () { // "Generator { }"
    let index = 0;
    while(index < 3)
        yield index++;
}
const gen=gee()
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // undefined
// ...

gee와 gen의 차이이다.

generatorFunction에서 generator object를 반환해주어야 next 함수를 원활하게 사용할 수 있다.
function declaration 방식을 이용하면 문제가 없지만, 위와 같이 expression 방식을 사용하면 다시 generator object를 반환해주어야 한다는 것을 기억하자.
저기서 gee.next()를 호출하면 next 함수가 없다는 Typeerror가 발생한다.

function* anotherGenerator(i) {
    yield i + 1;
    yield i + 2;
    yield i + 3;
}
function* generator(i){
    yield i;
    yield* anotherGenerator(i);
    yield i + 10;
}
const gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 11
console.log(gen.next().value); // 12
console.log(gen.next().value); // 13
console.log(gen.next().value); // 20
  • yield causes the call to the generator’s next() method to return an IteratorResult object with value and done (whether gen function is completed)
  • yield는 제너레이터 함수 내에서 사용되어, 해당 함수가 호출될 때 next() 메서드가 반환하는 IteratorResult 객체를 생성합니다.

yield 키워드를 통해 값을 반환하고 함수의 실행을 일시 중단할 수 있습니다.

Generator Example

function* rainbow() { // the asterisk marks this as a generator
    yield 'red';
    yield 'orange';
    yield 'yellow';
    yield 'green';
    yield 'blue';
    yield 'indigo';
    yield 'violet';
}
const it = rainbow();
it.next(); // { value: "red", done: false }
it.next(); // { value: "orange", done: false }
it.next(); // { value: "yellow", done: false }
it.next(); // { value: "green", done: false }
it.next(); // { value: "blue", done: false }
it.next(); // { value: "indigo", done: false }
it.next(); // { value: "violet", done: false }
it.next(); // { value: undefined, done: true }
// returns an object with two properties: value (which holds the // “color” you’re now on) and
// done (true after the last one)

yield Expressions and Two-Way Communication

The first call does not log anything, because the generator was not yielding anything initially.

  • two-way communication, between a generator and its caller, happens through the yield expression
    • yield is an expression that must evaluate to something (arguments provided by the caller)
function* interrogate() {
    const name = yield "What is your name?";
    const color = yield "What is your favorite color?";
    return `${name}'s favorite color is ${color}.`;
}
const it = interrogate();
console.log(it.next());
console.log(it.next('Ethan'));
console.log(it.next('orange'));

Generators and return

  • The yield statement by itself doesn’t end a generator, even if it’s the last statement in the generator.
  • Calling return from anywhere in the generator will result in done being true, with the value property being whatever you returned.
function* abc() {
    yield 'a';
    yield 'b';
    return 'c';
}
const it = abc();
console.log(it.next()); // { value: 'a', done: false }
console.log(it.next()); // { value: 'b', done: false }
console.log(it.next()); // { value: 'c', done: true }

generator example에서는 반환값이 undefined가 되어야 done 값이 true가 되었다는 것을 확인했다. 하지만 마지막 값을 반환하면서 done 값을 true로 하고 싶으면 마지막 구문은 yield가 아니라 return으로 작성하면 될 것이다.

Generator

  • Generators are synchronous in nature, but when combined with promises they offer a powerful technique for managing asynchronous code in JavaScript.
  • Asynchronous Programming Dilemma
    • Asynchronous programming 을 통해 높은 성능을 달성할 수 있음
    • 그러나, 프로그래머들은 “순차적 synchronous”으로 생각함
  • Question: Wouldn’t it be nice if you could have the performance benefits of asynchronous without the additional conceptual difficulty?
  • Since generators allow you to effectively pause code in the middle of execution, they open many possibilities related to asynchronous processing

Generator version for Callback Hell Code

const fs = require('fs');
fs.readFile('a.txt', function(err, dataA) {
    if(err) console.error(err);
    fs.readFile('b.txt', function(err, dataB) {
        if(err) console.error(err);
        fs.readFile('c.txt', function(err, dataC) {
            if(err) console.error(err);
            setTimeout(function() {
                fs.writeFile('d.txt', dataA+dataB+dataC, function(err) {
                    if(err) console.error(err);
                });
            }, 60*1000);
        });
    });
});

dataA = read contents of 'a.txt'
dataB = read contents of 'b.txt'
dataC = read contents of 'c.txt'
wait 60 seconds
write dataA + dataB + dataC to 'd.txt'

async / await

async function

  • The async function (and await) is added at ECMA2017

  • The async function declaration defines an asynchronous function, which returns an AsyncFunction object. (async function 선언은 AsyncFunction객체를 반환하는 하나의 비동기 함수를 정의한다)

  • An asynchronous function is a function which operates asynchronously via the event loop to return its result.

    • using an implicit Promise
  • But the syntax and structure using async functions is much more like using standard synchronous functions.

  • An async function can contain an await expression

  • An await expression
    1) pauses the execution of the async function and
    2) waits for the passed Promise's resolution, and then
    3) resumes the async function's execution and
    4) evaluates as the resolved value.

    • Note that while the async function is paused, the calling function continues running (having received the implicit Promise returned by the async function).
  • 주의점: the await keyword is only valid inside async functions. If you use it outside of an async function's body, you will get a SyntaxError (i.e., await keyword는 반드시 async 함수 안에서만 쓰여야 함)

    • async 함수 안에 또 다른 일반 함수가 있고, 그 안에 await이 있으면 안됨
  • Syntax
async function name([param[, param[, ... param]]]) {
    statements
}
  • Async functions can contain zero or more await expressions.
  • await expressions make promise-returning functions behave as though they're synchronous by suspending(중지하다) execution until the returned promise is fulfilled or rejected.
function resolveAfter2Seconds() {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve('resolved');
        }, 2000);
    });
}
async function asyncCall() {
    console.log('calling');
    const result = await resolveAfter2Seconds();
    console.log(result);
// expected output: "resolved“
}
asyncCall();

let resolveAfter2Seconds = function() {
    console.log("starting slow promise");
    return new Promise(resolve => {
        setTimeout(function() {
            resolve(20);
            console.log("slow promise is done");
        }, 2000);
    });
};
let resolveAfter1Second = function() {
    console.log("starting fast promise");
    return new Promise(resolve => {
        setTimeout(function() {
            resolve(10);
            console.log("fast promise is done");
        }, 1000);
    });
};
let sequentialStart = async function() {
    const slow = await resolveAfter2Seconds();
    console.log(slow);
    const fast = await resolveAfter1Second();
    console.log(fast);
}
let concurrentStart = async function() {
    const slow = resolveAfter2Seconds(); // starts timer immediately
    const fast = resolveAfter1Second();
    console.log(await slow);
    console.log(await fast); // waits for slow to finish, even though fast is already done!
}

sequentialStart()

concurrentStart()

0개의 댓글