JavaScript Callback 甘苦談

by 班陳

前言

最近為 JavaScript 的 Callback 所苦
所以這就來個筆記把這個概念給弄清楚明白些!
(雖然說我本來是打算寫 PHP的XD)

從結論來說
Callback 基本概念不難
只是因為在實務上有太多奇怪的 code 會出現搗亂
以致有時就會很神奇的發生

有時候跳 error 跟你說哪裡錯還方便 debug 些
但有時候 javascript 就是不報錯還可以執行 只是哪裡怪怪的
那就真的是OOXX……

JavaScript Callback 是什麼?


就如何字面上表達的
是我在某段 code 裡呼叫了(call)某 function 之後,再回去(back)執行先前的 function
也就是說,寫在前面的 function 不會先執行,而是會等到被呼叫的時候才會
這有助於確保我們想要讓執行順序固定下來的情境

舉例來說
假說我們現在有兩個 function
分別是 funcAfuncB
如果我們想要讓執行的順序定下來是funcA -> funcB的話
就會用到 Callback

情境1: 在 PHP 裡

由於 PHP 原則上是沒有非同步處理的機制
(有關同步與非同步,可以看這篇文章參考)
所以我們來看一下這段 code

<?php

function funcA() {
    $i = rand(0.1,1);
    sleep($i);
    echo('function A');
};

function funcB() {
    $i = rand(0.1,1);
    sleep($i);
    echo('function B');
};

funcA();
funcB();

/**
* result: 
* function A
* function B
*/

隨機生成一個 sleep time 給funcAfuncB
但由於 PHP 是同步處理的
程式會由上到下,一行一行處理
所以結果一定是先A後B
也就是

function A
function B

這種情況下就不會用到callback

情境2: 在 JS 裡

相較於 PHP,JS是非同處理的
所以我們來看一段類似 php 的 code

<script>
var funcA = function(){
  var i = Math.random();
  window.setTimeout(function(){
    console.log('function A');
  }, i * 1000);
};

var funcB = function(){
  var i = Math.random();
  window.setTimeout(function(){
    console.log('function B');
  }, i * 1000);
};

funcA();
funcB();

/**
* result:
* 可能是
* function A
* function B
*
* 也可能是
* function B
* function A
*/

我們一樣也生成一個隨機的 timeout 給funcAfuncB
但在執行後
由於 JS 為非同步處理
也就是說funcB()不會等funcA()處理完才處理
所以最後console.log的結果會是不確定的,可能是

function A
function B

也可能是

function B
function A

這樣就有點為難
假如說我們一定要讓funcA先,funcB後執行的話,那該怎麼辦呢?
這就是Callback上場的時候啦

使用 callback 來解決 JS 非同步處理問題

來個簡單的例子

var funcA = function(callback){ // 我們在funcA裡先代一個callback參數進來
  var i = Math.random();  // 這邊就都一樣
  window.setTimeout(function(){
    console.log('function A');
    if( typeof callback === 'function' ){ // 如果callback是function的話 就執行callback();
      callback();
    }
  }, i * 1000);
};

var funcB = function(){
  var i = Math.random();
  window.setTimeout(function(){
    console.log('function B');
  }, i * 1000);
};

// 將 funcB 作為參數帶入 funcA()
funcA( funcB );

/**
* result:
* 一定是:
* function A
* function B
*/

從上面就看的出來
如果把funcB當作是一個參數丟到funcA裡去,而段funcA裡有定義Callback的話
funcB就會成為一個Callback function
要等到在funcA裡被呼叫的時候,才會被執行

實務操作

code

// 首先我們呼叫post這個function,並傳入相關參數與callback function
post(11, 22, 'Ready', '', function (data){ //區塊A
    console.log(111); // output 就讓它是111吧
}, undefined, 77, 88);

function post(shopId, itemId, status, time, _afterCallback, _errorCallback, isAuto, Hash) { // 區塊B

    var delayTime = 1000;
    // 在post function 裡再寫一個function去call ajax,裡面也有一個completeCallback
    reserveCallback(shopId, itemId, status, function(){ // 區塊C
        $.ajax({
            type: 'POST',
            url: '/entry/ajax/update',
            dataType:'json',
            async:true,
            data: {
                    'shopId':shopId,
                    'itemId':itemId,
                    'status':status,
                    'time':time,
                    'isAuto':isAuto,
                    'Hash':Hash
            },
            success: function(data){
                if (_afterCallback !== undefined){
                    _afterCallback(data);
                }
                console.log(222);
            },
            error: function(ret){
                if (_errorCallback !== undefined){
                    _errorCallback();
                }
                if (_afterCallback !== undefined){ // 關鍵A
                    _afterCallback();
                }
                console.log(333); // 關鍵B
            },
            complete: function(ret){
                console.log(444); // 關鍵C
            }
        });
    }, delayTime);
}

function reserveCallback(shopId, itemId, status, callback, delayTime){ // 區塊D
    console.log(555);
    var closureCallback = callback;
    runReservedTimerId = setTimeout(function(){ // 區塊E
        console.log(666);
        lockCallback = true
        closureCallback();
        delete closureCallback;
    }, delayTime);
}

說明

好的
根據之前的說明
我們的預期會印出來的東西應該是這樣吧!

555
666
333
444
111

但很不幸地,結果卻是

555
666
111
333
444

什麼情況? 不對欸~
Not Correct
為什麼呢?
照這樣子看來,順序是:
區塊D->區塊E->區塊A->區塊C

原因在哪?
關鍵就在於_afterCallback();的位置

關鍵A的地方,我先寫了_afterCallback()
以致於程式又跳回區塊Afunction(data)給執行完後 (印出111)
才會回來關鍵B關鍵C去把這兩個333和444印出來
最後的結果才會是

555
666
111
333
444

很好! 經過此役
希望可以不用再苦於callback之中
希望有幫助到閱讀這篇文章的捧油
有什麼問題也可以留言提問喔!

You may also like

Leave a Comment