JavaScript基礎——回撥(callback)是什麼

前端達人發表於2019-01-03

上篇文章《JavaScript基礎——你真的瞭解JavaScript嗎?》,我們明白了JavaScript是一個單執行緒、非阻塞、非同步、解釋性語言,清楚了什麼是單執行緒、程式、阻塞、呼叫堆疊、非同步回撥、任務迴圈等感念,沒看的或者不清楚的建議點選《JavaScript基礎——你真的瞭解JavaScript嗎?》再看一遍,只有理解了,才能輕鬆閱讀理解本篇文章內容。

什麼是callback?

JavaScript 是單執行緒工作,這意味著兩段指令碼不能同時執行,而是必須一個接一個地執行。我們人類是多執行緒工作。您可以使用多個手指打字,可以一邊開車一邊與人交談。唯一一個會妨礙我們的是打噴嚏,因為當我們打噴嚏的時候,所有當前進行的活動都必須暫停。這真是非常討厭,尤其是當您在開車並想與人交談時。您可不想編寫像打噴嚏似的程式碼。JavaScript由於單執行緒限制,防止阻塞,只能通過非同步函式的呼叫方式,把需要延遲處理的事件放入事件迴圈佇列。到目前為止,回撥是編寫和處理JavaScript程式非同步邏輯的最常用方式。說了這麼多,既然回撥這麼重要,到底什麼是回撥(callback)呢?

簡單的定義:回撥就是一個在另外一個函式執行完後要執行的函式
複雜的定義:在JavaScript中,函式是物件。因此函式可以將函式作為引數,並且可以由其他函式進行返回。執行此操作的函式稱為高階函式。任何作為引數傳遞的函式都稱為回撥函式。
為什麼需要回撥?
開篇已經介紹了JavaScript是單執行緒的,需要通回撥函式處理非同步相關的邏輯,理論還是過於生硬,我們還是來看段程式碼吧:

function first(){
console.log(1);
}
function second(){
console.log(2);
}
first();
second();複製程式碼
正如你所料,先執行first函式,再執行second函式,控制檯將輸出以下內容:

1
2複製程式碼
目前看來沒什麼問題,如果first()函式中含有某種無法立即執行的函式呢?例如,我們必須傳送請求然後等待結果響應的API請求?為了模擬API請求,我們可以使用setTimeout函式模擬。我們將函式延遲500毫秒來模擬請求,我們更改後程式碼如下:

function first(){
// Simulate a code delay
setTimeout( function(){
console.log(1);
}, 500 );
}
function second(){
console.log(2);
}
first();
second();複製程式碼
我們將 console.log(1) 延遲500毫秒輸出,這段程式碼會怎麼輸出呢?

2
1複製程式碼
我們希望的順序先執行first,再執行second,但是由於JavaScript是非同步的,所有的延遲處理都要放入迴圈佇列裡,因此事與願違,不能按照我們的希望順序輸出。如果希望這段程式碼按照我們的意願輸出,我們可以使用回撥函式,確保某些程式碼執行完了,在循序執行另外一段程式碼。
建立回撥
說了這麼多,讓我們建立一個簡單的回撥!
我們開啟編輯器,先輸入如下程式碼:

function doHomework(subject) {
alert(`Starting my ${subject} homework.`);
}複製程式碼
上面我們建立了doHomeWork的函式,我們接受一個變數,通過控制檯呼叫,將得到下面的提示:

doHomework('math');
// Alerts: Starting my math homework.複製程式碼
接著,我們開始新增回撥,在doHomework函式中新增一個引數callback,然後在第二個引數中回撥我們定義的函式。程式碼如下:

function doHomework(subject, callback) {
alert(`Starting my ${subject} homework.`);
callback();
}
doHomework('math', function() {
alert('Finished my homework');
});
複製程式碼

正如你希望的,我們在控制檯裡執行上述程式碼,將會受到兩個連續的alert,Starting my math homework,然後彈出 Finished my homework。

但是回撥函式並不是非得在呼叫函式中定義,我們可以單獨定義,修改後的程式碼如下:

function doHomework(subject, callback) {
alert(`Starting my ${subject} homework.`);
callback();
}
function alertFinished(){
alert('Finished my homework');
}
doHomework('math', alertFinished);

複製程式碼

此示例的輸出結果和上段程式碼的結果一致,我們實現了在doHomework函式中呼叫alertFinished,實現了函式作為引數進行傳遞,實現了回撥函式的建立。

用回撥寫一段真實業務場景的程式碼!

例如我們有一個需求,用NodeJs實現從論壇帖子列表中顯示其中的一個帖子的資訊及留言列表資訊,程式碼如下:

DB/posts.json(帖子列表資料)

[
{
"id": "001",
"title": "Greeting",
"text": "Hello World",
"author": "Jane Doe"
},
{
"id": "002",
"title": "JavaScript 101",
"text": "The fundamentals of programming.",
"author": "Alberta Williams"
},
{
"id": "003",
"title": "Async Programming",
"text": "Callbacks, Promises and Async/Await.",
"author": "Alberta Williams"
}
]複製程式碼

DB/comments.json(評論列表)

[
{
"id": "phx732",
"postId": "003",
"text": "I don't get this callback stuff."
},
{
"id": "avj9438",
"postId": "003",
"text": "This is really useful info."
},
{
"id": "gnk368",
"postId": "001",
"text": "This is a test comment."
}
]複製程式碼

Index.js

const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url, callback) {
fs.readFile(url, 'utf8', function(error, data) {
if (error) {
console.log(error);
} else {
return callback(JSON.parse(data));
}
});
}
//return an object by id
function getRecord(collection, id, callback) {
var collectobj=collection.find(function(element){
return element.id == id;
});
callback(collectobj);
return collectobj;
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
return comments.filter(function(comment){
return comment.postId == postId;
});
}
loadCollection(postsUrl, function(posts){
loadCollection(commentsUrl, function(comments){
getRecord(posts, "001", function(post){
const postComments = getCommentsByPost(comments, post.id);
console.log(post);
console.log(postComments);
});
});
});

複製程式碼

大家請注意,我們在loadCollection函式中我們沒有使用try/catch,使用的是if/else,因為catch無法從readFile方法中獲取錯誤。上述程式碼還需要完善,我沒有包含任何錯誤處理。如果在任何步驟中發生錯誤,程式將無法繼續。

錯誤處理是很重要的事情,我們寫程式碼時要認證對待,要嚴格對待,比如我們要編寫一個使用者登入的功能。涉及從網頁表單裡獲取使用者名稱和密碼,查詢我們的資料庫,確認使用者資訊是否正確,驗證通過後,將使用者引導到使用者中心頁面。如果使用者名稱密碼格式不正確,使用者名稱密碼不正確,我們應該將錯誤資訊返回給使用者,並引導使用者重新登入。

總結

很好!我們一起把回撥的內容學完了,理解了什麼是回撥,非同步程式設計是我們的程式碼中使用的一種方法,用於推遲事件以便以後執行。當您處理非同步任務時,回撥是一種解決方案,以便它們按順序執行。

如果我們有多個任務依賴於前幾個任務的結果,那我們就要使用多個巢狀回撥,但是就會引發“回撥地域”(過多的回撥巢狀會使得程式碼變得難以理解與維護),還好Promise解決了“回撥地獄”的問題,讓我們以同步的方式編寫程式碼,小編將會再下篇文章裡進行詳細介紹,敬請期待!

更多精彩內容,請微信關注”前端達人”公眾號!


相關文章