瀏覽器的渲染進程是多線程的,以下:javascript
而js由於防止對DOM的操做產生混亂,所以它是單線程的。單線程就是一次只能只能一個任務,有多個任務的話須要一個個的執行,爲了解決異步事件,js引擎產生了Event Loop機制。java
js引擎不是獨立運行的,它運行在宿主環境中,咱們常見的即是瀏覽器,可是隨着發展,nodej.s已經進入了服務器的領域,js還滲透到了其餘的一些領域。這些宿主環境每一個人都提供了各自的事件循環機制。node
那麼什麼是事件循環機制呢?js是單線程的,單線程就是一次只能只能一個任務,有多個任務的話須要一個個的執行,爲了解決異步事件,js引擎產生了Event Loop機制。js中任務執行時會有任務隊列,setTimeout是在設定的時間後加到任務隊列的尾部。所以它雖然是定時器,可是在設定的時間結束時,回調函數是否執行取決於任務隊列的狀態。換個通俗點的話來講,setTimeout是一個「不太準確」的定時器。面試
直到ES6中,js中才從本質上改變了在哪裏管理事件循環,ES6精確得制定了事件循環的工做細節,其中最主要的緣由是Promise的引入,這使得對事件循環隊列調度的運行能直接進行精細的控制,而不像上面說到的」不太準確「的定時器。ajax
(1)對每一個宏任務而言,內部有一個都有一個微任務shell
(2)引入微任務的初衷是爲了解決異步回調的問題npm
採用改方式,那麼執行回調的時機應該是在前面全部的宏任務完成以後,假若如今的任務隊列很是長,那麼回調遲遲得不到執行,形成應用卡頓。編程
爲了規避第一種方式中的這樣的問題,V8 引入了第二種方式,這就是微任務的解決方式。在每個宏任務中定義一個微任務隊列,當該宏任務執行完成,會檢查其中的微任務隊列,若是爲空則直接執行下一個宏任務,若是不爲空,則依次執行微任務,執行完成纔去執行下一個宏任務。json
(3)常見的微任務有:數組
咱們來看一個常見的面試題:
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
複製代碼
再看一個例子:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
});
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0);
console.log('start');
// start
// Promise1
// setTimeout1
// Promise2
// setTimeout2
複製代碼
接下來從js異步發展的歷史來學習異步的相關知識
回調是js中最基礎的異步模式。
listen("click", function handle(evt){
setTimeout(function request(){
ajax("...", function response(test){
if (text === "hello") {
handle();
} else {
request();
}
})
}, 500)
})
複製代碼
這種代碼經常被成爲回調地獄, 有時候也叫毀滅金字塔。由於多個異步操做造成了強耦合,只要有一個操做須要修改,只要有一個操做須要修改,它的上層回調函數和下層回調函數就須要跟着修改,想要理解、更新或維護這樣的代碼十分的困難。
有的回調函數不是由你本身編寫的,也不是在你直接的控制下的。多數狀況下是第三方提供的。這種稱位控制反轉,就i是把本身程序的一部分執行控制交給了第三方。而你的代碼和第三方工具之間沒有一份明確表達的契約。會形成大量的混亂邏輯,致使信任鏈徹底斷裂。
回調函數的兩個缺陷:回調地獄和缺少可信任性。Promise解決了這兩個問題。
Promise簡單來講就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。
Promise至關於購餐時的訂單號,當咱們付錢購買了想要的食物後,便會拿到小票。這時餐廳就在廚房後面爲你準備可口的午飯,你在等待的過程當中能夠作點其餘的事情,好比看個視頻,打個遊戲。當服務員喊道咱們的訂單時,咱們就能夠拿着小票去前臺換咱們的午飯。固然有時候,前臺會跟你說你點的雞腿沒有了。這就是Promise的工做方式。
ES6規定,Promise對象是一個構造函數,用來生成Promise實例。
var promise = new Promise(function(resolvem reject) {
// some code
if (/*異步操做成功*/) {
resolve(value);
} else {
reject(error);
}
})
複製代碼
Promise實例生成之後,可使用then方法分別指定Resolved狀態和Rejected狀態的回調函數
promise.then(function(value) {
// success
}, function(error) {
// failure
})
複製代碼
then方法接受兩個參數:第一個回調函數是Promise狀態變爲Resolved時調用的,第二個是Promise狀態變成Rejected時調用
第二個參數是可選的,不必定要提供
兩個函數都接受Promise對象傳出去的值作參數。
reject函數傳遞的參數一半時Error對象的實例,表示拋出錯誤。
resolve函數除了傳遞正常值之外,還能夠傳遞一個Promise實例
var p1 = new Promise(function(resolve, reject) {
//...
});
// 這種狀況下,p1的狀態決定了p2的狀態。p2必須等到p1的狀態變爲resolve或reject纔會執行回調函數
var p2 = new Promise(function(resolve, reject) {
//...
resolve(p1);
});
複製代碼
then方法是定義在原型對象Promise.prototype上的。它的做用是爲Promise實例添加改變狀態時的回調函數。
then方法接受兩個參數:第一個回調函數是Promise狀態變爲Resolved時調用的,第二個是Promise狀態變成Rejected時調用
then方法返回的是一個新的Promise實例。所以能夠採用鏈式的寫法。
promise((resolve, reject) => {
// ...
}).then(() => {
// ...
}).then(() => {
// ...
})
複製代碼
採用鏈式的寫法能夠指定一組按照次序調用的回調函數。若是前一個回調函數返回了一個Promise實例,那麼後一個回調函數就會等待該Promise對象狀態的變化再被調用。
promise((resolve, reject) => {
// ...
}).then(() => {
// ...
return new Promise((resolve, reject) => {
// ...
})
}).then((comments) => {
console.log("resolved: ", comments)
}, (err) => {
console.log("rejected: ", err)
})
// 或者能夠寫的更加簡潔一些
promise((resolve, reject) => {
// ...
})
.then(() => new Promise((resolve, reject) => {...})
.then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
)
複製代碼
Promise.prototype.catch()是方法.then(null, rejection)的別名,用於指定發生錯誤時的回調函數。
getJSON('/post.json').then((posts) => {
// ....
}).catch((error) => {
console.log("發生錯誤", error);
})
複製代碼
getJSON返回一個Promise對象,若是該對象變成Resolved則會調用then()方法
若是異步發生錯誤或者then方法發生錯誤,則會被catch捕捉
Promise在resolve語句後面再拋出錯誤不會被捕獲,由於Promise的狀態一旦改變就不會再改變了。
var promise = new Promise((resolve, reject) => {
resolve('ok');
throw new Error('test')
})
promise
.then((value) => {console.log(value)})
.catch((error) => {console.log(error)})
複製代碼
Promise對象的錯誤具備「冒泡」的性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch捕獲。通常來講不要再then中定義第二個函數,而老是用catch方法。
var promise = new Promise((resolve, reject) => {
resolve('ok');
throw new Error('test')
})
// 不推薦
promise
.then(
(value) => {console.log(value)},
(error) => {console.log(error)}
)
//推薦
promise
.then((value) => {console.log(value)})
.catch((error) => {console.log(error)})
複製代碼
和傳統的try/catch不一樣,若是沒有使用catch指定錯誤處理的回調函數,promise對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應
catch返回的也是一個Promise對象,後面還能夠跟then
不管Promise對象的回調鏈是以then方法結束仍是以catch方法結束,只要最後一個方法拋出錯誤,都有可能沒法捕捉到(由於Promise內部的錯誤不會冒泡到全局)。爲此能夠提供一個done()方法,他老是在回調鏈的尾部,保證拋出任何可能出現的錯誤。
asyncFunc ()
.then(f1)
.catch(f2)
.then(f3)
.done()
複製代碼
它的源碼實現很簡單:
Promise.prototypr.done = function (onFulfilled, onRejected) {
this.then(onFulfilled, onRejected)
.catch(function(reason){
// 拋出一個全局錯誤
setTimeout(() => {throw reason}, 0)
})
}
複製代碼
finally方法用於指定無論Promise對象最後如何都會執行的操做。他與done方法的最大區別在於它接受一個回調函數做爲參數,該函數無論怎麼樣都會執行。來看看它的實現方式。
Promise.prototype.finally = function (callback) {
let P = this.constructor
// 巧妙的使用Promise.resolve方法,達到無論前面的Promise狀態是fulfilled仍是rejected,都會執行回調函數
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => throw reason)
)
}
複製代碼
Promise.all方法用於將多個Promise實例包裝成一個新的Promise實例
var p = Promise.all([p1, p2, p3])
複製代碼
p一、p二、p3都是Promise實例,若是不是,則會使用Promise.resolve方法,將參數轉化爲Promise實例,再進行處理
該方法的參數不必定是要數組,但必需要有Iterator接口,且每一個組員都是Promise實例
p的狀態由p一、p二、p3決定
var promises = [2, 3, 4, 5, 6, 7].map((id) => {
return getJSON(`/post/${id}.json`)
})
Promise.all(promises).then((posts) => {
//...
}).catch((error) => {
//...
})
複製代碼
若是做爲參數的Promise實例自身定義了catch方法,那麼它被rejected時並不會出發Promise.all()的catch方法
const p1 = new Promise((resolve, reject) => {
resolve('hello')
})
.then(result => result)
.catch(e => e)
const p2 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
.catch(e => e)
const p3 = new Promise(resolve, reject) => {
throw new Error('error')
})
.then(result => result)
// p2的catch返回了一個新的Promise實例,該實例的最終狀態是resolved
Promise.all([p1, p2])
.then(result => result)
.catch(e => e)
// ["hello", Error: error]
// p3沒有本身的catch,因此錯誤被Promise.all的catch捕獲倒了
Promise.all([p1, p3])
.then(result => result)
.catch(e => e)
// Error: error
複製代碼
Promise.race方法用於將多個Promise實例包裝成一個新的Promise實例
var p = Promise.race([p1, p2, p3])
複製代碼
p一、p二、p3都是Promise實例,若是不是,則會使用Promise.resolve方法,將參數轉化爲Promise實例,再進行處理
該方法的參數不必定是要數組,但必需要有Iterator接口,且每一個組員都是Promise實例
p的狀態由p一、p二、p3決定,只要p一、p二、p3有一個實例率先改變狀態,p的狀態就會跟着改變。率先改變狀態的實例的返回值傳遞給p的回調函數。
Promise.resolve方法將現有對象轉換成Promise對象,分爲如下四種狀況:
Promise.resolve不作任何改變
thenable對象是指具備then方法的對象
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
let p1 = Promise.resolve(thenable)
p1.then(function(value) {
console.log(value) // 42
})
複製代碼
Promise.resolve會將這個對象轉換成Promise對象,而後當即執行thenable對象的then方法
該狀況下,Promise.resolve返回一個新的Promise對象,狀態爲Resolved
var p = Promise.resolve('hello');
p.then((s) => {
console.log(s)
})
// hello
複製代碼
此狀況下,Promise.resolve方法返回一個Resolved狀態的Promise對象
console.log('start');
setTimeout(() => {
console.log('timeout');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
複製代碼
(1)先執行同步隊列的任務,所以先打印start和end (2)setTimeout 做爲一個宏任務放入宏任務隊列 (3)Promise.then做爲一個爲微任務放入到微任務隊列 (4)Promise.resolve()將Promise的狀態變爲已成功,即至關於本次宏任務執行完,檢查微任務隊列,發現一個Promise.then, 執行 (5)接下來進入到下一個宏任務——setTimeout, 執行
Promise.reject方法會返回一個新的Promise實例,狀態爲Rejected
與Promise.resolve不一樣,Promise.reject會原封不動的將其參數做爲reject的理由傳遞給後續的方法,所以沒有那麼多的狀況分類
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
//true
複製代碼
Promise解決了回調函數的回調地獄的問題,可是Promise最大的問題是代碼的冗餘,原來的任務被Promise包裝後,不管什麼操做,一眼看過去都是許多then的堆積,原來的語義變得很不清楚。
傳統的編程語言中早有異步編程的解決方案,其中一個叫作協程,意思爲多個線程相互做用,完成異步任務。它的運行流程以下:
function *asyncJob () {
// ...
var f = yield readFile(fileA);
// ...
}
複製代碼
它最大的優勢就是,代碼寫法很像同步操做。
Generator函數是協程在ES6中最大的實現,最大的特色就是能夠交出函數的執行權。
整個Generator函數就是一個封裝的異步任務容器,異步操做須要用yield代表。Generator他能封裝異步任務的緣由以下:
上面代碼的Generator函數的語法相關已經在上一篇博客中總結了,不能理解此處能夠前往復習。
Generator函數是一個異步操做的容器,它的自動執行須要一種機制,當異步操做有告終果,這種機制須要自動交回執行權,有兩種方法能夠作到:
回調函數:將異步操做包裝成Thunk函數,在回調函數裏面交回執行權
Promise對象:將異步操做包裝成Promise對象,使用then方法交回執行權
參數的求值策略有兩種,一種是傳值調用,另外一種是傳名調用
- 傳值調用,在參數進入函數體前就進行計算;可能會形成性能損失。
- 傳名調用,在參數被調用時再進行計算。
編譯器的傳名調用的實現將參數放到一個臨時函數之中,再將這個臨時函數傳入函數體。這個臨時函數就叫Thunk函數。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同於
var Thunk = function () {
return x + 5;
}
function f(thunk) () {
return thunk() * 2
}
複製代碼
js語言是按值調用的,它的Thunk函數含義和上述的有些不一樣。在js中,Thunk函數替換的不是表達式,而是多參數函數,將其替換成一個只接受回調函數做爲參數的單參數函數。
(1)在js中,任何函數,只要參數有回調函數就能夠寫成Thunk函數的形式。
// ES5
var Thunk = function (fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
return function (callback) {
args.push(callback);
return fn.apply(this, args)
}
}
}
}
// ES6
var Thunk = function (fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback)
}
}
}
// 實例
function f (a, cb) {
cb(a)
}
const ft = Thunk(f);
ft(1)(console.log); // 1
複製代碼
(2)生產環境中使用Thunkify模塊
$ npm install Thunkify
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str) {
// ...
})
複製代碼
前面提到了Thunk能夠用於Generator函數的自動流程管理
(1)Generator能夠自動執行
function *gen() {
// ...
}
var g = gen();
var res = g.next();
while (!res.done) {
console.log(res.value);
res = g.next();
}
複製代碼
可是這不適合異步操做,若是必須知足上一步執行完成才能執行下一步,上面的自動執行就不可行。
(2)Thunk函數自動執行
var thunkify = require('thunkify');
var fs = require('fs');
var readFileThunk = thunkify(fs.readFile);
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen();
// 將同一個函數反覆傳入next方法的value屬性
var r1 = g.next();
r1.value(function(err, data) {
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {
if (err) throw err;
g.next(data);
})
})
// Thunk函數自動化流程管理
function run (fn) {
var gen = fn();
function next (err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next)
}
next();
}
run(g)
複製代碼
上述的run函數就是以一個Generator函數自動執行器。有了這個執行器,無論內部有多少個異步操做,直接在將Generator函數傳入run函數便可,可是要注意,每個異步操做都是Thunk函數,也就是說yield後面必須是Thunk函數。
co模塊不須要編寫Generator函數的執行器
var co = require('co');
// gen函數自動執行
co(gen);
// co函數返回一個Promise對象,所以能夠用then方法添加回調
co(gen).then(function () {
console.log('Generator函數執行完畢')
})
複製代碼
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function (error, data) {
if (error) return reject(error);
resolve(data);
})
})
}
var gen = function* () {
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shell');
console.log(r2.toString());
}
var g = gen()
// 手動執行,使用then方法層層添加回調函數
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data)
})
})
// 根據手動執行,寫一個自動執行器
function run (gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function (data) {
next(data);
})
}
next();
}
run(gen)
複製代碼
ES2017標準引入了async函數,使得異步操做變得更加方便。async函數就是Generator函數的語法糖。
async函數就是將Generator函數的*換成async,將yield換成await。
varasyncReadFile = async function () {
var r1 = await readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = await readFileThunk('/etc/shell');
console.log(r2.toString());
}
複製代碼
async對於Generator的改進有三點:
// 函數式聲明
async function foo() {}
// 函數表達式
const foo = async function() {}
// 箭頭函數
const foo = async () => {}
// 對象方法
let obj = { async foo() {} }
obj.foo().then(...)
// class方法
class Storage {
constructor () { ... }
async getName() {}
}
複製代碼
(1)async函數返回一個Promise對象
async function f() {
return 'hello'
}
f().then(v => console.log(v)) // hello
複製代碼
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v)
e => console.log(e)
)
// Error: 出錯了
複製代碼
(2)await命令
正常狀況下await命令後面是一個Promise對象,若是不是會被resolve當即轉成一個Promise對象
await命令後面的Promise對象若是變成reject狀態,則reject的參數會被catch方法的而回調函數接收到
有時不但願拋出錯誤終止後面的步驟
async function f() {
try {
await Promise.reject('出錯了')
} catch(e) {
}
return await Promise.resolve('hello')
}
f().then( v => console.log(v)) // hello
async function f1() {
await Promise.reject('出錯了')
.catch(e => console.log(e));
return await Promise.resolve('hello')
}
f1().then( v => console.log(v)) // hello
複製代碼
await命令只能在async函數中使用,不然會報錯
若是await命令後面的異步操做不是繼發關係,最好讓他們同步觸發
let foo = getFoo();
let bar = getBar();
// 寫法1
let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 寫法2
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
複製代碼
參考資料:
- 偶像神三元的博客
- 阮一峯老師的ES6
- 你不知道的JavaScript(中)