異步、任務隊列、事件循環

線程

Task A --> Task B --> Task Chtml

  • 每一個線程一次只能執行一個任務。
  • 每一個任務順序執行,只有前面的結束了,後面的才能開始。
  • JavaScript 是單線程的:即便有多個內核,也只能在單一線程上運行多個任務,此線程稱爲主線程(main thread)。

任務隊列和事件循環

事件循環和任務隊列.png

任務隊列

宏任務:script(全局任務),setTimeout,setInterval,setImmediate,I/O,UI rendering
微任務:process.nextTick,Promise,Object.observer,MutaionObserver
執行棧:任務在執行棧上執行。前端

瀏覽器中的事件循環

分三步:node

  1. 取一個宏任務來執行。執行完畢後,下一步。
  2. 取一個微任務來執行,執行完畢後,再取一個微任務來執行。直到微任務隊列爲空,執行下一步。
  3. 更新UI渲染。

代碼執行都是從script(全局任務)開始。web

Nodejs中的事件循環

Node.js採用V8做爲js的解析引擎,而I/O處理方面使用了本身設計的libuv,libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統一些底層特性,對外提供統一的API,事件循環機制也是它裏面的實現。
node.png
每次事件循環都包含了6個階段:
1.timersajax

處理全部 setTimeout 和 setInterval 的回調。

這些回調被保存在一個最小堆(min heap) 中. 這樣引擎只須要每次判斷頭元素, 若是符合條件就拿出來執行, 直到遇到一個不符合條件或者隊列空了, 才結束 Timer Phase.
Timer Phase 中判斷某個回調是否符合條件的方法:消息循環每次進入 Timer Phase 的時候都會保存一下當時的系統時間(T1),而後只要看上述最小堆中的回調函數設置的啓動時間(T0)是否超過進入 Timer Phase 時保存的時間(T0<T1), 若是超過就拿出來執行.
Nodejs 爲了防止某個 Phase 任務太多,每一個 Phase 執行回調都有個最大數量. 若是超過數量的話也會強行結束當前 Phase 而進入下一個 Phase. 這一條規則適用於消息循環中的每個 Phase.
2. I/O callbacksegmentfault

執行你的 fs.read, socket 等 IO 操做的回調函數, 同時也包括各類 error 的回調.

3.Idle, Prepare 階段
內部使用,不討論.
4. poll 階段api

等待異步請求和數據。

首先會執行 watch_queue 隊列中的 IO 請求, 一旦 watch_queue 隊列空, 則整個消息循環就會進入 sleep , 從而等待被內核事件喚醒.
當js層代碼註冊的事件回調都沒有返回的時候,事件循環會阻塞在poll階段。看到這裏,你可能會想了,會永遠阻塞在此處嗎?固然 Poll Phase 不能一直等下去.數組

  1. 它首先會判斷後面的 Check Phase 以及 Close Phase 是否還有等待處理的回調. 若是有, 則不等待, 直接進入下一個 Phase.
  2. 若是沒有其餘回調等待執行, 它會給 epoll 這樣的方法設置一個 timeout.

Nodejs 就是經過 Poll Phase, 對 IO 事件的等待和內核異步事件的到達來驅動整個消息循環的.
5. check 階段promise

這個階段只處理 setImmediate 的回調函數.

6. close callback 階段
專門處理一些 close 類型的回調. 好比 socket.on('close', ...). 用於資源清理.
總結:
1.Node.js 的事件循環分爲6個階段
2.瀏覽器和Node 環境下,microtask 任務隊列的執行時機不一樣
Node.js中,microtask 在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。
瀏覽器端,microtask 在事件循環的 macrotask 執行完以後執行
搜狗截圖20200316184546.png瀏覽器

同步和異步

  • 同步會阻塞代碼執行
  • 異步不會阻塞代碼執行

JavaScript是一種同步的、阻塞的、單線程的語言,在這種語言中,一次只能執行一個操做。但web瀏覽器定義了函數和API,容許咱們當某些事件發生時不是按照同步方式,而是異步地調用函數(好比,時間的推移,用戶經過鼠標的交互,或者獲取網絡數據)。這意味着您的代碼能夠同時作幾件事情,而不須要中止或阻塞主線程。

前端使用異步的場景有哪些?

  • 網絡請求,如ajax、圖片加載
  • 定時任務:setTimeout,setInverval
setTimeout()指定的延遲時間以後來執行代碼。
clearTimeout()取消setTimeout設置。
setInterval()每隔指定的時間執行代碼
clearInterval()取消setInterval()設置
  • DOM事件綁定:

用addEventListener註冊一個類型的事件的時候,瀏覽器會有一個單獨的模塊去接收這個東西,當事件被觸發的時候,瀏覽器的某個模塊,會把相應的函數扔到異步隊列中,

  • ES6中的Promise

回調函數

回調函數callback(執行結束回來調用的函數)

定義:回調函數是一個函數,它做爲參數傳遞給另外一個函數,並在父函數完成後執行。
請注意,不是全部的回調函數都是異步的,有一些是同步的。
如:Array.prototype.forEach()

forEach() 須要的參數是一個回調函數,回調函數自己帶有兩個參數,數組元素和索引值。它無需等待任何事情,當即運行。

const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];

gods.forEach(function (eachName, index){
  console.log(index + '. ' + eachName);
});

一般所說的回調函數是指異步的。
回調函數的問題:

  1. 回調地獄:致使的調試困難,和大腦的思惟方式不符。
  2. 控制反轉:把本身程序一部分的執行控制交給某個第三方致使請求併發回調函數執行順序沒法肯定。

Promise

基本用法

通常狀況下是有異步操做時,使用Promise對這個異步操做進行封裝。

三種狀態

  • pending: 初始狀態,既不是成功,也不是失敗狀態。
  • fulfilled: 意味着操做成功完成。
  • rejected: 意味着操做失敗。

Promise有兩種狀態改變的方式,既能夠從pending轉變爲fulfilled,也能夠從pending轉變爲rejected。一旦狀態改變,就「凝固」了,會一直保持這個狀態,不會再發生變化。當狀態發生變化,promise.then綁定的函數就會被調用。
promises.png

resolvereject

new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}, error => {
  console.log(error);
});

resolve函數的做用:在異步操做成功時調用,並將異步操做的結果做爲參數傳遞出去;
reject函數的做用:在異步操做失敗時調用,並將異步操做報出的錯誤做爲參數傳遞出去。
promise對象的錯誤,會一直向後傳遞,直到被捕獲。即錯誤總會被下一個catch所捕獲。then方法指定的回調函數,若拋出錯誤,也會被下一個catch捕獲。catch中也能拋錯,則須要後面的catch來捕獲。

基本api

.then()

then()有兩個參數,分別爲Promise從pending變爲fulfilledrejected時的回調函數(第二個參數非必選)。這兩個函數都接受Promise對象傳出的值做爲參數
簡單來講,then就是定義resolvereject函數的,

.catch()

該方法是.then(undefined, onRejected)的別名,用於指定發生錯誤時的回調函數。

new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}, error => {
  console.log(error);
});

//等同於
new Promise((resolve, reject) => {
  setTimeout(() => {
    //resolve("Hello World");
    reject("error");
  }, 100)
}).then(data => {
  console.log(data);
}).catch(error => {
  console.log(error);
});

.all()

Promise.all(iterable)

該方法用於將多個Promise實例,包裝成一個新的Promise實例。
Promise.all方法接受一個數組(或具備(迭代器)Iterator接口)做參數,數組中的對象(p一、p二、p3)均爲promise實例(若是不是一個promise,該項會被用Promise.resolve轉換爲一個promise)。它的狀態由這三個promise實例決定。

  • 當p1, p2, p3狀態都變爲fulfilled,p的狀態纔會變爲fulfilled,並將三個promise返回的結果,按參數的順序(而不是resolved的順序)存入數組,傳給p的回調函數,如例3.8。
  • 當p1, p2, p3其中之一狀態變爲rejected,p的狀態也會變爲rejected,並把第一個被reject的promise的返回值,傳給p的回調函數。

這多個 promise 是同時開始、並行執行的,而不是順序執行。

let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.all([p1, p2, p3]).then(arr => {
  console.log(arr);
});
//[ 'first', 'second', 'third' ]
let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(reject, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.all([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
//second

.race()

Promise.race(iterable)

Promise.race方法一樣接受一個數組(或具備Iterator接口)做參數。當p1, p2, p3中有一個實例的狀態發生改變(變爲fulfilledrejected),p的狀態就跟着改變。並把第一個改變狀態的promise的返回值,傳給p的回調函數。

let p1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 3000, "first");
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(reject, 2000, "second");
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 1000, "third");
});
Promise.race([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
//third

在第一個promise對象變爲resolve後,並不會取消其餘promise對象的執行

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("first");
    console.log(1);
  }, 3000)
});
let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("second");
    console.log(2);
  }, 2000)
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("third");
    console.log(3);
  }, 1000)
});
Promise.race([p1, p2, p3]).then(arr => {
  console.log(arr);
}, error => {
  console.log(error);
});
// 3
// third
// 2
// 1

.resolve()

Promise.resolve('Success');

/*******等同於*******/
new Promise(function (resolve) {
    resolve('Success');
});

.reject()

Promise.reject(new Error('error'));

/*******等同於*******/
new Promise(function (resolve, reject) {
    reject(new Error('error'));
});

Promise常見問題

建立promise的流程:

  1. 使用new Promise(fn)或者它的快捷方式Promise.resolve()Promise.reject(),返回一個promise對象
  2. fn中指定異步的處理
    處理結果正常,調用resolve
    處理結果錯誤,調用reject

情景1:若是在then中拋錯,而沒有對錯誤進行處理(即catch),那麼會一直保持reject狀態,直到catch了錯誤

function taskA() {
    console.log(x);
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}
var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);
    
-------output-------
Catch Error: A or B,ReferenceError: x is not defined
Final Task
Promise.resolve().then(() => {
    console.log('ok1');
    throw 'throw error1'
}).then(() => {
    console.log('ok2');
}, err => {
    // 捕獲錯誤
    console.log('err->', err);
}).then(() => {
    // 該函數將被調用
    console.log('ok3');
    throw 'throw error3'
}).then(() => {
    // 錯誤捕獲前的函數不會被調用
    console.log('ok4');
}).catch(err => {
    console.log('err->', err);
});

//ok1
//err-> throw error1
//ok3
//err-> throw error3
console.log('script start');

async function async1() {
    console.log('async1');
    await async2();
    console.log('end');
}

async function async2() {
    console.log('async2');
}

setTimeout(function () {
    console.log('setTimeout');
}, 0);

async1();

new Promise(function (resolve) {
    console.log('promise1');
    resolve();
}).then(function () {
    console.log('promise2');
});

console.log('script end');

//script start
//async1
//async2
//promise1
//script end
//end
//promise2
//setTimeout

情景二:在異步回調中拋錯,不會被catch

new Promise((resolve, reject) => {
  throw "error"
}).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);
});
//error

//對比
new Promise((resolve, reject) => {
  setTimeout(() => {
    throw "error";
  }, 1000)
}).then(data => {
  console.log(data);
}).catch(err => {
  console.log(err);  //This is never called
});

promise的優缺點

優勢:

  1. 結束回調地獄
  2. Promise老是嚴格按照它們放置在事件隊列中的順序調用。
  3. 錯誤處理要好地多

缺點:
Promise一旦運行,不能終止掉。

Promise 加載一張圖片

promise.js
function loadImg(src) {
    return new Promise((resolve, reject) => {
        let img = document.createElement("img");
        img.src = src;
        img.onload = function (url) {
            resolve(img);
        };
        img.onerror = function () {
            reject(err);
        }
    });
}

const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg';
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg';

loadImg(url1).then((img) => {
    console.log("圖片1加載成功");
    console.log(`圖片1的寬度爲:${img.width}`);
}).then(() => {
    console.log("準備加載圖片2");
    return loadImg(url2);
}).then((img) => {
    console.log("圖片2加載成功");
    console.log(`圖片2的寬度爲:${img.width}`);
}).catch((err) => {
    console.log(err);
});
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

</body>
<script src="./promise.js"></script>
</html>

Generator

Generator解決了回調函數處理異步流程的第一個問題:不符合大腦順序、線性的思惟方式。

Async/Await

  • 同步的書寫方式,邏輯和數據依賴都很是清楚,只須要把異步的東西用Promise封裝出去,而後使用await調用就能夠了,也不須要像Generator同樣須要手動控制next()執行。
  • Async/Await是Generator和Promise的組合,徹底解決了基於回調的異步流程存在的兩個問題,多是如今最好的JavaScript處理異步的方式了。
// 使用setTimeout模擬異步
function ajax (url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log(url + ' result.');
            resolve(url + ' result.');
        }, 100);
    });
}

async function ajaxAsync () {
    var aResult = await ajax('/api/a'); 
    console.log('aResult: ' + aResult);
    var bResult = await ajax('/api/b'); 
    console.log('bResult: ' + bResult);
}

ajaxAsync();

參考文章:
http://www.javashuo.com/article/p-odwhqqel-bp.html
http://www.javashuo.com/article/p-ogqwcufq-go.html

相關文章
相關標籤/搜索