JavaScript:回調模式(Callback Pattern) (轉載)

JavaScript:回調模式(Callback Pattern)
函數就是對象,因此他們能夠做爲一個參數傳遞給其它函數;

當你將introduceBugs()做爲一個參數傳遞給writeCode(),而後在某個時間點,writeCode()有可能執行(調用)introduceBugs();

這種狀況下,introduceBugs()被稱爲回調函數(callback function)或簡稱爲回調(callback:):

 

function writeCode(callback) {  
    // do something...  
    callback();  
    // ...  
}  
function introduceBugs() {  
    // ... make bugs  
}  
writeCode(introduceBugs);  
function writeCode(callback) {
    // do something...
    callback();
    // ...
}
function introduceBugs() {
    // ... make bugs
}
writeCode(introduceBugs);
注意introduceBugs()做爲一參數傳遞給writeCode()是沒有使用括號的;

 

使用括號會當即執行函數,然而在這種狀況下,咱們但願的是隻傳遞一個指向函數的引用,讓writeCode()在適當的時候去執行;

 

一個回調的例子(A Callback Example)
咱們先從一個不使用回調的例子開始,而後在後面重構它;
假如,你有一個通用的函數,它會作一些複雜的工做而且返回一個包含不少數據的集合;
這個通用的函數可能被調用,而且它的工做就是去抓取一個頁面的DOM樹,返回一個數組裏麪包含着你感興趣的頁面元素的數組,好比findNodes();
var findNodes = function() {  
    var i = 100000,  
    // big, heavy loop  
    nodes = [],  
    // stores the result  
    found; // the next node found  
    while (i) {  
        i -= 1;  
        // complex logic here...  
        nodes.push(found);  
    }  
    return nodes;  
};  
var findNodes = function() {
    var i = 100000,
    // big, heavy loop
    nodes = [],
    // stores the result
    found; // the next node found
    while (i) {
        i -= 1;
        // complex logic here...
        nodes.push(found);
    }
    return nodes;
};
將這個函數保持通用性並讓它返回一個DOM節點(node)的數組是個好主意,但沒有對實際的元素作任何事情;
修改節點的邏輯可能在不一樣的函數中,好比一個叫hide()的函數,見名知意,它的做用是從頁面中隱藏節點:
var hide = function(nodes) {  
    var i = 0,  
    max = nodes.length;  
    for (; i < max; i += 1) {  
        nodes[i].style.display = "none";  
    }  
};  
// executing the functions  
hide(findNodes());  
var hide = function(nodes) {
    var i = 0,
    max = nodes.length;
    for (; i < max; i += 1) {
        nodes[i].style.display = "none";
    }
};
// executing the functions
hide(findNodes());
這種實現是沒有效率的,由於hide()不得再也不遍歷一次findNodes()返回的的數組;
若是你能避免這個遍歷而且讓節點在findNodes()中一被選中就隱藏起來會更有效率;
可是如何你在findNodes()實現了隱藏的邏輯,那麼它將再也不是一個通用的函數,由於查詢和修改的邏輯產生了耦合;
加入回調模式——傳遞你隱藏節點的邏輯做爲一個回調函數而且代理它的執行:
// refactored findNodes() to accept a callback  
var findNodes = function(callback) {  
    var i = 100000,  
    nodes = [],  
    found;  
    // check if callback is callable  
    if (typeof callback !== "function") {  
        callback = false;  
    }  
    while (i) {  
        i -= 1;  
        // complex logic here...  
        // now callback:  
        if (callback) {  
            callback(found);  
        }  
        nodes.push(found);  
    }  
    return nodes;  
};  
// refactored findNodes() to accept a callback
var findNodes = function(callback) {
    var i = 100000,
    nodes = [],
    found;
    // check if callback is callable
    if (typeof callback !== "function") {
        callback = false;
    }
    while (i) {
        i -= 1;
        // complex logic here...
        // now callback:
        if (callback) {
            callback(found);
        }
        nodes.push(found);
    }
    return nodes;
};
這樣的實現是簡單明確的,惟一增長的工做就是findNodes()檢查了可選的回調函數是否有被提供,若是有,就執行它;
回調函數是可選的,因此重構後的findNodes()仍然能像之前同樣被使用,而且不會破壞依賴於舊的API的遺留代碼。
 
hide()函數的實現也能夠更加簡單,由於它不須要去遍歷節點數組:
// a callback function  
var hide = function(node) {  
    node.style.display = "none";  
};  
// find the nodes and hide them as you go  
findNodes(hide);  
// a callback function
var hide = function(node) {
    node.style.display = "none";
};
// find the nodes and hide them as you go
findNodes(hide);
 
回調函數能夠是一個在代碼中已經存在的函數,也能夠是一個匿名函數(當你調用主函數的時候纔會建立);
好比,怎樣使用相同的通用函數findNodes()去顯示節點:
// passing an anonymous callback  
findNodes(function (node) {  
    node.style.display = "block";  
});  
// passing an anonymous callback
findNodes(function (node) {
    node.style.display = "block";
});
 
回調和做用域(Callbacks and Scope)
在前面這個例子中,回調函數執行的部分可能像:
callback(parameters);  
callback(parameters);
雖然這樣很簡單而且在不少狀況下都已經足夠了;
但常常有一些場景,回調函數不是匿名函數或者全局函數,而是一個對象的一個方法;
若是回調函數使用this去訪問函數屬於的對象,這就會產生意想不到的錯誤。
 
假若有一個parint()的回調函數,它是myapp對象的一個方法:
var myapp = {};  
myapp.color = "green";  
myapp.paint = function(node) {  
    node.style.color = this.color;  
};  
var myapp = {};
myapp.color = "green";
myapp.paint = function(node) {
    node.style.color = this.color;
};
findNodes()函數作了相似下面的事:
var findNodes = function(callback) {  
    // ...  
    if (typeof callback === "function") {  
        callback(found);  
    }  
    // ...  
};  
var findNodes = function(callback) {
    // ...
    if (typeof callback === "function") {
        callback(found);
    }
    // ...
};
若是你調用了findNodes(myapp.paint),它並不能按照預期的那樣工做,由於this.color將會是undefined;
這裏this將會指向全局對象,由於findNodes()是一個全局函數;
若是findNodes()是一個叫作dom對象的方法,那麼在回調函數中的this將會指向dom而不是指望的myapp;
 
解決這個問題的方法就是傳遞一個回調函數,此外再傳遞這個回調函數屬於的對象做爲一個參數:
findNodes(myapp.paint, myapp);  
findNodes(myapp.paint, myapp);
緊跟着,咱們須要去修改findNodes()去綁定(bind)傳遞進來的對象:
var findNodes = function(callback, callback_obj) {  
    //...  
    if (typeof callback === "function") {  
        callback.call(callback_obj, found);  
    }  
    // ...  
};  
var findNodes = function(callback, callback_obj) {
    //...
    if (typeof callback === "function") {
        callback.call(callback_obj, found);
    }
    // ...
};
 
對於傳遞一個對象和一個被用來回調的方法,另外一個可選的方法就是將方法做爲字符串傳遞,那麼你就不會重複對象兩次;
換言之:
findNodes(myapp.paint, myapp);  
findNodes(myapp.paint, myapp);
會變成:
findNodes("paint", myapp);  
findNodes("paint", myapp);
那麼findNodes()可能會作一些事,就像下面幾行:
var findNodes = function(callback, callback_obj) {  
    if (typeof callback === "string") {  
        callback = callback_obj[callback];  
    }  
    //...  
    if (typeof callback === "function") {  
        callback.call(callback_obj, found);  
    }  
    // ...  
};  
var findNodes = function(callback, callback_obj) {
    if (typeof callback === "string") {
        callback = callback_obj[callback];
    }
    //...
    if (typeof callback === "function") {
        callback.call(callback_obj, found);
    }
    // ...
};
 
匿名的事件監聽器(Asynchronous Event Listeners)
回調模式在平常中被常用,好比,當你附加一個事件監聽器給頁面上的某個元素時,你實際上提供了一個指向了回調函數的引用,而且在事件發生時被調用;
這裏有個例子,怎麼將console.log()做爲一個回調函數監聽文檔的click事件:
document.addEventListener("click", console.log, false);  
document.addEventListener("click", console.log, false);
絕大部分客戶端瀏覽器都是事件驅動的(event-driven);
當一個頁面加載完成,會觸發load事件,而後用戶能夠經過和頁面交互觸發各類各樣的事件,好比:click, keypress, mouseover, mousemove等等; 由於回調模式,JavaScript特別適合事件驅動編程,能讓你的程序異步的工做,換言之,就是不受順序限制。
 
「Don’t call us, we’ll call you」 在好萊塢中是句名言,在好萊塢對於一部電影中的一個角色每每有很候選人,劇組人員不可能一直答覆全部候選人打來的電話;
在異步的事件驅動的JavaScript,有個類似的情景,你提供一個回調函數用於在正確的時候被調用(to be called),而不是電話號碼;
你甚至可能提供比實際請求還要多的回調函數,由於某些事件可能不會發生;
好比:若是用戶不點擊「購買」按鈕,那麼你用於驗證表單格式的函數永遠不會被調用。
 
Timeouts
 
另外一個使用回調模式的例子就是使用瀏覽器的window對象的setTimeout()和setInterval()方法,這些方法也能夠接受和執行回調函數:
var thePlotThickens = function () {  
    console.log('500ms later...');  
};  
setTimeout(thePlotThickens, 500);  
var thePlotThickens = function () {
    console.log('500ms later...');
};
setTimeout(thePlotThickens, 500);
再次注意一下,thePlotThickens是如何被做爲一個參數傳遞的,沒有使用括號;
由於你不想它當即執行;傳遞字符串"thePlotThickens()"取代函數的引用和eval()相似,是很差的模式。
類庫中的回調(Callbacks in Libraries)
回調是一種簡單而強大的模式,當你在設計類庫的時候會派的上用場;
在軟件類庫中的代碼應該儘量的通用和複用,回調能夠幫助咱們解決這種泛化;
你不須要預測和實現你能夠想到的全部功能,由於它們會使類庫膨脹,而且大部分用戶都不會須要這麼多功能;
取而代之的是,集中精力在覈心的功能並提供以回調函數形式的「鉤子」(hook),這會讓類庫的方法更加簡單的去構建,擴展和定製。
相關文章
相關標籤/搜索