初步講解JS中的callback回撥原理

EsunR醬發表於2018-12-02

JS的非同步執行機制

什麼是非同步執行

為了提高Javascript程式碼的執行效率,JS對於部分函式方法採用了非同步呼叫機制(如Ajax的操作)。非同步執行的函式方法的執行並非為一個佇列挨個執行的,而是相互獨立,同時呼叫執行的,從而避免程式碼執行阻塞,減少不必要的等待時間。

非同步執行機制

我們來舉一個栗子

大部分新手程式設計時,都會按照一種線性思維的方法去設計程式碼,這就跟JS中的非同步執行機制相沖突。

如:我們在node中,希望在一個讀取文件流的操作後,將讀取到的檔案中的字串賦值給變數,str 之後再用 console.log() 方法輸出讀取到的檔案內容,這時如果按照我們的線性思維去設計程式碼,會寫出如下的操作:

// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應的檔案
const fs = require('fs');
const path = require('path');

// 給定檔案路徑,返回讀取到的內容
function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      var str = dataStr;
    }
  })
}

// 呼叫讀取檔案方法
getFileByPath(path.join(__dirname, './files/1.txt'));

console.log(str);
複製程式碼

控制檯輸出的結果為

ReferenceError: str is not defined

這就是由於我們是按照線性思維去考慮問題,理所當然的認為變數 str 的定義和賦值操作在 console.log() 操作之前,然而真實的情況是,JS在被解析之後,可以瞬間執行的操作,如 console.log()for迴圈 等基礎操作,都是按照佇列執行的,如:

var test = function () {
  console.log('1');
}
test();

for (var i = 2; i < 5; i++) {
  console.log(i);
}

console.log('6');
複製程式碼

控制檯輸出的結果為

1 2 3 4 5 6

然而讀取檔案操作是一個會導致程式碼阻塞的操作,所以JS會將其放置在非同步佇列中,執行後方程式碼,所以栗子中執行程式碼的正確順序應該是先執行console.log() 再執行 getFileByPath()

回撥函式

那倘若說我們就是需要有一步操作,放在讀取檔案之後再執行,而不是跳過讀取檔案操作直接執行,那該怎麼辦呢?

這就需要用“回撥函式”的思想來拯救我們。大部分人都知道回撥函式在 jQuery 中被髮揮的淋漓盡致,然而新手往往很少知道回撥函式原理,所以接下來我們仍以這個栗子為代表探討回撥函式。

我們先拋開回撥函式,用最原始的方法讓一些操作在讀取檔案操作後執行該怎麼辦呢?那就是直接改寫整個 getFileByPath() 方法:

// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應的檔案
const fs = require('fs');
const path = require('path');

// 給定檔案路徑,返回讀取到的內容
function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      var str = dataStr;
+	    console.log(str);
    }
  })
}

// 呼叫讀取檔案方法
getFileByPath(path.join(__dirname, './files/1.txt'));
複製程式碼

這樣我們就可以在直行完 getFileByPath 方法之後在控制檯輸出讀取的檔案內容。但是這樣的操作並不能很好的解決我們的問題,倘若方法被封裝拿給別人使用,其他人需要更改原始碼才可以實現功能方法,很顯然這樣並不靈活,甚至還會更改該方法原有的功能。

所以我們就需要設定一個回撥函式,在非同步操作完成之後,再進行我們需要的下一步的操作。

為了理解回撥函式的原理,我們先將變更後的這一部分程式碼分理出來:

if (err)
      throw err;
    else {
      var str = dataStr;
+     console.log(str);
    }
複製程式碼

可以看出,讀取完檔案之後,會直行else下的操作,如果我們把 var str = dataStr; console.log(str)封裝成一個方法命名為 clg,那我們在 else 之後執行 clg() 方法就可以實現同樣的操作:

function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      clg(dataStr);
    }
  })
}

function clg(dataStr){
	var str = dataStr;
	console.log(str);
}

getFileByPath(path.join(__dirname, './files/1.txt'));
複製程式碼

這樣我們就可以將讀取檔案操作後執行的操作放在 clg 方法中就可以執行,這個 clg() 實際上就可以稱之為一個回撥函式,但是這樣還是會讓程式碼變得繁雜。

我們來看一下jQuery的回撥函式:

$('#demo').animate({"opacity":"1"}, 1000, fucntion(){... 回撥函式 ..});

jQuery將回撥函式作為一個引數傳入到方法中,所以我們只要在 getFileByPath() 方法中追加一個引數,這個引數是一個函式,我們就可以在原始碼的 else 後執行傳入的這個函式,這個函式就稱之為 "回撥函式" 。

改寫後的 getFileByPath() 方法

function getFileByPath(fpath, callback) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      callback(dataStr);
    }
  })
}
複製程式碼

之後我們再在呼叫的時候,在引數位寫入一個方法函式,這個方法就會被傳入getFileByPath()方法內部,等檔案讀取操作完成之後再直行。

getFileByPath(path.join(__dirname, './files/1.txt'), function(dataStr){
	var str = dataStr;
	console.log(str);
});
複製程式碼

值得注意的是,我們在原始碼中設定傳入的引數位時,對回撥函式設定了一個引數

callback(dataStr);

這個dataStr就是檔案讀取操作讀取的檔案內容,我們將個變數傳入在callback()方法中,在呼叫getFileByPath()時寫入的回撥函式中就可以呼叫dataStr這個變數了。

這就是對回撥函式的簡單講解,萌新程式設計師,歡迎糾錯- ̗̀(๑ᵔ⌔ᵔ๑)

JS的非同步執行機制

什麼是非同步執行

為了提高Javascript程式碼的執行效率,JS對於部分函式方法採用了非同步呼叫機制(如Ajax的操作)。非同步執行的函式方法的執行並非為一個佇列挨個執行的,而是相互獨立,同時呼叫執行的,從而避免程式碼執行阻塞,減少不必要的等待時間。

非同步執行機制

我們來舉一個栗子

大部分新手程式設計時,都會按照一種線性思維的方法去設計程式碼,這就跟JS中的非同步執行機制相沖突。

如:我們在node中,希望在一個讀取文件流的操作後,將讀取到的檔案中的字串賦值給變數,str 之後再用 console.log() 方法輸出讀取到的檔案內容,這時如果按照我們的線性思維去設計程式碼,會寫出如下的操作:

// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應的檔案
const fs = require('fs');
const path = require('path');

// 給定檔案路徑,返回讀取到的內容
function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      var str = dataStr;
    }
  })
}

// 呼叫讀取檔案方法
getFileByPath(path.join(__dirname, './files/1.txt'));

console.log(str);
複製程式碼

控制檯輸出的結果為

ReferenceError: str is not defined

這就是由於我們是按照線性思維去考慮問題,理所當然的認為變數 str 的定義和賦值操作在 console.log() 操作之前,然而真實的情況是,JS在被解析之後,可以瞬間執行的操作,如 console.log()for迴圈 等基礎操作,都是按照佇列執行的,如:

var test = function () {
  console.log('1');
}
test();

for (var i = 2; i < 5; i++) {
  console.log(i);
}

console.log('6');
複製程式碼

控制檯輸出的結果為

1 2 3 4 5 6

然而讀取檔案操作是一個會導致程式碼阻塞的操作,所以JS會將其放置在非同步佇列中,執行後方程式碼,所以栗子中執行程式碼的正確順序應該是先執行console.log() 再執行 getFileByPath()

回撥函式

那倘若說我們就是需要有一步操作,放在讀取檔案之後再執行,而不是跳過讀取檔案操作直接執行,那該怎麼辦呢?

這就需要用“回撥函式”的思想來拯救我們。大部分人都知道回撥函式在 jQuery 中被髮揮的淋漓盡致,然而新手往往很少知道回撥函式原理,所以接下來我們仍以這個栗子為代表探討回撥函式。

我們先拋開回撥函式,用最原始的方法讓一些操作在讀取檔案操作後執行該怎麼辦呢?那就是直接改寫整個 getFileByPath() 方法:

// 需求:封裝一個方法,傳入一個路徑,可以讀取相對應的檔案
const fs = require('fs');
const path = require('path');

// 給定檔案路徑,返回讀取到的內容
function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      var str = dataStr;
+	    console.log(str);
    }
  })
}

// 呼叫讀取檔案方法
getFileByPath(path.join(__dirname, './files/1.txt'));
複製程式碼

這樣我們就可以在直行完 getFileByPath 方法之後在控制檯輸出讀取的檔案內容。但是這樣的操作並不能很好的解決我們的問題,倘若方法被封裝拿給別人使用,其他人需要更改原始碼才可以實現功能方法,很顯然這樣並不靈活,甚至還會更改該方法原有的功能。

所以我們就需要設定一個回撥函式,在非同步操作完成之後,再進行我們需要的下一步的操作。

為了理解回撥函式的原理,我們先將變更後的這一部分程式碼分理出來:

if (err)
      throw err;
    else {
      var str = dataStr;
+     console.log(str);
    }
複製程式碼

可以看出,讀取完檔案之後,會直行else下的操作,如果我們把 var str = dataStr; console.log(str)封裝成一個方法命名為 clg,那我們在 else 之後執行 clg() 方法就可以實現同樣的操作:

function getFileByPath(fpath) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      clg(dataStr);
    }
  })
}

function clg(dataStr){
	var str = dataStr;
	console.log(str);
}

getFileByPath(path.join(__dirname, './files/1.txt'));
複製程式碼

這樣我們就可以將讀取檔案操作後執行的操作放在 clg 方法中就可以執行,這個 clg() 實際上就可以稱之為一個回撥函式,但是這樣還是會讓程式碼變得繁雜。

我們來看一下jQuery的回撥函式:

$('#demo').animate({"opacity":"1"}, 1000, fucntion(){... 回撥函式 ..});

jQuery將回撥函式作為一個引數傳入到方法中,所以我們只要在 getFileByPath() 方法中追加一個引數,這個引數是一個函式,我們就可以在原始碼的 else 後執行傳入的這個函式,這個函式就稱之為 "回撥函式" 。

改寫後的 getFileByPath() 方法

function getFileByPath(fpath, callback) {
  fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    if (err)
      throw err;
    else {
      callback(dataStr);
    }
  })
}
複製程式碼

之後我們再在呼叫的時候,在引數位寫入一個方法函式,這個方法就會被傳入getFileByPath()方法內部,等檔案讀取操作完成之後再直行。

getFileByPath(path.join(__dirname, './files/1.txt'), function(dataStr){
	var str = dataStr;
	console.log(str);
});
複製程式碼

值得注意的是,我們在原始碼中設定傳入的引數位時,對回撥函式設定了一個引數

callback(dataStr);

這個dataStr就是檔案讀取操作讀取的檔案內容,我們將個變數傳入在callback()方法中,在呼叫getFileByPath()時寫入的回撥函式中就可以呼叫dataStr這個變數了。

這就是對回撥函式的簡單講解,萌新程式設計師,歡迎糾錯- ̗̀(๑ᵔ⌔ᵔ๑)

相關文章