On Callback to Hell

Posted by noclist on Tue, 25 Jun 2019 23:07:56 +0200

Callback to Hell

JavaScript Asynchronous Program Guide

What is "Callback Hell"?

It's hard to understand asynchronous JavaScript at a glance, or JavaScript programs that use callback functions. For example, the following code:

fs.readdir(source, function (err, files) {
  if (err) {
    console.log('Error finding files: ' + err)
  } else {
    files.forEach(function (filename, fileIndex) {
      console.log(filename)
      gm(source + filename).size(function (err, values) {
        if (err) {
          console.log('Error identifying file size: ' + err)
        } else {
          console.log(filename + ' : ' + values)
          aspect = (values.width / values.height)
          widths.forEach(function (width, widthIndex) {
            height = Math.round(width / aspect)
            console.log('resizing ' + filename + 'to ' + height + 'x' + height)
            this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
              if (err) console.log('Error writing file: ' + err)
            })
          }.bind(this))
        }
      })
    })
  }
})

This pile of pyramids ending with} is what we cordially call "Callback Hell".

Callback hell occurs because we write JavaScript visually from top to bottom. Many people made this mistake! In other languages such as C, Ruby or Python, the first line of code must have run out before the second line of code runs. However, as mentioned later, JavaScript is different.

What is a callback function?

Callback function is a kind of function which is conventionally formed by js. In fact, there is no definite "callback function", but we just take care of the function at that location as the callback function. Unlike most functions that give results immediately after running, using callback functions takes some time to get results. The word "asynchrony" means "take time, run in the future."
". Usually callback functions are used to download files, read files, or database-related transactions.

When you call a normal function, you get its value immediately:

var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 50 gets printed out

The callback function does not get immediate feedback.

var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo is 'undefined'!

At this time, this gif may take a long time to download, you can't let the program stop and wait for it to download.

Instead, you can save the code triggered after downloading to a function function, which is called a callback! Write a downloadPhoto function. When he downloads successfully, he runs the callback function.

downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
  if (error) console.error('Download error!', error)
  else console.log('Download finished', photo)
}

console.log('Download started')

The hardest part of understanding callbacks is understanding the sequence in which the program runs. Three main events occurred in the example. First, the handlePhoto function was declared, then called as a callback function by the downloadPhoto function, and finally the console printed'Download started'.

Note that handlePhoto has not yet been called, it is only created and then the most callback function is passed into downloadPhoto. He won't run until download Photo finishes downloading.

This example illustrates two problems:

  • The handlePhoto callback function simply stores what will be running.

  • Don't read the program from top to bottom, it will jump according to the completion of the work.

How to repair callback hell?

1. Write the code a little shallower

Here are some browser-side code for AJAX:

var form = document.querySelector('form')
form.onsubmit = function (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

This code has two anonymous functions to give them a name!

var form = document.querySelector('form')
form.onsubmit = function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, function postResponse (err, response, body) {
    var statusMessage = document.querySelector('.status')
    if (err) return statusMessage.value = err
    statusMessage.value = body
  })
}

You see, naming functions are simple, but they have many advantages:

  • With a name, it's easy to know what this code does.

  • When a console debugging error occurs, the console will tell you which function is wrong, not an anonymous function.

  • It allows you to move these functions to the right place and call them with the function name when you use them.

Now we all write to the outermost layer of the program:

document.querySelector('form').onsubmit = formSubmit

function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

Note that the function declaration is at the bottom, but it can still be called, thanks to the function promotion.

2. Modularization

With the example above, we will split it into multiple files.

Create a new file formuploader.js containing the first two functions:

module.exports.submit = formSubmit

function formSubmit (submitEvent) {
  var name = document.querySelector('input').value
  request({
    uri: "http://example.com/upload",
    body: name,
    method: "POST"
  }, postResponse)
}

function postResponse (err, response, body) {
  var statusMessage = document.querySelector('.status')
  if (err) return statusMessage.value = err
  statusMessage.value = body
}

module.exports come from the module system of node.js. They can be used in node, Electron and browser. I like this style very much because it works everywhere and is easy to understand without relying on other complex settings.

We got formuploader.js, just require it!

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit

At present, our application has only two lines, which have the following advantages:

  • It's easy for new developers to understand that they won't get bogged down in reading all the formuploader functions.

  • formuploader does not need to copy and paste code, just download the shared code in github or npm.

3. Deal with every error

There are several common errors

  • Syntax Error (Run Failure)

  • Runtime errors (runnable but bug gy)

  • Platform errors (file permissions, disk issues, network issues)

The first two rules are mainly to improve the readability of your code, and this one is to make your code more stable. When dealing with callbacks, you will process the tasks sent according to the definition, perform some operations in the background, and finally successfully complete or fail to abort.
Any experienced developer will tell you that you will never know when these errors occurred, so you must have a solution when your problems arise.

The hottest callback error handling is Node.js style, which means that the first parameter of the callback function is always the wrong parameter.

 var fs = require('fs')

 fs.readFile('/Does/not/exist', handleFile)

 function handleFile (error, file) {
   if (error) return console.error('Uhoh, there was an error', error)
   // otherwise, continue on and use `file` in your code
 }

The first parameter is that error is a simple consensus that reminds you that you have to deal with your errors. If it's the second parameter, it's easy to write the code as function handleFile (file) {} and then forget to handle the error.
Code normalization tools can also remind you to handle callback errors, one of the easiest ways is to use standard . Just running $standard in your file directory checks if your code is missing error handling.

summary

  1. Don't nest functions, it's better to call them after they're named.

  2. Use function elevation.

  3. Handle every error in the callback function.

  4. Create reusable functions and write modules to make it easier for you to read the code. Breaking your code into small chunks can help you deal with errors, write tests, refactor, and write more stable API s for your code.

The most important aspect of avoiding callback hell is moving functions so that the process can be understood more easily, and the successor programmer can know the function of the program without looking through the entire file.

You can start by moving the function to the bottom, then learn to write the function into the module file, and then use require to introduce it.

Some writing module experience:

  • Write a function of frequently reused functions first

  • When the function is large enough, move it to another file, expose it with module.exports, and introduce it with require

  • If your code is generic, you can write readme files and package.json and publish it to npm or github

  • A good module, small, and for only one problem

  • A single file in a module should not exceed about 150 lines

  • Modules should not have multiple levels of nested folders that contain JavaScript files. If so, it may have done too much.

  • Let experienced programmers introduce you to some useful modules and try to understand the function of this module. If it takes a few minutes, this module may not be good enough.

About promise/generator/ES6?

Before looking at more advanced solutions, remember that callbacks are a basic part of JavaScript (because they are just functions). You should learn how to read and write them, and then move to more advanced language functions, because they depend on understanding callbacks. If you can't write maintainable callback code, please continue to work hard!

If you really want your asynchronous code to "read from top to bottom", you can try these wonderful methods. Note that these functions will have compatibility problems in different platforms. Please investigate before using them.

Promise is a way to write callback functions from top to bottom, which encourages you to use try/catch to handle more types of errors.

Generator lets you "pause" a function (without pausing the whole program), and it also lets you write asynchronous functions from top to bottom, but at the cost of a bit of complexity and incomprehensibility. wat That's how it works.

Async functions is a feature of ES7. It's a more advanced package for generators and promise s. I'm interested in Google for myself.

Personally, I use callback functions to handle 90% of the asynchronous code. When things get complicated, I rely on libraries, such as run-parallel perhaps run-series . I don't think it's helpful for me to study other ways to call back vs promise vs. The most important thing is to keep the code simple, non-nested and divided into small modules.

Whatever method you choose, always handle every error and keep the code simple.

Remember, only you can prevent callbacks to hell and forest fires.

Original: http://callbackhell.com/

Topics: Javascript github npm Ruby