IT虾米网

自己动手写 async/await

developer 2022年05月13日 编程语言 172 0

这段时间在学习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()

评论关闭
IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

多叉树结合JavaScript树形组件实现无限级树形视图(一种构建多级有序树形结构JSON(或XML)数据源的方法)