Day30:回頭呼喊你的愛情:Callback回呼函式

甚麼是「Callback function」?

MDN的解釋如下:

「回呼函式(callback function)是指能藉由參數(argument)通往另一個函式的函式。它會在外部函式內調用、以完成某些事情。」

我改寫一下MDN上的範例:

function aFunc(name) {
  alert('Hello ' + name);
}

function bFunc(callback) {
  var name = prompt('報名華山論劍大會,請輸入你的名字:');
  callback(name);
}

bFunc(aFunc);

最後呼叫函式 bFunc (aFunc),aFunc是bFunc的callback參數,bFunc的變數name又藉由callback傳入bFunc之中。所以我們可以理解「把A函式當成B函式的參數,透過B函式來呼叫它」,A函式就是一個Callback function。

解釋有點抽象嗎?讓我們換個場景,想想之前提過的「事件監聽」。

例如:看到紅燈,然後踩剎車!踩剎車這個動作,在「看到紅燈」這個條件滿足的時候才執行。所以我們會監聽「看到紅燈」這個事件,一旦事件觸發,就去呼叫「剎車」這個動作(函式)。這也是把「剎車」這個函式當成事件監聽的參數。

還有一個常常會用到的window.setTimeout()也是callback function的經典案例:

window.setTimeout(function(){//do something},1000);

所以我們可以歸納出:

  • 把A函式當成B函式的參數,透過B函式來呼叫它」,A函式就是一個Callback function。
  • 滿足某個條件才去執行的函式,就可以稱為Callback function。

那在甚麼時候適合使用callback function呢?我想是在「控制函式執行的時機」的情境下適合使用:

  • 滿足條件,才去執行的函式。
  • 控制函式執行的先後順序。

假設郭靖跟歐陽克都中了毒:

let poisonA = function(){
	alert('歐陽克中毒身亡!');
};

let poisonB = function(){
 alert('郭靖中毒身亡!');
}

poisonA();
poisonB();

這樣的執行順序當然是先跳(‘歐陽克中毒身亡!’)的視窗,再跳(‘郭靖中毒身亡!’)。但是如果加上一個隨機生成的等待時間,那視窗的彈跳順序就不一定了。

let poisonA = function(){
	var i = Math.random()+1;
	window.setTimeout(function () {
		alert('歐陽克中毒身亡!');
	}, i * 1000)
};

let poisonB = function(){
	var i = Math.random()+1;
	window.setTimeout( () {
	alert('郭靖中毒身亡!');
	}, i * 1000 )
}

poisonA();
poisonB();

有時是(‘郭靖中毒身亡!’)會先跳出來,有時是(‘歐陽克中毒身亡!’)會先跳出來!

如果我們想確保(‘歐陽克中毒身亡!’)比(‘郭靖中毒身亡!’)早跳出來,可以這樣寫:

const poisonA = function(callback){
	const i = Math.random() + 1;
	window.setTimeout(function () {
		alert('歐陽克中毒身亡!');
    if (typeof callback === 'function'){
      callback();
    };
	}, i * 1000)  
};

const poisonB = function(){
	const i = Math.random() + 1;
	window.setTimeout (function() {
	alert('郭靖中毒身亡!');
	}, i* 1000 );
}

poisonA( poisonB );

這樣歐陽克就會比郭靖還要早毒發身亡了!

但是如果中毒的人越來越多,一個函式呼叫另一個函式,一層一層包下去,就變成「回呼地獄」了。

再假設另外一個情境:

「王重陽參加華山論劍,只要打敗黃藥師、洪七公、段皇爺與歐陽鋒,就會奪得『武功第一』的封號。但是不用去管王重陽與人決鬥的先後順序,只要與每個人都打過就可以。」

這時候我們可以這樣做:

let fightProcess = []; //設一個空陣列,王重陽每次比武,都push到陣列中
let step = 4;  //王重陽與4個人比武

function fightA () {
  window.setTimeout(function(){
    fightProcess.push('王重陽打敗黃藥師');
    console.log('王重陽打敗黃藥師');
    if (fightProcess.length === step){   
		//比較空陣列fightProcess的長度是否與step相等,如果相等,就執行ightWinner()
      fightWinner();
    }
  },(Math.random()+1) * 1000);
};

function fightB () {
  window.setTimeout(function(){
    fightProcess.push('王重陽打敗洪七公');
    console.log('王重陽打敗洪七公');
    if (fightProcess.length === step){
      fightWinner();
    }
  },(Math.random()+1) * 1000);
}

function fightC () {
  window.setTimeout(function(){
    fightProcess.push('王重陽打敗段皇爺');
    console.log('王重陽打敗段皇爺');
    if (fightProcess.length === step){
      fightWinner();
    }
  },(Math.random()+1) * 1000);
}

function fightD () {
  window.setTimeout(function(){
    fightProcess.push('王重陽打敗歐陽鋒');
    console.log('王重陽打敗歐陽鋒');
    if (fightProcess.length === step){
      fightWinner();
    }
  },(Math.random()+1) * 1000);
}

function fightWinner(){
  console.log('王重陽天下武功第一,人稱「中神通」');
  console.log(fightProcess);
}

fightA();
fightB();
fightC();
fightD();

Promise物件

Promise物件是ES6之後新增的物件,照字面的解釋就是「承諾」,回傳的結果只有兩種:「解決」與「拒絕」。

Promise物件長成這個樣子:

let myPromise = new Promise((resolve, reject) =>{
	resolve('解決');
	reject('拒絕');
})

要在一個函式中使用Promise功能,只要讓它回傳一個Promise物件就行了:

function urPromise(){
	return new Promise((resolve,reject) => {
		//resolve()或reject()
	});
}

Promise還提供了三種方法:

  • .then():依順序串聯執行多個promise功能。
  • Promise.all():直到全部函式都回覆resolve,或其中一個reject,才繼續後面功能
  • Promise.race():只要其中一個函式resolve,不等待其他含式執行,直接行後續動作,

像剛剛那個「王重陽與四大高手比武」的過程就可以這樣寫:

function fightA () {
  return new Promise(function(resolve,reject){
    window.setTimeout(function() {
      console.log('王重陽打敗黃藥師');
      resolve('王重陽打敗黃藥師');
    },(Math.random()+1) * 1000);
  });
}

function fightB () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗洪七公');
      resolve('王重陽打敗洪七公');
    },(Math.random()+1) * 1000);
  });
}

function fightC () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗段皇爺');
      resolve('王重陽打敗段皇爺');
    },(Math.random()+1) * 1000);
  });
}

function fightD () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗歐陽鋒');
      resolve('王重陽打敗歐陽鋒');
    },(Math.random()+1) * 1000);
  });
}

function fightWinner(){
	console.log('王重陽天下武功第一,人稱「中神通」');  
}

//加上.then可以做到依次執行
fightA()
  .then(fightB)
  .then(fightC)
  .then(fightD)
	.then(fightWinner);

我們在呼叫fightA()之後,用.then串接後面要執行的函式,這樣我們就可以做到依順序執行了。

來看看promise.all的情況:

function fightA () {
  return new Promise(function(resolve,reject){
    window.setTimeout(function() {
      console.log('王重陽打敗黃藥師');
      resolve('王重陽打敗黃藥師');
    },(Math.random()+1) * 1000);
  });
}

function fightB () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗洪七公');
      resolve('王重陽打敗洪七公');
    },(Math.random()+1) * 1000);
  });
}

function fightC () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗段皇爺');
      resolve('王重陽打敗段皇爺');
    },(Math.random()+1) * 1000);
  });
}

function fightD () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗歐陽鋒');
      resolve('王重陽打敗歐陽鋒');
    },(Math.random()+1) * 1000);
  });
}

function fightWinner(){
	console.log('王重陽天下武功第一,人稱「中神通」');  
}
//不管fightA(),fightB(),fightC(),fightD()的執行順序,只要都執行了就繼續後面的程式
Promise.all([fightA(),fightB(),fightC(),fightD()])
  .then(fightWinner);

Promise.all()則會等待全部的Promise函式都執行了,才會進行後面的.then函式。

然後是promise.race:

function fightA () {
  return new Promise(function(resolve,reject){
    window.setTimeout(function() {
      console.log('王重陽打敗黃藥師');
      resolve('王重陽打敗黃藥師');
    },(Math.random()+1) * 1000);
  });
}

function fightB () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗洪七公');
      resolve('王重陽打敗洪七公');
    },(Math.random()+1) * 1000);
  });
}

function fightC () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗段皇爺');
      resolve('王重陽打敗段皇爺');
    },(Math.random()+1) * 1000);
  });
}

function fightD () {
  return new Promise((resolve,reject) =>{
    window.setTimeout(() =>{
      console.log('王重陽打敗歐陽鋒');
      resolve('王重陽打敗歐陽鋒');
    },(Math.random()+1) * 1000);
  });
}

function fightWinner(){
	console.log('王重陽天下武功第一,人稱「中神通」');  
}
//只要fightA(),fightB(),fightC(),fightD()其中之一執行,就繼續執行後面的程式
//但是fightA(),fightB(),fightC(),fightD()都會執行,不會取消
Promise.race([fightA(),fightB(),fightC(),fightD()])
  .then(fightWinner);

Promise.race就如同「競賽」一樣,只要有其中一個Promise函式先做到,不待其它的Promise函式完成,就直接進行.then後面的程式。但是其他的Promise函式還是會繼續執行,不會取消。


Day30:回頭呼喊你的愛情:Callback回呼函式
https://popeye-ux.github.io/2021/09/30/21-day30-callBack/
作者
POPEYE
發布於
2021年9月30日
許可協議