async、await 實現原理
JavaScript 非同步程式設計回顧
由於 JavaScript 是單執行緒執行模型,因此必須支援非同步程式設計才能提高執行效率。非同步程式設計的語法目標是讓
非同步過程
寫起來像同步過程。
1. 回撥函式
回撥函式,就是把任務的第二段單獨寫在一個函數里面,等到重新執行這個任務的時候,就直接呼叫這個函式。
const
fs
=
require
(
‘fs’
)
fs
。
readFile
(
‘/etc/passwd’
,
(
err
,
data
)
=>
{
if
(
err
)
{
console
。
error
(
err
)
return
}
console
。
log
(
data
。
toString
())
})
回撥函式最大的問題是容易形成回撥地獄,即多個回撥函式巢狀,降低程式碼可讀性,增加邏輯的複雜性,容易出錯。
fs
。
readFile
(
fileA
,
function
(
err
,
data
)
{
fs
。
readFile
(
fileB
,
function
(
err
,
data
)
{
// 。。。
})
}
2. Promise
為解決回撥函式的不足,社群創造出 Promise。
const
fs
=
require
(
‘fs’
)
const
readFileWithPromise
=
file
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
fs
。
readFile
(
file
,
(
err
,
data
)
=>
{
if
(
err
)
{
reject
(
err
)
}
else
{
resolve
(
data
)
}
})
})
}
readFileWithPromise
(
‘/etc/passwd’
)
。
then
(
data
=>
{
console
。
log
(
data
。
toString
())
return
readFileWithPromise
(
‘/etc/profile’
)
})
。
then
(
data
=>
{
console
。
log
(
data
。
toString
())
})
。
catch
(
err
=>
{
console
。
log
(
err
)
})
Promise 實際上是利用程式設計技巧(可以學習下 Promise 的簡單實現)將回調函式改成鏈式呼叫,避免回撥地獄。最大問題是程式碼冗餘,原來的任務被 Promise 包裝了一下,不管什麼操作,一眼看去都是一堆 then,原來的語義變得很不清楚。
3. async、await
為了解決 Promise 的問題,async、await 在 ES7 中被提了出來,是目前為止最好的解決方案。
const
fs
=
require
(
‘fs’
)
async
function
readFile
()
{
try
{
var
f1
=
await
readFileWithPromise
(
‘/etc/passwd’
)
console
。
log
(
f1
。
toString
())
var
f2
=
await
readFileWithPromise
(
‘/etc/profile’
)
console
。
log
(
f2
。
toString
())
}
catch
(
err
)
{
console
。
log
(
err
)
}
}
async、await 函式寫起來跟同步函式一樣,條件是需要接收 Promise 或原始型別的值。非同步程式設計的最終目標是轉換成人類最容易理解的形式。
實現原理
分析 async、await 實現原理之前,先介紹下預備知識
1. generator
generator 函式是協程在 ES6 的實現。協程簡單來說就是多個執行緒互相協作,完成非同步任務。
整個 generator 函式就是一個封裝的非同步任務,非同步操作需要暫停的地方,都用 yield 語句註明。generator 函式的執行方法如下:
function
*
gen
(
x
)
{
console
。
log
(
‘start’
)
const
y
=
yield
x
*
2
return
y
}
const
g
=
gen
(
1
)
g
。
next
()
// start { value: 2, done: false }
g
。
next
(
4
)
// { value: 4, done: true }
gen() 不會立即執行,而是一上來就暫停,返回一個 Iterator 物件(具體可以參考 Iterator遍歷器)
每次 g。next() 都會打破暫停狀態去執行,直到遇到下一個 yield 或者 return
遇到 yield 時,會執行 yeild 後面的表示式,並返回執行之後的值,然後再次進入暫停狀態,此時 done: false。
next 函式可以接受引數,作為上個階段非同步任務的返回結果,被函式體內的變數接收
遇到 return 時,會返回值,執行結束,即 done: true
每次 g。next() 的
返回值
永遠都是 {value: 。。。 , done: 。。。} 的形式
2. thunk 函式
JavaScript 中的 thunk 函式(譯為轉換程式)簡單來說就是把帶有回撥函式的多引數函式轉換成只接收回調函式的單引數版本。
const
fs
=
require
(
‘fs’
)
const
thunkify
=
fn
=>
(。。。
rest
)
=>
callback
=>
fn
(。。。
rest
,
callback
)
const
thunk
=
thunkify
(
fs
。
readFile
)
const
readFileThunk
=
thunk
(
‘/etc/passwd’
,
‘utf8’
)
readFileThunk
((
err
,
data
)
=>
{
// 。。。
})
單純的 thunk 函式並沒有很大的用處, 大牛們想到了和 generator 結合:
function
*
readFileThunkWithGen
()
{
try
{
const
content1
=
yield
readFileThunk
(
‘/etc/passwd’
,
‘utf8’
)
console
。
log
(
content1
)
const
content2
=
yield
readFileThunk
(
‘/etc/profile’
,
‘utf8’
)
console
。
log
(
content2
)
return
‘done’
}
catch
(
err
)
{
console
。
error
(
err
)
return
‘fail’
}
}
const
g
=
readFileThunkWithGen
()
g
。
next
()。
value
((
err
,
data
)
=>
{
if
(
err
)
{
return
g
。
throw
(
err
)。
value
}
g
。
next
(
data
。
toString
())。
value
((
err
,
data
)
=>
{
if
(
err
)
{
return
g
。
throw
(
err
)。
value
}
g
。
next
(
data
。
toString
())
})
})
thunk 函式的真正作用是統一多引數函式的呼叫方式,在 next 呼叫時把控制權交還給 generator,使 generator 函式可以使用遞迴方式自啟動流程。
const
run
=
generator
=>
{
const
g
=
generator
()
const
next
=
(
err
,
。。。
rest
)
=>
{
if
(
err
)
{
return
g
。
throw
(
err
)。
value
}
const
result
=
g
。
next
(
rest
。
length
>
1
?
rest
:
rest
[
0
])
if
(
result
。
done
)
{
return
result
。
value
}
result
。
value
(
next
)
}
next
()
}
run
(
readFileThunkWithGen
)
有了自啟動的加持之後,generator 函式內就可以寫“同步”的程式碼了。generator 函式也可以與 Promise 結合:
function
*
readFileWithGen
()
{
try
{
const
content1
=
yield
readFileWithPromise
(
‘/etc/passwd’
,
‘utf8’
)
console
。
log
(
content1
)
const
content2
=
yield
readFileWithPromise
(
‘/etc/profile’
,
‘utf8’
)
console
。
log
(
content2
)
return
‘done’
}
catch
(
err
)
{
console
。
error
(
err
)
return
‘fail’
}
}
const
run
=
generator
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
g
=
generator
()
const
next
=
res
=>
{
const
result
=
g
。
next
(
res
)
if
(
result
。
done
)
{
return
resolve
(
result
。
value
)
}
result
。
value
。
then
(
next
,
err
=>
reject
(
gen
。
throw
(
err
)。
value
)
)
}
next
()
})
}
run
(
readFileWithGen
)
。
then
(
res
=>
console
。
log
(
res
))
。
catch
(
err
=>
console
。
log
(
err
))
generator 可以暫停執行,很容易讓它和非同步操作產生聯絡,因為我們在處理
非同步操作
時,在等待的時候可以暫停當前任務,把
程式控制權
交還給其他程式,當非同步任務有返回時,在回撥中再把
控制權
交還給之前的任務。generator 實際上並沒有改變 JavaScript 單執行緒、使用回撥處理非同步任務的本質。
3. co 函式庫
每次執行 generator 函式時自己寫啟動器比較麻煩。co函式庫 是一個 generator 函式的自啟動執行器,使用條件是 generator 函式的 yield 命令後面,只能是 thunk 函式或 Promise 物件,co 函式執行完返回一個 Promise 物件。
const
co
=
require
(
‘co’
)
co
(
readFileWithGen
)。
then
(
res
=>
console
。
log
(
res
))
// ‘done’
co
(
readFileThunkWithGen
)。
then
(
res
=>
console
。
log
(
res
))
// ‘done’
co 函式庫的原始碼實現其實就是把上面兩種情況做了綜合:
// 做了簡化,與原始碼基本一致
const
co
=
(
generator
,
。。。
rest
)
=>
{
const
ctx
=
this
return
new
Promise
((
resolve
,
reject
)
=>
{
const
gen
=
generator
。
call
(
ctx
,
。。。
rest
)
if
(
!
gen
||
typeof
gen
。
next
!==
‘function’
)
{
return
resolve
(
gen
)
}
const
onFulfilled
=
res
=>
{
let
ret
try
{
ret
=
gen
。
next
(
res
)
}
catch
(
e
)
{
return
reject
(
e
)
}
next
(
ret
)
}
const
onRejected
=
err
=>
{
let
ret
try
{
ret
=
gen
。
throw
(
err
)
}
catch
(
e
)
{
return
reject
(
e
)
}
next
(
ret
)
}
const
next
=
result
=>
{
if
(
result
。
done
)
{
return
resolve
(
result
。
value
)
}
toPromise
(
result
。
value
)。
then
(
onFulfilled
,
onRejected
)
}
onFulfilled
()
})
}
const
toPromise
=
value
=>
{
if
(
isPromise
(
value
))
return
value
if
(
‘function’
==
typeof
value
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
value
((
err
,
。。。
rest
)
=>
{
if
(
err
)
{
return
reject
(
err
)
}
resolve
(
rest
。
length
>
1
?
rest
:
rest
[
0
])
})
})
}
}
4。 理解 async、await
一句話,async、await 是 co 庫的官方實現。也可以看作自帶啟動器的 generator 函式的語法糖。不同的是,async、await 只支援 Promise 和原始型別的值,不支援 thunk 函式。
// generator with co
co
(
function
*
()
{
try
{
const
content1
=
yield
readFileWithPromise
(
‘/etc/passwd’
,
‘utf8’
)
console
。
log
(
content1
)
const
content2
=
yield
readFileWithPromise
(
‘/etc/profile’
,
‘utf8’
)
console
。
log
(
content2
)
return
‘done’
}
catch
(
err
)
{
console
。
error
(
err
)
return
‘fail’
}
})
// async await
async
function
readfile
()
{
try
{
const
content1
=
await
readFileWithPromise
(
‘/etc/passwd’
,
‘utf8’
)
console
。
log
(
content1
)
const
content2
=
await
readFileWithPromise
(
‘/etc/profile’
,
‘utf8’
)
console
。
log
(
content2
)
return
‘done’
}
catch
(
err
)
{
throw
(
err
)
}
}
readfile
()。
then
(
res
=>
console
。
log
(
res
),
err
=>
console
。
error
(
err
)
)
總結
不論以上哪種方式,都沒有改變 JavaScript 單執行緒、使用回撥處理非同步任務的本質。人類總是追求最簡單易於理解的程式設計方式。
參考文章
《深入理解 JavaScript 非同步》
《深入掌握 ECMAScript 6 非同步程式設計》
上一篇:能指點一下我的速寫嗎?
下一篇:催眠藥、性功能、睡個好覺