异步函数没有返回值,但是console.log()确实:怎么办?

时间:2017-12-05 23:51:06

标签: javascript ecmascript-6 async-await

我有一个es6类,init()方法负责获取数据,转换它,然后用新转换的数据更新类的属性this.data。 到现在为止还挺好。 这个类本身有另一个getPostById()方法,就像它听起来那样。这是该类的代码:

class Posts {
  constructor(url) {
    this.ready = false
    this.data = {}
    this.url = url
  }
  async init() {
      try { 
        let res = await fetch( this.url )
        if (res.ok) {
            let data = await res.json()

          // Do bunch of transformation stuff here

          this.data = data
          this.ready = true
            return data
        }
      } 
      catch (e) { 
         console.log(e)
      }
  }
  getPostById(id){
     return this.data.find( p => p.id === id )
  }
}  

直截了当,除了我在async/await方法中有init()机制。 现在,此代码将正常工作:

let allPosts = new Posts('https://jsonplaceholder.typicode.com/posts')

allPosts.init()
        .then( d => console.log(allPosts.getPostById(4)) )
// resulting Object correctly logged in console

但它只会打印到控制台: 我如何使用allPosts.getPostById(4)作为函数的return

像:

let myFunc = async () => {
   const postId = 4
   await allPosts.init()  // I need to wait for this to finish before returning

   // This is logging correct value
   console.log( 'logging: ' + JSON.stringify(allPosts.getPostById( postId ), null, 4) )

   // How can I return the RESULT of allPosts.getPostById( postId ) ???
   return allPosts.getPostById( postId )
}

myFunc()返回Promise但不是最终值。我已经阅读了关于这个主题的几个相关帖子,但它们都提供了日志记录的例子,从未返回。

Here is a fiddle包含两种处理方式init():使用Promise和使用async/await。无论我尝试什么,我都无法使用getPostById(id)的最终价值。

这篇文章的问题是:如何创建一个能够返回getPostById(id)的值的函数?

修改

很多好的答案试图解释Promises在主执行循环方面的含义。 经过大量的视频和其他好的阅读后,这就是我现在所理解的:

我的函数init()正确返回。但是,在主事件循环中:它返回一个Promise ,然后我的工作是从有点并行循环中捕获此Promise的结果(不是新的真实线)。为了从并行循环中捕获结果,有两种方法:

  1. 使用.then( value => doSomethingWithMy(value) )

  2. 使用let value = await myAsyncFn()。现在这是愚蠢的打嗝:

  3.   

    await只能在async函数中使用:p

    因此本身会返回一个Promise,可以与await一起使用,它应该嵌入async函数中,该函数可以与await等一起使用...

    这意味着我们无法真正等待Promise:相反,我们应该无限期地捕获并行循环:使用.then()async/await

    感谢您的帮助!

3 个答案:

答案 0 :(得分:8)

至于你的评论;我会把它添加为答案。

您在JavaScript中编写的代码在一个线程上运行,这意味着如果您的代码实际上可以等待某些内容,它将阻止您的其他任何代码被执行。 JavaScript的事件循环在this video中得到了很好的解释,如果您想阅读this page

在浏览器中阻止代码的一个很好的例子是alert("cannot do anything until you click ok");。警报阻止所有内容,用户甚至无法滚动或点击页面中的任何内容,您的代码也会阻止执行。

Promise.resolve(22)
.then(x=>alert("blocking")||"Hello World")
.then(
  x=>console.log(
    "does not resolve untill you click ok on the alert:",
    x
  )
);

在控制台中运行它,你可以通过阻止看到我的意思。

当您想要做一些需要时间的事情时,这会产生问题。在其他框架中,你使用一个或多个线程,但在JavaScript中没有这样的东西(技术上有节点中的web worker和fork,但这是另一个故事,通常比使用async api复杂得多)。

因此,当您想要发出http请求时,可以使用fetch但是fetch需要一些时间才能完成,并且您的函数不应该阻塞(必须尽可能快地返回内容)。这就是fetch返回promise的原因。

请注意,fetch是由浏览器/节点实现的,并且在另一个线程中运行,只有您编写的代码在一个线程中运行,因此启动大量只运行您编写的代码的promise将不会加速除了调用本机异步api之外的任何内容平行的意志。

在承诺异步代码使用回调之前或者会返回一个可观察对象(比如XmlHttpRequest)但是让我们覆盖承诺,因为你可以将更传统的代码转换为承诺。

承诺是一个具有then函数的对象(以及一堆当时为糖但却相同的东西),此函数需要2个参数。

  1. 解析处理程序:当promise解析时将由promise调用的函数(没有错误并且已完成)。该函数将通过一个带有resolve值的参数传递(对于http请求,这通常是响应)。
  2. 拒绝处理程序:当promise拒绝(有错误)时将由promise调用的函数。这个函数将传递一个参数,这通常是拒绝的错误或原因(可以是字符串,数字或任何东西)。
  3. 将回调转换为承诺。

    传统的api(特别是nodejs api)使用回调:

    traditionalApi(
      arg
      ,function callback(err,value){ 
        err ? handleFail(err) : processValue(value);
      }
    );
    

    这使程序员难以捕获错误或以线性方式处理返回值(从上到下)。更不可能尝试并行处理并与错误处理并行进行并行处理(无法读取)。

    您可以使用new Promise

    将传统的api转换为承诺
    const apiAsPromise = arg =>
      new Promise(
        (resolve,reject)=>
          traditionalApi(
            arg,
            (err,val) => (err) ? reject(err) : resolve(val)
          )
      )
    

    async await

    这就是所谓的承诺语法糖。它使承诺消费功能看起来更传统,更容易阅读。也就是说,如果你喜欢编写传统代码,我会认为编写小函数更容易阅读。例如,你能猜出这是做什么的吗?:

    const handleSearch = search =>
      compose([
        showLoading,
        makeSearchRequest,
        processRespose,
        hideLoading
      ])(search)
      .then(
        undefined,//don't care about the resolve
        compose([
          showError,
          hideLoading
        ])
      );
    

    Anayway;足够的咆哮。重要的是要理解async await实际上并没有启动另一个线程,async函数总是返回一个promise,await实际上不会阻塞或等待。它是someFn().then(result=>...,error=>...)的语法糖,看起来像:

    async someMethod = () =>
      //syntax sugar for:
      //return someFn().then(result=>...,error=>...)
      try{
        const result = await someFn();
        ...
       }catch(error){
         ...
       }
    }
    

    示例总是显示try catch,但您不需要这样做,例如:

    var alwaysReject = async () => { throw "Always returns rejected promise"; };
    alwaysReject()
    .then(
      x=>console.log("never happens, doesn't resolve")
      ,err=>console.warn("got rejected:",err)
    );
    

    抛出任何错误或await返回被拒绝的promise将导致异步函数返回被拒绝的promise(除非您尝试捕获它)。很多时候,只是让它失败并让调用者处理错误是可取的。

    当您希望承诺成功使用特殊值来拒绝承诺时,可能需要捕获错误,以便您可以在以后处理它,但承诺在技术上不会拒绝,因此将始终解决。

    一个示例是Promise.all,它接受​​一系列承诺并返回一个新的承诺,该承诺解析为一组已解析的值,或者当其中任何一个拒绝时拒绝。您可能只想获得所有承诺的结果并过滤掉被拒绝的承诺:

    const Fail = function(details){this.details=details;},
    isFail = item => (item && item.constructor)===Fail;
    Promise.all(
      urls.map(//map array of urls to array of promises that don't reject
        url =>
          fetch(url)
          .then(
            undefined,//do not handle resolve yet
            //when you handle the reject this ".then" will return
            //  a promise that RESOLVES to the value returned below (new Fail([url,err]))
            err=>new Fail([url,err])
          )
      )
    )
    .then(
      responses => {
        console.log("failed requests:");
        console.log(
          responses.filter(//only Fail type
            isFail
          )
        );
        console.log("resolved requests:");
        console.log(
          responses.filter(//anything not Fail type
            response=>!isFail(response)
          )
        );
      }
    );
    

答案 1 :(得分:2)

您的问题和评论建议您可以使用一些关于事件循环工作方式的直觉推动。它真的起初很混乱,但过了一段时间它变成了第二天性。

不考虑最终价值,而是考虑一个事实,即你有一个单独的线程并且你无法阻止它 - 所以你想要未来价值 - 下一个或未来某个事件循环的价值。您编写的非异步的所有内容几乎都会立即发生 - 函数返回某些值或未定义立即。你无能为力。当您需要异步时,需要设置一个系统,以便在将来某个时间返回时准备好处理异步值。这就是事件,回调,承诺(和异步/等待)都试图帮助的事情。如果某些数据是异步的,您只需 在同一个事件循环中使用它。

那你做什么?

如果你想要一个创建实例的模式,调用init(),然后调用一些进一步处理它的函数,你只需要设置一个系统,在数据到达时进行处理。有很多方法可以做到这一点。这是您班上变化的一种方式:



function someAsync() {
  console.log("someAsync called")
  return new Promise(resolve => {
    setTimeout(() => resolve(Math.random()), 1000)
  })
}

class Posts {
  constructor(url) {
    this.ready = false
    this.data = "uninitilized"
    this.url = url
  }
  init() {
    this.data = someAsync()

  }
  time100() {
    // it's important to return the promise here
    return this.data.then(d => d * 100)
  }
}

let p = new Posts()
p.init()
processData(p)
// called twice to illustrate point
processData(p)

async function processData(posts) {
  let p = await posts.time100()
  console.log("randomin * 100:", p)
}




init()保存从someAsync()返回的承诺。 someAsync()可以是任何返回承诺的东西。它在实例属性中保存了promise。现在您可以调用then()或使用async / await来获取值。如果promise已经解决,它将立即返回值,或者当它已经解决时它将处理它。我两次致电processData(p)只是为了说明它没有两次调用someAsync()

这只是一种模式。还有更多 - 使用事件,可观察对象,直接使用then(),甚至是不时髦的回调,但仍然有用。

答案 2 :(得分:0)

注意:无论您在何处使用await,都必须在async函数内。

查看UPDATED FIDDLE

您需要使用await myFunc()来获取getPostById所期望的值,因为异步函数始终返回一个promise。

这有时非常令人沮丧,因为整个链需要转换为async函数,但这是你将其转换为同步代码所付出的代价,我猜。我不确定这是否可以避免,但我有兴趣听取那些对此有更多经验的人的意见。

通过复制功能,然后访问finalawait final,在控制台中试用以下代码。

注意:

  

异步功能 CAN 包含await表达式。   https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function

为了甚至声明异步函数,没有必须等待的规则。 下面的示例使用异步函数,而不仅仅是为了表明异步函数总是返回一个promise。

const sample = async () => {
  return 100;
}

// sample() WILL RETURN A PROMISE AND NOT 100
// await sample() WILL RETURN 100

const init = async (num) => {
  return new Promise((resolve, reject) => {
    resolve(num);
  });
}

const myFunc = async (num) => {
  const k = await init(num);
  return k;
}

// const final = myFunc();
// final; This returns a promise
// await final; This returns the number you provided to myFunc