marijnh / Eloquent-JavaScript

The sources for the Eloquent JavaScript book
https://eloquentjavascript.net
3.01k stars 795 forks source link

Ch 11, Promises section: handler function #399

Closed dhollinden closed 4 years ago

dhollinden commented 6 years ago

In this paragraph:

"But that’s not all the then method does. It returns another promise, which resolves to the value that the handler function returns or, if that returns a promise, waits for that promise and then resolves to its result."

There's not been any mention of handlers in the Promises section yet. Does "the handler function" in the above refer to the callback function registered by the then method?

Thanks

MaddyGit commented 5 years ago

then method takes two parameters in form of functions .then( resolve(), reject() ) Now we assume that the Promise was fulfilled and the resolve() function was called and it returned a value say 'resolveValue', Now resolveValue would be wrapped in a promise regardless of whatever type it is. Now if resolveValue is a primitive then it becomes the value of it's wrapping Promise and if resolveValue is another Promise then automatically it's resolution/rejection will be waited and upon resolution/rejection this Promise's reject/resolve callbacks would be called accordingly.

amine177 commented 5 years ago

The handler is the then() itself in that context.

aluminum1 commented 4 years ago

I have a slightly different query, which is closely related to the original question by dhollinden. Two paragraphs later, it says:

To create a promise, you can use Promise as a constructor. It has a somewhat odd interface - the constructor expects a function as argument, which it immediately calls, passing it a function that it can use to resolve the promise.

(Italics added)

This confused me for a long time. How can the constructor pass a function resolve which resolves the promise, since we haven't yet defined the function resolve? What I think happens is that the constructor takes your code, and passes it a dummy resolve function. This dummy version of resolve simply creates a log of how the resolve function was called in your code, and it stores that in the state of the Promise instance. (Unfortunately, it seems that this state is internal data and not accessible to us.) Then, later on, when you call the then method of your Promise instance and supply it with an actual resolve function, it takes the "log" that it stored, and uses it to call the actual resolve function in the correct way.

jacekkopecky commented 4 years ago

There's no need to call the resolve function a "dummy" one. The purpose of the resolve and reject functions given to your code is simply to record, whenever that happens, what the promise resolves to, and if any .then() callbacks are already set up, to trigger that they will be called, but asynchronously.

If your code calls resolve immediately, the passed value goes in the internal state of the new promise, the same way as if your code called resolve later, on timeout or on some event.

When a promise gets resolved, if it already has some callbacks registered with .then(), it will call them, but not immediately – instead it will do that in something called a microtask when all other code runs to completion.

This can be shown with a promise that resolves on timeout:

console.log('before calling new Promise()');
const p = new Promise((resolve, reject) => {
  console.log('inside new Promise(), before setting timeout');
  setTimeout(() => {
    console.log('in timeout, before calling resolve');
    resolve(42);
    console.log('in timeout, after calling resolve');
  }, 100);
  console.log('inside new Promise(), after setting timeout');
});
console.log('after calling new Promise(), before setting up then()');

p.then(val => {
  console.log('inside then() callback, got value ' + val);
});
console.log('after setting up then() callback');

We get output like this:

before calling new Promise()
inside new Promise(), before setting timeout
inside new Promise(), after setting timeout
after calling new Promise(), before setting up then()
after setting up then() callback
in timeout, before calling resolve
in timeout, after calling resolve
inside then() callback, got value 42

First all top-level code and all code inside the new Promise() constructor runs: we set up the timeout and we set up the then callback. Later, the timeout times out and the whole inside of that happens, which calls resolve(); and only after all that has finished, the .then() callback is called.

If a promise is already resolved when you register a new callback with .then(), that callback also gets called in a microtask after your code runs to completion.

Example:

console.log('before calling new Promise()');
const p = new Promise((resolve, reject) => {
  console.log('inside new Promise(), before calling resolve()');
  resolve(42);
  console.log('inside new Promise(), after calling resolve()');
});
console.log('after calling new Promise(), before setting up then()');

p.then(val => {
  console.log('inside then() callback, got value ' + val);
});
console.log('after setting up then() callback');

This gives the following output:

before calling new Promise()
inside new Promise(), before calling resolve()
inside new Promise(), after calling resolve()
after calling new Promise(), before setting up then()
after setting up then() callback
inside then() callback, got value 42

The last two lines show that the .then() callback gets called later, after the code that sets up that callback runs to completion.

Hope this helps; promises are really complicated.

jacekkopecky commented 4 years ago

To add, of course you can have multiple callbacks set up with .then():

console.log('before calling new Promise()');
const p = new Promise((resolve, reject) => {
  console.log('inside new Promise(), before calling resolve()');
  resolve(42);
  console.log('inside new Promise(), after calling resolve()');
});
console.log('after calling new Promise(), before setting up then() callbacks');

p.then(val => {
  console.log('inside then() callback, got value ' + val);
});
p.then(val => {
  console.log('inside second then() callback, got value ' + val);
});
console.log('after setting up both then() callbacks');

Which outputs

before calling new Promise()
inside new Promise(), before calling resolve()
inside new Promise(), after calling resolve()
after calling new Promise(), before setting up then() callbacks
after setting up both then() callbacks
inside then() callback, got value 42
inside second then() callback, got value 42

Try the same with the timeout example.

aluminum1 commented 4 years ago

This does help - thanks. But I would like a bit more explanation for why you say that the engine does not make a "dummy resolve function" when the constructor of the Promise is first called.

Consider your second example:

console.log('before calling new Promise()');
const p = new Promise((resolve, reject) => {
  console.log('inside new Promise(), before calling resolve()');
  resolve(42);
  console.log('inside new Promise(), after calling resolve()');
});
console.log('after calling new Promise(), before setting up then()');

p.then(val => {
  console.log('inside then() callback, got value ' + val);
});
console.log('after setting up then() callback');

If you run the Chrome debugger on this example, and step line by line through the code, a "miracle" occurs. The debugger merrily executes the line

resolve(42)  // <--- it executes this, no problem

with absolutely no qualms, and simply continues to the next line.

How can that be, when the resolve function has not yet been defined?

In normal execution, trying to execute a function which has not been defined yet would throw an error.

I don't see that you directly answered this question. The only answer I can suggest is my original one. Namely, that when the Chrome V8 engine encounters the line

resolve(42)  

it stores on the internal state of the promise (which we can't see) some memo to itself which says: "the code wanted to call the resolve function with argument 42".

Then, much later in your code, when you finally define the resolve function to be some explicit function f via the then() method, eg.

p.then(f);

the V8 engine will now call f with the argument that it recorded in the memo it wrote to itself earlier, that is, it will execute f(42). Of course, that execution only happens once the engine has finished executing all your code. That is, the call f(42) is put on the stack and only executed at the very end, as you explained.

jacekkopecky commented 4 years ago

Ah, I think I see. When we say p.then(f), I wouldn't say f is the resolve function, rather a callback for when p resolves, or equivalently a function that should be called once with the value of p.

It is an interesting angle to see resolve(value) as calling any (even future) .then() callbacks, but I don't think it matches my understanding of program flow most of the time.

Consider this code:

let x=42;
//… some more code
f(x);

I wouldn't think of the line let x=42 as calling function f; in this way, resolve(42) to me feels more like storing a value than like calling further code.

aluminum1 commented 4 years ago

Ok - but that means you are agreeing with me. We are in agreement that when the browser engine (eg V8) executes the code

resolve(42)  // <-- it executes this happily, before resolve is defined

it is storing a value for later use. In other words, before the real resolve function is provided some time later (by calling the .this method), the V8 engine is using a dummy version of resolve. This dummy version has the following pseudocode definition

function resolve(value) {
//   Store a log of value on the stack. One day, when the programmer supplies
//   me with an actual function realResolve by using the .this method, I will wait
//  until the execution thread is finished, and then I will  execute the code 
//   realResolve(value).

}
jacekkopecky commented 4 years ago

Hi, thanks for continuing this – it's very interesting to try to come to the source of our different understanding.

In the above, we agree on what happens, but not on how things should be named. I still don't think that there is a dummy and real resolve. It would be like describing this:

let x=42;
//… some more code
f(x);

as using a dummy function dummyF before the code provides a real function (here f) by calling something with x as a parameter.

To me, resolve() is always real, calling .then() does not provide a real resolve. resolve() is like the assignment operator, not like a calling a function.

Maybe this might be a good example:

const p = new Promise(…); // create a promise like we've done above
p.then(f);
p.then(g);
p.then(h);

All of f, g, and h will be called when p has a value. Would you call all of them the real resolve?

aluminum1 commented 4 years ago

Hi Jack - yes, let's keep going. Thank you for continuing civilly.

Let's backtrack a bit. I want to make sure we are on the same starting page. I'm not sure you understand just how strange promises are. Let me return to the code you wrote. I have removed the .this call as I think it confuses the issue.

function executor(resolveFunc) {
    console.log('inside executor, before calling resolveFunc');
    resolveFunc(42);
    console.log('inside executor, after calling resolveFunc!');
}

let P = new Promise(executor);

Remarkably, this is perfectly acceptable code to the engine, even though we never define the resolveFunc function. What happens when we execute it? It is much more instructive to execute it line by line using a debugger. What happens has to be seen to be believed! Anyway, we get the following output:

inside executor, before calling resolveFunc
inside executor, after calling resolveFunc!

Do you accept that this is incredibly strange? When the engine executes the line

let P = new Promise(executor);

it doesn't just call the constructor of the Promise class (which is what you seemed to think it did). That constructor also immediately calls the executor function! That's what Marijn says in his book, and it is true. It has to be seen to be believed! This means, that the engine merrily executes this code:

    console.log('inside executor, before calling resolveFunc');
    resolveFunc(42);  // <---- WOW! It happily calls an undefined function!
    console.log('inside executor, after calling resolveFunc!');

without having a defintion of resolveFunc!!! If you don't believe me, step through it with the debugger. It executes that line, no problem. I'm not sure you appreciate that. Are we on the same page? Do you agree it is very, very weird?

The only way to explain what the engine does here is that when the Promise constructor function calls the executor function, it gives it a dummy function of resolveFunc, as I explained above. There is no other way to explain it.

In fact, I believe my interpretation of what is happening is consistent with what it says on the Mozilla docs. The documentation is a bit confusing, but it says at one point:

At the time when the constructor generates the new promiseObj, it also generates a corresponding pair of functions for resolutionFunc and rejectionFunc; these are "tethered" to the promiseObj.

These "generated" functions are exactly the "dummy" functions I was talking about.

marijnh commented 4 years ago

There's really nothing magical about these functions. Does looking at a something like this simplified promise implementation help clear things up?

(Edit: link I posted originally doesn't actually use this constructor format.)

marijnh commented 4 years ago

To answer the original question in this issue, which I for some reason hadn't yet, yes, it does refer to that function.

aluminum1 commented 4 years ago

Does looking at a something like this [simplified promise implementation](simplified promise implementation) help clear things up?

Thanks for this. I will try to work through it. I've seen other implementations, but they didn't address the issues that were confusing me, but this one seems to do it somehow.