How to deal with nested callbacks and avoid 「callback hell」(翻譯)

(渣渣小怪獸翻譯,若是看翻譯不開心能夠看->原文)javascript

js是一門強大語言。有時候,你必須處理另外一個回調中的回調,而這個回調這是另外一個回調中的回調。java

人們形象的描述這種模式爲回調地獄node

它有點像這樣:promise

firstFunction(args, function() {
  secondFunction(args, function() {
    thirdFunction(args, function() {
      // And so on…
    });
  });
});
複製代碼

這是js中的回調。當你看到這樣的嵌套回調, 它可能令你的大腦難以置信,但我不認爲這是「地獄」.這個「地獄」能夠更好的管理,若是你知道怎麼處理它的話。異步

關於回調

我假設當你在閱讀這篇文章的時候你知道回調的概念.若是你不知道, 請閱讀這篇文章, 在咱們繼續往下走以前這篇文章會介紹什麼是回調。在那裏,咱們討論回調是什麼以及爲何在JavaScript中使用它們。async

回調的處理方案

這是四個處理回調地獄的方案函數

  1. 寫註釋
  2. 拆分函數成爲更小的函數
  3. 使用Promise
  4. 使用Async/await

在咱們拆分講解這個解決方案以前, 讓咱們一塊兒構造一個回調地獄. 爲何?由於它真的太抽象了,當咱們看到上面的例子: firstFunction, secondFunciton 和 thirdFunction.咱們讓構造一個真是的回調, 讓咱們的例子更具體。ui

構造一個回調地獄

讓咱們想象, 如今咱們要作一個漢堡.爲了製做一個漢堡, 咱們須要執行如下步驟來達到目的:spa

  1. 獲取配料
  2. 烹飪牛肉
  3. 獲取漢堡的麪包
  4. 把煮好的牛肉放在麪包之間
  5. 供應漢堡

若是這些步驟都是同步的, 你將看到一個函數像下面這樣:翻譯

const makeBurger = () => {
  const beef = getBeef();
  const patty = cookBeef(beef);
  const buns = getBuns();
  const burger = putBeefBetweenBuns(buns, beef);
  return burger;
};
const burger = makeBurger();
serve(burger);
複製代碼

然而, 在咱們的步驟中,咱們不能本身製做漢堡。咱們必須指導助手製做漢堡的步驟。在咱們指示助手以後,咱們必須等待助手完成步驟,而後纔開始下一步。

若是咱們想在js中等待一會再執行, 咱們就須要使用回調。爲了製做漢堡, 咱們不得不先獲取牛肉。咱們只能在獲取牛肉後再煮牛肉。

const makeBurger = () => {
  getBeef(function(beef) {
    // We can only cook beef after we get it.
  });
};
複製代碼

爲了煮牛肉, 咱們須要傳遞牛肉進入cookBeef函數內.不然咱們將沒有東西煮。所以咱們不得不等待牛肉煮熟.

一旦牛肉煮好了,咱們就要獲取麪包

const makeBurger = () => {
  getBeef(function(beef) {
    cookBeef(beef, function(cookedBeef) {
      getBuns(function(buns) {
        // Put patty in bun
      });
    });
  });
};
複製代碼

在咱們獲取麪包後,咱們須要放肉餅在兩個麪包之間。這就是漢堡造成的地方.

const makeBurger = () => {
  getBeef(function(beef) {
    cookBeef(beef, function(cookedBeef) {
      getBuns(function(buns) {
        putBeefBetweenBuns(buns, beef, function(burger) {
            // Serve the burger
        });
      });
    });
  });
};
複製代碼

最終, 咱們能夠提供漢堡了。可是咱們不能從makeBurger中返回burger, 由於這是異步的。咱們須要用一個回調去接收漢堡.

const makeBurger = nextStep => {
  getBeef(function (beef) {
    cookBeef(beef, function (cookedBeef) {
      getBuns(function (buns) {
        putBeefBetweenBuns(buns, beef, function(burger) {
          nextStep(burger)
        })
      })
    })
  })
}
// Make and serve the burger
makeBurger(function (burger) => {
  serve(burger)
})
複製代碼

製做這個回調的例子頗有趣啊

解決方案1: 寫註釋

這個makeBurger回調是簡單易於理解。咱們能夠讀懂。它只是有一些很差看。

若是你第一次讀到makeBurger這個函數, 你可能在想「爲何咱們須要這麼多回調才能製做漢堡呢?這沒有意義!」

在這種狀況下,您須要留下注釋來解釋您的代碼。

// Makes a burger
// makeBurger contains four steps:
// 1. Get beef
// 2. Cook the beef
// 3. Get buns for the burger
// 4. Put the cooked beef between the buns
// 5. Serve the burger (from the callback)
// We use callbacks here because each step is asynchronous.
// We have to wait for the helper to complete the one step
// before we can start the next step
const makeBurger = nextStep => {
  getBeef(function(beef) {
    cookBeef(beef, function(cookedBeef) {
      getBuns(function(buns) {
        putBeefBetweenBuns(buns, beef, function(burger) {
          nextStep(burger);
        });
      });
    });
  });
};

複製代碼

如今,不會是在想「WTF」, 當你看到這個回調地獄, 你能夠理解爲何要用這個方式去寫它了。

解決方案2:將回調拆分爲不一樣的函數

咱們的回調地獄的例子已是一個很棒拆分的例子。讓我給你一步步的拆分代碼, 就會明白爲何我這樣說了。

拿getBeef, 咱們的第一個回調來講, 咱們爲了拿到牛肉不得不先去冰箱。廚房裏有兩個冰箱,咱們須要去右手邊的冰箱拿牛肉。

const getBeef = nextStep => {
  const fridge = leftFright;
  const beef = getBeefFromFridge(fridge);
  nextStep(beef);
};
複製代碼

爲了烹飪牛肉,咱們須要把牛肉放進烤箱裏, 把烤箱開到200度, 而且等20分鐘

const cookBeef = (beef, nextStep) => {
  const workInProgress = putBeefinOven(beef);
  setTimeout(function() {
    nextStep(workInProgress);
  }, 1000 * 60 * 20);
};
複製代碼

如今想象一下, 若是你不得不寫每一個步驟在makeBurger ,那麼這個函數就會很是的龐大。

有關將回調拆分爲較小函數的具體示例,您能夠在個人回調文章中閱讀這一小部分。

解決方案3: 使用Promise

我猜你應該知道什麼是Promise.若是你不知道, 請先閱讀這一篇文章

Promise能讓回調地獄更易於管理.而不是像上面的嵌套回調.你將看到像這樣:

const makeBurger = () => {
  return getBeef()
    .then(beef => cookBeef(beef))
    .then(cookedBeef => getBuns(beef))
    .then(bunsAndBeef => putBeefBetweenBuns(bunsAndBeef));
};
// Make and serve burger
makeBurger().then(burger => serve(burger));
複製代碼

若是你更傾向於單參數風格的promise, 你能把上面的例子轉換成這樣

const makeBurger = () => {
  return getBeef()
    .then(cookBeef)
    .then(getBuns)
    .then(putBeefBetweenBuns);
};
// Make and serve burger
makeBurger().then(serve);
複製代碼

更易讀和管理。

可是問題是, 如何把回調地獄轉換成Promise?

把回調轉成Promise

爲了轉成Promise, 咱們須要爲每一個回調先new一個Promise.當這個回調成功執行,那麼給這個Promise使用resolve返回。或當這個回調失敗或者拋出錯誤的時候,咱們就要使用reject。

const getBeefPromise = _ => {
  const fridge = leftFright;
  const beef = getBeefFromFridge(fridge);
  return new Promise((resolve, reject) => {
    if (beef) {
      resolve(beef);
    } else {
      reject(new Error(「No more beef!」));
    }
  });
};
const cookBeefPromise = beef => {
  const workInProgress = putBeefinOven(beef);
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve(workInProgress);
    }, 1000 * 60 * 20);
  });
};
複製代碼

在咱們的練習中,可能已經爲您編寫了回調函數。若是使用Node,則包含回調的每一個函數將具備相同的語法:

  1. 回調函數將是最後一個參數
  2. 這個回調函數將一直有兩個參數。這些參數都有相同的順序(error是第一個, 隨後是你感興趣的任何東西)
// The function that’s defined for you
const functionName = (arg1, arg2, callback) => {
  // Do stuff here
  callback(err, stuff);
};
// How you use the function
functionName(arg1, arg2, (err, stuff) => {
  if (err) {
  console.error(err);
  }
  // Do stuff
});
複製代碼

若是你的回調已經有了相同的語法, 你可使用像ES6 Promisify or Denodeify 把回調轉換成Promise.若是你使用Node v8.0或者之上, 你可使用util.promisify.

他們三個均可以使用。你能夠選擇任意一個庫來使用。不過,每種方法之間都有細微的差異。我建議你查看它的文檔,瞭解方法。

解決方案4: 使用異步函數(async/await)

爲了使用異步函數, 你首先須要知道兩件事。

  1. 如何把callback轉換爲Promise(就是咱們解決方案三中說的內容)
  2. 怎麼使用異步函數(若是你須要幫助,你能夠讀一下這篇文章

使用異步函數,您能夠編寫makeBurger,就像寫同步同樣!

const makeBurger = async () => {
  const beef = await getBeef();
  const cookedBeef = await cookBeef(beef);
  const buns = await getBuns();
  const burger = await putBeefBetweenBuns(cookedBeef, buns);
  return burger;
};
// Make and serve burger
makeBurger().then(serve);
複製代碼

咱們能夠在這裏對makeBurger進行一項改進。 你能夠同時得到兩個getBuns和getBeef的助手。 這意味着您可使用Promise.all語法, 而後使用await。

const makeBurger = async () => {
  const [beef, buns] = await Promise.all(getBeef, getBuns);
  const cookedBeef = await cookBeef(beef);
  const burger = await putBeefBetweenBuns(cookedBeef, buns);
  return burger;
};
// Make and serve burger
makeBurger().then(serve);
複製代碼

(注意: 你能夠在Promise作到相同的效果, 可是它的語法並非很棒, 不像async/await那麼清晰。)

總結

回調地獄並非像你想象中的那麼可怕.這裏有四種方式管理回調地獄。

  1. 寫註釋
  2. 切分函數爲更小的函數
  3. 使用Promise
  4. 使用async/await
相關文章
相關標籤/搜索