Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

promise, async, await, execution order #60

Closed
xianshenglu opened this issue Dec 19, 2018 · 4 comments
Closed

promise, async, await, execution order #60

xianshenglu opened this issue Dec 19, 2018 · 4 comments

Comments

@xianshenglu
Copy link
Owner

xianshenglu commented Dec 19, 2018

https://segmentfault.com/q/1010000016147496
https://www.zhihu.com/question/268007969/answer/339811998

@xianshenglu xianshenglu pinned this issue Dec 19, 2018
@xianshenglu xianshenglu added the todo Next thing to do label Dec 22, 2018
@xianshenglu xianshenglu unpinned this issue Dec 22, 2018
@xianshenglu
Copy link
Owner Author

xianshenglu commented Dec 27, 2018

async can be transformed to promise. So, if we want to understand async, we have to understand promise first.

Promise

Normally, promise is easy to understand, especially when using like this:

promise
  .then(() => {
    //
  })
  .then(() => {
    //
  })
  .then(() => {
    //
  })

then after then would make the order of async callbacks clear. Actually, we shouldn't rely on async callbacks order. But a certain execution order would make me feel more comfortable. However, sometimes, things are different.

RESOLVE() and Promise.resolve()

Normally, I initialize a promise by Promise.resolve() because it seems too troublesome to use Promise constructor like below which I called it RESOLVE() in this article.

new Promise((resolve,reject)=>{
  resolve()
})

Normally, I used it by Promise.resolve(nonThenable) which is equivalent to RESOLVE(nonThenable)

new Promise((resolve, reject) => {
  resolve(nonThenable)
})

So, it doesn't matter which one you choose. RESOLVE(nonThenable) or Promise.resolve(nonThenable).

However, when it comes to thenable, things are different. Promise.resolve(thenable) is not equivalent to RESOLVE(thenable)

new Promise((resolve, reject) => {
  resolve(thenable)
})

Some tips:

  • Promise is a subset of thenable.
  • You can ignore the related things of thenable and focus on promise instead because promise is the object we always used.
  • After you have understood the main part, you can expand your understanding to thenable.

I explained it carefully in What's the difference between resolve(promise) and resolve('non-thenable-object')?.

And here is the conclusion:

  • Promise.resolve(nonThenable) can be transformed into RESOLVE(nonThenable). They have the same effects.
  • Promise.resolve(thenable) is different from RESOLVE(thenable). They have different effects.
  • Both RESOLVE(thenable) and RESOLVE(promise) can be transformed into
 new Promise((resolve, reject) => {
      Promise.resolve().then(() => {
        thenable.then(resolve)
      })
 })
  • Promise.resolve(promise) === promise while Promise.resolve(nonPromiseThenable) can be transformed into
    new Promise(resolve => { 
       Promise.resolve().then(() => { 
         nonPromiseThenable.then(resolve) 
       }) 
     })

It's obviously that RESOLVE(promise) is not equivalent to Promise.resolve(promise). You can test it by this example:

let p1 = Promise.resolve(1)
Promise.resolve(p1).then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
})
//1
//2

while

let p1 = Promise.resolve(1)
new Promise((resolve, reject) => {
  resolve(p1)
}).then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
})
//2
//1

So, here comes another question. When would we use RESOLVE(promise)? It doesn't seem to be that common.

Well, there is some cases we have to use RESOLVE(promise).

Cases of RESOLVE(promise)

promise.then

Take a look at this demo:

Promise.resolve()
  .then(function () {
    console.log(0);
    return Promise.resolve(4);
  })
  .then(function (r) {
    console.log(r);
  });

Promise.resolve()
  .then(function () {
    console.log(1);
  })
  .then(function () {
    console.log(2);
  })
  .then(function (r) {
    console.log(3);
  })
  .then(function () {
    console.log(5);
  });

The log of above demo is 0,1,2,3,4,5. Did you get it?

According to the spec promise.prototype.then, the promise.prototype.then finally returns resultCapability.[[Promise]] which is NewPromiseCapability(C).

So, the above demo can be transformed into

Promise.resolve().then((res) => {
  console.log(0);
  new Promise((resolve) => {
    resolve(Promise.resolve(4));
  }).then(function (r) {
    console.log(r);
  });
});

Promise.resolve()
  .then(function () {
    console.log(1);
  })
  .then(function () {
    console.log(2);
  })
  .then(function (r) {
    console.log(3);
  })
  .then(function () {
    console.log(5);
  });

and finally can be

Promise.resolve().then((res) => {
  console.log(0);
  new Promise((resolve) => {
    Promise.resolve().then(() => {
      Promise.resolve(4).then(resolve);
    });
  }).then(function (r) {
    console.log(r);
  });
});

Promise.resolve()
  .then(function () {
    console.log(1);
  })
  .then(function () {
    console.log(2);
  })
  .then(function (r) {
    console.log(3);
  })
  .then(function () {
    console.log(5);
  });

In practice, if the execution order between 4 and 1,2,3,5 really matters code shouldn't be written like this. The demo above didn't express the execution order dependencies clearly.

So, this case is just for demonstration.

async and await

As we all know or spec says that the result of async returns promise. For example:

(async function(){}()).toString() //"[object Promise]"

And await can be used in async.

await

So, how does await work in async? According to spec:Await:

image

We can transform await code

const p1 = Promise.resolve(1)
const async1 = async function() {
  const res1 = await p1
  console.log(res1)
}
async1()
p1.then(() => console.log('after gen'))

to

const p1 = Promise.resolve(1)
const async1 = async function() {
  new Promise(resolve => {
    resolve(p1)
  }).then(res => {
    const res1 = res
    console.log(res1)
  })
}
async1()
p1.then(() => console.log('after gen'))

The result is the same on chrome 70:

after gen
1

However, in chrome canary 73 the former result is

1
after gen

Why?

The reason can be found in tc39/ecma262#1250. Simply say, the spec to await was going to change to:

await-spec-change

What's the difference?

The difference is exactly the difference between RESOLVE(promise) and Promise.resolve(promise).

In the past and chrome 70, we are using RESOLVE(promise), so I transformed the code like above. If using this new spec, the code should be transformed to

const p1 = Promise.resolve(1)
const async1 = async function() {
  Promise.resolve(p1).then(res => {
    const res1 = res
    console.log(res1)
  })
}
async1()
p1.then(() => console.log('after gen'))

In this case, chrome 70 and canary 73 would all get

1
after gen

That's the spec change for await. Hope you both understand the way before and after the change.

async

Now, let's talk about async. How does async work? According to spec:

desugar-async

The spawn used in the above desugaring is a call to the following algorithm. This algorithm does not need to be exposed directly as an API to user code, it is part of the semantics of async functions.

And the spawn is

function spawn (genF, self) {
  return new Promise(function (resolve, reject) {
    var gen = genF.call(self)
    function step (nextF) {
      var next
      try {
        next = nextF()
      } catch (e) {
        // finished with failure, reject the promise
        reject(e)
        return
      }
      if (next.done) {
        // finished with success, resolve the promise
        resolve(next.value)
        return
      }
      // not finished, chain off the yielded promise and `step` again
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v)
          })
        },
        function (e) {
          step(function () {
            return gen.throw(e)
          })
        }
      )
    }
    step(function () {
      return gen.next(undefined)
    })
  })
}

However, I think the spawn is the future version which doesn't apply to chrome 70 because it used Promise.resolve(next.value) instead of RESOLVE(next.value) to transform await. So, I thought the old version or version applied to chrome 70 should be

function spawn (genF, self) {
  return new Promise(function (resolve, reject) {
    var gen = genF.call(self)
    function step (nextF) {
      var next
      try {
        next = nextF()
      } catch (e) {
        // finished with failure, reject the promise
        reject(e)
        return
      }
      if (next.done) {
        // finished with success, resolve the promise
        resolve(next.value)
        return
      }
      // not finished, chain off the yielded promise and `step` again
      /* modified line */
      new Promise(resolve => resolve(next.value)).then(
        /* origin line */
        // Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v)
          })
        },
        function (e) {
          step(function () {
            return gen.throw(e)
          })
        }
      )
    }
    step(function () {
      return gen.next(undefined)
    })
  })
}

You can tested it by comparing the result of below example.

const p1 = Promise.resolve(1)
const async1 = async function () {
  const res1 = await p1
  console.log(res1)
}
async1()
p1.then(() => console.log('after gen'))

with

const p1 = Promise.resolve(1)
const gen = function* () {
  const res1 = yield p1
  console.log(res1)
}
const async1Eq = function () {
  spawn(gen, this)
}
async1Eq()
p1.then(() => console.log('after gen'))

The result would be:

  • On Chrome 70, with the former spawn, you will get the different result. While you will get the same result with the latter spawn.

  • In the same way, on chrome 73, with the former spawn, you will get the same result. While you will get the different result with the latter spawn.

Problems in async

As async return value is using a RESOLVE, so it might be a little delay when return promise in async function body. For example:

const p1 = Promise.resolve(1)
const async1 = async () => {
  return p1
}
async1().then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
}).then(res => {
  console.log(3)
})

chrome 70 ,73 all returns

2
3
1

That's correct. Because spec: Runtime Semantics: EvaluateBody uses RESOLVE in async implementation.

image

So, why not using Promise.resolve() in async implementation like await? @MayaLekova explained in tc39/ecma262#1250 ,

Deferring the implicit creation of the wrapper promise inside async functions in case we actually need to await on an asynchronous task (which excludes async functions without await or with awaits only on resolved promises) will indeed remove the performance overhead of turning a synchronous function to asynchronous. It will though introduce the possibility to create starvation, for instance if the await statement is in a loop. This possibility outweighs the performance gain IMO, so it's better not to do it.

Second, if we want to remove the extra tick for chaining native, non-patched promises (introduce "eager" chaining), this will effectively change observable behaviour for applications already shipping with native promises. Think of it as changing the behaviour of line 2 from above to be equivalent to line 3, which will actually introduce further inconsistency.

So, in the current situation, we can only transform the code above by RESOLVE.

const p1 = Promise.resolve(1)
const async1 = () => {
  return new Promise(resolve => {
    resolve(p1)
  })
}
async1().then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
}).then(res => {
  console.log(3)
})

which is equivalent to

const p1 = Promise.resolve(1)
const async1 = () => {
  return new Promise(resolve => {
    return Promise.resolve().then(() => {
      p1.then(resolve)
    })
  })
}
async1().then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
}).then(res => {
  console.log(3)
})

So, if implementation or spec of async doesn't change, we may need to avoid return promise in async expect you really know what's happening.

In the case above, if you really want to avoid the delay, you can avoid using async. You can do it by:

const p1 = Promise.resolve(1)
const async1 = () => {
  return p1
}
async1().then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
}).then(res => {
  console.log(3)
})

which can be written to

const p1 = Promise.resolve(1)
p1.then(res => {
  console.log(res)
})
p1.then(res => {
  console.log(2)
}).then(res => {
  console.log(3)
})

Conclusion

I hope you can understand async, await and promise execution order now. When talking about these words, the most important thing is to figure out which resolve it should use.

RESOLVE or Promise.resolve()? RESOLVE(promise) is different from Promise.resolve(promise)

@xianshenglu xianshenglu changed the title async, promise order promise, async, await, execution order Dec 27, 2018
@xianshenglu xianshenglu added brewing Wait for more idea or experience and removed todo Next thing to do labels Dec 28, 2018
@xianshenglu xianshenglu reopened this Dec 28, 2018
@xianshenglu xianshenglu added todo Next thing to do and removed brewing Wait for more idea or experience todo Next thing to do labels Dec 28, 2018
@xianshenglu xianshenglu removed the todo Next thing to do label Jan 25, 2019
@xianshenglu
Copy link
Owner Author

xianshenglu commented Aug 13, 2019

(async function(){}()).toString() // [object Promise]
是否是碰巧呢?
我记得用法是Object.prototype.toString.call(变量)啊,还是说promise类型根本都没实现自己的toString啊???
卧槽,刚才试了一下,确实Promise.protoType不存在toString.....学习了。。。

Actually, it took me three days to read the specification. -_-||| @ZhaoTim

@yh284914425
Copy link

有没有中文版的呀,看着好累

@xianshenglu
Copy link
Owner Author

@yh284914425 I didn't write it in Chinese. Maybe someone has.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants