这段时间在学习async/await,但始终不得要领。为了更好地了解async/await,我决定本人实现一个简易版的async/await,以学习async/await工作原理。该工程名为ToyAsync,仓库地址如下:
https://gitee.com/pandengyang/toyasync.git
https://github.com/pandengyang/toyasync.git
一个常见场景:用户进入某网站后,需登录并获取用户profile。要实现该性能,需编写如下代码:
function login(username, password, callback) {
setTimeout(function () {
if (username === 'scratchlab' && password === '123456') {
callback(null, '8ef9e41db21905efdefd45241466f9b3')
} else {
callback(new Error('username or password error'), null)
}
}, 3000)
}
function fetchProfile(token, callback) {
setTimeout(function () {
if (token === '8ef9e41db21905efdefd45241466f9b3') {
callback(null, "{'name':'scratchlab','age':18}")
} else {
callback(new Error('token error'), null)
}
}, 3000)
}
login('scratchlab', '123456', function onLogin(error, token) {
if (error) {
console.log(error)
return
}
console.log(token)
fetchProfile(token, function onFetchProfile(error, profile) {
if (error) {
console.log(error)
return
}
console.log(profile)
})
})
这是典型的回调函数的写法。ES6中实现了Generator,应用Generator,开发者能够像写同步代码一样写异步代码。比方,上述的代码可被写为如下形式:
var token = login('scratchlab', '123456')
var profile = fetchProfile(profile)
首先,应用Generator对之前的回调代码进行革新,代码如下:
var g
function* main() {
var token
var profile
try {
token = yield login( // 1
'scratchlab',
'123456',
function onLogin(error, data) {
if (error) {
g.throw(error) // 2.1
return
}
g.next(data) // 2.2
}
)
console.log(token)
profile = yield fetchProfile(token, function onFetchProfile(error, data) {
if (error) {
g.threw(error)
return
}
g.next(data)
})
console.log(profile)
} catch (e) { // 3
console.log(e)
}
}
g = main()
g.next()
代码1处,login运行完后,让出执行权。yiled阻塞了Generator函数*main的运行,然而不会阻塞*main外代码的运行。
try {
token = yield login( // 1
当login外部的定时器超时后,在login的回调函数中复原*main的运行,并将后果传递给*main。当login胜利时,通过g.next将后果传入*main(代码2.2处),当login失败时,通过g.throw将谬误抛给*main(代码2.1处)。代码如下:
function onLogin(error, data) {
if (error) {
g.throw(error) // 2.1
return
}
g.next(data) // 2.2
}
当login胜利时,token被赋予通过g.next传入的值,代码如下:
try {
token = yield login( // 1
当login失败时,*main可捕捉通过g.throw抛出的异样,代码如下:
} catch (e) { // 3
fetchProfile的应用与login相似,此种形式须要在回调函数中编写调度Generator的代码,非常繁琐。
借助thunk函数,能够主动执行Generator函数。在js中,thunk函数可将多参数的函数转换为单参数的函数。举个例子,有如下计算3个数乘积的函数,代码如下:
function mul3(x, y, z) {
return x * y * z
}
圆的周长的计算公式为:2 π 半径,利用thunk函数可从mul3生成一个用于计算圆周长的函数,示例如下:
function ThunkMul3(x, y) {
return function (radius) {
return mul3(x, y, radius)
}
}
var circleCircumference = ThunkMul3(2, Math.PI)
console.log(circleCircumference(1))
console.log(circleCircumference(2))
为了主动执行Generator函数,咱们须要将login、fetchProfile转换成单参数的版本。之所以这样做是因为用户编写的异步函数的参数各不相同,但至多有一个回调函数参数,而且该回调函数通常用于接管后果并决定下一步的操作。
能够由执行器提供一个通用的回调函数next用于接管后果并调度Generator运行,因而,须要将用户函数thunk化为只承受一个回调函数参数的版本。
首先,编写一个通用的thunk化函数,代码如下:
function thunkify(fn) {
var args = Array.prototype.slice.call(arguments, 1)
return function (cb) {
args.push(cb)
return fn.apply(null, args)
}
}
利用thunkify可将login、fetchProfile转换成单参数的版本,代码如下:
var thunkLogin = thunkify(login, 'scratchlab', '123456')
var thunkFetchProfile = thunkify(fetchProfile, token)
对于login来说,上面两种调用办法是等价的:
login('scratchlab', '123456', cb)
thunkLogin(cb)
对于fetchProfile来说,上面两种调用办法是等价的:
fetchProfile(token, cb)
thunkFetchProfile(cb)
应用执行器主动执行Generator函数的代码如下:
function* main() {
var token
var profile
try {
token = yield thunkify(login, 'scratchlab', '123456') // 3
console.log(token)
profile = yield thunkify(fetchProfile, token)
console.log(profile)
} catch (e) { // 5
console.log(e)
}
}
function run(fn) {
g = fn()
function next(error, data) {
if (error) {
g.throw(error) // 2.1
return
}
var result = g.next(data) // 2.2
if (result.done) {
return
}
result.value(next) // 4
}
next(null, null) // 1
}
run(main)
代码3处,thunkify运行完后,让出执行权,并返回login的thunk版。此时,代码2.1处接管到的返回值如下:
{'value': thunkLogin, 'done': false}
代码4处,执行thunkLogin,代码如下:
result.value(next) // 4
该行等价于:
thunkLogin(next)
也就是说在run函数中真正执行了login函数。该行运行后,代码1处的next函数、run函数就退出了,继续执行后续代码。
从中咱们能够看出js没有被阻塞,会继续执行run(main)前面的代码。然而*main却被阻塞了,期待异步执行的后果。
当login中的定时器超时后,代码4处,传入的回调函数next被调度运行:
result.value(next) // 4
next运行时,代码2.1或2.2处,login函数执行的后果通过g.next或g.throw传入到\main中,代码如下:
if (error) {
g.throw(error) // 2.1
return
}
var result = g.next(data) // 2.2
代码3处,login运行后果被赋值给token,代码如下:
token = yield thunkify(login, 'scratchlab', '123456') // 3
代码5处,若login运行出错,*main可捕捉login抛入的异样。
} catch (e) { // 5
fetchProfile的应用同login相似,通过此种形式能够实现Generator函数的主动执行。
上述场景也可应用Promise实现,代码如下:
function login(username, password) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (username === 'scratchlab' && password === '123456') {
resolve('8ef9e41db21905efdefd45241466f9b3')
} else {
reject(new Error('username or password error'))
}
}, 3000)
})
}
function fetchProfile(token) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
if (token === '8ef9e41db21905efdefd45241466f9b3') {
resolve("{'name':'kernelnewbies','age':18}")
} else {
reject(new Error('token error'))
}
}, 3000)
})
}
login('kernelnewbies', '123456')
.then(function fullfilled(value) {
console.log(value)
return fetchProfile(value)
})
.then(function fullfilled(value) {
console.log(value)
})
.catch(function rejected(error) {
console.log(error)
})
应用Generator对Promise代码进行革新,代码如下:
var g
function* main() {
var token
var profile
try {
token = yield login('kernelnewbies', '123456').then(
function fullfilled(data) {
g.next(data) // 2.1
},
function rejected(error) {
g.throw(error) // 2.2
}
) // 1
console.log(token)
profile = yield fetchProfile(token).then(
function fullfilled(data) {
g.next(data)
},
function rejected(error) {
g.throw(error)
}
)
console.log(profile)
} catch (error) { // 3
console.log(error)
}
}
g = main()
g.next()
代码1处,login及then执行结束后,让出执行权,代码如下:
token = yield login('kernelnewbies', '123456').then(
) // 1
login是立刻执行的,并返回一个Promise。当定时器超时后,该Promise决定并执行then注册的决定/回绝函数。代码2.1或2.2处,在决定函数中将执行后果传回*main,代码如下:
token = yield login('kernelnewbies', '123456').then(
function fullfilled(data) {
g.next(data) // 2.1
},
function rejected(error) {
g.throw(error) // 2.2
}
) // 1
*main中,通过如下代码接管login执行返回的后果:
token = yield login('kernelnewbies', '123456').then(
或捕捉login抛出的异样:
} catch (error) { // 3
fetchProfile的应用与login相似,此种形式依然须要调用then函数(尽管then链很短)。
同回调函数形式相似,能够编写一个基于Promise的Generator执行器来优化掉then函数,代码如下:
function* main() {
var token
var profile
try {
token = yield login('kernelnewbies', '123456') // 1
console.log(token)
profile = yield fetchProfile(token)
console.log(profile)
} catch (error) { // 6
console.log(error)
}
}
function run(fn) {
var g = fn()
function next(error, data) {
if (error) {
g.throw(error) // 2.1
return
}
var result = g.next(data) // 2.2
if (result.done) {
return
}
result.value.then(
function fullfilled(data) {
next(null, data) // 4
},
function rejected(error) {
next(error, null) // 5
}
) // 3
}
next(null, null)
}
run(main)
代码1处,login执行完后,通过yield向run返回一个Promise,并暂停*main的运行,代码如下:
token = yield login('kernelnewbies', '123456') // 1
代码2.2处,run通过result接管该Promise,代码如下:
var result = g.next(data)
此时,result的值如下:
{'value': loginPromise, 'done': false}
代码3处,在run中为该Promise注册决定/回绝回调函数,代码如下:
result.value.then(
) // 3
此时next(null, null)与run(main)运行结束并退出。当login中的定时器超时后,loginPromise被决定,调用next函数,并将后果交由next函数解决:
result.value.then(
function fullfilled(data) {
next(null, data) // 4
},
function rejected(error) {
next(error, null) // 5
}
) // 3
代码2.1与2.2处,在next函数中将login运行后果传回*main函数,代码如下:
if (error) {
g.throw(error) // 2.1
return
}
var result = g.next(data) // 2.2
代码1与6处,在*main函数中接管login运行后果或捕捉login抛出的异样,代码如下:
try {
token = yield login('kernelnewbies', '123456') // 1
} catch (error) { // 6
fetchProfile的应用同login。
ES6为咱们提供了Generator执行器的官网实现,这就是async/await。利用async/await实现上述性能,代码如下:
async function main() {
var token
var profile
try {
token = await login('kernelnewbies', '123456')
console.log(token)
profile = await fetchProfile(token)
console.log(profile)
} catch (error) {
console.log(error)
}
}
main()
发表回复