JavaScript異步與回調(diào)
一、前言
首先我們要記住的是異步和并行有著本質(zhì)的區(qū)別。
并行,簡單來說是一般指并行計算,就是說同一時刻有多條指令同時被執(zhí)行,這些指令可能執(zhí)行于同一CPU的多核上,或者多個CPU上,或者多個物理主機甚至多個網(wǎng)絡中。
同步,一般指按照預定的順序依次執(zhí)行任務,只有當上一個任務完成后,才開始執(zhí)行下一個任務。
異步,與同步相對應,異步指的是讓CPU暫時擱置當前任務,先處理下一個任務,當收到上個任務的回調(diào)通知后,再返回上個任務繼續(xù)執(zhí)行,整個過程無需第二個線程參與。
文字表述很讓人很費解,現(xiàn)在就用圖片形式來表達并行、同步和異步更為直觀,假設(shè)現(xiàn)在有A、B兩個任務需要處理,使用并行、同步和異步的處理方式會分別采用如下圖所示的執(zhí)行方式:
二、異步函數(shù)
JavaScript為我們提供了許多異步的函數(shù),這些函數(shù)允許我們方便的執(zhí)行異步任務,也就是說,我們現(xiàn)在開始執(zhí)行一個任務(函數(shù)),但任務會在稍后完成,具體完成時間并不清楚。
例如,setTimeout函數(shù)就是一個非常典型的異步函數(shù),此外,fs.readFile、fs.writeFile同樣也是異步函數(shù)。
我們可以自己定義一個異步任務的案例,例如自定義一個文件復制函數(shù)copyFile(from,to):
const fs = require('fs') function copyFile(from, to) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') }) }) }
函數(shù)copyFile首先從參數(shù)from讀取文件數(shù)據(jù),隨后將數(shù)據(jù)寫入?yún)?shù)to指向的文件。
下面展示的就是調(diào)用copyFile:
copyFile('./from.txt','./to.txt')//復制文件
現(xiàn)在要注意這個節(jié)點,copyFile(...)后面還有其他代碼,那么程序不會等待copyFile執(zhí)行結(jié)束,而是直接向下執(zhí)行,文件復制任務何時結(jié)束,程序并不關(guān)心。
copyFile('./from.txt','./to.txt') //下面的代碼不會等待上面的代碼執(zhí)行結(jié)束 ...
到目前為止是正常的,可后面,如果我們在copyFile(...)函數(shù)后,直接訪問文件./to.txt中的內(nèi)容會發(fā)生什么呢?
這將不會讀到復制過來的內(nèi)容,就行這樣:
copyFile('./from.txt','./to.txt') fs.readFile('./to.txt',(err,data)=>{ ... })
如果在執(zhí)行程序之前,./to.txt文件還沒有創(chuàng)建,將得到如下錯誤:
PS E:\Code\Node\demos\03-callback> node .\index.js
finished
Copy finished
PS E:\Code\Node\demos\03-callback> node .\index.js
錯誤:ENOENT: no such file or directory, open 'E:\Code\Node\demos\03-callback\to.txt'
Copy finished
即使./to.txt存在,也無法讀取其中復制的內(nèi)容。
造成這種現(xiàn)象的原因是:copyFile(...)是異步執(zhí)行的,程序執(zhí)行到copyFile(...)函數(shù)后,并不會等待其復制完畢,而是直接向下執(zhí)行,從而導致出現(xiàn)文件./to.txt不存在的錯誤,或者文件內(nèi)容為空錯誤(如果提前創(chuàng)建文件)。
三、回調(diào)函數(shù)
異步函數(shù)無法確定結(jié)束時間,例如readFile(from,to)函數(shù)的執(zhí)行結(jié)束時間大概率取決于文件from的大小。
那么,問題在于我們?nèi)绾尾拍軠蚀_的定位copyFile執(zhí)行結(jié)束,從而讀取to文件中的內(nèi)容呢?
這就需要使用回調(diào)函數(shù),我們可以修改copyFile函數(shù)如下:
function copyFile(from, to, callback) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') callback()//當復制操作完成后調(diào)用回調(diào)函數(shù) }) }) }
這樣,我們?nèi)绻枰谖募椭仆瓿珊?,立即?zhí)行一些操作,就可以把這些操作寫入回調(diào)函數(shù)中:
function copyFile(from, to, callback) { fs.readFile(from, (err, data) => { if (err) { console.log(err.message) return } fs.writeFile(to, data, (err) => { if (err) { console.log(err.message) return } console.log('Copy finished') callback()//當復制操作完成后調(diào)用回調(diào)函數(shù) }) }) } copyFile('./from.txt', './to.txt', function () { //傳入一個回調(diào)函數(shù),讀取“to.txt”文件中的內(nèi)容并輸出 fs.readFile('./to.txt', (err, data) => { if (err) { console.log(err.message) return } console.log(data.toString()) }) })
如果,你已經(jīng)準備好了./from.txt文件,那么以上代碼就可以直接運行:
PS E:\Code\Node\demos\03-callback> node .\index.js
Copy finished
加入社區(qū)“仙宗”,和我一起修仙吧
社區(qū)地址:http://t.csdn.cn/EKf1h
這種編程方式被稱為“基于回調(diào)”的異步編程風格,異步執(zhí)行的函數(shù)應當提供一個回調(diào)參數(shù)用于在任務結(jié)束后調(diào)用。
這種風格在JavaScript編程中普遍存在,例如文件讀取函數(shù)fs.readFile、fs.writeFile都是異步函數(shù)。
四、回調(diào)的回調(diào)
回調(diào)函數(shù)可以準確的在異步工作完成后處理后繼事宜,如果我們需要依次執(zhí)行多個異步操作,就需要嵌套回調(diào)函數(shù)。
案例場景:依次讀取文件A和文件B
代碼實現(xiàn):
fs.readFile('./A.txt', (err, data) => { if (err) { console.log(err.message) return } console.log('讀取文件A:' + data.toString()) fs.readFile('./B.txt', (err, data) => { if (err) { console.log(err.message) return } console.log("讀取文件B:" + data.toString()) }) })
執(zhí)行效果:
PS E:\Code\Node\demos\03-callback> node .\index.js
讀取文件A:仙宗無限好,只是缺了佬
讀取文件B:要想入仙宗,鏈接不能少
http://t.csdn.cn/H1faI
通過回調(diào)的方式,就可以在讀取文件A之后,緊接著讀取文件B。
如果我們還想在文件B之后,繼續(xù)讀取文件C呢?這就需要繼續(xù)嵌套回調(diào):
fs.readFile('./A.txt', (err, data) => {//第一次回調(diào) if (err) { console.log(err.message) return } console.log('讀取文件A:' + data.toString()) fs.readFile('./B.txt', (err, data) => {//第二次回調(diào) if (err) { console.log(err.message) return } console.log("讀取文件B:" + data.toString()) fs.readFile('./C.txt',(err,data)=>{//第三次回調(diào) ... }) }) })
現(xiàn)在我們總結(jié)下,如果我們想要依次執(zhí)行多個異步操作,需要多層嵌套回調(diào),這在層數(shù)較少時是行之有效的,但是當嵌套次數(shù)過多時,會出現(xiàn)一些問題。
回調(diào)的約定
實際上,fs.readFile中的回調(diào)函數(shù)的樣式并非個例,而是JavaScript中的普遍約定。我們?nèi)蘸髸远x大量的回調(diào)函數(shù),也需要遵守這種約定,形成良好的編碼習慣。
約定是:
callback的第一個參數(shù)是為 error 而保留的。一旦出現(xiàn) error,callback(err)就會被調(diào)用。
第二個以及后面的參數(shù)用于接收異步操作的成功結(jié)果。此時callback(null, result1, result2,...)就會被調(diào)用。
基于以上約定,一個回調(diào)函數(shù)擁有錯誤處理和結(jié)果接收兩個功能,例如fs.readFile('...',(err,data)=>{})的回調(diào)函數(shù)就遵循了這種約定。
五、回調(diào)地獄
如果我們不深究的話,基于回調(diào)的異步方法處理似乎是相當完美的處理方式。問題在于,如果我們有一個接一個 的異步行為,那么代碼就會變成這樣:
fs.readFile('./a.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結(jié)果操作 fs.readFile('./b.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結(jié)果操作 fs.readFile('./c.txt',(err,data)=>{ if(err){ console.log(err.message) return } //讀取結(jié)果操作 fs.readFile('./d.txt',(err,data)=>{ if(err){ console.log(err.message) return } ... }) }) }) })
以上代碼的執(zhí)行內(nèi)容是:
讀取文件a.txt,如果沒有發(fā)生錯誤的話;
讀取文件b.txt,如果沒有發(fā)生錯誤的話;
讀取文件c.txt,如果沒有發(fā)生錯誤的話;
讀取文件d.txt,…
隨著調(diào)用的增加,代碼嵌套層級越來越深,包含越來越多的條件語句,從而形成不斷向右縮進的混亂代碼,難以閱讀和維護。
我們稱這種不斷向右增長(向右縮進)的現(xiàn)象為“回調(diào)地獄”或者“末日金字塔”!
fs.readFile('a.txt',(err,data)=>{ fs.readFile('b.txt',(err,data)=>{ fs.readFile('c.txt',(err,data)=>{ fs.readFile('d.txt',(err,data)=>{ fs.readFile('e.txt',(err,data)=>{ fs.readFile('f.txt',(err,data)=>{ fs.readFile('g.txt',(err,data)=>{ fs.readFile('h.txt',(err,data)=>{ ... /* 通往地獄的大門 ===> */ }) }) }) }) }) }) }) })
上面看起來很規(guī)整,但并不是實用,主要是由于在通常業(yè)務邏輯中會有大量的條件語句、數(shù)據(jù)處理操作等代碼,從而打亂當前美好的秩序,讓代碼變的難以維護。
現(xiàn)在我們就找到了最優(yōu)解,Promise。
六、總結(jié)
其實這篇文章講的幾乎是異步和回調(diào)的基本概念,二者是JavaScript的核心內(nèi)容,要大家投入更多精力去解決。
異步、并行、同步的基本概念;
使用回調(diào)函數(shù)處理異步任務;
回調(diào)函數(shù)的嵌套和約定;
回調(diào)地獄的基本概念;
本篇文章講到這里,歡迎繼續(xù)關(guān)注更多精彩內(nèi)容!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/128279.html
摘要:它使用了事件通知以確定在一組非阻塞套接字中有哪些已經(jīng)就緒能夠進行相關(guān)的操作。目前,可以把看作是傳入入站或者傳出出站數(shù)據(jù)的載體。出站事件是未來將會觸發(fā)的某個動作的操作結(jié)果,這些動作包括打開或者關(guān)閉到遠程節(jié)點的連接將數(shù)據(jù)寫到或者沖刷到套接字。 netty的概念 定義 Netty 是一款異步的事件驅(qū)動的網(wǎng)絡應用程序框架,支持快速地開發(fā)可維護的高性能的面向協(xié)議的服務器和客戶端。我們可以很簡單的...
摘要:作用默認的,直接在當前線程運行總是開啟一個新線程用于密集型任務,如異步阻塞操作,這個調(diào)度器的線程池會根據(jù)需要增長對于普通的計算任務,請使用默認是一個,很像一個有線程緩存的新線程調(diào)度器計算所使用的。這個使用的固定的線程池,大小為核數(shù)。 轉(zhuǎn)載請注明出處:https://zhuanlan.zhihu.com/p/20687307 RxJava系列1(簡介) RxJava系列2(基本概念及使...
閱讀 570·2023-03-27 18:33
閱讀 761·2023-03-26 17:27
閱讀 658·2023-03-26 17:14
閱讀 612·2023-03-17 21:13
閱讀 546·2023-03-17 08:28
閱讀 1836·2023-02-27 22:32
閱讀 1330·2023-02-27 22:27
閱讀 2212·2023-01-20 08:28