JavaScript異步編程
眾所周知,目前主流的JavaScript環(huán)境都是以單線程模式去執(zhí)行的JavaScript代碼,JavaScript采用單線程模式工作的原因與它最早的設(shè)計初衷有關(guān),最早JavaScript就是運行在瀏覽器端的腳本語言,目的是為了實現(xiàn)頁面上的動態(tài)交互,而實現(xiàn)頁面交互的核心就是dom操作,這也決定了它必須使用單線程,否則會出現(xiàn)復雜的線程同步問題。試想一下,假定我們在JavaScript中同時又多個線程一起工作,其中一個線程修改了某個dom元素,而另外一個線程同時刪除這個元素,那此時瀏覽器就無法明確該以哪個線程工作結(jié)果為準,所以為了避免線程同步的問題,從一開始JavaScript就被設(shè)計為了單線程模式工作,這也成了這門語言最為核心的特性之一。這里的單線程指的是在js執(zhí)行環(huán)境中負責執(zhí)行代碼的線程只有一個。一次只能執(zhí)行一個任務,有多個任務就需要排隊,一個一個完成。這種模式最大的優(yōu)點就是更安全更簡單,缺點也同樣很明顯,如果遇到某個特別耗時的任務,后邊的任務都必須排隊等待這個任務的結(jié)束,這也就會導致整個程序的執(zhí)行會被拖延,出現(xiàn)假死的情況。為了解決耗時任務阻塞的問題,JavaScript將任務的執(zhí)行模式分成了兩種,同步模式和異步模式。
同步模式和異步模式
同步模式
同步模式指代碼當中的任務依次執(zhí)行,后一個任務必須等待前一個任務結(jié)束,按照代碼書寫的順序執(zhí)行。
異步模式
不會去等待這個任務的結(jié)束才開始下一個任務。對于耗時任務,開啟過后就立即往后執(zhí)行下一個任務,耗時任務的后續(xù)邏輯通過回調(diào)函數(shù)的方式定義。耗時任務完成會自動執(zhí)行這里的回調(diào)函數(shù)。如果沒有異步模式,單線程的JavaScript語言無法同時處理大量耗時任務。但是對于開發(fā)者而言,異步模式最大的問題就是代碼的執(zhí)行順序混亂。
回調(diào)函數(shù):由調(diào)用者定義,交給執(zhí)行者執(zhí)行的函數(shù)。
事件循環(huán)和消息隊列
JavaScript線程首先執(zhí)行同步任務,在遇到異步任務(eg:setTimeout)的時候,發(fā)起異步調(diào)用,然后繼續(xù)執(zhí)行同步任務。與此同時,異步調(diào)用線程執(zhí)行異步任務后,將異步任務的回調(diào)放入消息隊列。待同步任務執(zhí)行完畢后,Event Loop會去消息隊列中尋找任務,依次執(zhí)行消息隊列中的任務。
異步編程的幾種方式
Promise異步方案、宏任務/微任務隊列
Promise就是一個對象,用來表示一個異步任務最終結(jié)束過后,究竟是成功還是失敗。就像是一個承諾,一開始是待定的狀態(tài)- Pending,成功后叫Fulfilled,失敗后叫Rejected。承諾明確后會有對應的任務執(zhí)行,onFilfilled, onRejected.
基本用法
const promise = new Promise(function (resolve, reject) {
// 兌現(xiàn)承諾
// resolve(100) // 承諾達成
reject(new Error('promise rejected')) // 承諾失敗
})
promise.then(function (value) {
console.log('resolved', value)
return 1
}, function (error) {
console.log('rejected', error)
}).then(function(value){
console.log(value) // 1
})
- Promise對象的then方法會返回一個全新的Promise對象,所以可以使用鏈式調(diào)用
- 后面的then方法就是在為上一個then返回的Promise注冊回調(diào)
- 前面then方法中回調(diào)函數(shù)的返回值會作為后面then方法回調(diào)的參數(shù)
- 如果回調(diào)中返回的是Promise,那后面的then方法的回調(diào)會等待這個Promise結(jié)束
異常處理
function ajax(url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = function () {
if (this.status === 200) {
resolve(this.response)
} else {
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// then方法的第二個回調(diào)函數(shù)進行異常捕獲
ajax('/api/users.json').then(
function onFulfilled(value) {
console.log('onFulfilled', value)
},
function onRejected(error) {
console.log('onRejected', error)
}
)
// 使用catch進行異常捕獲
ajax('/api/users.json')
.then(function onFulfilled(value) {
console.log('onFulfilled', value)
})
.catch(function onRejected(error) {
console.log('onRejected', error)
})
使用then的第二個回調(diào)捕獲異常,只能捕獲到前一個拋出的異常,而使用catch,因為每一個then都會返回一個promise對象,所以catch首先捕獲的是前一個then 的異常,然后會捕獲鏈上往前的異常,也就是catch會捕獲鏈上catch以前的異常。
Promise 靜態(tài)方法
Promise.resolve()
Promise.reject()
Promise 并行執(zhí)行
Promise.all()
// Promise.all 返回一個全新的Promise
var promise = Promise.all([
ajax('/api/user.json'),
ajax('api/posts.json')
])
// 所有的Promise完成,全新的promise才會完成
// 所以的異步任務都成功,promise才成功
// 只要有一個異步任務失敗,promise就失敗
promise.then(function (values) {
// 接收的是數(shù)組,包含每個異步任務執(zhí)行的結(jié)果
console.log(values)
}).catch(function (error) {
console.log(error)
})
Promise.race()
Promise.race()也會將多個promise對象組合返回一個新的promise對象,但與 all 不同的是:
all 等待所有任務結(jié)束,它才會結(jié)束
race 只會等待第一個結(jié)束的任務,也就是只要有一個任務完成了,新的promise對象也就完成了。
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject => {
setTimeout(() => reject(new Error('timeout')), 500)
}))
// Promise.race()將多個異步任務組合后返回一個新的promise對象
// 多個異步任務中只要有一個完成(成功或失?。?,新的promise對象就完成了
// 這里如果request請求在500毫秒內(nèi)請求成功,就返回成功,使用.then方法
// 如果500毫秒請求沒有返回結(jié)果,就會reject一個錯誤,走到catch
Promise.race([require, timeout])
.then(value => {
console.log(value)
})
.catch(error => {
console.log(error)
})
const p = Promise.all([p1,p2,p3])
p.then(() => {})
.catch(err => {})
- Promise.all(): p1, p2, p3全部返回成功,p 才會返回成功, p1, p2, p3中任意一個返回失敗,p 就返回失敗。 失敗后,其他異步任務仍會繼續(xù)執(zhí)行。
- Promise.race(): p1, p2, p3任意一個返回成功,p 就返回成功, p1, p2, p3中任意一個返回失敗,p 就返回失敗。 失敗后,其他異步任務仍會繼續(xù)執(zhí)行。
- Promise.allSettled():等到p1,p2,p3全部執(zhí)行完,不管成功失敗,p 的狀態(tài)為fulfilled。監(jiān)聽函數(shù)接收到的參數(shù)時數(shù)組
[{status:'fulfilled', value: 42}, {status:'rejeceted}, reason:-1] - Promise.any(): p1, p2, p3只要有一個成功,p 就返回成功,p1,p2,p3全部失敗,p 才返回失敗
Promise 執(zhí)行時序
console.log('global start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve()
.then(() => {
console.log('promise')
})
.then(() => {
console.log('promise 2')
})
.then(() => {
console.log('promise 3')
})
console.log('global end')
// global start
// global end
// promise
// promise 2
// promise 3
// setTimeout
按照前面說的,回調(diào)進入回調(diào)隊列,依次執(zhí)行,可能我們會認為先打印setTimeout,再打印promise,但是結(jié)果不是這樣的。這是因為js將任務分為了宏任務和微任務。微任務會插隊,在本輪任務的末尾直接執(zhí)行。
大部分異步任務都會作為宏任務。
微任務包括Promise,MutationObserver, process.nextTick/
Generator異步方案、Async/Await語法糖
基本使用
// 比普通的函數(shù)多了一個 *
function * foo() {
console.log('start')
// 用 yield 返回一個值,next 方法返回的就是這個值
// yield 不會結(jié)束生成器的執(zhí)行,只是 暫停
// 如果next方法傳入一個參數(shù),會作為上一個yield 的返回值
// yield 'foo'
// const res = yield 'foo'
// console.log(res) // bar
try {
const res = yield 'foo'
console.log(res) // bar
} catch (e) {
console.log(e)
}
}
// 調(diào)用生成器并不會立即執(zhí)行,而是得到一個生成器對象
const generator = foo()
// 調(diào)用next方法,函數(shù)體才會執(zhí)行
const result = generator.next()
// 返回結(jié)果中有一個done屬性,表示生成器是否一起執(zhí)行完了
console.log(result) //{value: "foo", done: false}
// 再一次調(diào)用next方法時,會從 yield 位置開始執(zhí)行
// generator.next('bar')
// 如果調(diào)用生成器的throw方法,也會繼續(xù)往下執(zhí)行,但是它會拋出一個異常
// 在生成器內(nèi)部使用try{}catch(){}語句來接收異常
generator.throw(new Error('Generator error'))
function* main() {
try {
const users = yield ajax(url1)
console.log(users)
const posts = yield ajax(url2)
console.log(posts)
} catch (e) {
console.log(e)
}
}
function co(generator) {
const g = generator()
function handleResult(result) {
if (result.done) return
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}
handleResult(g.next())
}
co(main)
Async / Await 語法糖
// 將生成器的 * 改為 async ,yield 改為 await
async function main() {
try {
const users = await ajax(url1)
console.log(users)
const posts = await ajax(url2)
console.log(posts)
} catch (e) {
console.log(e)
}
}
// 直接調(diào)用,不需要 co
// async 函數(shù)返回一個promise對象
const promise = main()
promise.then(() => {
console.log('all completed')
})
手撕Promise
/**
* 手撕Promise
* 首先,promise是一個類,傳入一個函數(shù)作為參數(shù),直接調(diào)用
* promise 有三個狀態(tài), pending, fulfilled, rejected
* 在 resolve 和 reject調(diào)用后狀態(tài)修改,且狀態(tài)修改后不能再修改
* 將 resolve 和 reject 中的參數(shù)記錄下來,作為 then 方法成功和失敗回調(diào)的參數(shù)
* 如果 promise 中執(zhí)行出錯,要捕獲錯誤,可以使用try catch來捕獲
* 需要捕獲錯誤的地方包括promise傳入的函數(shù)執(zhí)行器,和 then 方法的回調(diào)
*/
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECRED = 'rejected'
class MyPromise {
constructor(fn) {
try {
// promise 傳入一個函數(shù),直接調(diào)用,函數(shù)的參數(shù)為 resolve 和 reject
fn(this.resolve, this.reject)
} catch (err) {
this.reject(err)
}
}
// 定義初始狀態(tài)
status = PENDING
// then 方法成功回調(diào)的參數(shù)
value = undefined
// then 方法失敗回調(diào)的參數(shù)
error = undefined
// 初始化存儲 then 回調(diào)的值
sCallback = []
fCallback = []
resolve = (value) => {
// 如果狀態(tài)不是 pending ,不做修改
if (this.status !== PENDING) return
// resolve 后將狀態(tài)修改為成功
this.status = FULFILLED
// 將結(jié)果記錄
this.value = value
// 如果有儲存的成功回調(diào),則調(diào)用,數(shù)組需要循環(huán)調(diào)用
// this.sCallback && this.sCallback(value)
while (this.sCallback.length) this.sCallback.shift()()
}
reject = (error) => {
// 如果狀態(tài)不是 pending ,不做修改
if (this.status !== PENDING) return
// reject 后將狀態(tài)修改為失敗
this.status = REJECRED
// 將結(jié)果記錄
this.error = error
// 如果有儲存的失敗回調(diào),則調(diào)用,數(shù)組需要循環(huán)調(diào)用
// this.fCallback && this.fCallback(error)
while (this.fCallback.length) this.fCallback.shift()()
}
/**
* then 方法參數(shù)為成功回調(diào)和失敗回調(diào)
* 根據(jù)狀態(tài)判斷執(zhí)行哪個回調(diào)
* 如果是異步調(diào)用,執(zhí)行 then 方法時狀態(tài)還是 pending,則要將兩個回調(diào)儲存起來
* 儲存的方法在 resolve 和 reject 的方法里對應的調(diào)用
* 同一個promise可能會有多個 then 調(diào)用,也就會有多組成功和失敗的回調(diào),將異步時回調(diào)儲存為數(shù)組
* then 方法可以鏈式調(diào)用,所以它返回的是一個promise對象,將回調(diào)中返回的值作為下一個then方法的參數(shù)
* then 方法返回的promise對象不能是自身,將 newPromise 與 返回值進行判斷
* 在pending狀態(tài)也要判斷不能返回自身
* then 方法可以不傳遞參數(shù),不傳遞參數(shù)時,下一個then可以拿到這個then應該拿到的結(jié)果
* 所以 then 不傳遞參數(shù)時,相當于把結(jié)果傳遞到下一個then
*/
then(sCallback = value => value, fCallback = error => { throw error }) {
let newPromise = new MyPromise((resolve, reject) => {
// 這里是同步執(zhí)行,所以可以將要執(zhí)行的操作放在這里
if (this.status === FULFILLED) {
setTimeout(() => {
try {
// 調(diào)用后獲取返回的值
const x = sCallback(this.value)
// 判斷返回的值如果是 promise 對象,根據(jù)promise的結(jié)果進行resolve和reject
// 如果是普通值,直接resolve
// 這個操作在失敗是也會調(diào)用,所以包裝成一個方法
// then 方法不能返回自己,所以將 newPromise 傳進去判斷
// 但是這里其實拿不到newPromise,可以將這段代碼放入 setTimeout 中
// 放入setTimeout 中并不是為了延時,只是為了等 newPromise 創(chuàng)建好了可以引用,所以時間設(shè)為0
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
} else if (this.status === REJECRED) {
setTimeout(() => {
try {
const x = fCallback(this.error)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
} else {
// 調(diào)用 then 方法時,promise的異步還沒執(zhí)行完,狀態(tài)還是pending,把兩個回調(diào)儲存
// 判斷不能返回自身
this.sCallback.push(() => {
setTimeout(() => {
try {
const x = sCallback(this.value)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
this.fCallback.push(() => {
setTimeout(() => {
try {
const x = fCallback(this.error)
thenValue(newPromise, x, resolve, reject)
} catch (err) {
reject(err)
}
}, 0)
})
}
})
return newPromise
}
/**
* 實現(xiàn)finally方法, finally 方法不管promise成功失敗都會執(zhí)行回調(diào)
* finally 會將promise的結(jié)果往下傳
* 可以利用 then 方法來實現(xiàn)
* finally 方法返回一個新的promise對象,由于then方法就是返回一個promise對象,所以直接返回
* 如果finally返回一個promise對象,要等promise對象有了結(jié)果,才會執(zhí)行下方的 then
*/
finally(callback) {
return this.then(value => {
return MyPromise.resolve(callback()).then(() => value)
}, err => {
return MyPromise.resolve(callback()).then(() => { throw err })
})
}
/**
* 實現(xiàn) catch,catch方法只有一個回調(diào),就是失敗回調(diào),返回一個promise
*/
catch(callback) {
return this.then(undefined, callback)
}
/**
* 實現(xiàn)一個all方法, all 方法傳入一個數(shù)組,數(shù)組中會有異步調(diào)用,返回一個新的promise對象
* 數(shù)組中所有異步都成功,將結(jié)果以數(shù)組形式返回,否則一個出錯就出錯
*/
static all(args) {
let results = []
let index = 0
return new MyPromise((resolve, reject) => {
function addData(key, value) {
results[key] = value
// index代表給results中添加了幾個值,如果index和args長度相等,說明全部成功
// 不能用results長度來判斷,因為results賦值不是通過 push 方法,而是針對 key 來賦值的
index++
if (index == args.length) {
resolve(results)
}
}
for (let i = 0; i < args.length; i++) {
// 判斷是promise對象還是普通值,普通值直接加入results數(shù)組
if (args[i] instanceof MyPromise) {
// promise 對象
args[i].then(value => {
addData(i, value)
}, reject)
} else {
// 普通值
addData(i, args[i])
}
}
})
}
/**
* 實現(xiàn)一個Promise.resolve方法
* Promise.resolve方法后面要接 then 方法
* 參數(shù)如果是個promise對象,就按照這個promise執(zhí)行,返回它
* 參數(shù)如果是個普通值,創(chuàng)建一個新的promise對象
*/
static resolve(value) {
if (value instanceof MyPromise) return value
return new MyPromise(resolve => { resolve(value) })
}
/**
* Promise.reject 方法,返回一個新的Promise,狀態(tài)為reject
* 參數(shù)原封不動的作為reject的理由
*/
static reject(reason) {
return new Promise((resolve, reject) => { reject(reason) })
}
}
function thenValue(newPromise, x, resolve, reject) {
if (newPromise === x) return reject(new TypeError('then方法不能返回自己'))
if (x instanceof MyPromise) {
// 如果是promise對象
x.then(resolve, reject)
} else {
// 如果是普通值
resolve(x)
}
}