日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

淺析JavaScript異步到底是怎么實現的?

一直以來都知道用JAVAScript是一門單線程語言,在筆試過程中不斷的遇到一些輸出結果的問題,考量的是對異步編程掌握情況。一般被問到異步的時候腦子里第一反應就是 Ajax, setTimseout...這些東西。在平時做項目過程中,基本大多數操作都是異步的。 JavaScript異步都是通過回調形式完成的,開發過程中一直在處理回調,可能不知不覺中自己就已經處在 回調地獄中。

瀏覽器線程

在開始之前簡單的說一下瀏覽器的線程,對瀏覽器的作業有個基礎的認識。之前說過 JavaScript是單線程作業,但是并不代表瀏覽器就是單線程的。

在 JavaScript引擎中負責解析和執行 JavaScript代碼的線程只有一個。但是除了這個主進程以外,還有其他很多輔助線程。那么諸如 onclick回調, setTimeout, Ajax這些都是怎么實現的呢?即瀏覽器搞了幾個其他線程去輔助 JavaScript線程的運行。

瀏覽器有很多線程,例如:

  1. GUI渲染線程 - GUI渲染線程處于掛起狀態的,也就是凍結狀態
  2. JavaScript引擎線程 - 用于解析JavaScript代碼
  3. 定時器觸發線程 - 瀏覽器定時計數器并不是 js引擎計數
  4. 瀏覽器事件線程 - 用于解析BOM渲染等工作
  5. http線程 - 主要負責數據請求
  6. EventLoop輪詢處理線程 - 事件被觸發時該線程會把事件添加到待處理隊列的隊尾
  7. 等等等

從上面來看可以得出,瀏覽器其實也做了很多事情,遠遠的沒有想象中的那么簡單,上面這些線程中 GUI渲染線程, JavaScript引擎線程, 瀏覽器事件線程是瀏覽器的常駐線程。

當瀏覽器開始解析代碼的時候,會根據代碼去分配給不同的輔助線程去作業。

進程

進程是指在操作系統中正在運行的一個應用程序

線程

線程是指進程內獨立執行某個任務的一個單元。線程自己基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧)。

進程中包含線程,一個進程中可以有N個進程。我們可以在電腦的任務管理器中查看到正在運行的進程,可以認為一個進程就是在運行一個程序,比如用瀏覽器打開一個網頁,這就是開啟了一個進程。但是比如打開3個瀏覽器,那么就開啟了3個進程。

同步&異步

既然要了解同步異步當然要簡單的說一下同步和異步。說到同步和異步最有發言權的真的就屬 Ajax了,為了讓例子更加明顯沒有使用 Ajax舉例。(●ˇ∀ˇ●)

同步

同步會逐行執行代碼,會對后續代碼造成阻塞,直至代碼接收到預期的結果之后,才會繼續向下執行。

console.log(1);
alert("同步");
console.log(2);
// 結果:
// 1
// 同步
// 2
異步

如果在函數返回的時候,調用者還不能夠得到預期結果,而是將來通過一定的手段得到結果(例如回調函數),這就是異步。

console.log(1);
setTimeout(() => {
	alert("異步");
},0);
console.log(2);// 結果:// 1// 2// 異步

為什么JavaScript要采用異步編程

一開始就說過, JavaScript是一種單線程執行的腳本語言(這可能是由于歷史原因或為了簡單而采取的設計)。它的單線程表現在任何一個函數都要從頭到尾執行完畢之后,才會執行另一個函數,界面的更新、鼠標事件的處理、計時器( setTimeout、setInterval等)的執行也需要先排隊,后串行執行。假如有一段 JavaScript從頭到尾執行時間比較長,那么在執行期間任何 UI更新都會被阻塞,界面事件處理也會停止響應。這種情況下就需要異步編程模式,目的就是把代碼的運行打散或者讓 IO調用(例如 AJAX)在后臺運行,讓界面更新和事件處理能夠及時地運行。

JavaScript語言的設計者意識到,這時主線程完全可以不管 IO設備,掛起處于等待中的任務,先運行排在后面的任務。等到 IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。

異步運行機制:

  1. 所有同步任務都在主線程上執行,形成一個執行棧。
  2. 主線程之外,還存在一個 任務隊列。只要異步任務有了運行結果,就在 任務隊列之中放置一個事件。
  3. 一旦 執行棧中的所有同步任務執行完畢,系統就會讀取 任務隊列,看看里面有哪些事件。那些對應的異步任務,于是結束等待狀態,進入執行棧,開始執行。
  4. 主線程不斷重復上面的第三步。

舉個例子:

<button onclick="updateSync()">同步</button>
<button onclick="updateAsync()">異步</button>
<div id="output"></div>
<script>function updateSync()
{
for (var i = 0; i < 1000000; i++) {
	document.getElementById('output').innerhtml = i;
}
}
function updateAsync() {
var i = 0; function updateLater() {
document.getElementById('output').innerHTML = (i++);
if (i < 1000000) {
s	etTimeout(updateLater, 0);
} } updateLater();}
</script>

點擊 同步按鈕會調用 updateSync的同步函數,邏輯非常簡單,循環體內每次更新 output結點的內容為 i。如果在其他多線程模型下的語言,你可能會看到界面上以非常快的速度顯示從 0到 999999后停止。但是在 JavaScript中,你會感覺按鈕按下去的時候卡了一下,然后看到一個最終結果 999999,而沒有中間過程,這就是因為在 updateSync函數運行過程中 UI更新被阻塞,只有當它結束退出后才會更新 UI。反之,當點擊 異步的時候,會明顯的看到 Dom在逐步更新的過程。

從上面的例子中可以明顯的看出,異步編程對于 JavaScript來說是多么多么的重要。

異步編程有什么好處

從編程方式來講當然是同步編程的方式更為簡單,但是同步有其局限性一是假如是單線程那么一旦遇到阻塞調用,會造成整個線程阻塞,導致 cpu無法得到有效利用,而瀏覽器的 JavaScript執行和瀏覽器渲染是運行在單線程中,一旦遇到阻塞調用不僅意味 JavaScript的執行被阻塞更意味整個瀏覽器渲染也被阻塞這就導致界面的卡死,若是多線程則不可避免的要考慮互斥和同步問題,而互斥和同步帶來復雜度也很大,實際上瀏覽器下因為同時只能執行一段 JavaScript代碼這意味著不存在互斥問題,但是同步問題仍然不可避免,以往回調風格中異步的流程控制(其實就是同步問題)也比較復雜。瀏覽器端的編程方式也即是 GUI編程,其本質就是事件驅動的(鼠標點擊, Http請求結束等)異步編程更為自然。

突然有個疑問,既然如此為什么 JavaScript沒有使用多線程作業呢?就此就去 google了一下 JavaScript多線程,在 HTML5推出之后是提供了多線程只是比較局限。在使用多線程的時候無法使用 window對象。若 JavaScript使用多線程,在 A線程中正在操作 DOM,但是 B線程中已經把該 DOM已經刪除了(只是簡單的小例子,可能還有很多問題,至于這些歷史問題無從考究了)。會給編程作業帶來很大的負擔。就我而言我想這也就說明了為什么 JavaScript沒有使用異步編程的原因吧。

異步與回調

回調到底屬于異步么?會想起剛剛開始學習 JavaScript的時候常常把這兩個概念混合在一起。在搞清楚這個問題,首先要明白什么是回調函數。

百科:回調函數是一個函數,它作為參數傳遞給另一個函數,并在父函數完成后執行。回調的特殊之處在于,出現在“父類”之后的函數可以在回調執行之前執行。另一件需要知道的重要事情是如何正確地傳遞回調。這就是我經常忘記正確語法的地方。

通過上面的解釋可以得出,回調函數本質上其實就是一種設計模式,例如我們熟悉的 JQuery也只不過是遵循了這個設計原則而已。在 JavaScript中,回調函數具體的定義為:函數 A作為參數(函數引用)傳遞到另一個函數 B中,并且這個函數 B執行函數 A。我們就說函數 A叫做回調函數。如果沒有名稱(函數表達式),就叫做匿名回調函數。

簡單的舉個小例子:

function test (n,fn){
  console.log(n);
  fn && fn(n);}console.log(1);
test(2);
test(3,function(n){
	console.log(n+1)
});console.log(5)
// 結果
// 1
// 2
// 3
// 4
// 5

通過上面的代碼輸出的結果可以得出回調函數不一定屬于異步,一般同步會阻塞后面的代碼,通過輸出結果也就得出了這個結論。回調函數,一般在同步情境下是最后執行的,而在異步情境下有可能不執行,因為事件沒有被觸發或者條件不滿足。

回調函數應用場景

  1. 資源加載:動態加載js文件后執行回調,加載iframe后執行回調,ajax操作回調,圖片加載完成執行回調,AJAX等等。
  2. DOM事件及Node.js事件基于回調機制(Node.js回調可能會出現多層回調嵌套的問題)。
  3. setTimeout的延遲時間為0,這個hack經常被用到,settimeout調用的函數其實就是一個callback的體現
  4. 鏈式調用:鏈式調用的時候,在賦值器(setter)方法中(或者本身沒有返回值的方法中)很容易實現鏈式調用,而取值器(getter)相對來說不好實現鏈式調用,因為你需要取值器返回你需要的數據而不是this指針,如果要實現鏈式方法,可以用回調函數來實現。
  5. setTimeout、setInterval的函數調用得到其返回值。由于兩個函數都是異步的,即:調用時序和程序的主流程是相對獨立的,所以沒有辦法在主體里面等待它們的返回值,它們被打開的時候程序也不會停下來等待,否則也就失去了setTimeout及setInterval的意義了,所以用return已經沒有意義,只能使用callback。callback的意義在于將timer執行的結果通知給代理函數進行及時處理。

JavaScript中的那些異步操作

JavaScript既然有很多的輔助線程,不可能所有的工作都是通過主線程去做,既然分配給輔助線程去做事情。

XMLHttpRequest

XMLHttpRequest對象應該不是很陌生的,主要用于瀏覽器的數據請求與數據交互。 XMLHttpRequest對象提供兩種請求數據的方式,一種是 同步,一種是 異步。可以通過參數進行配置。默認為異步。

對于 XMLHttpRequest這里就不作太多的贅述了。

var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); //同步方式請求
xhr.open("GET", url, true); //異步
xhr.send();

同步 Ajax請求:

當請求開始發送時, 瀏覽器事件線程通知 主線程,讓 Http線程發送數據請求,主線程收到請求之后,通知 Http線程發送請求, Http線程收到 主線程通知之后就去請求數據,等待服務器響應,過了 N年之后,收到請求回來的數據,返回給 主線程數據已經請求完成, 主線程把結果返回給了 瀏覽器事件線程,去完成后續操作。

異步 Ajax請求:

當請求開始發送時, 瀏覽器事件線程通知, 瀏覽器事件線程通知 主線程,讓 Http線程發送數據請求,主線程收到請求之后,通知 Http線程發送請求, Http線程收到 主線程通知之后就去請求數據,并通知 主線程請求已經發送, 主進程通知 瀏覽器事件線程已經去請求數據,則

瀏覽器事件線程,只需要等待結果,并不影響其他工作。

setInterval&setTimeout

setInterval與 setTimeout同屬于異步方法,其異步是通過回調函數方式實現。其兩者的區別則 setInterval會連續調用回調函數,則 setTimeout會延時調用回調函數只會執行一次。

setInterval(() => {
alert(1)
},2000)
// 每隔2s彈出一次1
setTimeout(() => {
alert(2)
},2000)

// 進入頁面后2s彈出2,則不會再次彈出

requestAnimationFarme

requestAnimationFrame字面意思就是去請求動畫幀,在沒有 API之前都是基于 setInterval,與 setInterval相比, requestAnimationFrame最大的優勢是由系統來決定回調函數的執行時機。具體一點講,如果屏幕刷新率是 60Hz,那么回調函數就每 16.7ms被執行一次,如果刷新率是 75Hz,那么這個時間間隔就變成了 1000/75=13.3ms,換句話說就是, requestAnimationFrame的步伐跟著系統的刷新步伐走。它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引起丟幀現象,也不會導致動畫出現卡頓的問題。

舉個小例子:

var progress = 0;//回調函數
function render() {
progress += 1; //修改圖像的位置
if (progress < 100) { //在動畫沒有結束前,遞歸渲染
window.requestAnimationFrame(render);}}//第一幀渲染window.requestAnimationFrame(render);

Object.observe - 觀察者

Object.observe是一個提供數據監視的 API,在 chrome中已經可以使用。是 ECMAScript 7 的一個提案規范,官方建議的是 謹慎使用級別,但是個人認為這個 API非常有用,例如可以對現在流行的 MVVM框架作一些簡化和優化。雖然標準還沒定,但是標準往往是滯后于實現的,只要是有用的東西,肯定會有越來越多的人去使用,越來越多的引擎會支持,最終促使標準的生成。從 observe字面意思就可以知道,這玩意兒就是用來做觀察者模式之類。

var obj = {a: 1};
Object.observe(obj, output);
obj.b = 2;obj.a = 2;
Object.defineProperties(obj, {a: { enumerable: false}}); //修改屬性設定
delete obj.b;function output(change) {
console.log(1)
}Promise

Promise是對異步編程的一種抽象。它是一個代理對象,代表一個必須進行異步處理的函數返回的值或拋出的異常。也就是說 Promise對象代表了一個異步操作,可以將異步對象和回調函數脫離開來,通過 then方法在這個異步操作上面綁定回調函數。

在Promise中最直觀的例子就是 Promise.all統一去請求,返回結果。

var p1 = Promise.resolve(3);
var p2 = 42;
var p3 = new Promise(
  function(resolve, reject) {
		setTimeout(resolve, 100, 'foo');
	});Promise.all([p1, p2, p3])
.then(function(values) {
console.log(values);
});// expected output: Array [3, 42, "foo"]
Generator&Async/Await

ES6的 Generator卻給異步操作又提供了新的思路,馬上就有人給出了如何用 Generator來更加優雅的處理異步操作。 Generator函數是協程在 ES6的實現,最大特點就是可以交出函數的執行權(即暫停執行)。整個 Generator函數就是一個封裝的異步任務,或者說是異步任務的容器。異步操作需要暫停的地方,都用yield語句注明。 Generator函數的執行方法如下。

function * greneratorDome(){
  yield "Hello";
  yield "World";
  return "Ending";
}let grenDome = greneratorDome();console.log(grenDome.next());// {value: "Hello", done: false}
console.log(grenDome.next());// {value: "World", done: false}
console.log(grenDome.next());// {value: "Ending", done: true}
console.log(grenDome.next());// {value: undefined, done: true}

粗略實現 Generator

function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}};}var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
Async/

與 Generator類似, Async/await是 Javascript編寫異步程序的新方法。以往的異步方法無外乎回調函數和 Promise。但是 Async/await建立于Promise之上,個人理解是使用了 Generator函數做了語法糖。 async函數就是隧道盡頭的亮光,很多人認為它是異步操作的終極解決方案。

function a(){
return new Promise((resolve,reject) => {
  console.log("a函數")
  resolve("a函數")
})}function b (){
return new Promise((resolve,reject) => {
  console.log("b函數")
  resolve("b函數")
})}async function dome (){
  let A = await a();
  let B = await b();
  return Promise.resolve([A,B]);
}dome().then((res) => {
	console.log(res);
});

 

Node.js異步I/O

當我們發起 IO請求時,調用的是各個不同平臺的操作系統內部實現的線程池內的線程。這里的 IO請求可不僅僅是讀寫磁盤文件,在 *nix中,將計算機抽象了一層,磁盤文件、硬件、套接字等幾乎所有計算機資源都被抽象為文件,常說的 IO請求就是抽象后的文件。完成 Node整個異步 IO環節的有事件循環、觀察者、請求對象。

事件循環機制

單線程就意味著,所有任務需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等著。于是就有一個概念,任務隊列。如果排隊是因為計算量大, CPU忙不過來,倒也算了,但是很多時候 CPU是閑著的,因為 IO設備(輸入輸出設備)很慢(比如 Ajax操作從網絡讀取數據),不得不等著結果出來,再往下執行。

事件循環是 Node的自身執行模型,正是事件循環使得回調函數得以在 Node中大量的使用。在進程啟動時 Node會創建一個 while(true)死循環,這個和 Netty也是一樣的,每次執行循環體,都會完成一次 Tick。每個 Tick的過程就是查看是否有事件等待被處理。如果有,就取出事件及相關的回調函數,并執行關聯的回調函數。如果不再有事件處理就退出進程。

淺析JavaScript異步到底是怎么實現的?

 

線程只會做一件事情,就是從事件隊列里面取事件、執行事件,再取事件、再事件。當消息隊列為空時,就會等待直到消息隊列變成非空。而且主線程只有在將當前的消息執行完成后,才會去取下一個消息。這種機制就叫做事件循環機制,取一個消息并執行的過程叫做一次循環。

while(true) {
  var message = queue.get();
  execute(message);}

我們可以把整個事件循環想象成一個事件隊列,在進入事件隊列時開始對事件進行彈出操作,直至事件為 0為止。

process.nextTick

process.nextTick()方法可以在當前"執行棧"的尾部-->下一次 EventLoop(主線程讀取"任務隊列")之前-->觸發 process指定的回調函數。也就是說,它指定的任務總是發生在所有異步任務之前,當前主線程的末尾。( nextTick雖然也會異步執行,但是不會給其他 io事件執行的任何機會);

process.nextTick(function A() {
  console.log(1);
  process.nextTick(    function B(){
      console.log(2);
    }	);});setTimeout(function C() { 
  console.log(3);
}, 0);// 1// 2// 3

 

異步過程的構成要素

異步函數實際上很快就調用完成了,但是后面還有工作線程執行異步任務,通知主線程,主線程調用回調函數等很多步驟。我們把整個過程叫做異步過程,異步函數的調用在整個異步過程中只是一小部分。

一個異步過程的整個過程:主線程發一起一個異步請求,相應的工作線程接收請求并告知主線程已收到通知(異步函數返回);主線程可以繼續執行后面的代碼,同時工作線程執行異步任務;工作線程完成工作后,通知主線程;主線程收到通知后,執行一定的動作(調用回調函數)。

它可以叫做異步過程的發起函數,或者叫做異步任務注冊函數。 args是這個函數需要的參數, callbackFn(回調函數)也是這個函數的參數,但是它比較特殊所以單獨列出來。所以,從主線程的角度看,一個異步過程包括下面兩個要素:

  1. 發起函數;
  2. 回調函數callbackFn

它們都是主線程上調用的,其中注冊函數用來發起異步過程,回調函數用來處理結果。

舉個具體的例子:

setTimeout(function,1000);
其中 setTimeout就是異步過程的發起函數, function是回調函數。

注:前面說的形式 A(args...,callbackFn)只是一種抽象的表示,并不代表回調函數一定要作為發起函數的參數,例如:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = xxx;xhr.open('GET', url);
xhr.send();  

總結

JavaScript的異步編程模式不僅是一種趨勢,而且是一種必要,因此作為 HTML5開發者是非常有必要掌握的。采用第三方的異步編程庫和異步同步化的方法,會讓代碼結構相對簡潔,便于維護,推薦開發人員掌握一二,提高團隊開發效率。

作者:Aaron

https://segmentfault.com/a/1190000019253700

分享到:
標簽:JavaScript
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定