关于javascript:自己动手写-asyncawait

4次阅读

共计 7949 个字符,预计需要花费 20 分钟才能阅读完成。

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