回調(diào)
回調(diào)函數(shù)就是一個(gè)通過(guò)函數(shù)指針調(diào)用的函數(shù)聚至。如果你把函數(shù)的指針(地址)作為參數(shù)傳遞給另一個(gè)函數(shù)酷勺,當(dāng)這個(gè)指針被用來(lái)調(diào)用其所指向的函數(shù)時(shí),我們就說(shuō)這是回調(diào)函數(shù)扳躬〈嗨撸回調(diào)函數(shù)不是由該函數(shù)的實(shí)現(xiàn)方直接調(diào)用,而是在特定的事件或條件發(fā)生時(shí)由另外的一方調(diào)用的贷币,用于對(duì)該事件或條件進(jìn)行響應(yīng)击胜。
簡(jiǎn)單的說(shuō)回調(diào)函數(shù)是參數(shù)傳遞函數(shù),并指定它在響應(yīng)某個(gè)事件(定時(shí)器片择、鼠標(biāo)點(diǎn) 擊潜的、Ajax 響應(yīng)等)時(shí)執(zhí)行這個(gè)參數(shù)傳遞的函數(shù)。這個(gè)參數(shù)就是回調(diào)字管玻回頭調(diào)用。回調(diào)傳參
假定有兩個(gè)函數(shù)f1和f2嘲叔,后者等待前者的執(zhí)行結(jié)果亡呵。
如果f1是一個(gè)很耗時(shí)的任務(wù),可以考慮改寫(xiě)f1硫戈,把f2寫(xiě)成f1的回調(diào)函數(shù)锰什。
function f1(callback){
setTimeout(function () {
// f1的任務(wù)代碼
callback(); // 這是回調(diào) 因?yàn)槭莝etTimeout計(jì)時(shí)后觸發(fā)的。
}, 1000);
}
function f1(callback){
callback(); // 這不是回調(diào) 因?yàn)槭莄allback()自己直接調(diào)用的。
}
執(zhí)行代碼就變成下面這樣:f1(f2);
回調(diào)會(huì)造成的問(wèn)題:
- 層次的嵌套汁胆,俗稱(chēng)回調(diào)金字塔
還有什么問(wèn)題嗎梭姓??jī)H僅是代碼看起來(lái)混亂嗎?
順序的大腦
//偽代碼
doA(function(){
doB();
doC(function() {
doD();
});
doE();
});
doF();
無(wú)論多么熟悉JS異步的人嫩码,要完全搞懂這段代碼實(shí)際的運(yùn)行順序誉尖,恐怕也得思考一番。
為什么會(huì)這樣铸题? → 因?yàn)槿说拇竽X是順序的铡恕,天生適合順序的思考,難以理解(不是不能理解)非順序的東西丢间。
無(wú)論是在書(shū)寫(xiě)還是在閱讀這段代碼的時(shí)候探熔,我們的大腦都會(huì)下意識(shí)地以為這段代碼的執(zhí)行邏輯是這樣的doA→doB→doC→doD→doE→doF
,然而實(shí)際運(yùn)行邏輯很可能(假設(shè))是這樣的doA→doF→doB→doC→doE→doD
烘挫。
思想實(shí)驗(yàn)
我們來(lái)進(jìn)行兩種游戲
- 第一種游戲诀艰,舉辦方提前跟你說(shuō):“這游戲總共有X關(guān)。第一關(guān)你應(yīng)該做.....然后在....(地方)進(jìn)入第二關(guān)墙牌。第二關(guān)你應(yīng)該做....然后在....(地方)進(jìn)入第三關(guān)涡驮。……"喜滨。我稱(chēng)之為呆板的游戲捉捅。
- 第二種游戲,舉辦方提前跟你說(shuō):”你只管從這個(gè)門(mén)口進(jìn)去虽风,等你快到下一關(guān)的時(shí)候棒口,自然會(huì)有人出來(lái)給你提示」枷ィ“我稱(chēng)之為靈活的游戲无牵。
對(duì)應(yīng)到代碼當(dāng)中,我們便能發(fā)現(xiàn)回調(diào)的另一個(gè)嚴(yán)重問(wèn)題:硬編碼厂抖。
前后的操作被回調(diào)強(qiáng)制硬編碼綁定到一起了茎毁。在調(diào)用函數(shù)A的時(shí)候,你必須指定A結(jié)束之后該干什么忱辅,并且顯式地傳遞進(jìn)去七蜘。這樣,其實(shí)你已經(jīng)指定了所有的可能事件和路徑墙懂,代碼將變得僵硬且難以維護(hù)橡卤。同時(shí),在閱讀代碼的時(shí)候损搬,由于必須記住前后的關(guān)聯(lián)操作碧库,這也加重了大腦記憶的負(fù)擔(dān)柜与。
Promise
Promise 是 ES 2015 原生支持的,他把原來(lái)嵌套的回調(diào)改為了級(jí)聯(lián)的方式嵌灰。
var a = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('1')
}, 2000)
})
a.then(function(val) {
console.log(val)
})
如果要涉及到多個(gè)異步操作的順序執(zhí)行問(wèn)題弄匕,我們可以這樣寫(xiě):
var a = new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('1')
}, 2000)
})
a
.then(function(val){
console.log(val)
return new Promise(function(resolve, reject) { // 必須加return不然promise不止到這個(gè)then函數(shù)中又是個(gè)promise。只有通過(guò)返回值判斷一下伞鲫。
setTimeout(function() {
resolve('2')
}, 2000)
})
})
.then(function(val) {
console.log(val)
})
以Ajax為例粘茄。我們都知道,在Ajax執(zhí)行成功之后秕脓,指定的回調(diào)函數(shù)會(huì)被放入”任務(wù)隊(duì)列“中。JS執(zhí)行引擎在主線程空閑的時(shí)候便會(huì)輪詢(xún)?nèi)蝿?wù)隊(duì)列儒搭,執(zhí)行其中的任務(wù)吠架。
我們仔細(xì)想想,是不是漏了一個(gè)關(guān)鍵點(diǎn):”我知道最終是JS引擎執(zhí)行了這個(gè)回調(diào)函數(shù)搂鲫。但是傍药,到底是誰(shuí)調(diào)度這個(gè)回調(diào)函數(shù)的?到底是誰(shuí)在特定的時(shí)間點(diǎn)把這個(gè)回調(diào)函數(shù)放入任務(wù)隊(duì)列中去魂仍?“
答案是宿主環(huán)境拐辽,在本例中也就是瀏覽器。是瀏覽器檢測(cè)到Ajax已經(jīng)成功返回擦酌,是瀏覽器主動(dòng)將指定的回調(diào)函數(shù)放到”任務(wù)隊(duì)列”中俱诸,JS引擎只不過(guò)是執(zhí)行而已。
由此赊舶,我們澄清了一件(可能令人震驚)的事情: 在回調(diào)時(shí)代睁搭,盡管你已經(jīng)能夠編寫(xiě)異步代碼了。但是笼平,其實(shí)JS本身园骆,從來(lái)沒(méi)有真正內(nèi)建直接的異步概念,直到ES6的出現(xiàn)寓调。
事實(shí)就是如此锌唾。JS引擎本身所做的只不過(guò)是在不斷輪詢(xún)?nèi)蝿?wù)隊(duì)列,然后執(zhí)行其中的任務(wù)夺英。JS引擎根本不能做到自己主動(dòng)把任務(wù)放到任務(wù)隊(duì)列中晌涕,任務(wù)的調(diào)度從來(lái)都是宿主完成的。舉個(gè)形象的例子就是:“JS引擎就像是流水線上的工人秋麸,宿主就像是派活的老板渐排。工人只知道不斷地干活异雁,不斷地完成流水線上出現(xiàn)的任務(wù)锁蠕,這些任務(wù)都是老板給工人指定的将饺。工人從來(lái)沒(méi)有(也不能)自己給自己派活,自己給自己的流水線上放任務(wù)蹋半。”
ES6從本質(zhì)上改變了在哪里管理事件循環(huán)弃甥,這意味著在技術(shù)上將其納入了JavaScript引擎的勢(shì)力范圍惶室,而不再是由宿主來(lái)管理。
為什么說(shuō)ES6的Promise才真正有了異步呢帘靡?因?yàn)镻romise是基于任務(wù)隊(duì)列(job queue)知给。可以js方面的在事件循環(huán)中插隊(duì)描姚。并不是宿主環(huán)境控制的涩赢。
Promise 是一個(gè) Job,所以必然異步的轩勘,因?yàn)?then 總是返回 Promise筒扒,xxx.then(a => a) 的效果實(shí)際上是 return new Promise(resolve => resolve(a)),所以then也是異步绊寻。
es6引入了Generator
為什么會(huì)出現(xiàn)Generator
本質(zhì)上花墩,generator是一個(gè)函數(shù),它執(zhí)行的結(jié)果是一個(gè)iterator迭代器澄步,每一次調(diào)用迭代器的next方法冰蘑,就會(huì)產(chǎn)生一個(gè)新的值。迭代器本身就是用來(lái)生成一系列值的村缸,同時(shí)也廣泛應(yīng)用于擴(kuò)展運(yùn)算符...祠肥、解構(gòu)賦值和for...of循環(huán)遍歷等地方。
generator函數(shù)的函數(shù)是分段的王凑。第一次執(zhí)行next的時(shí)候搪柑,程序會(huì)執(zhí)行到第一個(gè)yield,然后返回{ value:1, done:false }索烹,表示yield后面返回1工碾,但是函數(shù)Hello還沒(méi)執(zhí)行完,函數(shù)既不會(huì)退出百姓,也不會(huì)往下執(zhí)行渊额。
function p(time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Date());
}, time)
});
}
function* delay(){
let time1 = yield p(1000);
console.log(1, time1);
let time2 = yield p(1000);
console.log(2, time2)
let time3 = yield p(2000);
console.log(3, time3);
}
function co(gen){
let it = gen();
next();
function next(arg){
let ret = it.next(arg);
if(ret.done) return;
ret.value.then((data) => {
next(data)
})
}
}
co(delay);// 這里寫(xiě)的co函數(shù)只適用于yield后跟promise。
await/async
ES7中提供async函數(shù)垒拢,利用它旬迹,不需要依賴(lài)co庫(kù),也一樣可以解決這個(gè)問(wèn)題求类。
使用方法和 co 非常類(lèi)似奔垦,同時(shí)也支持同步寫(xiě)法的異常捕獲。async的返回值為Promise 對(duì)象尸疆。
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
var b = async function() {
var val = await a()
console.log(val)
}
b()
使用await/async或者Generator進(jìn)行http請(qǐng)求的時(shí)候經(jīng)常是配合這Promise的椿猎。
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value)
}
asyncPrint('hello world', 50);
async既可以處理promise又可以處理普通的惶岭,不過(guò)此時(shí)相當(dāng)于同步,而且更有語(yǔ)義化犯眠。
async function asyncPrint() {
await yibu1();
await yibu2();
}
使用async/await yibu1請(qǐng)求完了再去請(qǐng)求yibu2按灶。如果使用Promise。是不等yibu1的結(jié)果就去請(qǐng)求yibu2筐咧。就等于async可以把本身的回調(diào)或者then里的內(nèi)容拿出來(lái)當(dāng)做同步代碼寫(xiě)鸯旁。但是一般http請(qǐng)求函數(shù)都沒(méi)有返回值,只有請(qǐng)求有了結(jié)果才會(huì)有結(jié)果量蕊,所以await后一般跟著promise铺罢。不然await也不知道異步函數(shù)請(qǐng)求結(jié)束了呀。
yield 命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象残炮,而 async 函數(shù)的 await 命令后面畏铆,可以跟 Promise 對(duì)象和原始類(lèi)型的值(數(shù)值、字符串和布爾值吉殃,但這時(shí)等同于同步操作)。
所以
async function hhh () {
var ll = await setTimeout(function() {
console.log('ss')
}, 3000)
console.log('ll')
}
hhh()
回調(diào)主要有兩個(gè)問(wèn)題楷怒,信任問(wèn)題和順序問(wèn)題蛋勺,Promise解決信任問(wèn)題,Generator解決順序問(wèn)題鸠删。但我認(rèn)為Generator也不是把順序問(wèn)題完完全全的解決了抱完。因?yàn)槿绻?/p>
async function async1(value, ms) {
console.log(value)
await new Promise()
async function async2(value, ms) {
//
}
console.log(value1)
}
假設(shè) console.log(value1)本身是一個(gè)很費(fèi)時(shí)的同步任務(wù),并且不需要等待async2執(zhí)行完成刃泡。這時(shí)候巧娱,我們還是會(huì)先執(zhí)行console.log(value1) ,后執(zhí)行async2的回調(diào)烘贴。await會(huì)使得async函數(shù)卡住不動(dòng)禁添,所以只能解決一層一層的嵌套的順序問(wèn)題,不能解決這種同一級(jí)別的順序問(wèn)題桨踪。
async函數(shù)的返回值是一個(gè)Promise老翘,上面這個(gè)問(wèn)題可以改為
async function async1(value, ms) {
console.log(value)
await new Promise()
console.log(value1)
}
async1().then(function(){
async2(value, ms)
})