您當前的位置:首頁 > 書法

async、await 實現原理

作者:由 tongyang 發表于 書法時間:2020-03-21

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 的實現。協程簡單來說就是多個執行緒互相協作,完成非同步任務。

async、await 實現原理

整個 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 非同步程式設計》

標簽: err  console  log  generator  函式