Javascript異步編程

同步模式與異步模式


事件循環(huán)與消息隊(duì)列


??JavaScript 單線程指的是瀏覽器中負(fù)責(zé)解釋和執(zhí)行 JavaScript 代碼的只有一個線程拘鞋,即為JS引擎線程,但是瀏覽器的渲染進(jìn)程是提供多個線程的矢门,如下:

  • JS引擎線程
  • 事件觸發(fā)線程
  • 定時(shí)觸發(fā)器線程
  • 異步http請求線程
  • GUI渲染線程

??當(dāng)遇到計(jì)時(shí)器盆色、DOM事件監(jiān)聽或者是網(wǎng)絡(luò)請求的任務(wù)時(shí),JS引擎會將它們直接交給 webapi祟剔,也就是瀏覽器提供的相應(yīng)線程(如定時(shí)器線程為setTimeout計(jì)時(shí)隔躲、異步http請求線程處理網(wǎng)絡(luò)請求)去處理,而JS引擎線程繼續(xù)后面的其他任務(wù)物延,這樣便實(shí)現(xiàn)了 異步非阻塞宣旱。

??定時(shí)器觸發(fā)線程也只是為 setTimeout(..., 1000) 定時(shí)而已,時(shí)間一到叛薯,還會把它對應(yīng)的回調(diào)函數(shù)(callback)交給 任務(wù)隊(duì)列 去維護(hù)浑吟,JS引擎線程會在適當(dāng)?shù)臅r(shí)候去任務(wù)隊(duì)列取出任務(wù)并執(zhí)行。

JS引擎線程什么時(shí)候去處理呢耗溜?消息隊(duì)列又是什么组力?

JavaScript 通過 事件循環(huán) event loop 的機(jī)制來解決這個問題。

其實(shí) 事件循環(huán) 機(jī)制和 任務(wù)隊(duì)列 的維護(hù)是由事件觸發(fā)線程控制的抖拴。

事件觸發(fā)線程 同樣是瀏覽器渲染引擎提供的燎字,它會維護(hù)一個 任務(wù)隊(duì)列。

??JS引擎線程遇到異步(DOM事件監(jiān)聽阿宅、網(wǎng)絡(luò)請求候衍、setTimeout計(jì)時(shí)器等...),會交給相應(yīng)的線程單獨(dú)去維護(hù)異步任務(wù)家夺,等待某個時(shí)機(jī)(計(jì)時(shí)器結(jié)束脱柱、網(wǎng)絡(luò)請求成功、用戶點(diǎn)擊DOM)拉馋,然后由 事件觸發(fā)線程 將異步對應(yīng)的 回調(diào)函數(shù) 加入到消息隊(duì)列中榨为,消息隊(duì)列中的回調(diào)函數(shù)等待被執(zhí)行。

同時(shí)煌茴,JS引擎線程會維護(hù)一個 執(zhí)行棧随闺,同步代碼會依次加入執(zhí)行棧然后執(zhí)行,結(jié)束會退出執(zhí)行棧蔓腐。

如果執(zhí)行棧里的任務(wù)執(zhí)行完成矩乐,即執(zhí)行棧為空的時(shí)候(即JS引擎線程空閑),事件觸發(fā)線程才會從消息隊(duì)列取出一個任務(wù)(即異步的回調(diào)函數(shù))放入執(zhí)行棧中執(zhí)行。

消息隊(duì)列是類似隊(duì)列的數(shù)據(jù)結(jié)構(gòu)散罕,遵循先入先出(FIFO)的規(guī)則分歇。

  1. 所有同步任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧(execution context stack)欧漱。
  2. 主線程之外职抡,還存在一個"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果误甚,就在"任務(wù)隊(duì)列"之中放置一個事件缚甩。
  3. 一但"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會讀取"任務(wù)隊(duì)列"窑邦,看看里面有哪些事件擅威。那些對應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài)冈钦,進(jìn)入執(zhí)行棧郊丛,開始執(zhí)行。
  4. 主線程不斷重復(fù)上面的第三步派继。
    只要主線程空了宾袜,就會去讀取"任務(wù)隊(duì)列",這就是JavaScript的運(yùn)行機(jī)制驾窟。這個過程會不斷重復(fù)庆猫,這種機(jī)制就被稱為事件循環(huán)(event loop)機(jī)制。

異步編程的幾種方式


Promise異步方案 宏任務(wù)/微任務(wù)隊(duì)列


Promise

  • promise是一個類绅络,在執(zhí)行這個類的時(shí)候需要傳遞一個執(zhí)行器進(jìn)去 執(zhí)行器月培,執(zhí)行器會立即執(zhí)行
  • promise中有三種狀態(tài) 分別為成功fulfilled 失敗rejected 等待pending
    一旦狀態(tài)確定就不可更改
  • resolve和reject函數(shù)是用來更改狀態(tài)的
  • then方法內(nèi)部做的事情就是判斷狀態(tài),成功就調(diào)用成功函數(shù)恩急,反之調(diào)用失敗函數(shù)杉畜,then方法是被定義到原型對象中的
  • then成功回調(diào)有一個參數(shù)表示成功之后的值,同樣失敗也有參數(shù)
  • then方法是可以被鏈?zhǔn)秸{(diào)用的衷恭,后面的then拿到的值是此叠,是上一個then的返回值
const PENDING='pending'
const FULFILLED='fulfilled'
const REJECTED='rejected'

class MyPromise{
  construtor(executor){
    try{
      executor(this.resolve,this.reject)
    }catch(e){
      this.reject(e)
    }
  }
  
  status=PENDING
//成功之后的值
  value=undefined
//失敗后的原因
  reason=undefined

//成功回調(diào)
successCallback=[]
//失敗回調(diào)
failCallback=[]

  resolve=(value)=>{  
  //如果狀態(tài)不是等待阻止程序向下執(zhí)行
  if(this.status!==PENDING) return;
    this.status=FULFILLED
    //保存成功之后的值
    this.value=value
    while(this.successCallback.length){
      this.successCallback.shift()()
    }
  }
  reject=()=>{
    if(this.status!==PENDING) return随珠;
    this.status=REJECTED
    //保存失敗后的原因
    this.reason=reason
    
    while(this.failCallback.length){
      this.failCallback.shift()()
    }
  }
  then(successCallback,failCallback){
    let promise2=new MyPromise((resolve,reject)=>{
      if(this.status===FULFILLED){
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            //判斷x的值是普通值還是promise對象
            //如果是普通值直接調(diào)用resolve
            //如果是promise對象查看promise對象的返回結(jié)果
            //再根據(jù)promise對象返回的結(jié)果決定調(diào)用resolve還是reject
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
      }
      if(this.status===REJECTED){
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        
      }else{
      //等待,存儲成功和失敗回調(diào)
        this.successCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= successCallback(this.value)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
        this.failCallback.push(()=>{
        setTimeout(()=>{
          try{
            let x= failCallback(this.reason)
            resolvePromise(promise2,x,resolve,reject)
          }catch(e){
            reject(e)
          }
        },0)
        })
      }
    })
    
    return promise2
  }
}

function resolvePromise(promise2,x,resolve,reject){
  if(x===promise2){
  reject(new TypeError('chaining cycle detected for promise'))
  }
  if(x instanceof MyPromise){
    //x.then(value=>resolve(value),reason=>reject(reason))
    x.then(resolve,reject)
  }else{
    resolve(x)
  }
}

macro-task(宏任務(wù)) 和 micro-task(微任務(wù))灭袁。


所有任務(wù)分為 macro-task 和 micro-task:

  • macro-task:主代碼塊、setTimeout窗看、setInterval等(可以看到茸歧,事件隊(duì)列中的每一個事件都是一個 macro-task,現(xiàn)在稱之為宏任務(wù)隊(duì)列)
  • micro-task:Promise显沈、process.nextTick等

JS引擎線程首先執(zhí)行主代碼塊软瞎。

每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(wù)逢唤,包括任務(wù)隊(duì)列(宏任務(wù)隊(duì)列)中的,因?yàn)閳?zhí)行棧中的宏任務(wù)執(zhí)行完會去取任務(wù)隊(duì)列(宏任務(wù)隊(duì)列)中的任務(wù)加入執(zhí)行棧中涤浇,即同樣是事件循環(huán)的機(jī)制鳖藕。

在執(zhí)行宏任務(wù)時(shí)遇到Promise等,會創(chuàng)建微任務(wù)(.then()里面的回調(diào))只锭,并加入到微任務(wù)隊(duì)列隊(duì)尾吊奢。

micro-task必然是在某個宏任務(wù)執(zhí)行的時(shí)候創(chuàng)建的,而在下一個宏任務(wù)開始之前纹烹,瀏覽器會對頁面重新渲染(task >> 渲染 >> 下一個task(從任務(wù)隊(duì)列中取一個))。同時(shí)召边,在上一個宏任務(wù)執(zhí)行完成后铺呵,渲染頁面之前,會執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)隧熙。

也就是說片挂,在某一個macro-task執(zhí)行完后,在重新渲染與開始下一個宏任務(wù)之前贞盯,就會將在它執(zhí)行期間產(chǎn)生的所有micro-task都執(zhí)行完畢(在渲染前)音念。

這樣就可以解釋 "promise 1" "promise 2" 在 "timer over" 之前打印了。"promise 1" "promise 2" 做為微任務(wù)加入到微任務(wù)隊(duì)列中躏敢,而 "timer over" 做為宏任務(wù)加入到宏任務(wù)隊(duì)列中闷愤,它們同時(shí)在等待被執(zhí)行,但是微任務(wù)隊(duì)列中的所有微任務(wù)都會在開始下一個宏任務(wù)之前都被執(zhí)行完件余。

在node環(huán)境下讥脐,process.nextTick的優(yōu)先級高于Promise,也就是說:在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue啼器,然后才會執(zhí)行微任務(wù)中的Promise旬渠。
執(zhí)行機(jī)制:

  1. 執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊(duì)列中獲取)
  2. 執(zhí)行過程中如果遇到微任務(wù)端壳,就將它添加到微任務(wù)的任務(wù)隊(duì)列中
  3. 宏任務(wù)執(zhí)行完畢后告丢,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
  4. 當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染损谦,然后GUI線程接管渲染
  5. 渲染完畢后岖免,JS引擎線程繼續(xù),開始下一個宏任務(wù)(從宏任務(wù)隊(duì)列中獲瘸婶妗)

宏任務(wù) macro-task(Task)

一個event loop有一個或者多個task隊(duì)列觅捆。task任務(wù)源非常寬泛,比如ajax的onload麻敌,click事件栅炒,基本上我們經(jīng)常綁定的各種事件都是task任務(wù)源,還有數(shù)據(jù)庫操作(IndexedDB ),需要注意的是setTimeout赢赊、setInterval乙漓、setImmediate也是task任務(wù)源∈鸵疲總結(jié)來說task任務(wù)源:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任務(wù) micro-task(Job)

microtask 隊(duì)列和task 隊(duì)列有些相似叭披,都是先進(jìn)先出的隊(duì)列,由指定的任務(wù)源去提供任務(wù)玩讳,不同的是一個 event loop里只有一個microtask 隊(duì)列涩蜘。另外microtask執(zhí)行時(shí)機(jī)和Macrotasks也有所差異

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

宏任務(wù)和微任務(wù)的區(qū)別

  • 宏隊(duì)列可以有多個,微任務(wù)隊(duì)列只有一個,所以每創(chuàng)建一個新的settimeout都是一個新的宏任務(wù)隊(duì)列熏纯,執(zhí)行完一個宏任務(wù)隊(duì)列后同诫,都會去checkpoint 微任務(wù)。
  • 一個事件循環(huán)后樟澜,微任務(wù)隊(duì)列執(zhí)行完了误窖,再執(zhí)行宏任務(wù)隊(duì)列
  • 一個事件循環(huán)中,在執(zhí)行完一個宏隊(duì)列之后秩贰,就會去check 微任務(wù)隊(duì)列

Generator異步方案 async/await語法糖

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末霹俺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子毒费,更是在濱河造成了極大的恐慌丙唧,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝗罗,死亡現(xiàn)場離奇詭異艇棕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)串塑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進(jìn)店門沼琉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桩匪,你說我怎么就攤上這事打瘪。” “怎么了傻昙?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵闺骚,是天一觀的道長。 經(jīng)常有香客問我妆档,道長僻爽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任贾惦,我火速辦了婚禮胸梆,結(jié)果婚禮上敦捧,老公的妹妹穿的比我還像新娘。我一直安慰自己碰镜,他們只是感情好兢卵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绪颖,像睡著了一般秽荤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上柠横,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天窃款,我揣著相機(jī)與錄音,去河邊找鬼牍氛。 笑死雁乡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的糜俗。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼曲饱,長吁一口氣:“原來是場噩夢啊……” “哼悠抹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起扩淀,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤楔敌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后驻谆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卵凑,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年胜臊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勺卢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡象对,死狀恐怖黑忱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情勒魔,我是刑警寧澤甫煞,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站冠绢,受9級特大地震影響抚吠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜弟胀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一楷力、第九天 我趴在偏房一處隱蔽的房頂上張望喊式。 院中可真熱鬧,春花似錦弥雹、人聲如沸垃帅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽贸诚。三九已至,卻和暖如春厕吉,著一層夾襖步出監(jiān)牢的瞬間酱固,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工头朱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留运悲,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓项钮,卻偏偏與公主長得像班眯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子烁巫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評論 2 348