弄懂js異步
講異步之前,我們必須掌握一個基礎知識-event-loop规伐。
我們知道JavaScript的一大特點就是單線程,而這個線程中擁有唯一的一個事件循環(huán)。當然新標準中的web worker涉及到了多線程镐依,但它的原理是利用一個父線程和多個子線程,歸根結底來說js仍逃不過是單線程的事實天试。
JavaScript代碼的執(zhí)行過程中,除了依靠函數(shù)調用棧來搞定函數(shù)的執(zhí)行順序外然低,還依靠任務隊列(task queue)(先進先出)來搞定另外一些代碼的執(zhí)行
一個線程中喜每,事件循環(huán)是唯一的务唐,但是任務隊列可以擁有多個。
任務隊列又分為macro-task(宏任務)與micro-task(微任務)带兜,在最新標準中枫笛,它們被分別稱為task與jobs。
macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering刚照。
micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
setTimeout/Promise等我們稱之為任務源刑巧。而進入任務隊列的是他們指定的具體執(zhí)行任務。
//setTimeout本身是一個任務分發(fā)器无畔,他作為一個任務源啊楚,會立即執(zhí)行
setTimeout(function() {
//里面的函數(shù)是具體任務,它會延遲執(zhí)行
console.log('dc');
})
來自不同任務源的任務會進入到不同的任務隊列浑彰。其中setTimeout與setInterval是同源的恭理。
事件循環(huán)的順序,決定了JavaScript代碼的執(zhí)行順序郭变。它從script(整體代碼)開始第一次循環(huán)颜价。之后全局上下文進入函數(shù)調用棧。直到調用棧清空(只剩全局)诉濒,然后執(zhí)行所有的micro-task周伦。當所有可執(zhí)行的micro-task執(zhí)行完畢之后。循環(huán)再次從macro-task開始未荒,找到其中一個任務隊列執(zhí)行完畢专挪,然后再執(zhí)行所有的micro-task,這樣一直循環(huán)下去茄猫。
其中每一個任務的執(zhí)行狈蚤,無論是macro-task還是micro-task,都是借助函數(shù)調用棧來完成划纽。
// 為了方便理解脆侮,我以打印出來的字符作為當前的任務名稱
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
//////////////
promise1
promise2
global1
then1
timeout1
首先,事件循環(huán)從宏任務隊列開始勇劣,這個時候靖避,宏任務隊列中,只有一個script(整體代碼)任務比默。每一個任務的執(zhí)行順序幻捏,都依靠函數(shù)調用棧來搞定,而當遇到任務源時命咐,則會先分發(fā)任務到對應的隊列中去篡九,所以,上面例子的第一步執(zhí)行如下圖所示醋奠。
第二步:script任務執(zhí)行時首先遇到了setTimeout,setTimeout為一個宏任務源沛善,那么他的作用就是將任務分發(fā)到它對應的隊列中
第三步:script執(zhí)行時遇到Promise實例航揉。Promise構造函數(shù)中的第一個參數(shù),是在new的時候執(zhí)行金刁,因此不會進入任何其他的隊列帅涂,而是直接在當前任務直接執(zhí)行了,而后續(xù)的.then則會被分發(fā)到micro-task的Promise隊列中去尤蛮。
因此媳友,構造函數(shù)執(zhí)行時,里面的參數(shù)進入函數(shù)調用棧執(zhí)行抵屿。for循環(huán)不會進入任何隊列庆锦,因此代碼會依次執(zhí)行,所以這里的promise1和promise2會依次輸出
script任務繼續(xù)往下執(zhí)行泊脐,最后只有一句輸出了globa1空幻,然后,全局任務就執(zhí)行完畢了容客。
第四步:第一個宏任務script執(zhí)行完畢之后秕铛,就開始執(zhí)行所有的可執(zhí)行的微任務。這個時候缩挑,微任務中但两,只有Promise隊列中的一個任務then1,因此直接執(zhí)行就行了供置,執(zhí)行結果輸出then1谨湘,當然,他的執(zhí)行,也是進入函數(shù)調用棧中執(zhí)行的悲关。
第五步:當所有的micro-tast執(zhí)行完畢之后谎僻,表示第一輪的循環(huán)就結束了。這個時候就得開始第二輪的循環(huán)寓辱。第二輪循環(huán)仍然從宏任務macro-task開始
這個時候,我們發(fā)現(xiàn)宏任務中赤拒,只有在setTimeout隊列中還要一個timeout1的任務等待執(zhí)行秫筏。因此就直接執(zhí)行即可
這個時候宏任務隊列與微任務隊列中都沒有任務了,所以代碼就不會再輸出其他東西了挎挖。
那么上面這個例子的輸出結果就顯而易見这敬。大家可以自行嘗試體會。
這個例子比較簡答蕉朵,涉及到的隊列任務并不多崔涂,因此讀懂了它還不能全面的了解到事件循環(huán)機制的全貌。所以我下面弄了一個復雜一點的例子始衅,再給大家解析一番冷蚂,相信讀懂之后,事件循環(huán)這個問題汛闸,再面試中再次被問到就難不倒大家了
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
第一步:宏任務script首先執(zhí)行蝙茶。全局入棧。glob1輸出诸老。
第二步隆夯,執(zhí)行過程遇到setTimeout。setTimeout作為任務分發(fā)器别伏,將任務分發(fā)到對應的宏任務隊列中
第三步:執(zhí)行過程遇到setImmediate蹄衷。setImmediate也是一個宏任務分發(fā)器,將任務分發(fā)到對應的任務隊列中厘肮。setImmediate的任務隊列會在setTimeout隊列的后面執(zhí)行
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
第四步:執(zhí)行遇到nextTick愧口,process.nextTick是一個微任務分發(fā)器,它會將任務分發(fā)到對應的微任務隊列中去
process.nextTick(function() {
console.log('glob1_nextTick');
})
第五步:執(zhí)行遇到Promise轴脐。Promise的then方法會將任務分發(fā)到對應的微任務隊列中调卑,但是它構造函數(shù)中的方法會直接執(zhí)行。因此大咱,glob1_promise會第二個輸出恬涧。
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
第六步:執(zhí)行遇到第二個setTimeout。
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
第七步:先后遇到nextTick與Promise
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
第八步:再次遇到setImmediate碴巾。
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
})
這個時候溯捆,script中的代碼就執(zhí)行完畢了,執(zhí)行過程中,遇到不同的任務分發(fā)器提揍,就將任務分發(fā)到各自對應的隊列中去啤月。接下來,將會執(zhí)行所有的微任務隊列中的任務劳跃。
其中谎仲,nextTick隊列會比Promie先執(zhí)行。nextTick中的可執(zhí)行任務執(zhí)行完畢之后刨仑,才會開始執(zhí)行Promise隊列中的任務郑诺。
當所有可執(zhí)行的微任務執(zhí)行完畢之后,這一輪循環(huán)就表示結束了杉武。下一輪循環(huán)繼續(xù)從宏任務隊列開始執(zhí)行辙诞。
這個時候,script已經執(zhí)行完畢轻抱,所以就從setTimeout隊列開始執(zhí)行飞涂。
setTimeout任務的執(zhí)行,也依然是借助函數(shù)調用棧來完成祈搜,并且遇到任務分發(fā)器的時候也會將任務分發(fā)到對應的隊列中去较店。
只有當setTimeout中所有的任務執(zhí)行完畢之后,才會再次開始執(zhí)行微任務隊列夭问。并且清空所有的可執(zhí)行微任務泽西。
setTiemout隊列產生的微任務執(zhí)行完畢之后,循環(huán)則回過頭來開始執(zhí)行setImmediate隊列缰趋。仍然是先將setImmediate隊列中的任務執(zhí)行完畢,再執(zhí)行所產生的微任務秘血。
當setImmediate隊列執(zhí)行產生的微任務全部執(zhí)行之后味抖,第二輪循環(huán)也就結束了
// 用數(shù)組模擬一個隊列
var tasks = [];
// 模擬一個事件分發(fā)器
var addFn1 = function(task) {
tasks.push(task);
}
// 執(zhí)行所有的任務
var flush = function() {
tasks.map(function(task) {
task();
})
}
// 最后利用setTimeout/或者其他你認為合適的方式丟入事件循環(huán)中
setTimeout(function() {
flush();
})
// 當然,也可以不用丟進事件循環(huán)灰粮,而是我們自己手動在適當?shù)臅r機去執(zhí)行對應的某一個方法
var dispatch = function(name) {
tasks.map(function(item) {
if(item.name == name) {
item.handler();
}
})
}
// 當然仔涩,我們把任務丟進去的時候,多保存一個name即可粘舟。
// 這時候熔脂,task的格式就如下
demoTask = {
name: 'demo',
handler: function() {}
}
// 于是,一個訂閱-通知的設計模式就這樣輕松的被實現(xiàn)了
最終結果:
golb1
glob1_promise
glob2_promise
glob1_nextTick
glob2_nextTick
glob1_then
glob2_then
timeout1
timeout1_promise
timeout2
timeout2_promise
timeout1_nextTick
timeout2_nextTick
timeout1_then
timeout2_then
immediate1
immediate1_promise
immediate2
immediate2_promise
immediate1_nextTick
immediate2_nextTick
immediate1_then
immediate2_then
總結一下:雖然宏任務總是比微任務先執(zhí)行,但是我們往往會忽略script這個宏任務,所以實際上最開始的宏任務會比微任務后執(zhí)行痹栖,promise構造函數(shù)里的函數(shù)不會在隊列中温峭,而是直接執(zhí)行迹鹅,他跟隨promise這個任務分發(fā)器一樣會立即執(zhí)行,而then中的函數(shù)會進入微任務隊列
回到正題茴迁,那么什么是異步呢夸盟,眾所周知秽荞,js是單線程的語言骤公,腦袋一根筋,對于拿到的程序扬跋,一行一行的執(zhí)行阶捆,上面的執(zhí)行為完成,就傻傻的等著
var i, t = Date.now()
for (i = 0; i < 100000000; i++) {
}
console.log(Date.now() - t) // 274(chrome瀏覽器)
執(zhí)行程序這樣沒有問題钦听,但是對于 JS 最初使用的環(huán)境 ———— 瀏覽器客戶端 ———— 就不一樣了趁猴。因此在瀏覽器端運行的 js ,可能會有大量的網(wǎng)絡請求彪见,而一個網(wǎng)絡資源啥時候返回,這個時間是不可預估的娱挨。這種情況也要傻傻的等著余指、卡頓著、啥都不做嗎跷坝?———— 那肯定不行酵镜。
因此,JS 對于這種場景就設計了異步 ———— 即柴钻,發(fā)起一個網(wǎng)絡請求淮韭,就先不管這邊了,先干其他事兒贴届,網(wǎng)絡請求啥時候返回結果靠粪,到時候再說。這樣就能保證一個網(wǎng)頁的流程運行
同步:一件事接著一件事做毫蚓,上一件沒做完占键,下一件事也不做,連續(xù)的執(zhí)行
異步:就是一個任務分成兩段元潘,先執(zhí)行第一段畔乙,然后轉而執(zhí)行其他任務,等做好了準備翩概,再回過頭執(zhí)行第二段牲距,不連續(xù)的執(zhí)行
JavaScript 語言對異步編程的實現(xiàn),就是回調函數(shù)钥庇。所謂回調函數(shù)牍鞠,就是把任務的第二段單獨寫在一個函數(shù)里面,等到重新執(zhí)行這個任務的時候上沐,就直接調用這個函數(shù)
f1(f2)
回調函數(shù)就是將函數(shù)f2當做參數(shù)傳給f1皮服,f1執(zhí)行之后才執(zhí)行f2,f2就是f1的回調函數(shù),所有的異步原理都是基于回調函數(shù)龄广,無論是promise還是async硫眯,他們都只是回調函數(shù)的語法糖。(回調函數(shù)|事件監(jiān)聽|發(fā)布/訂閱|Promise)
事件綁定與異步操作原理相似择同,但是它與異步有兩個不同的地方
1.event-loop 執(zhí)行時两入,調用的源不一樣。異步操作是系統(tǒng)自動調用敲才,無論是setTimeout時間到了還是$.ajax請求返回了裹纳,系統(tǒng)會自動調用。而事件綁定就需要用戶手動觸發(fā)
2.從設計上來將紧武,事件綁定有著明顯的“訂閱-發(fā)布”的設計模式剃氧,而異步操作卻沒有
開發(fā)中比較常用的異步操作有:
網(wǎng)絡請求,如ajax http.get
IO 操作阻星,如readFile readdir
定時函數(shù)朋鞍,如setTimeout setInterval
promise:
先把規(guī)范理解一下,再來講講它的api妥箕,最后實現(xiàn)一個promise來徹底理解滥酥。
Promise 本質是一個狀態(tài)機。每個 promise 只能是 3 種狀態(tài)中的一種:pending畦幢、fulfilled 或 rejected坎吻。狀態(tài)轉變只能是 pending -> fulfilled 或者 pending -> rejected。狀態(tài)轉變不可逆宇葱。
then 方法可以被同一個 promise 調用多次瘦真,then方法返回一個新的Promise
then 方法必須返回一個 promise。規(guī)范里沒有明確說明返回一個新的 promise 還是復用老的 promise(即 return this)贝搁,大多數(shù)實現(xiàn)都是返回一個新的 promise吗氏,而且復用老的 promise 可能改變內部狀態(tài),這與規(guī)范也是相違背的雷逆。
值穿透
只有一個then方法弦讽,沒有catch,race膀哲,all等方法往产,甚至沒有構造函數(shù)
Promise標準中僅指定了Promise對象的then方法的行為,其它一切我們常見的方法/函數(shù)都并沒有指定某宪,包括catch仿村,race,all等常用方法兴喂,甚至也沒有指定該如何構造出一個Promise對象
- 不同Promise的實現(xiàn)需要可以相互調用
Promise的構造函數(shù)接收一個參數(shù)蔼囊,是函數(shù)焚志,并且傳入兩個參數(shù):resolve,reject畏鼓,分別表示異步操作執(zhí)行成功后的回調函數(shù)和異步操作執(zhí)行失敗后的回調函數(shù)酱酬。其實這里用“成功”和“失敗”來描述并不準確,按照標準來講云矫,resolve是將Promise的狀態(tài)置為fullfiled膳沽,reject是將Promise的狀態(tài)置為rejected
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)');
}, 2000);
});
//執(zhí)行完成
運行代碼,會在2秒后輸出“執(zhí)行完成”让禀。注意挑社!我只是new了一個對象,并沒有調用它巡揍,我們傳進去的函數(shù)就已經執(zhí)行了痛阻,這是需要注意的一個細節(jié)。所以我們用Promise的時候一般是包在一個函數(shù)中腮敌,在需要的時候去運行這個函數(shù)
function runAsync(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)');
}, 2000);
});
return p;
}
runAsync()
在我們包裝好的函數(shù)最后录平,會return出Promise對象,也就是說缀皱,執(zhí)行這個函數(shù)我們得到了一個Promise對象。還記得Promise對象上有then动猬、catch方法吧啤斗?這就是強大之處了
function runAsync(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)');
}, 2000);
});
return p;
}
runAsync().then(function(data){
console.log(data);
//后面可以用傳過來的數(shù)據(jù)做些其他操作
//......
});
//執(zhí)行完成
//隨便什么數(shù)據(jù)
在runAsync()的返回上直接調用then方法,then接收一個參數(shù)赁咙,是函數(shù)钮莲,并且會拿到我們在runAsync中調用resolve時傳的的參數(shù)。運行這段代碼彼水,會在2秒后輸出“執(zhí)行完成”崔拥,緊接著輸出“隨便什么數(shù)據(jù)
這時候你應該有所領悟了,原來then里面的函數(shù)就跟我們平時的回調函數(shù)一個意思凤覆,能夠在runAsync這個異步任務執(zhí)行完成之后被執(zhí)行链瓦。這就是Promise的作用了,簡單來講盯桦,就是能把原來的回調寫法分離出來慈俯,在異步操作執(zhí)行完后,用鏈式調用的方式執(zhí)行回調函數(shù)
從表面上看拥峦,Promise只是能夠簡化層層回調的寫法贴膘,而實質上,Promise的精髓是“狀態(tài)”略号,用維護狀態(tài)刑峡、傳遞狀態(tài)的方式來使得回調函數(shù)能夠及時調用洋闽,它比傳遞callback函數(shù)要簡單、靈活的多突梦。所以使用Promise的正確場景是這樣的:
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務1執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務2執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
console.log('異步任務3執(zhí)行完成');
resolve('隨便什么數(shù)據(jù)3');
}, 2000);
});
return p;
}
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
//異步任務1執(zhí)行完成
//隨便什么數(shù)據(jù)1
//異步任務2執(zhí)行完成
//隨便什么數(shù)據(jù)2
//異步任務3執(zhí)行完成
//隨便什么數(shù)據(jù)3
我們光用了resolve诫舅,還沒用reject呢,它是做什么的呢阳似?事實上骚勘,我們前面的例子都是只有“執(zhí)行成功”的回調,還沒有“失敗”的情況撮奏,reject的作用就是把Promise的狀態(tài)置為rejected俏讹,這樣我們在then中就能捕捉到,然后執(zhí)行“失敗”情況的回調
function getNumber(){
var p = new Promise(function(resolve, reject){
//做一些異步操作
setTimeout(function(){
var num = Math.ceil(Math.random()*10); //生成1-10的隨機數(shù)
if(num<=5){
resolve(num);
}
else{
reject('數(shù)字太大了');
}
}, 2000);
});
return p;
}
getNumber()
.then(
function(data){
console.log('resolved');
console.log(data);
},
function(reason, data){
console.log('rejected');
console.log(reason);
}
);
getNumber函數(shù)用來異步獲取一個數(shù)字畜吊,2秒后執(zhí)行完成泽疆,如果數(shù)字小于等于5,我們認為是“成功”了玲献,調用resolve修改Promise的狀態(tài)殉疼。否則我們認為是“失敗”了,調用reject并傳遞一個參數(shù)捌年,作為失敗的原因瓢娜。
運行getNumber并且在then中傳了兩個參數(shù),then方法可以接受兩個參數(shù)礼预,第一個對應resolve的回調眠砾,第二個對應reject的回調。所以我們能夠分別拿到他們傳過來的數(shù)據(jù)托酸。多次運行這段代碼褒颈,你會隨機得到下面兩種結果
resolver 2
||
rejected 太大了
我們知道Promise對象除了then方法,還有一個catch方法励堡,它是做什么用的呢谷丸?其實它和then的第二個參數(shù)一樣,用來指定reject的回調应结,用法是這樣:
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
效果和寫在then的第二個參數(shù)里面一樣,相當于then(null,fn)刨疼。不過它還有另外一個作用:在執(zhí)行resolve的回調(也就是上面then中的第一個參數(shù))時,如果拋出異常了(代碼出錯了)鹅龄,那么并不會報錯卡死js币狠,而是會進到這個catch方法中,所以我們要多用catch少用rejected
getNumber()
.then(function(data){
console.log('resolved');
console.log(data);
console.log(somedata); //此處的somedata未定義
})
.catch(function(reason){
console.log('rejected');
console.log(reason);
});
在resolve的回調中,我們console.log(somedata);而somedata這個變量是沒有被定義的砾层。如果我們不用Promise漩绵,代碼運行到這里就直接在控制臺報錯了,不往下運行了肛炮。但是在這里止吐,會得到這樣的結果:
resolved
4
rejected
somedata is not defined
也就是說進到catch方法里面去了宝踪,而且把錯誤原因傳到了reason參數(shù)中。即便是有錯誤的代碼也不會報錯了碍扔,這與我們的try/catch語句有相同的功能
Promise的all方法提供了并行執(zhí)行異步操作的能力瘩燥,并且在所有異步操作執(zhí)行完后才執(zhí)行回調。我們仍舊使用上面定義好的runAsync1不同、runAsync2厉膀、runAsync3這三個函數(shù),看下面的例子:
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
用Promise.all來執(zhí)行二拐,all接收一個數(shù)組參數(shù)服鹅,里面的值最終都算返回Promise對象。這樣百新,三個異步操作的并行執(zhí)行的企软,等到它們都執(zhí)行完后才會進到then里面。那么饭望,三個異步操作返回的數(shù)據(jù)哪里去了呢仗哨?都在then里面呢,all會把所有異步操作的結果放進一個數(shù)組中傳給then铅辞,就是上面的results厌漂。所以上面代碼的輸出結果就是:
//異步任務1執(zhí)行完成
//異步任務2執(zhí)行完成
//異步任務3執(zhí)行完成
//[隨便什么數(shù)據(jù)1,隨便什么數(shù)據(jù)2,隨便什么數(shù)據(jù)3]
有了all,你就可以并行執(zhí)行多個異步操作斟珊,并且在一個回調中處理所有的返回數(shù)據(jù)桩卵,是不是很酷?有一個場景是很適合用這個的倍宾,一些游戲類的素材比較多的應用,打開網(wǎng)頁時胜嗓,預先加載需要用到的各種資源如圖片高职、flash以及各種靜態(tài)文件。所有的都加載完后辞州,我們再進行頁面的初始化
ll方法的效果實際上是「誰跑的慢怔锌,以誰為準執(zhí)行回調」,那么相對的就有另一個方法「誰跑的快变过,以誰為準執(zhí)行回調」埃元,這就是race方法,這個詞本來就是賽跑的意思媚狰。race的用法與all一樣岛杀,我們把上面runAsync1的延時改為1秒來看一下
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
這三個異步操作同樣是并行執(zhí)行的。結果你應該可以猜到崭孤,1秒后runAsync1已經執(zhí)行完了类嗤,此時then里面的就執(zhí)行了糊肠。結果是這樣的
//異步任務1執(zhí)行完成
//隨便什么數(shù)據(jù)1
//異步任務2執(zhí)行完成
//異步任務3執(zhí)行完成
在then里面的回調開始執(zhí)行時,runAsync2()和runAsync3()并沒有停止遗锣,仍舊再執(zhí)行货裹。于是再過1秒后,輸出了他們結束的標志精偿。
這個race有什么用呢弧圆?使用場景還是很多的,比如我們可以用race給某個異步請求設置超時時間笔咽,并且在超時后執(zhí)行相應的操作搔预,代碼如下:
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延時函數(shù),用于給請求計時
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('圖片請求超時');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
requestImg函數(shù)會異步請求一張圖片拓轻,我把地址寫為"xxxxxx"斯撮,所以肯定是無法成功請求到的。timeout函數(shù)是一個延時5秒的異步操作扶叉。我們把這兩個返回Promise對象的函數(shù)放進race勿锅,于是他倆就會賽跑,如果5秒之內圖片請求成功了枣氧,那么遍進入then方法溢十,執(zhí)行正常的流程。如果5秒鐘圖片還未成功返回达吞,那么timeout就跑贏了张弛,則進入catch,報出“圖片請求超時”的信息
搞懂了這些就基本理解了promise酪劫,以下八句話更能理解到promsie的精髓:
- Promise的立即執(zhí)行性
var p = new Promise(function(resolve, reject){
console.log("create a promise");
resolve("success");
});
console.log("after new Promise");
p.then(function(value){
console.log(value);
});
// create a promise
// after new Promise
// value
Promise對象表示未來某個將要發(fā)生的事件吞鸭,但在創(chuàng)建(new)Promise時,作為Promise參數(shù)傳入的函數(shù)是會被立即執(zhí)行的覆糟,只是其中執(zhí)行的代碼可以是異步代碼刻剥。有些同學會認為,當Promise對象調用then方法時滩字,Promise接收的函數(shù)才會執(zhí)行造虏,這是錯誤的。因此麦箍,代碼中"create a promise"先于"after new Promise"輸出
2.Promise 三種狀態(tài)
var p1 = new Promise(function(resolve,reject){
resolve(1);
});
//p1 的函數(shù)參數(shù)中執(zhí)行的是一段同步代碼漓藕,Promise剛創(chuàng)建完成,resolve方法就已經被調用了挟裂,因而緊跟著的輸出顯示p1是resolved狀態(tài)
var p2 = new Promise(function(resolve,reject){
setTimeout(function(){
resolve(2);
}, 500);
});
var p3 = new Promise(function(resolve,reject){
setTimeout(function(){
reject(3);
}, 500);
});
console.log(p1);
console.log(p2);
console.log(p3);
setTimeout(function(){
console.log(p2);
}, 1000);
setTimeout(function(){
console.log(p3);
}, 1000);
p1.then(function(value){
console.log(value);
});
p2.then(function(value){
console.log(value);
});
p3.catch(function(err){
console.log(err);
});
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
1
2
3
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 3}
Promise的內部實現(xiàn)是一個狀態(tài)機享钞。Promise有三種狀態(tài):pending,resolved诀蓉,rejected嫩与。當Promise剛創(chuàng)建完成時寝姿,處于pending狀態(tài);當Promise中的函數(shù)參數(shù)執(zhí)行了resolve后划滋,Promise由pending狀態(tài)變成resolved狀態(tài)饵筑;如果在Promise的函數(shù)參數(shù)中執(zhí)行的不是resolve方法,而是reject方法处坪,那么Promise會由pending狀態(tài)變成rejected狀態(tài)根资。
p2、p3剛創(chuàng)建完成時同窘,控制臺輸出的這兩臺Promise都處于pending狀態(tài)玄帕,但為什么p1是resolved狀態(tài)呢? 這是因為p1 的函數(shù)參數(shù)中執(zhí)行的是一段同步代碼想邦,Promise剛創(chuàng)建完成裤纹,resolve方法就已經被調用了,因而緊跟著的輸出顯示p1是resolved狀態(tài)丧没。我們通過兩個setTimeout函數(shù)鹰椒,延遲1s后再次輸出p2、p3的狀態(tài)呕童,此時p2漆际、p3已經執(zhí)行完成,狀態(tài)分別變成resolved和rejected
分別分析就看得很清楚了
var p1 = new Promise(function(resolve,reject){
resolve(1);
});
console.log(p1);
p1.then(function(value){
console.log(value);
});
//
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
1
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
var p2 = new Promise(function(resolve,reject){
setTimeout(function(){
resolve(2);
}, 500);
});
console.log(p2);
setTimeout(function(){
console.log(p2);
}, 1000);
p2.then(function(value){
console.log(value);
});
//
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
2
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
var p3 = new Promise(function(resolve,reject){
setTimeout(function(){
reject(3);
}, 500);
});
console.log(p3);
setTimeout(function(){
console.log(p3);
}, 1000);
p3.then(function(value){
console.log(value);
});
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
3
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 2}
- Promise 狀態(tài)的不可逆性
var p1 = new Promise(function(resolve, reject){
resolve("success1");
resolve("success2");
});
var p2 = new Promise(function(resolve, reject){
resolve("success");
reject("reject");
});
p1.then(function(value){
console.log(value);
});
p2.then(function(value){
console.log(value);
});
"success1"
"success"
Promise狀態(tài)的一旦變成resolved或rejected時夺饲,Promise的狀態(tài)和值就固定下來了奸汇,不論你后續(xù)再怎么調用resolve或reject方法,都不能改變它的狀態(tài)和值往声。因此擂找,p1中resolve("success2")并不能將p1的值更改為success2,p2中reject("reject")也不能將p2的狀態(tài)由resolved改變?yōu)閞ejected.
- 鏈式調用
var p = new Promise(function(resolve, reject){
resolve(1);
});
p.then(function(value){ //第一個then
console.log(value);
return value*2;
}).then(function(value){ //第二個then
console.log(value);
}).then(function(value){ //第三個then
console.log(value);
return Promise.resolve('resolve');
}).then(function(value){ //第四個then
console.log(value);
return Promise.reject('reject');
}).then(function(value){ //第五個then
console.log('resolve: '+ value);
}, function(err){
console.log('reject: ' + err);
})
1
2
undefined
"resolve"
"reject: reject"
Promise對象的then方法返回一個新的Promise對象浩销,因此可以通過鏈式調用then方法贯涎。then方法接收兩個函數(shù)作為參數(shù),第一個參數(shù)是Promise執(zhí)行成功時的回調撼嗓,第二個參數(shù)是Promise執(zhí)行失敗時的回調。兩個函數(shù)只會有一個被調用欢唾,函數(shù)的返回值將被用作創(chuàng)建then返回的Promise對象且警。這兩個參數(shù)的返回值可以是以下三種情況中的一種:
return 一個同步的值 ,或者 undefined(當沒有返回一個有效值時礁遣,默認返回undefined)斑芜,then方法將返回一個resolved狀態(tài)的Promise對象,Promise對象的值就是這個返回值祟霍。
return 另一個 Promise杏头,then方法將根據(jù)這個Promise的狀態(tài)和值創(chuàng)建一個新的Promise對象返回盈包。
throw 一個同步異常,then方法將返回一個rejected狀態(tài)的Promise, 值是該異常
根據(jù)以上分析醇王,代碼中第一個then會返回一個值為2(1*2)呢燥,狀態(tài)為resolved的Promise對象,于是第二個then輸出的值是2寓娩。第二個then中沒有返回值叛氨,因此將返回默認的undefined,于是在第三個then中輸出undefined棘伴。第三個then和第四個then中分別返回一個狀態(tài)是resolved的Promise和一個狀態(tài)是rejected的Promise寞埠,依次由第四個then中成功的回調函數(shù)和第五個then中失敗的回調函數(shù)處理
- Promise then() 回調異步性
var p = new Promise(function(resolve, reject){
resolve("success");
});
p.then(function(value){
console.log(value);
});
console.log("which one is called first ?");
"which one is called first ?"
"success"
Promise接收的函數(shù)參數(shù)是同步執(zhí)行的,但then方法中的回調函數(shù)執(zhí)行則是異步的始腾,因此材失,"success"會在后面輸出
6.Promise 中的異常
var p1 = new Promise( function(resolve,reject){
foo.bar();
resolve( 1 );
});
p1.then(
function(value){
console.log('p1 then value: ' + value);
},
function(err){
console.log('p1 then err: ' + err);
}
).then(
function(value){
console.log('p1 then then value: '+value);
},
function(err){
console.log('p1 then then err: ' + err);
}
);
var p2 = new Promise(function(resolve,reject){
resolve( 2 );
});
p2.then(
function(value){
console.log('p2 then value: ' + value);
foo.bar();
},
function(err){
console.log('p2 then err: ' + err);
}
).then(
function(value){
console.log('p2 then then value: ' + value);
},
function(err){
console.log('p2 then then err: ' + err);
return 1;
}
).then(
function(value){
console.log('p2 then then then value: ' + value);
},
function(err){
console.log('p2 then then then err: ' + err);
}
);
///
p1 then err: ReferenceError: foo is not defined
p2 then value: 2
p1 then then value: undefined
p2 then then err: ReferenceError: foo is not defined
p2 then then then value: 1
Promise中的異常由then參數(shù)中第二個回調函數(shù)(Promise執(zhí)行失敗的回調)處理镰矿,異常信息將作為Promise的值。異常一旦得到處理饭冬,then返回的后續(xù)Promise對象將恢復正常,并會被Promise執(zhí)行成功的回調函數(shù)處理颇象。另外伍伤,需要注意p1、p2 多級then的回調函數(shù)是交替執(zhí)行的 遣钳,這正是由Promise then回調的異步性決定的
7.Promise.resolve()
var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
resolve(1);
});
var p4 = new Promise(function(resolve, reject){
resolve(p1);
});
console.log(p1 === p2);
console.log(p1 === p3);
console.log(p1 === p4);
console.log(p3 === p4);
p4.then(function(value){
console.log('p4=' + value);
});
p2.then(function(value){
console.log('p2=' + value);
})
p1.then(function(value){
console.log('p1=' + value);
})
////
true
false
false
false
p2=1
p1=1
p4=1
- resolve vs reject
var p1 = new Promise(function(resolve, reject){
resolve(Promise.resolve('resolve'));
});
var p2 = new Promise(function(resolve, reject){
resolve(Promise.reject('reject'));
});
var p3 = new Promise(function(resolve, reject){
reject(Promise.resolve('resolve'));
});
p1.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
p2.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
p3.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
////
p3 rejected: [object Promise]
p1 fulfilled: resolve
p2 rejected: reject
拆分:
var p1 = new Promise(function(resolve, reject){
resolve(Promise.resolve('resolve'));
});
p1.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
fulfilled: resolve
Promise回調函數(shù)中的第一個參數(shù)resolve扰魂,會對Promise執(zhí)行"拆箱"動作。即當resolve的參數(shù)是一個Promise對象時蕴茴,resolve會"拆箱"獲取這個Promise對象的狀態(tài)和值劝评,但這個過程是異步的。p1"拆箱"后倦淀,獲取到Promise對象的狀態(tài)是resolved蒋畜,因此fulfilled回調被執(zhí)行
var p2 = new Promise(function(resolve, reject){
resolve(Promise.reject('reject'));
});
p2.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
//
rejected: reject
var p3 = new Promise(function(resolve, reject){
reject(Promise.resolve('resolve'));
//reject的參數(shù)會直接傳遞給then方法中的rejected回調
});
p3.then(
function fulfilled(value){
console.log('fulfilled: ' + value);
},
function rejected(err){
console.log('rejected: ' + err);
}
);
rejected: [object Promise]
p2"拆箱"后,獲取到Promise對象的狀態(tài)是rejected撞叽,因此rejected回調被執(zhí)行姻成。但Promise回調函數(shù)中的第二個參數(shù)reject不具備”拆箱“的能力,reject的參數(shù)會直接傳遞給then方法中的rejected回調愿棋。因此科展,即使p3 reject接收了一個resolved狀態(tài)的Promise,then方法中被調用的依然是rejected糠雨,并且參數(shù)就是reject接收到的Promise對象
promise面試題:紅燈三秒亮一次才睹,綠燈一秒亮一次,黃燈2秒亮一次;如何讓三個燈不斷交替重復亮燈琅攘?(用Promse實現(xiàn)) 三個亮燈函數(shù)已經存在: function red(){ console.log('red'); } function green(){ console.log('green'); } function yellow(){ console.log('yellow'); }
setTimeout相關的異步隊列會掛起直到主進程空閑垮庐。如果使用while無限循環(huán),主進程永遠不會空閑坞琴,setTimeout的函數(shù)永遠不會執(zhí)行哨查!
function red(){
console.log('red');
}
function green(){
console.log('green');
}
function yellow(){
console.log('yellow');
}
var tic = function(timmer, cb){
return new Promise(function(resolve, reject) {
setTimeout(function() {
cb();
resolve();
}, timmer);
});
};
var d = new Promise(function(resolve, reject){resolve();});
var step = function(def) {
def.then(function(){
return tic(3000, red);
}).then(function(){
return tic(2000, green);
}).then(function(){
return tic(1000, yellow);
}).then(function(){
step(def);
});
}
step(d);
var tic = function(timmer, str){
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log(str);
resolve(1);
}, timmer);
});
};
function *gen(){
yield tic(3000, 'red');
yield tic(1000, 'green');
yield tic(2000, 'yellow');
}
var iterator = gen();
var step = function(gen, iterator){
var s = iterator.next();
if (s.done) {
step(gen, gen());
} else {
s.value.then(function() {
step(gen, iterator);
});
}
}
step(gen, iterator);
使用then方法添加回調函數(shù):
// 寫法一
doSomething().then(function () {
return doSomethingElse();
});
// 寫法二
doSomething().then(function () {
doSomethingElse();
});
// 寫法三
doSomething().then(doSomethingElse());
// 寫法四
doSomething().then(doSomethingElse);
為了便于講解,下面這四種寫法都再用then方法接一個回調函數(shù)finalHandler置济。寫法一的finalHandler回調函數(shù)的參數(shù)解恰,是doSomethingElse函數(shù)的運行結果
doSomething().then(function () {
return doSomethingElse();
}).then(finalHandler);
寫法二的finalHandler回調函數(shù)的參數(shù)是undefined
doSomething().then(function () {
doSomethingElse();
return;
}).then(finalHandler);
寫法三的finalHandler回調函數(shù)的參數(shù),是doSomethingElse函數(shù)返回的回調函數(shù)的運行結果
doSomething().then(doSomethingElse())
.then(finalHandler);
寫法四與寫法一只有一個差別浙于,那就是doSomethingElse會接收到doSomething()返回的結果
doSomething().then(doSomethingElse)
.then(finalHandler);
實現(xiàn)一個promise
var Promise = (function() {
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('Promise resolver ' + resolver + ' is not a function')
}
if (!(this instanceof Promise)) return new Promise(resolver)
var self = this
self.callbacks = []
self.status = 'pending'
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject)
}
setTimeout(function() {
if (self.status !== 'pending') {
return
}
self.status = 'resolved'
self.data = value
for (var i = 0; i < self.callbacks.length; i++) {
self.callbacks[i].onResolved(value)
}
})
}
function reject(reason) {
setTimeout(function(){
if (self.status !== 'pending') {
return
}
self.status = 'rejected'
self.data = reason
for (var i = 0; i < self.callbacks.length; i++) {
self.callbacks[i].onRejected(reason)
}
})
}
try{
resolver(resolve, reject)
} catch(e) {
reject(e)
}
}
function resolvePromise(promise, x, resolve, reject) {
var then
var thenCalledOrThrow = false
if (promise === x) {
return reject(new TypeError('Chaining cycle detected for promise!'))
}
if (x instanceof Promise) {
if (x.status === 'pending') {
x.then(function(v) {
resolvePromise(promise, v, resolve, reject);
}, reject);
} else {
x.then(resolve, reject)
}
return
}
if ((x !== null) && ((typeof x === 'object') || (typeof x === 'function'))) {
try {
then = x.then
if (typeof then === 'function') {
then.call(x, function rs(y) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return resolvePromise(promise, y, resolve, reject)
}, function rj(r) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(r)
})
} else {
return resolve(x)
}
} catch(e) {
if (thenCalledOrThrow) return
thenCalledOrThrow = true
return reject(e)
}
} else {
return resolve(x)
}
}
Promise.prototype.then = function(onResolved, onRejected) {
onResolved = typeof onResolved === 'function' ? onResolved : function(v){return v}
onRejected = typeof onRejected === 'function' ? onRejected : function(r){throw r}
var self = this
var promise2
if (self.status === 'resolved') {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
var value = onResolved(self.data)
resolvePromise(promise2, value, resolve, reject)
} catch(e) {
return reject(e)
}
})
})
}
if (self.status === 'rejected') {
return promise2 = new Promise(function(resolve, reject) {
setTimeout(function() {
try {
var value = onRejected(self.data)
resolvePromise(promise2, value, resolve, reject)
} catch(e) {
return reject(e)
}
})
})
}
if (self.status === 'pending') {
return promise2 = new Promise(function(resolve, reject) {
self.callbacks.push({
onResolved: function(value) {
try {
var value = onResolved(value)
resolvePromise(promise2, value, resolve, reject)
} catch(e) {
return reject(e)
}
},
onRejected: function(reason) {
try {
var value = onRejected(reason)
resolvePromise(promise2, value, resolve, reject)
} catch(e) {
return reject(e)
}
}
})
})
}
}
Promise.prototype.valueOf = function() {
return this.data
}
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected)
}
Promise.prototype.finally = function(fn) {
// 為什么這里可以呢护盈,因為所有的then調用是一起的,但是這個then里調用fn又異步了一次羞酗,所以它總是最后調用的腐宋。
// 當然這里只能保證在已添加的函數(shù)里是最后一次,不過這也是必然檀轨。
// 不過看起來比其它的實現(xiàn)要簡單以及容易理解的多胸竞。
// 貌似對finally的行為沒有一個公認的定義,所以這個實現(xiàn)目前是跟Q保持一致参萄,會返回一個新的Promise而不是原來那個卫枝。
return this.then(function(v){
setTimeout(fn)
return v
}, function(r){
setTimeout(fn)
throw r
})
}
Promise.prototype.spread = function(fn, onRejected) {
return this.then(function(values) {
return fn.apply(null, values)
}, onRejected)
}
Promise.prototype.inject = function(fn, onRejected) {
return this.then(function(v) {
return fn.apply(null, fn.toString().match(/\((.*?)\)/)[1].split(',').map(function(key){
return v[key];
}))
}, onRejected)
}
Promise.prototype.delay = function(duration) {
return this.then(function(value) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(value)
}, duration)
})
}, function(reason) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
reject(reason)
}, duration)
})
})
}
Promise.all = function(promises) {
return new Promise(function(resolve, reject) {
var resolvedCounter = 0
var promiseNum = promises.length
var resolvedValues = new Array(promiseNum)
for (var i = 0; i < promiseNum; i++) {
(function(i) {
Promise.resolve(promises[i]).then(function(value) {
resolvedCounter++
resolvedValues[i] = value
if (resolvedCounter == promiseNum) {
return resolve(resolvedValues)
}
}, function(reason) {
return reject(reason)
})
})(i)
}
})
}
Promise.race = function(promises) {
return new Promise(function(resolve, reject) {
for (var i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(function(value) {
return resolve(value)
}, function(reason) {
return reject(reason)
})
}
})
}
Promise.resolve = function(value) {
return new Promise(function(resolve) {
resolve(value)
})
}
Promise.reject = function(reason) {
return new Promise(function(resolve, reject) {
reject(reason)
})
}
Promise.fcall = function(fn){
// 雖然fn可以接收到上一層then里傳來的參數(shù),但是其實是undefined讹挎,所以跟沒有是一樣的校赤,因為resolve沒參數(shù)啊
return Promise.resolve().then(fn)
}
Promise.done = Promise.stop = function(){
return new Promise(function(){})
}
Promise.deferred = Promise.defer = function() {
var dfd = {}
dfd.promise = new Promise(function(resolve, reject) {
dfd.resolve = resolve
dfd.reject = reject
})
return dfd
}
try { // CommonJS compliance
module.exports = Promise
} catch(e) {}
return Promise
})()
最后提一下值穿透:
var promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('haha')
}, 1000)
})
promise
.then('hehe')
.then(console.log)
最終打印 haha 而不是 hehe
Generator:
說Generator之前,還是談談Iterator 遍歷器比較好
Symbol是一個特殊的數(shù)據(jù)類型筒溃,和number string等并列
console.log(Array.prototype.slice) // ? slice() { [native code] }
console.log(Array.prototype[Symbol.iterator])
// ? values() { [native code] }
我們獲取Array.prototype[Symbol.iterator]可以得到一個函數(shù)马篮,只不過這里的[Symbol.iterator]是Symbol數(shù)據(jù)類型,不是字符串怜奖。但是沒關系浑测,Symbol數(shù)據(jù)類型也可以作為對象屬性的key
var obj = {}
obj.a = 100
obj[Symbol.iterator] = 200
console.log(obj) // {a: 100, Symbol(Symbol.iterator): 200}
只需要知道[Symbol.iterator]是一個特殊的數(shù)據(jù)類型Symbol類型,但是也可以像number string類型一樣歪玲,作為對象的屬性key來使用
原生具有[Symbol.iterator]屬性數(shù)據(jù)類型有:數(shù)組迁央、某些類似數(shù)組的對象(如arguments、NodeList)滥崩、Set和Map
原生具有[Symbol.iterator]屬性數(shù)據(jù)類型有一個特點岖圈,就是可以使用for...of來取值
var item
for (item of [100, 200, 300]) {
console.log(item)
}
// 打印出:100 200 300
// 注意,這里每次獲取的 item 是數(shù)組的 value夭委,而不是 index 幅狮,這一點和 傳統(tǒng) for 循環(huán)以及 for...in 完全不一樣
而具有[Symbol.iterator]屬性的對象,都可以一鍵生成一個Iterator對象
const arr = [100, 200, 300]
const iterator = arr[Symbol.iterator]() // 通過執(zhí)行 [Symbol.iterator] 的屬性值(函數(shù))來返回一個 iterator 對象
現(xiàn)在生成了iterator株灸,那么該如何使用它呢 ———— 有兩種方式:next和for...of
console.log(iterator.next()) // { value: 100, done: false }
console.log(iterator.next()) // { value: 200, done: false }
console.log(iterator.next()) // { value: 300, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
iterator對象可以通過next()方法逐步獲取每個元素的值崇摄,以{ value: ..., done: ... }形式返回,value就是值慌烧,done表示是否到已經獲取完成
再說第二種逐抑,for...of
let i
for (i of iterator) {
console.log(i)
}
// 打印:100 200 300
上面使用for...of遍歷iterator對象屹蚊,可以直接將其值獲取出來厕氨。這里的“值”就對應著上面next()返回的結果的value屬性
Generator返回的也是Iterator對象,因此才會有next(),也可以通過for...of來遍歷
正是因為Generator返回的是Iterator對象汹粤,所以我先講生成器對象
先來一段最基礎的Generator代碼
function* Hello() {
yield 100
yield (function () {return 200})()
return 300
}
var h = Hello()
console.log(typeof h) // object
console.log(h.next()) // { value: 100, done: false }
console.log(h.next()) // { value: 200, done: false }
console.log(h.next()) // { value: 300, done: true }
console.log(h.next()) // { value: undefined, done: true }
定義Generator時命斧,需要使用function*,其他的和定義函數(shù)一樣嘱兼。內部使用yield国葬,至于yield的用處以后再說
執(zhí)行var h = Hello()生成一個Generator對象,經驗驗證typeof h發(fā)現(xiàn)不是普通的函數(shù)
執(zhí)行Hello()之后芹壕,Hello內部的代碼不會立即執(zhí)行汇四,而是出于一個暫停狀態(tài)
執(zhí)行第一個h.next()時,會激活剛才的暫停狀態(tài)踢涌,開始執(zhí)行Hello內部的語句通孽,但是,直到遇到y(tǒng)ield語句睁壁。一旦遇到y(tǒng)ield語句時背苦,它就會將yield后面的表達式執(zhí)行,并返回執(zhí)行的結果堡僻,然后又立即進入暫停狀態(tài)糠惫。
因此第一個console.log(h.next())打印出來的是{ value: 100, done: false },value是第一個yield返回的值钉疫,done: false表示目前處于暫停狀態(tài)硼讽,尚未執(zhí)行結束,還可以再繼續(xù)往下執(zhí)行牲阁。
執(zhí)行第二個h.next()和第一個一樣固阁,不在贅述。此時會執(zhí)行完第二個yield后面的表達式并返回結果城菊,然后再次進入暫停狀態(tài)
執(zhí)行第三個h.next()時备燃,程序會打破暫停狀態(tài),繼續(xù)往下執(zhí)行凌唬,但是遇到的不是yield而是return并齐。這就預示著,即將執(zhí)行結束了。因此最后返回的是{ value: 300, done: true }况褪,done: true表示執(zhí)行結束撕贞,無法再繼續(xù)往下執(zhí)行了。
再去執(zhí)行第四次h.next()時测垛,就只能得到{ value: undefined, done: true }捏膨,因為已經結束,沒有返回值了
需要明白以下幾點:
Generator不是函數(shù)食侮,不是函數(shù)号涯,不是函數(shù)
Hello()不會立即出發(fā)執(zhí)行,而是一上來就暫停
每次h.next()都會打破暫停狀態(tài)去執(zhí)行锯七,直到遇到下一個yield或者return
遇到y(tǒng)ield時链快,會執(zhí)行yeild后面的表達式,并返回執(zhí)行之后的值眉尸,然后再次進入暫停狀態(tài)久又,此時done: false。
遇到return時效五,會返回值地消,執(zhí)行結束,即done: true
每次h.next()的返回值永遠都是{value: ... , done: ...}的形式
第一個next()無需傳入?yún)?shù)畏妖,它總是啟動一個生成器脉执,并運行到第一個yield處,不過戒劫,第二個next(..)調用第一個暫停的yield表達式半夷,第三個next(..)調用第二個暫停的yield表達式,最后多出了一個next(),有return來回答它
生成器消息是雙向傳遞的迅细,yield(...)作為一個表達式可以發(fā)出消息響應next.value()巫橄,next(...)也可以向暫停的yield表達式發(fā)送值,可以看下面這個demo
function *(){
var y = x * (yield "hello")
return y
}
var it = foo(6) //將6傳給x
var res = it.next() //啟動生成器
res.value() //hello
res = it.next(7) 向等待的yield傳入7
res.value()//42
function* G() {
const a = yield 100
console.log('a', a) // a aaa
const b = yield 200
console.log('b', b) // b bbb
const c = yield 300
console.log('c', c) // c ccc
}
const g = G()
g.next() // value: 100, done: false
g.next('aaa') // value: 200, done: false
g.next('bbb') // value: 300, done: false
g.next('ccc') // value: undefined, done: true
- 執(zhí)行第一個g.next()時茵典,為傳遞任何參數(shù)湘换,返回的{value: 100, done: false},這個應該沒有疑問
- 執(zhí)行第二個g.next('aaa')時统阿,傳遞的參數(shù)是'aaa'彩倚,這個'aaa'就會被賦值到G內部的a標量中,然后執(zhí)行console.log('a', a)打印出來扶平,最后返回{value: 200, done: false}
- 執(zhí)行第三個帆离、第四個時,道理都是完全一樣的结澄,大家自己捋一捋
有一個要點需要注意哥谷,就g.next('aaa')是將'aaa'傳遞給上一個已經執(zhí)行完了的yield語句前面的變量岸夯,而不是即將執(zhí)行的yield前面的變量。這句話要能看明白
function* fibonacci() {
let [prev, curr] = [0, 1]
for (;;) {
[prev, curr] = [curr, prev + curr]
// 將中間值通過 yield 返回们妥,并且保留函數(shù)執(zhí)行的狀態(tài)囱修,因此可以非常簡單的實現(xiàn) fibonacci
yield curr
}
}
for (let n of fibonacci()) {
if (n > 1000) {
break
}
console.log(n)
}
如果有兩個Generator,想要在第一個中包含第二個
function* G1() {
yield 'a'
yield* G2() // 使用 yield* 執(zhí)行 G2()
yield 'b'
}
function* G2() {
yield 'x'
yield 'y'
}
for (let item of G1()) {
console.log(item)
yield后面會接一個普通的 JS 對象王悍,而yield后面會接一個Generator,而且會把它其中的yield按照規(guī)則來一步一步執(zhí)行餐曼。如果有多個Generator串聯(lián)使用的話(例如Koa源碼中)压储,用yield來操作非常方便
Thunk 函數(shù):
往往是將參數(shù)放到一個臨時函數(shù)之中,再將這個臨時函數(shù)傳入函數(shù)體源譬。這個臨時函數(shù)就叫做 Thunk 函數(shù)
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
const thunk = function (fileName, codeType) {
// 返回一個只接受 callback 參數(shù)的函數(shù)
return function (callback) {
fs.readFile(fileName, codeType, callback)
}
}
const readFileThunk = thunk('data1.json', 'utf-8')
readFileThunk((err, data) => {
// 獲取文件內容
})
執(zhí)行const readFileThunk = thunk('data1.json', 'utf-8')返回的其實是一個函數(shù)
readFileThunk這個函數(shù)集惋,只接受一個參數(shù),而且這個參數(shù)是一個callback函數(shù)
就上上面的代碼踩娘,我們經過對傳統(tǒng)的異步操作函數(shù)進行封裝刮刑,得到一個只有一個參數(shù)的函數(shù),而且這個參數(shù)是一個callback函數(shù)养渴,那這就是一個thunk函數(shù)雷绢。就像上面代碼中readFileThunk一樣
在Genertor中使用thunk函數(shù)
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1)
const r2 = yield readFileThunk('data2.json')
console.log(r2)
}
挨個讀取兩個文件的內容
復制代碼
const g = gen()
// 試著打印 g.next() 這里一定要明白 value 是一個 thunk函數(shù) ,否則下面的代碼你都看不懂
// console.log( g.next() ) // g.next() 返回 {{ value: thunk函數(shù), done: false }}
// 下一行中理卑,g.next().value 是一個 thunk 函數(shù)翘紊,它需要一個 callback 函數(shù)作為參數(shù)傳遞進去
g.next().value((err, data1) => {
// 這里的 data1 獲取的就是第一個文件的內容。下一行中藐唠,g.next(data1) 可以將數(shù)據(jù)傳遞給上面的 r1 變量帆疟,此前已經講過這種參數(shù)傳遞的形式
// 下一行中,g.next(data1).value 又是一個 thunk 函數(shù)宇立,它又需要一個 callback 函數(shù)作為參數(shù)傳遞進去
g.next(data1).value((err, data2) => {
// 這里的 data2 獲取的是第二個文件的內容踪宠,通過 g.next(data2) 將數(shù)據(jù)傳遞個上面的 r2 變量
g.next(data2)
})
})
自驅動流程:
// 自動流程管理的函數(shù)
function run(generator) {
const g = generator()
function next(err, data) {
const result = g.next(data) // 返回 { value: thunk函數(shù), done: ... }
if (result.done) {
// result.done 表示是否結束,如果結束了那就 return 作罷
return
}
result.value(next) // result.value 是一個 thunk 函數(shù)妈嘹,需要一個 callback 函數(shù)作為參數(shù)柳琢,而 next 就是一個 callback 形式的函數(shù)
}
next() // 手動執(zhí)行以啟動第一次 next
}
// 定義 Generator
const readFileThunk = thunkify(fs.readFile)
const gen = function* () {
const r1 = yield readFileThunk('data1.json')
console.log(r1.toString())
const r2 = yield readFileThunk('data2.json')
console.log(r2.toString())
}
// 啟動執(zhí)行
run(gen)
其實這段代碼和上面的手動編寫讀取兩個文件內容的代碼,原理上是一模一樣的润脸,只不過這里把流程驅動給封裝起來了染厅。我們簡單分析一下這段代碼
最后一行run(gen)之后,進入run函數(shù)內部執(zhí)行
先const g = generator()創(chuàng)建Generator實例津函,然后定義一個next方法肖粮,并且立即執(zhí)行next()
注意這個next函數(shù)的參數(shù)是err, data兩個,和我們fs.readFile用到的callback函數(shù)形式完全一樣
第一次執(zhí)行next時尔苦,會執(zhí)行const result = g.next(data)涩馆,而g.next(data)返回的是{ value: thunk函數(shù), done: ... }行施,value是一個thunk函數(shù),done表示是否結束
如果done: true魂那,那就直接return了蛾号,否則繼續(xù)進行
result.value是一個thunk函數(shù),需要接受一個callback函數(shù)作為參數(shù)傳遞進去涯雅,因此正好把next給傳遞進去鲜结,讓next一直被執(zhí)行下去
koa 中如何應用Generator:
oa 是一個 web 框架,處理 http 請求活逆,但是這里我們不去管它如何處理 http 請求精刷,而是直接關注它使用Genertor的部分————中間件
let info = ''
function* g1() {
info += '1' // 拼接 1
yield* g2() // 拼接 234
info += '5' // 拼接 5
}
function* g2() {
info += '2' // 拼接 2
yield* g3() // 拼接 3
info += '4' // 拼接 4
}
function* g3() {
info += '3' // 拼接 3
}
var g = g1()
g.next()
console.log(info) // 12345
但是如果用 koa 的 中間件 的思路來做,就需要如下這么寫
app.use(function *(next){
this.body = '1';
yield next;
this.body += '5';
console.log(this.body);
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(next){
this.body += '3';
});
app.use()中傳入的每一個Generator就是一個 中間件蔗候,中間件按照傳入的順序排列怒允,順序不能亂
每個中間件內部,next表示下一個中間件锈遥。yield next就是先將程序暫停纫事,先去執(zhí)行下一個中間件,等next被執(zhí)行完之后所灸,再回過頭來執(zhí)行當前代碼的下一行丽惶。因此,koa 的中間件執(zhí)行順序是一種洋蔥圈模型爬立,不過這里看不懂也沒問題蚊夫。
每個中間件內部,this可以共享變量懦尝。即第一個中間件改變了this的屬性知纷,在第二個中間件中可以看到效果
koa 的這種應用機制是如何實現(xiàn)的
class MyKoa extends Object {
constructor(props) {
super(props);
// 存儲所有的中間件
this.middlewares = []
}
// 注入中間件
use (generator) {
this.middlewares.push(generator)
}
// 執(zhí)行中間件
listen () {
this._run()
}
_run () {
const ctx = this
const middlewares = ctx.middlewares
co(function* () {
let prev = null
let i = middlewares.length
//從最后一個中間件到第一個中間件的順序開始遍歷
while (i--) {
// ctx 作為函數(shù)執(zhí)行時的 this 才能保證多個中間件中數(shù)據(jù)的共享
//prev 將前面一個中間件傳遞給當前中間件,才使得中間件里面的 next 指向下一個中間件
prev = middlewares[i].call(ctx, prev);
}
//執(zhí)行第一個中間件
yield prev;
})
}
}
var app = new MyKoa();
app.use(function *(next){
this.body = '1';
yield next;
this.body += '5';
console.log(this.body); // 12345
});
app.use(function *(next){
this.body += '2';
yield next;
this.body += '4';
});
app.use(function *(next){
this.body += '3';
});
app.listen();
Promise其實是利用了callback才能實現(xiàn)的陵霉。而這里琅轧,Generator也必須利用callback才能實現(xiàn),如果yield后面用的是thunk函數(shù),那么thunk函數(shù)需要的就是一個callback參數(shù)踊挠。如果yield后面用的是Promise對象
因此乍桂,Generator離不開callback,Promise離不開callback效床,異步也離不開callback
co(function* () {
const r1 = yield readFilePromise('some1.json')
console.log(r1) // 打印第 1 個文件內容
const r2 = yield readFilePromise('some2.json')
console.log(r2) // 打印第 2 個文件內容
})
再來一段async-await的執(zhí)行代碼如下睹酌,兩者做一個比較。
const readFilePromise = Q.denodeify(fs.readFile)
// 定義 async 函數(shù)
const readFileAsync = async function () {
const f1 = await readFilePromise('data1.json')
const f2 = await readFilePromise('data2.json')
console.log('data1.json', f1.toString())
console.log('data2.json', f2.toString())
return 'done' // 先忽略剩檀,后面會講到
}
// 執(zhí)行
const result = readFileAsync()
從上面兩端代碼比較看來憋沿,async function代替了function*,await代替了yield沪猴,其他的再沒有什么區(qū)別了辐啄。哦采章,還有,使用async-await時候不用再引用co這種第三方庫了壶辜,直接執(zhí)行即可
使用async-await的不同和好處:
await后面不能再跟thunk函數(shù)悯舟,而必須跟一個Promise對象(因此,Promise才是異步的終極解決方案和未來)砸民。跟其他類型的數(shù)據(jù)也OK抵怎,但是會直接同步執(zhí)行,而不是異步
執(zhí)行const result = readFileAsync()返回的是個Promise對象岭参,而且上面代碼中的return 'done'會直接被下面的then函數(shù)接收到執(zhí)行const result = readFileAsync()返回的是個Promise對象反惕,而且上面代碼中的return 'done'會直接被下面的then函數(shù)接收到
result.then(data => {
console.log(data) // done
})
- 從代碼的易讀性來將,async-await更加易讀簡介冗荸,也更加符合代碼的語意。而且還不用引用第三方庫利耍,也無需學習Generator那一堆東西蚌本,使用成本非常低
異步操作代碼的變化:
callback方式:
fs.readFile('some1.json', (err, data) => {
fs.readFile('some2.json', (err, data) => {
fs.readFile('some3.json', (err, data) => {
fs.readFile('some4.json', (err, data) => {
})
})
})
})
Promise方式:
readFilePromise('some1.json').then(data => {
return readFilePromise('some2.json')
}).then(data => {
return readFilePromise('some3.json')
}).then(data => {
return readFilePromise('some4.json')
})
Generator方式:
co(function* () {
const r1 = yield readFilePromise('some1.json')
const r2 = yield readFilePromise('some2.json')
const r3 = yield readFilePromise('some3.json')
const r4 = yield readFilePromise('some4.json')
})
async-await方式:
const readFileAsync = async function () {
const f1 = await readFilePromise('data1.json')
const f2 = await readFilePromise('data2.json')
const f3 = await readFilePromise('data3.json')
const f4 = await readFilePromise('data4.json')
}
以下是參考鏈接: