国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

由setTimeout深入JavaScript執(zhí)行環(huán)境的異步機制

codeGoogle / 1417人閱讀

摘要:圖片轉(zhuǎn)引自的演講和兩個定時器中回調(diào)的執(zhí)行邏輯便是典型的機制。異步編程關(guān)于異步編程我的理解是,在執(zhí)行環(huán)境所提供的異步機制之上,在應(yīng)用編碼層面上實現(xiàn)整體流程控制的異步風(fēng)格。

問題背景

在一次開發(fā)任務(wù)中,需要實現(xiàn)如下一個餅狀圖動畫,基于canvas進行繪圖,但由于對于JS運行環(huán)境中異步機制的不了解,所以遇到了一個棘手的問題,始終無法解決,之后在與同事交流之后才恍然大悟。問題的根節(jié)在于經(jīng)典的JS定時器異步問題,所以在解決問題之后,又通過了大量的資料閱讀擴展和一段時間的實戰(zhàn)總結(jié),現(xiàn)在對JS運行環(huán)境中異步機制做一個較為深入的分析。

上圖中為最終想要實現(xiàn)的效果,使得各扇形部分可以同時畫出并閉合圓形。點擊此處查看代碼清單。之前遇到的問題是沒有將myLoop作為一個函數(shù)抽離出來,而將其中的所有邏輯,包括定時器都寫在了for循環(huán)中,這樣雖然扇形角度、哨兵變量等的計算均正確,但圓形始終無法閉合,很是郁悶。這里我只是想借此問題來引入JS運行環(huán)境中對于異步機制理解的重要性,大可不必關(guān)心canvas畫圖的實現(xiàn)過程,讓大家明白對異步的理解會牽扯到業(yè)務(wù)邏輯執(zhí)行的準(zhǔn)確性,并非只是用于浮于紙面的面試題之上。至于為什么將定時器的邏輯放在一個函數(shù)中就執(zhí)行正常,而直接寫入for循環(huán)就無法達到預(yù)期,看過下文的詳細分析后,這個問題便會迎刃而解。

深入異步

關(guān)于異步的深入,這里基于現(xiàn)有的知識水平做盡可能詳盡準(zhǔn)確的分析。大家可以從一篇博客進一步了解牛人之間對于異步理解的爭論。一位是技術(shù)博客紅人阮一峰老師,一位是國內(nèi)Node技術(shù)的開山鼻祖樸靈老師,都是我持續(xù)關(guān)注的兩位偶像。事情發(fā)生的比較早了,這里只給出一個文章鏈接,其中在阮老師的博文中附帶了大量樸靈老師的批注,讀過之后定會受益匪淺,也會激發(fā)出你對技術(shù)外的一些思考。

同步與異步

首先來說明同步與異步兩個概念。

f1()
f2()

對于JavaScript語言的執(zhí)行方式,執(zhí)行環(huán)境會支持兩種模式,一種是同步執(zhí)行,一種是異步執(zhí)行。如上面兩個方法,同步執(zhí)行就是調(diào)用f1之后,等待返回結(jié)果,再執(zhí)行f2。異步是調(diào)用f1后,通過一系列其他的操作才可以得到預(yù)期的結(jié)果,比如網(wǎng)絡(luò)IO、磁盤IO等,在線程執(zhí)行這些其他操作的同時,程序還可以往下執(zhí)行,繼續(xù)調(diào)用f2,不用等待f1的結(jié)果返回再執(zhí)行f2。

我們知道,大部分的腳本和編程語言都是同步編程,開發(fā)者對于同步編程的執(zhí)行邏輯也比較容易理解。那么為什么對于JS的執(zhí)行要經(jīng)常用到異步編程,這應(yīng)該要追溯到最初JS適用的宿主環(huán)境--瀏覽器。

由于用于瀏覽器,所以操作DOM的JS只能使用單線程,否則無法保證DOM操作的安全性(比如一個線程將另一個線程正在使用的某個DOM刪掉)。又因為使用單線程,同步執(zhí)行代碼的話,如果遇到耗時較長的操作,那么瀏覽器將會長時間失去響應(yīng),用戶體驗及其不好。但如果將耗時較長的任務(wù),比如ajax請求異步執(zhí)行,那么客戶端的渲染便不會受到耗時任務(wù)的阻塞。

對于服務(wù)器端,JS異步執(zhí)行更為重要,因為執(zhí)行環(huán)境是單線程的,如果同步執(zhí)行所有并發(fā)請求,那么對于客戶端的響應(yīng)將會極其遲鈍,服務(wù)器性能急劇下降,這時必須使用異步模式來處理大量并發(fā)請求,不像Java、PHP等語言是通過多線程來解決并發(fā)問題。這點在現(xiàn)在高并發(fā)司空見慣的網(wǎng)絡(luò)環(huán)境中,反而成為了JS的優(yōu)勢,使得Node在短時間內(nèi)進入主流視野,成為DIRT應(yīng)用1的最佳解決方案。

實現(xiàn)異步的機制

在說實現(xiàn)異步的機制之前,首先需要搞清楚兩個概念,分別是JavaScript的執(zhí)行引擎執(zhí)行環(huán)境。我們常說Google的V8虛擬機便是JavaScript的執(zhí)行引擎,除此之外Safari的JavaScript Core、FireFox的SpiderMonckey都屬于Engine。而上述的瀏覽器和Node等便屬于JavaScript的執(zhí)行環(huán)境,是Runtime。前者Engine是去實現(xiàn)ECMAScript標(biāo)準(zhǔn),后者Runtime是去實現(xiàn)異步的具體機制。所以我們今天講的JS異步機制都是在說JS執(zhí)行環(huán)境的異步機制,與V8這樣的執(zhí)行引擎并無關(guān)系,主要是由各大瀏覽器廠商去做實現(xiàn)。

關(guān)于實現(xiàn)異步的方式,有我們接下來要詳細介紹的Event Loop,還有輪詢、事件等。所謂輪詢,就是你在收銀臺付款之后,不停的問服務(wù)員你的飯菜做好了嗎。所謂事件,就是你在付款之后,不用不停的問服務(wù)員,服務(wù)員在做好飯菜之后會主動告訴你。而大部分的執(zhí)行環(huán)境都是通過Event Loop去實現(xiàn)異步機制,所以下面重點來講解Event Loop。

Event Loop

Event Loop的實現(xiàn)邏輯如下圖。每當(dāng)程序啟動后,內(nèi)存會被分為堆(heap)和棧(stack)兩部分,其中棧中便是主線程的執(zhí)行邏輯所需內(nèi)存,我們根據(jù)這塊內(nèi)存的特殊作用,抽象的將其叫做執(zhí)行棧。在棧中的代碼會調(diào)用各種WebAPI,比如對DOM的操作,ajax請求,創(chuàng)建定時器等。這些操作會產(chǎn)生一些事件,而事件又會關(guān)聯(lián)相應(yīng)的handle(也就是注冊時的callback),將需要執(zhí)行的handle按照隊列的結(jié)構(gòu)放入callback queue(event queue)中。當(dāng)執(zhí)行棧中的代碼執(zhí)行完畢后,主線程會讀取callback queue,依次執(zhí)行其中的回調(diào)函數(shù),然后進入下一輪的事件循環(huán),執(zhí)行清空新產(chǎn)生的事件回調(diào)函數(shù)。由此可見,在執(zhí)行棧中的代碼總是在callback queue之前執(zhí)行

圖片轉(zhuǎn)引自Philip Roberts的演講《Help, I"m stuck in an event-loop》

setTimeout()和setInterval()兩個定時器中回調(diào)的執(zhí)行邏輯便是典型的Event Loop機制。相似的,程序在跑完執(zhí)行棧中的代碼后,事件循環(huán)會不停的檢查系統(tǒng)時間是否到達預(yù)設(shè)的時間點,每當(dāng)?shù)竭_預(yù)設(shè)的時間點時,就會產(chǎn)生一個timeout事件,并將其放入callback queue,等待下輪Event loop執(zhí)行。但在實際應(yīng)用中,有可能執(zhí)行棧中的代碼耗時過長,這樣在執(zhí)行完執(zhí)行棧中的代碼后,再去執(zhí)行callback queue中由setTimeout()產(chǎn)生的回調(diào)時就不能保證在預(yù)期的時間點執(zhí)行,所以JS中的定時器并不總能保證其精準(zhǔn)性。而在詳細了解其特性原理后,我們可以在編程應(yīng)用層面做一些優(yōu)化,盡量使定時器中回調(diào)函數(shù)的執(zhí)行時間點與我們預(yù)期保持一致。由于setTimeout()與setInterval()在本質(zhì)上是一致的,所以在下面的實例分析一節(jié)中我們將會以setTimeout()來做關(guān)于異步機制的分析。

異步編程

關(guān)于異步編程我的理解是,在JS執(zhí)行環(huán)境所提供的異步機制之上,在應(yīng)用編碼層面上實現(xiàn)整體流程控制的異步風(fēng)格。具體地,我們可以用類似setTimeout()中的回調(diào)函數(shù)的形式進行異步編程,或者用類似事件驅(qū)動的發(fā)布/訂閱模式,或者用ES6為我們提供的異步編程的統(tǒng)一接口Promise實現(xiàn),再或者可以嘗試最新最酷的ES7中Async/Await方案,還有一些像Node社區(qū)提供的異步流控庫Step等。這里只是為大家明確異步編程這個概念范疇,具體用法不再深入。

實例分析

這一節(jié)中我將會舉出多例來分析,請大家結(jié)合上述理論細細體會JS中的同步與異步。首先我們從一個經(jīng)典的JS異步面試題開始,然后逐漸深入。

for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
}
 
console.log(new Date, i);

上述代碼片段的運行結(jié)果應(yīng)該是,先立即輸出一個5,然后在1秒以后同時輸出五個5。程序開始執(zhí)行后,首先執(zhí)行執(zhí)行棧中的同步代碼,幾乎同時創(chuàng)建了5個定時器,然后繼續(xù)執(zhí)行第7行的同步代碼。這樣,首先在控制臺輸出一個5,然后在1s以后,5個定時器同時產(chǎn)生5個timeout事件放入callback queue,Event loop依次執(zhí)行隊列中的回調(diào)函數(shù),這里因為閉包的特性,每一個定時器的回調(diào)都與其定義上下文,for循環(huán)中的i變量做了綁定,而i的值已變?yōu)?,所以同時輸出五個5。

如果現(xiàn)在提出一個新需求,要求程序運行后,先立即輸出一個5,然后在1s以后同時輸出0,1,2,3,4,如何改造上述代碼?

//方法一
for (var i = 0; i < 5; i++) {
    (function(j) {  
        setTimeout(function() {
            console.log(new Date, j);
        }, 1000);
    })(i);
}
 
console.log(new Date, i);

//方法二
function output (i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
};
 
for (var i = 0; i < 5; i++) {
    output(i);  
}
 
console.log(new Date, i);

上面給出的兩種方法其實都是一種思路,都是利用JS中,函數(shù)作用域作為一個獨立的作用域,來保存一個局部的上下文環(huán)境,并通過閉包的特性使其與setTimeout中的回調(diào)函數(shù)做綁定。只不過第一種方法是利用IIFE2來實現(xiàn),第二種方法是通過定義一個函數(shù),再來逐個調(diào)用實現(xiàn)。看到這里,應(yīng)該想到對于篇首問題背景一節(jié)中所提到的問題便與此處如出一轍。

接下來我們進一步深入,提出一個新的需求。如何在代碼執(zhí)行時,立即輸出 0,之后每隔1s依次輸出 1,2,3,4,循環(huán)結(jié)束后在大概第5秒的時候輸出5?

因為前邊每隔1s輸出的0,1,2,3,4是五個定時器輸出的,也就是五個異步操作,那么我們是不是可以把這次的需求抽象為:在一系列異步操作完成(每次循環(huán)都產(chǎn)生了 1 個異步操作)之后,再做其他的事情。現(xiàn)在熟悉ES6的同學(xué)應(yīng)該想到了Promise。

const tasks = []; // 這里存放異步操作的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});
 
// 生成全部的異步操作
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}
 
// 異步操作完成之后,輸出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});

如果你熟悉ES7中的Async/Await,那么也可以嘗試用這種方案解決。

// 模擬其他語言中的 sleep,實際上可以是任何異步操作
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});
 
(async () => {  // 聲明即執(zhí)行的 async 函數(shù)表達式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }
 
    await sleep(1000);
    console.log(new Date, i);
})();

這里需要著重注意的是瀏覽器對Async/Await標(biāo)準(zhǔn)的支持,如果你的瀏覽器不在以下所支持版本當(dāng)中,那么可以升級瀏覽器或使用babel轉(zhuǎn)譯處理。

能把上邊這一系列的實例理解到位,相信對JS中異步的這個概念會一些新的體會。下面這個實例會更加細化的考察一下異步代碼中回調(diào)的執(zhí)行時機。

let a = new Promise(
  function(resolve, reject) {
    console.log(1)
    setTimeout(() => console.log(2), 0)
    console.log(3)
    console.log(4)
    resolve(true)
  }
)
a.then(v => {
  console.log(8)
})
 
let b = new Promise(
  function() {
    console.log(5)
    setTimeout(() => console.log(6), 0)
  }
)
 
console.log(7)

這里首先來明確一點,Promise是ES6中為異步編程所提供的一套API標(biāo)準(zhǔn),其本身是同步的。所以我們在new一個Promise對象的時候,其所執(zhí)行的構(gòu)造器中的邏輯是同步的。由此得知,上述代碼片段先從上到下依次執(zhí)行同步代碼,輸出1,3,4,5,7。然后是先執(zhí)行then中的異步代碼還是先執(zhí)行setTimeout中的回調(diào)代碼?這里需要記住前者要比后者先進入執(zhí)行棧執(zhí)行,所以后邊輸出8,2,6。這是因為立即resolved的Promise是在本輪事件循環(huán)的末尾執(zhí)行,類似于node中的process.nextTick方法,它可以在當(dāng)前"執(zhí)行棧"的尾部,下一次Event Loop(主線程讀取"任務(wù)隊列")之前,觸發(fā)回調(diào)函數(shù)。setTimeout(fn, 0)則是在當(dāng)前"任務(wù)隊列"的尾部添加事件,也就是說,它指定的任務(wù)總是在下一輪次Event Loop時執(zhí)行,這與node中的setImmediate方法很像。

最后我們來說一個關(guān)于setInterval優(yōu)化的例子。我們知道setTimeout中的回調(diào)觸發(fā)是不準(zhǔn)確的,主要原因是由于在需要執(zhí)行回調(diào)時,可能執(zhí)行棧中的代碼還沒有執(zhí)行完,無法將CPU資源及時的調(diào)度給callback queue中的回調(diào)執(zhí)行。而setInterval也會存在一些問題,比如時間間隔可能會跳過,
時間間隔可能小于定時器設(shè)定的時間。發(fā)生這類情況其實也是由于其他的程序占用長時間的CPU時間片引起,以下面代碼片段為例:

function click() { 
    // code block1... 
    setInterval(function() { 
        // process ... 
    }, 200); 
    // code block2 ...
}

如果process中的代碼執(zhí)行時間過長,占用了超過400ms,那么此時JS執(zhí)行環(huán)境就會跳過中間一次時間間隔,因為callback queue中只允許有一份process代碼存在,所以也會產(chǎn)生觸發(fā)時機不精準(zhǔn)的情況。

為了避免這種情況的出現(xiàn),我們可以利用遞歸的方式進行優(yōu)化處理,以下提供兩種寫法,但是建議使用第一種寫法。因為第二種寫法中,在嚴(yán)格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當(dāng)一個函數(shù)必須調(diào)用自身的時候, 避免使用 arguments.callee(), 通過要么給函數(shù)表達式一個名字,要么使用一個函數(shù)聲明參見MDN解釋

    // 寫法一
    setTimeout(function bar (){ 
        // processing
        foo = setTimeout(bar, 1000); 
    }, 1000);
    
    // 寫法二
    setTimeout(function(){ 
        // processing 
        foo = setTimeout(arguments.callee, interval); 
    }, interval);
    
    clearTimeout(foo) // 停止循環(huán)
  • Data-Intensive Real-Time 這里指數(shù)據(jù)密集、實時交互類應(yīng)用。 ?

  • Immediately Invoked Function Expression:聲明即執(zhí)行的函數(shù)表達式。 ?

  • 文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

    轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/90516.html

    相關(guān)文章

    • JavaScript執(zhí)行順序分析

      摘要:每個線程的任務(wù)執(zhí)行順序都是先進先出在運行的環(huán)境中,有一個負(fù)責(zé)程序本身的運行,作為主線程另一個負(fù)責(zé)主線程與其他線程的通信,被稱為線程。主線程繼續(xù)執(zhí)行我是第一主線程執(zhí)行完畢,從線程讀取回調(diào)函數(shù)。 前言 上星期面試被問到了事件執(zhí)行順序的問題,想起來之前看《深入淺出Node.js》時看到這一章就忽略了,這次來分析一下JavaScript的事件執(zhí)行順序。廢話少說,正題開始。 單線程JavaScr...

      chnmagnus 評論0 收藏0
    • 【轉(zhuǎn)】深入理解JS單線程機制【原文作者:MasterYao】

      摘要:的單線程,與它的用途有關(guān)。只要指定過回調(diào)函數(shù),這些事件發(fā)生時就會進入任務(wù)隊列,等待主線程讀取。四主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為事件循環(huán)。令人困惑的是,文檔中稱,指定的回調(diào)函數(shù),總是排在前面。 原文:http://www.cnblogs.com/Master... 一、為什么JavaScript是單線程? JavaScript語言的一大特點...

      LittleLiByte 評論0 收藏0
    • 深入淺出JavaScript運行機制

      摘要:主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為事件循環(huán)。上面也提到,在到達指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊列尾部。這就是定時器功能。關(guān)于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...

      mochixuan 評論0 收藏0
    • 深入淺出JavaScript運行機制

      摘要:主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為事件循環(huán)。上面也提到,在到達指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊列尾部。這就是定時器功能。關(guān)于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...

      魏明 評論0 收藏0
    • 深入淺出JavaScript運行機制

      摘要:主線程從任務(wù)隊列中讀取事件,這個過程是循環(huán)不斷的,所以整個的這種運行機制又稱為事件循環(huán)。上面也提到,在到達指定時間時,定時器就會將相應(yīng)回調(diào)函數(shù)插入任務(wù)隊列尾部。這就是定時器功能。關(guān)于定時器的重要補充定時器包括與兩個方法。 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: console.log(1); setTimeout(function()...

      chaosx110 評論0 收藏0

    發(fā)表評論

    0條評論

    codeGoogle

    |高級講師

    TA的文章

    閱讀更多
    最新活動
    閱讀需要支付1元查看
    <