-
Notifications
You must be signed in to change notification settings - Fork 20.6k
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
Core: implement ready without Deferred #2850
Conversation
- Keeps it Promise-compatible Fixes jquerygh-1778
} | ||
} ); | ||
|
||
// Make jQuery.ready promise compatible (gh-1778) | ||
jQuery.ready.then = jQuery.fn.ready; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you may want to add some unit tests using a Promise lib, or just plain old native Promises. Since the implementation of jQuery.fn.ready
returns this
, and you're assigning that function to your thenable object, you're essentially getting into this scenario:
var myCrazyPromise = { then: function() { return this; };
Promise.resolve(myCrazyPromise).then(console.log.bind(console));
If you run that, you'll see that the then
handler is never invoked since this is infinitely recursive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test added, but no changes required. Notice that while jQuery.ready
is a thenable, jQuery.fn.ready
is not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, thinking about this wrong. Maybe it's only a problem pre-ready event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about it wrong (context
is still jQuery.ready
), but I don't see the problem you're describing whether it's before the ready event or not. Do you see anything wrong with my test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, blurp, I think this is a misunderstanding of Promise.resolve(thenable)
. Promise.resolve(thenable)
follows the thenable and eventually defers to its state. This means that the thenable is responsible for calling .then
. Notice that your example doesn't resolve even when you don't return this
. For example, this works fine...
var myCrazyPromise = { then: function (fn) { fn(); return this; } };
Promise.resolve(myCrazyPromise).then(function() { console.log('resolved'); });
// Promise instance resolves asynchronously
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I agree with that. Apparently I'm not as familiar with thenables as I thought.
FWIW part of my confusion may be attributed to something that seems weird in Chrome, but I have no idea if this is a legitimate thing.
If you run Promise.resolve(jQuery.ready)
then inspect the internal [[PromiseValue]]
the returned promise, you can see that the value the promise was resolved with is not what was passed to the .then
handler; the resolved value is jQuery.ready
as I had initially expected. Might be an implementation and/or devTools bug, but it doesn't look like it has any observable side effects other than confusion.
whenReady = function( fn ) { | ||
readyCallbacks.push( fn ); | ||
|
||
if ( !++readyFiring ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not the easiest to read. Would breaking it up increase the size? What size penalty would it bring to just use booleans?
I'd prefer to such "clever coding" to be performed by Uglify, not in source.
while ( readyCallbacks.length ) { | ||
fn = readyCallbacks.shift(); | ||
if ( jQuery.isFunction( fn ) ) { | ||
fn.call( document, jQuery ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the invocations are synchronous and not wrapped in a try
block, jQuery.ready
is not a conforming Promises/A+ promise. This should either include the explanatory comment from #1778 (comment) or be corrected (not that that would be sufficient, but it would prevent callback lockups).
Other than the missing comment or missing safeguards (depending on your preference), this looks good. Also, what of the deprecations from #1778 (comment) ? Have we done those?
|
} | ||
} ); | ||
|
||
// Make jQuery.ready promise compatible (gh-1778) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd update this to "promise-consumable"; it's more clear and might head off too much effort being spent on bugs like jQuery.ready.then( fn ).then(…)
not working as expected.
I'm not in favor of deprecating |
Given how code is started to look like without the
Me too, but i would wait long time before removing it, next couple majors or so.
Why? It seems it still has valid use-cases? |
I think you misread. I don't want to deprecate that.
@gibson042 pointed out those cases can be covered by promises. |
Oh, i did, sorry.
Like? |
Really? Because I contend that all semantic meaning is lost by ignoring the argument to
|
|
Agreed, but I don't see that often. I was referring to how |
Yeah, |
And what if you want other plugins that depend on |
Then those plugins would appear in |
Although there are a lot of dups here, seems like https://github.com/search?q=jquery.holdReady+true&type=Code&utf8=✓ |
That's fine. This is a deprecation, not a removal. |
User usually download their deps with builded file, which means we affect how app should be loaded and initiated, it seems we became opinionated here |
Whereas |
Buy that i mean, as i understand, you want user to push plugins load resolve and data on which those plugins operate under |
Looking at some of the usage it seems like many people have taken the action-at-a-distance behavior of |
Well, probably because we didn't use word "atrocity" but "advanced feature" in the docs :-) |
I think we agree that |
I'm not sure what you mean... any dependencies either expose their own thenable (valid as a |
Added a fix for gh-1823. |
} | ||
} | ||
} finally { | ||
readyFiring = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As coded, this still halts execution until a subsequent handler is added. But you could fix it with a setTimeout( whenReady )
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you say, this only fixes the case where ready could never be called again. I'm not sure we need to go beyond that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would certainly be more Promise-compatible to have
jQuery.ready.then( throws );
jQuery.ready.then( shouldRun );
work without waiting for a subsequent jQuery.ready.then
invocation. And fixes don't get much simpler than setTimeout( whenReady )
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we'd have to add a catch
. Wouldn't putting the whenReady
call in the finally block cause an infinite loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need a catch
, and the whenReady
doesn't go infinite because the throwing callback has already been .shift()
ed off the list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how it wouldn't be a loop. A finally is called regardless of whether an error is thrown, so it would just keep calling itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So by not having a catch, it also barfs immediately on the console right? That's a good thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It keeps calling itself, but will terminate when the callbacks list is empty: https://jsfiddle.net/ewjschzw/
So by not having a catch, it also barfs immediately on the console right? That's a good thing.
Yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops; I see what you mean @timmywil... you just need to move the check try
into the loop (I thought it was already there)readyCallbacks.length
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes, that would work.
var readyCallbacks = [], | ||
readyFiring = false, | ||
whenReady = function( fn ) { | ||
readyCallbacks.push( fn ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it help for the check for fn
being a function be done here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as you duplicate it in the redefinition of whenReady
. Doing it this way is just conceptually simpler, and it's not like the function test is expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, given that it's inexpensive, I'd want to do whichever is smaller. Right now, that would be what we currently have.
// If there was an error in a ready callback, | ||
// continue with the rest (gh-1823) | ||
if ( readyCallbacks.length ) { | ||
whenReady(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Retrying synchronously here swallows all but the last exception generated by a block of readyCallbacks
(e.g., jQuery.ready.then( thrower ); jQuery.ready.then( thrower )
): https://jsfiddle.net/ewjschzw/1/
I think a setTimeout
is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, didn't notice that.
@@ -67,4 +91,32 @@ QUnit.module( "ready" ); | |||
"Argument passed to fn in jQuery(document).ready( fn ) should be jQuery" ); | |||
} ); | |||
|
|||
QUnit.test( "Promise.resolve(jQuery.ready)", function( assert ) { | |||
assert.expect( 2 ); | |||
var done = jQuery.map( new Array( 2 ), function() { return assert.async(); } ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole point of deprecating stop() semaphore in favour of assert.async() was to discourage fragile patterns like this. The code should reflect care of which callback gets popped by which sub path. E.g. create them explicitly and have each mark its own subpath as complete.
var resolveDone = assert.async(),
promisifiedDone = assert.async();
..
promisifiedDone();
..
resolveDone();
Otherwise, at least simplify with var done = [ assert.async(), assert.async() ];
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a pattern we already use in several places. Regardless, we'll switch patterns when we update QUnit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want to somehow detect such pattern? You mean update to 2.0... or?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Krinkle assert.async
now accepts a number specifying how many times its result should get called so this is sort of blessed by upstream currently... We planned to switch to this pattern once we update QUnit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have updated QUnit to 1.20.0, so after rebase, you can start using new signature in this pull
Even though we unanimously decided to choose a different approach it's still would be cool to hear @scottgonzalez opinion. |
I don't have much to add here. I'll just say that I think |
Closing in favor of #2891 |
jQuery.ready
to avoid breakage on wikipedia.Fixes gh-1778