Asynchronous JavaScript

한윤서·2021년 11월 17일
0

Web

목록 보기
3/6

1. Asynchronous programming concept

Asynchronous

Normally javascript is a synchronous language that executes the code block by order after hoisting (hoisting : var, functions are moved up to the top of code in declaration). Therefore a program waits for other function to finish and return until another function runs. This may be inefficient and cause a delay in the execution.

Therefore asynchronous programming provides you with APIs that allow you to run such tasks asynchronously. Asynchronous programming is a means of parallel programming in which a unit of work runs separately from the main application thread and notifies the calling thread of its completion, failure or progress.

Blocking codes

When web app runs in a browser and it executes an intensive chunk of code without returning control to the browser, the browser can appear to be frozen : blocking

Therefore browser is blocked from continuing to handle user input and perform other tasks until the web app returns control of the processor.

This is because Javascript is single threaded

Threads

A thread is basically a single process that a program can use to complete tasks. Each thread can only do a single task at once.

However with multiple cores in computers, Programming languages that can support multiple threads can use multiple cores to complete multiple tasks simultaneously.

Javascript is single threaded

JavaScript is traditionally single-threaded. Even with multiple cores, you could only get it to run tasks on a single thread, called the main thread.

To solbe the inefficiency, initially a concept called web workers were introduced. These allow to send some of the JS processing off to seperate thread to prevent block. However this had 2 problems :

  • Workers werent able to access DOM -> Can't update UI
  • The workers are also synchronous -> Does not notify the completion which leads to confusion when retrieving all info back and use in main thread

Therefore the concept of Asynchronous programming was introduced. Features like Promises allow you to set an operation running, and then wait until the result has returned before running another operation.

2. Asynchronous Javascript

Synchronous Javascript

Synchronous Javascript : When code is executed, the result is returned as soon as the browser can do so.

Asynchronous Javascript : Program deals with codes that take a certain amount of time to get the required data.

Ex)

//Asynchronous callback
function printWithDelay(print, timeout) {
    setTimeout(print, timeout);
}
//Give a delay of 2 seconds using function
printWithDelay(() => console.log('hello 1'), 2000);

//Synchronous callback
function printImmediately(print) {
    print();
}
printImmediately(() => consosle.log('hello 2'));

The following example shows 2 functions.

  • printWithDelay : The first function uses the setTimeout() function to delay the execution of function (mimic the server system were data is received in random time)
  • printImmediately : Direct execution from the browser as it is a synchronous code

Therefore hello2 is printed first compared to hello1 due to the delay.

Another example shows an example to print 3 lists on the screen :

const posts = [
    {title: 'Post One', body: 'This is post one'},
    {title: 'Post Two', body: 'This is post two'},
];

function getPosts() {
    //Give a delay of 1s before executing function
    setTimeout(() => {
        let output = '';
        posts.forEach((post, index) => {
            output += `${post.title}`;
        });
        console.log(output);
    }, 1000);
}

function createPost(post) {
    //Give a delay of 2s before executing function
    setTimeout(() => {
        posts.push(post);
    }, 2000);
}

getPosts();
createPost({title : 'Post Three', body : 'This is post three'});

The following function has 2 functions getPosts() and createPost() which each has a delay of 1s and 2s. The code defines 2 objects initially and uses createPost() to add another object. However in the screen, only 2 lists are shown. This is because getPosts() takes a shorter time to execute and has already finished and shown result on the console while createPost() is still executing. This would require asynchronous programming to fix the problem.

To use asynchronous programming in JS, there are 2 ways :

  • Async callbacks
  • Promises

Async callbacks

Async callbacks are functions that are specified as arguments when calling a function which will start executing code in the background. When the background code finishes running, it calls the callback function to let you know the work is done, or to let you know that error has happened.

For the previous example, using callbacks to solve the problem is possible by :

//getPosts() defined above (ignored for space convinience)
function createPost(post, callback) {
    //Give a delay of 2s before executing function
    setTimeout(() => {
        posts.push(post);
        //Execute the function that has been received in parameter
        callback();
    }, 2000);
}

createPost({title : 'Post Three', body : 'This is post three'}, getPosts);

The getPosts() function has been passed as a callback function and therefore executes in the background till it reaches the result. Therefore the additional object is now able to be added in the array before being printed in console.

Callbacks are versatile

  • They allow you to control the order in which functions are run and what data is passed between them
  • They also allow you to pass data to different functions depending on circumstance.

A following is an example regarding id inputs and password inputs. It uses callbacks to clarify if id and password is correct :

class UserStorage {
    loginUser(id, password, onSuccess, onError) {
        setTimeout(() => {
            if(
                (id === 'Yoonseo' && password === 'dream') ||
                (id === 'jiwon' && password === 'hello')
            ) {
                onSuccess(id);
            } else {
                onError(new Error('not found'));
            }
        }, 2000);
    }

    getRoles(user, onSuccess, onError) {
        setTimeout(() => {
            if(user === 'Yoonseo') {
                onSuccess({name : 'yoonseo', role : 'CEO'});
            } else {
                onError(new Error('no access'));
            }
        }, 1000);
    }
}

//Using too much callback
const userStorage = new UserStorage();
const id = prompt('enter your id');
const password = prompt('enter your password');

userStorage.loginUser(
    id, 
    password, 
    (user) => { //When user inputs correct input : onSuccess(id) callback function activated (pass id)
        userStorage.getRoles(
            user, //The user becomes the first parameter in callback function
            (userWithRole) => { //check if user name is correct again and if so assign name and role using callback function
                alert(`Hello ${userWithRole.name}, your role is ${userWithRole.role}`);
            }, 
            (error) => {
                console.log(error);
            })
    }, 
    (error) => {    //When error in input, callbackfunction onError() with error object passed as parameter
        console.log(error);
    }
);
  • The following example uses the loginUser in the main to input the user id and password
  • Based on the input of id and password, if it is correct, a callback function getRoles() is called and the id is passed as a parameter
  • The callback function then runs in the background with its given user id. It does another clarification and if user id is correct, it assigns an object giving name and role
  • The followng name and role is printed out in the console and function finishes.
  • For both loginUser and getRoles(), there is an error handling function that deals with errors caused

However the following code is too complicated and has a low readability. Therefore a new concept called Promise has been introduced

Promises

Promise is a Javascript object for asynchronous operation. Promise enbales the asynchronous code to look more like a synchronous program by making the codes more simpler.

Promise initially is an object that represents an intermediate state of an operation - In effect a promise that a result of some kind will be returned at some point in future.

It has 3 states as a property

  • pending : The asyncrhonous process is not finished and ongoing
  • fulfilled : The asynchronous process has been successfully finished
  • rejected : The asynchronous process has failed and an error occured. Returns an error message stating why promise was rejected

Promise has 2 types depending on the role it has to take
1) Producer : When new promise is created, the executor runs automatically
The producer takes a callback function that has 2 arguments

  • resolve : When the async function has successfully finished the task and return data
  • reject : When an error has occured during the process of getting data

2) Consumer : Use the data received from the producer by codes like then, catch, finally

//Producer
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('Yoonseo');
        reject(new Error('no network'));
    }, 2000);
})

//Consumer
promise
    .then(value => {
        console.log(value);
    })
    .catch(error => {
        console.log(error);
    })
    .finally(() => {
        console.log('finsihed');
    });

In the consumer, each code has a different role

  • then() : Contain a callback function that will run if previous operation is successful(normally does error check in back-end).
    Each callback receives an input which is the result of previous successful opeation -> can continue more asynchronous operation (promise chain)
  • catch() : Runs if any of the then() blocks fail
  • finally() : Runs always regardless of whether the operation is successful or not

Promise chaining

As mentioned previously, then() is able to receive data and deal with it received from promise, but also get multiple other promise to be able to asyncrhonously deal with it.

const fetchNumber = new Promise((resolve, reject) => {
    setTimeout(() => resolve(1), 1000);
});

fetchNumber
    .then(num => num*2)
    .then(num => num*3)
    .then(num => {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(num-1), 1000);
        });
    })
    .then((num) =>  console.log(num));

A series of .then() has been chained together. Also there is an additional promise() that has been instantiated in which the result is linked with the original promise

Note : each .then() returns a newly generated promise object automatically, which can be used for chaining

Error handling

Error can be handled using catch(). The following example uses

const getHen = new Promise((resolve, reject) => {
        setTimeout(()=>resolve('Chicken', 1000));
    });

const getEgg = (hen) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${hen} => Egg`), 1000);
        //setTimeout(() => reject(`Error has occured!`), 1000);
    });
};

const cook = ((egg) => {
    return new Promise((resolve, reject) => {
        //setTimeout(() => resolve(`${egg} => cook done`), 1000);
        setTimeout(() => reject('Error!'), 1000);
    })
});

getHen
    .then(hen => getEgg(hen))
    .catch(error => {
        return 'Hamburger';
    })
    .then(egg => cook(egg))
    .catch(error => {
        return 'No food';
    })
    .then(meal => console.log(meal))
    .catch(event => console.log(event));
  • There are 3 promise objects instantiated and returned. Each has both resolve() and reject()
  • As front-end can't control server we use comments to select whether a certain promise operation is successfully activated or not
  • Also use setTimeout() for each promise instantiation to mimic the server operation
  • For each promise, then() and catch() is defined to deal with both success and failure cases
  • At the end, .catch() is defined to act as the default error operation : Generally noticing the user an error has occured
  • If an error has occured during one of the specific promise operations, .catch() returns an alternative to allow operations to act atleast similar to main operations objective

Using promise to improve id & password example

class UserStorage {
    loginStorage(id, password) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if(
                    (id === 'Yoonseo' && password === 'yooncerjiwon') ||
                    (id === 'yoonseo' && password === 'yooncerjiwon') ||
                    (id === 'you' && password === '123')
                ) {
                    resolve(id);
                } else {
                    reject(new Error('not found'));
                }
            }, 2000);
        });
    }

    getRoles (user) {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                if(user === 'yoonseo' || user === 'Yoonseo') {
                    resolve({name : 'Yoonseo', role : 'CEO'});
                } else {
                    reject (new Error('cant find role'));
                }
            });
        }, 1000);
    }
}

const userStorage = new UserStorage();
const id = prompt('Enter your id');
const password = prompt('Enter password');

userStorage
    .loginStorage(id,password)
    .then(id => userStorage.getRoles(id))
    .catch(error => console.log(error))
    .then(user => alert(`Hello ${user.name}, you are the ${user.role}`))
    .catch(error => console.log(error));

The overall structure mimics the structure of synchronous programming, making the code much more cleaner. Also better error handling can be done by using promise.

Using promise and fetch to get image

//Pass the URL of image to fetch from the network as parameter
fetch('/practice.jpeg')
    .then(response => {
        if(!response.ok) {
            throw new Error(`HTTP error, status : ${response.status}`);
        }
        else {
            return response.blob();
        }
    })
    .then(blob => {
        let objectURL = URL.createObjectURL(blob);
        let image = document.createElement('img');
        image.src = objectURL;
        document.body.appendChild(image);
    })
    .catch(error => {
        console.log(`There has been a problem with fetch operation : ${error.message}`);
    });

The following operation uses fetch() to input an image on the website. promise is used to deal with the fetch API.
Note that the fetch() API returns a promise object as a result and therefore additional instantiation of promise object is not required

  • It first checks if the response has been done successfully, and if not throws an error. Returns the blob if it is successful
  • The returned blob is then added to HTML
  • There is a catch() to check for any error during the process

Running code in response to multiple promise fulfilling

When we want to run some code only after multiple promises have all fulfilled, we would be able to use the Promise.all() static method.

For example when we’re fetching information to dynamically populate a UI feature on our page with content, you would need to receive all the data and only then show the complete content, rather than displaying partial information.

//fetch multiple image 
//Show all images at once when all of the fetch is done

//function to return the fetch promise based on URL input
function fetchAndDecode(url, type) {
    //Add return to be able to directly return the final result (promise returned by blob() or text())
    return fetch(url).then(response => {
        if(!response.ok) {
            throw new Error('HTTP error! status : '+ response.status);
        }
        //Various types of resources may exist
        //return different type of response based on resource type
        else {
            if(type === 'text') {
                return response.text();
            }
            else if(type === 'blob') {
                return response.blob();
            }
        }
    })
    .catch(error => {
        console.log(`Error has occured during operation for resource ${url}`);
    })
}

//Call the function individually to begin process of fetching and decoding the images
//Individual call allows faster operation as each call is independent to each other
//They do not wait for completion of each fetch operation
let practice = fetchAndDecode('/practice.jpeg', 'blob');
let practice1 = fetchAndDecode('/practice1.jpeg', 'blob');
let practice2 = fetchAndDecode('/practice2.jpeg', 'blob');

//Promise.all -> ensures the code runs only if all of the promise calls are finished
//The resulted individual promises are passed as an array to parameter
Promise.all([practice, practice1, practice2]).then(values => {
    console.log(values);
    // Store each value returned from the promises in separate variables; create object URLs from the blobs
    let objectURL1 = URL.createObjectURL(values[0]);

    // Display the images in <img> elements
    // Done only 1 image for convinience
    let image1 = document.createElement('img');
    image1.src = objectURL1;
    document.body.appendChild(image1);
});

The following program ensures that the execution of adding images to html page is not done only until all of the 3 images have finished fetching

Advantage of promise

Promises are similar to callbacks but in a different syntax. Promises are essentially a returned object to which you attach callback functions, rather than having to pass callbacks into a function. The advantages are

  • Able to chain multiple async operation by using .then() operations
  • Promise callbacks are always called in the strict order they are placed in the event queue.
  • Error handling is much better
  • Promises avoid inversion of control, unlike old-style callbacks, which lose full control of how the function will be executed when passing a callback to a third-party library

Nature of Asynchronous code

JavaScript is a synchronous, blocking, single-threaded language, in which only one operation can be in progress at a time. But web browsers define functions and APIs that allow us to register functions that should not be executed synchronously, and should instead be invoked asynchronously when some kind of event occurs (the passage of time, the user's interaction with the mouse, or the arrival of data over the network, for example). This means that you can let your code do several things at the same time without stopping or blocking your main thread.

3. Simplify code with async / await

Syntactic sugar built on top of promises that allows you to run asynchronous operations using syntax that's more like writing synchronous callback code.

Basic syntax of async / await

async

The async keyword is put infront of a function declaration to turn it into an async function. The syntax makes the function activate the async characteristics -> Their return values are guaranteed to be converted to promises

 function fetchUser() {
    return new Promise((resolve, reject) => {
        resolve('Yoonseo');
    });
 }

async function fetchUser() {
    return 'Yoonseo';
}

The first one returns a promise() object instantiated by using the new keyword. The second one does the same function as first one, however does not require an instantiation as the async keyword automatiacally convert the returned value to a promise object.

It is also available to create an async function expression :

//Both are actually the same written in different syntax
let hello = async function() {return 'hello'};
let hello = async () => "Hello";

await

The async comes with the await keyword. await can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.

function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve,ms));
}

function getString() {
    return delay(3000)
    .then(() => 'string');
}

async function getString() {
    await delay(3000);
    return 'string';
}

The 2 string functions do the same function however written in different syntax. The use of await is async function discards the requirement of using .then

Rewriting promise code with async/await

Use the try/catch arround the logic. The catch() deals with the resulted errors

Ex)

const findAndSaveUser = async (User) => {
    try {
      let user = await Users.findOne({});
      user.name = 'zero';
      user = await user.save();
      user = await Users.findOne({gender :'m'});
    } catch (error) {
      console.log(error);
    }
};

#1 Example

For a code that showss the relationship between a baby and grown up, asyncrhonous programming is required as baby must always be executed first, then grown up. Therefore control of flow is required :

async function getBaby() {
    await delay(2000);
    return 'baby';
}

async function getHuman() {
    await delay(2000);
    return 'Human';
}

function getPerson() {
    return getBaby().then(baby => {
        return getHuman().then(human => {console.log(`${baby} => ${human}`)});
    });
}

async function getPerson() {
    const baby = await getBaby();
    const human = await getHuman();
    return `${baby} => ${human}`
}

getPerson().then((result) => console.log(result));

The two getPerson() does the same function however the async function has a much more clear syntax by the use of await.

Improvement by parallel processing

However there is improvement that can be made for the async function. This is because the getBaby() and getHuman() is independent to each other and therefore getHuman()does not have to wait for the getBaby() to finish executing within the getPerson().

Initially due to the await, the overall process took 4s in total as getBaby() had to finish inorder to getHuman() to execute. With the call of each function storing them in seperate variables and connecting the variables back to main variable using await allows the overall time to decrease for efficiency and keep the overall flow of coding.

async function getPerson() {
    const babyPromise = getBaby();
    const humanPromise = getHuman();
    const baby = await babyPromise;
    const human = await humanPromise;
    return `${baby} => ${human}`
}

By storing the 3 Promise objects in variables, it has the effect of setting off their associated processes all running simultaneously.

Improvement by error handling

It is available to use synchronous try...catch structure with async/await. The catch() {} block is passed an error object and specifies the function to take when an error has happened.

async function getPerson() {
    try {
        const baby = await getBaby();
        const human = await getHuman();
        return `${baby} => ${human}`
    } catch(error) {
      console.log(error);
    }
}

#2 Example

The previous example using fetch() to bring image to HTML page can be improved in readability by mixing promise with async and await

async function myFetch() {
    let response = await fetch('/practice.jpeg');
    if(!response.ok) {
        throw new Error(`HTTP error, status : ${response.status}`);
    }
    return await response.blob();
}

myFetch().then((blob) => {
    let objectURL = URL.createObjectURL(blob);
    let image = document.createElement('img');
    image.src = objectURL;
    document.body.appendChild(image);
}).catch(error => console.log(error));

All of the .then() blocks can be replaced by using the await keyword before the method call, and then assign result to variable

Awaiting Promise.all()

It is possible to use the Promise.all() to be able to get all the results returned into a variable in a way that looks like synchronous code.

The previous example of getting multiple images using fetch() can be improved by

let values = await Promise.all([coffee, tea, description]);

By using await here we are able to get all the results of the three promises returned into the values array, when they are all available.

profile
future eo

0개의 댓글