初步講解JS中的callback回調(diào)原理
JS的異步執(zhí)行機(jī)制
什么是異步執(zhí)行
為了提高Javascript代碼的運(yùn)行效率,JS對于部分函數(shù)方法采用了異步調(diào)用機(jī)制(如Ajax的操作)。異步執(zhí)行的函數(shù)方法的執(zhí)行并非為一個隊(duì)列挨個執(zhí)行的,而是相互獨(dú)立,同時調(diào)用執(zhí)行的,從而避免代碼運(yùn)行阻塞,減少不必要的等待時間。

我們來舉一個栗子
大部分新手編程時,都會按照一種線性思維的方法去設(shè)計(jì)代碼,這就跟JS中的異步執(zhí)行機(jī)制相沖突。
如:我們在node中,希望在一個讀取文檔流的操作后,將讀取到的文件中的字符串賦值給變量,str 之后再用 console.log() 方法輸出讀取到的文件內(nèi)容,這時如果按照我們的線性思維去設(shè)計(jì)代碼,會寫出如下的操作:
// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應(yīng)的文件
const fs = require('fs');
const path = require('path');
var str;
// 給定文件路徑,返回讀取到的內(nèi)容
function getFileByPath(fpath) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err)
throw err;
else {
str = dataStr;
}
})
}
// 調(diào)用讀取文件方法
getFileByPath(path.join(__dirname, './files/1.txt'));
console.log(str);
控制臺輸出的結(jié)果為
Undefined
這就是由于我們是按照線性思維去考慮問題,理所當(dāng)然的認(rèn)為變量 str 的賦值操作在 console.log() 操作之前,然而真實(shí)的情況是,JS在被解析之后,可以瞬間執(zhí)行的操作,如 console.log()、for循環(huán) 等基礎(chǔ)操作,都是按照隊(duì)列執(zhí)行的,如:
var test = function () {
console.log('1');
}
test();
for (var i = 2; i < 5; i++) {
console.log(i);
}
console.log('6');
控制臺輸出的結(jié)果為
1
2
3
4
5
6
然而讀取文件操作是一個會導(dǎo)致代碼阻塞的操作,所以JS會將文件讀取操作放置在異步隊(duì)列中,同時執(zhí)行后方代碼。所以栗子中執(zhí)行代碼的流程是執(zhí)行到 getFileByPath() 這一步的時候開啟一個新隊(duì)列,在執(zhí)行 getFileByPath() 方法的同時執(zhí)行后方代碼,但是由于后方代碼執(zhí)行速度比 getFileByPath() 快得多,所以就會導(dǎo)致最終效果為先執(zhí)行了 console.log() 方法,后執(zhí)行了 getFileByPath() 方法。
回調(diào)函數(shù)
那倘若說我們就是需要有一步操作,放在讀取文件之后再執(zhí)行,而不是跳過讀取文件操作直接執(zhí)行,那該怎么辦呢?
這就需要用“回調(diào)函數(shù)”的思想來拯救我們。大部分人都知道回調(diào)函數(shù)在 jQuery 中被發(fā)揮的淋漓盡致,然而新手往往很少知道回調(diào)函數(shù)原理,所以接下來我們?nèi)砸赃@個栗子為代表探討回調(diào)函數(shù)。
我們先拋開回調(diào)函數(shù),用最原始的方法讓一些操作在讀取文件操作后執(zhí)行該怎么辦呢?那就是直接改寫整個 getFileByPath() 方法:
// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應(yīng)的文件
const fs = require('fs');
const path = require('path');
var str;
// 給定文件路徑,返回讀取到的內(nèi)容
function getFileByPath(fpath) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err)
throw err;
else {
str = dataStr;
+ console.log(str);
}
})
}
// 調(diào)用讀取文件方法
getFileByPath(path.join(__dirname, './files/1.txt'));
這樣我們就可以在直行完 getFileByPath 方法之后在控制臺輸出讀取的文件內(nèi)容。但是這樣的操作并不能很好的解決我們的問題,倘若方法被封裝拿給別人使用,其他人需要更改源碼才可以實(shí)現(xiàn)功能方法,很顯然這樣并不靈活,甚至還會更改該方法原有的功能。
所以我們就需要設(shè)置一個回調(diào)函數(shù),在異步操作完成之后,再進(jìn)行我們需要的下一步的操作。
為了理解回調(diào)函數(shù)的原理,我們先將變更后的 if...else... 這一部分代碼分離出來:
if (err)
throw err;
else {
var str = dataStr;
+ console.log(str);
}
可以看出,讀取完文件之后,會直行else下的操作,如果我們把 var str = dataStr; console.log(str) 封裝成一個方法命名為 clg,那我們在 else 之后執(zhí)行 clg() 方法就可以實(shí)現(xiàn)同樣的操作:
function getFileByPath(fpath) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err)
throw err;
else {
clg(dataStr);
}
})
}
function clg(dataStr){
str = dataStr;
console.log(str);
}
getFileByPath(path.join(__dirname, './files/1.txt'));
值得注意的是,我們在調(diào)用 clg() 方法時,對回調(diào)函數(shù)設(shè)置了一個參數(shù) clg(dataStr);
這個 dataStr 就是文件讀取操作讀取的文件內(nèi)容,我們要把讀取內(nèi)容傳入到 clg() 方法中,就需要在調(diào)用
clg() 方法時將其作為參數(shù)寫入,這樣我們才能在 clg() 方法內(nèi)部調(diào)用文件操作讀取出的內(nèi)容,即使用變量
dataStr
這樣我們就可以將讀取文件操作后執(zhí)行的任何操作放在 clg() 方法中就可以執(zhí)行,倘若要做出操作變更,只需要更改 clg() 方法就可以了,不需要去更改 getFileByPath() 方法源碼中的內(nèi)容,這就達(dá)到了我們要的結(jié)果。這個 clg() 實(shí)際上就可以稱之為一個回調(diào)函數(shù),但是這樣還是會讓代碼變得繁雜,操作不方便。
我們來看一下jQuery的回調(diào)函數(shù):
$(’#demo’).animate({“opacity”:“1”}, 1000, fucntion(){… 回調(diào)函數(shù) …});
jQuery將回調(diào)函數(shù)作為一個參數(shù)傳入到方法中,所以我們只要在 getFileByPath() 方法中追加一個參數(shù),這個參數(shù)是一個函數(shù),我們就可以在源碼的 else 后執(zhí)行傳入的這個函數(shù),這個函數(shù)就稱為真正意義上的 “回調(diào)函數(shù)” 了。
改寫后的 getFileByPath() 方法
function getFileByPath(fpath, callback) {
fs.readFile(fpath, 'utf-8', (err, dataStr) => {
if (err)
throw err;
else {
callback(dataStr);
}
})
}
之后我們再在調(diào)用的時候,在參數(shù)位寫入一個方法函數(shù),這個方法就會被傳入getFileByPath()方法內(nèi)部,等文件讀取操作完成之后再直行。
getFileByPath(path.join(__dirname, './files/1.txt'), function(dataStr){
str = dataStr;
console.log(str);
});
這就是對回調(diào)函數(shù)的簡單講解,萌新程序員,歡迎糾錯- ?(?????)