Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ksenzee’s picture

FileSize
6.02 KB

At the Drupal 8 JavaScript BoF in Chicago, we talked about how to develop JS patterns we can document and promote, and we decided we'd develop them gradually as we clean up the existing JS in core. So I'm documenting what I did here and why I thought it was a good idea. Please feel free to disagree with me. This kind of thing is notoriously intuitive -- a certain pattern just feels right or wrong, and it's hard to explain why. I'm hoping we can find good data to base our decisions on.

1. I'm suggesting a change to the documentation for Drupal.attachBehaviors. The example code passes an anonymous inline function to jQuery.once, and that's a style I'd love to move away from where we reasonably can. Inline functions are difficult to document, and they make the code flow harder to follow (unless you're a functional programmer, which most of us aren't). This one is also anonymous - it's not even function atLeastIHaveAName () {} - which makes the code harder to debug. As it happens, there's no real need to pass a function to jQuery.once in this example; it's just as easy to check whether any elements were returned, and act on them if so.

2. For Drupal.attachBehaviors and Drupal.detachBehaviors, I replaced jQuery.each and another anonymous inline function with a for-in loop. Function-based iteration is the slowest type there is; for-in loops are a little faster. Since this code runs on every page load and every AJAX response, any performance gain would be good. (Regular for loops are even faster, btw - up to eight times faster than jQuery.each - so anywhere we can replace jQuery.each with a for loop, it's a performance win.)

3. In Drupal.checkPlain, we were using an object as a hash, which meant we had to use a for-in loop. I changed the data structure to an array, which means we can use the much faster for loop.

4. We were using the $() shortcut syntax for $(document).ready(), which I think is harder to read. To me the few extra characters are worth the extra maintainability, especially since we don't use $(document).ready() very often.

5. We were using an anonymous function to add to the jQuery.support object. I added it to the Drupal namespace instead. In general I'd like to see all functions living somewhere in the Drupal namespace. Some blessed day we'll get API module parsing our JS, and I'm sure it would appreciate it if our functions all have unique names.

My main concern at this point is whether I've broken anything in all this refactoring. I've tested each function in the Firebug console and it all works for me, but we desperately need unit tests. It looks like http://drupal.org/project/qunit already has some; I'm going to look into that and see if I can help move the effort along.

ksenzee’s picture

Status: Active » Needs review
ksenzee’s picture

FWIW, the existing unit tests from the qunit module needed updating, but once I updated them to pass on HEAD, they passed with this patch also.

mfer’s picture

On an initial read through the changes look good. Nice work. I'll need to apply them, test them, and see if there is any more we can do. Adding this issue to my todo list.

mfer’s picture

FileSize
7.73 KB

I reviewed and made some updates. Some things I imagine we'll want to discuss.

  • Moved all vars to a single var statement at the top of the function.
  • Rewrote checkPlain to remove the loop. This provides for significant performance improvements. See http://jsperf.com/check-plain
  • Updated our wrapper closure to do dependency injection rather than globals. This should help the file be smaller when minified along with some other good benefits.
  • Global functions are really methods on the global object. Rather than using foo() updated to use window.foo(). This is the same pattern that Dojo, jQuery, etc use.

Thoughts?

pascalduez’s picture

Hi,

this is really exciting to see efforts in pushing Drupal js into a more modern and robust way, and I would definitely like to give time for it.

I applied the patch in #5, and after some testing, no error / misbehavior so far.
Both patch in #1 and #5 looks really good improvements, compliments.

- Patch in #1
in Drupal.behaviors the function type test is gone, is it intended ?

$.isFunction(this.attach)
$.isFunction(this.detach)

A couple of quick ideas for the next steps :

- Should we advocate on the use of strict comparison operators everywhere ? There's some discussion around it in the js community.
- As there might be more and more functions / utilities added the the Drupal object, leading toward a sort of small framework, should we compare the different module patterns / inheritance APIs ?

mfer’s picture

@opalescent You bring up some good points so let me try to address them.

First, the removal of the use of $.isFunction. While I cannot speak for ksenzee, I am not sure I see a reason for isFunction here. There is nothing in the API that would put anything but a function on Drupal.behaviors.example.attach/detach. If someone did so it would be considered a bug in that code so throwing a JS error is OK by me. And, since we are making sure only properties on the behaviors are being looked at (via hasOwnProperty()) outside code should not slip in to make sure we avoid.

Second, the comparison operators. There is not complete agreement on the best approach. I did a quick analysis of jQuery, YUI, and dojo on this. Seems jQuery uses ===/!== for everything but comparisons with null where == is used. Dojo uses == regularly for comparisons. YUI intermixes the use of == and ===. I'm not yet entirely sure of what we should do other than to suggest we default to === and fall back to == when it makes sense to.

Third, the patterns. This issue and the set we are working on right now are to clean up the existing code. In the JS community initiative we've separated cleanup from changing the architecture for some better patterns. So, yes we need some better patterns. This is just not the issue to hash it out. We don't want to put too many changes in one patch/issue and the patterns one will require a bit of research, discussion, and finding agreement. So, it's separated out.

mfer’s picture

Another possible change we can make is to move _enhancejQuerySupport() off of the Drupal object into a private var in the closure. It used strictly for setup code, which should not be bassed a second time. Thoughts?

pascalduez’s picture

Thanks for clearing up all those points. I have to say I completely agree with you on them.

The removal of the use of $.isFunction, totally make sense to me too, we shouldn't write code that think in place of the end user, but rather transfer some responsibilities to him, and bring the tools, documentation to achieve it.

Comparison operators, I'll try to bring good infos sources about it, so we can consider the arguments for or against. But I like your suggestion. Use the strict as a basis, but stay flexible to use cases.

The patterns. Sorry, I did not wanted to rush things. I had read your good blog posts, and also the initiative page, but it did not came in mind last time, was too late maybe...
I can only say, thank you for writing such a clear and smart working plan.

Regarding _enhancejQuerySupport(): once again, make sense, no need to have it populate the Drupal object while it's called only once.

mfer’s picture

Status: Needs review » Needs work

@opalescent I was thinking the exact same thing about _enhancejQuerySupport()

mfer’s picture

Status: Needs work » Needs review
FileSize
8.82 KB

This patch cleans up some ==/===, moves enhancejQuerySupport() off the Drupal object (I imagine this might cause some discussion), and uses a DOM element to figure out positionFixed (which is faster than using a jQuery object). This is in addition to the previous changes.

Anyone up for testing/reviewing?

pascalduez’s picture

Patch in #11 applied and tested (manually) and everything's fine in the browsers I tested in (FF, Chrome, Safari, Opera). I'll do some IE later on.

I'm particularly keen on the changes executed, so no critics from me.

Rewriting the enhancejQuerySupport() with native DOM elements/functions is what I've been planning to as well, so you was faster.
On question, although it seem to work nicely, the element is not injected inside the body element, which is not ready at the function execution time, but rather in between html and head. Just wondering whether this could be a concern or not. Does not seem so.
http://kangax.github.com/cft/#IS_POSITION_FIXED_SUPPORTED
https://gist.github.com/362170#L32

mfer’s picture

Looking at what paul created, we should hide our element and account for page scroll. I'm wondering if enhancejQuerySupport() is now way more difficult to debug and understand. For the performance improvement I wonder if its worth it?

ksenzee’s picture

I finally got time to review this. Sorry for the delay.

#5-1: Yes, moving var declarations to the top makes sense, since JS does var hoisting anyway. I wasn't going to be strict about it but sure, works for me.

#5-2: The check_plain() improvements look good.

#5-3: I'm not a huge fan of the file-level closure pattern anyway. It's bad DX and it's never commented, so nobody really understands what it's doing unless they get functional programming to a certain degree. So they just copy and paste and grumble. But I won't argue about it, since theoretically someday we'll be minimizing our code and it'll help there. Maybe we could come up with a way of documenting it clearly and add a comment with a link?

#5-4: Yes, window.foo() is a good idea. In general I'm a fan of anything that helps people new to JS figure out what's actually going on.

#6/7: Yes, removing isFunction() was deliberate. If somebody adds something that isn't a function, the browser should choke on it. Otherwise we're hiding people's errors from them, which is maddening when you can't figure out why your code won't run.

#8: This is where it gets into a philosophical discussion. I would very much prefer not to have private functions and variables hidden inside file-level closures. What if someone wants to change the enhancejQuerySupport function? In general private variables feel quite unDrupalish to me. We tend more toward letting people override things. Since JS has built-in mechanisms for that, I'd rather make it possible for people to use them.

#13: The changes to enhancejQuerySupport seem a little beyond the scope of cleanup. It seems a little dicey to me that we're suddenly running the function as soon as the script loads, instead of waiting for the DOM to be ready. I'm wondering if we could leave that for a separate issue with its own discussion.

pascalduez’s picture

Tagging...

ericduran’s picture

Status: Needs review » Needs work
+++ misc/drupal.jsundefined
@@ -43,18 +43,20 @@ jQuery.noConflict();
+    if (behaviors.hasOwnProperty(i) && behaviors[i].attach) {
+      behaviors[i].attach(context, settings);

There might be a lot of behaviors in a given drupal site. Does it make sense to cache the hasOwnProperty lookup.

Something like this:

Drupal.attachBehaviors = function (context, settings, trigger) {
  var behaviors = Drupal.behaviors,
        hasOwnProp = Object.prototype.hasOwnProperty,
        i;
    ....
   for (i in behaviors) {
    if (hasOwnProp.call(behaviors, i) && behaviors[i].attach) {
      .....
     }
  }
 };

This isn't an actual review I was just looking at the patch and that caught my attention.

ksenzee’s picture

I agree #16 is a good idea; two-deep object lookup like that is slow. Technically it's also possible for someone to override Object.prototype in a way that behaviors.hasOwnProperty() will quit working, although that's really farfetched in a Drupal context. :)

nod_’s picture

#16 not really, generally using .call hurts a more than a method lookup : http://jsperf.com/object-hasownproperty-vs-hasownproperty/2 needs some IE testing though.

I'm not sure about the function check: $.isFunction has to go I agree, changing that to typeof could be a good middle ground, it's a pretty critical part, it doesn't hurt to protect users against themselves. It's always possible to throw an exception or something too.

The $.support thing should just go away, it's buggy #1439462: Drop our custom $.support.positionFixed and use jQuery's $.support.fixedPosition.

JohnAlbin’s picture

Status: Needs work » Needs review
FileSize
8.38 KB

The patch in #11 is extremely stale, so I rerolled. A good chunk of it was already implemented with drupal.js’s new Drupal.formatString().

#13: The changes to enhancejQuerySupport seem a little beyond the scope of cleanup. It seems a little dicey to me that we're suddenly running the function as soon as the script loads, instead of waiting for the DOM to be ready. I'm wondering if we could leave that for a separate issue with its own discussion.

Unless I'm misreading this code:

$(function () {
  /**
   * Boolean indicating whether or not position:fixed is supported.
   */
  if (jQuery.support.positionFixed === undefined) {
    var el = $('<div style="position:fixed; top:10px" />').appendTo(document.body);
    jQuery.support.positionFixed = el[0].offsetTop === 10;
    el.remove();
  }
});

We are already running that function as soon as possible. Note that it isn't wrapped in $(document).ready(). mfer's cleanup just puts the enhancejQuerySupport name around it and cleans it up.

#5-3: I'm not a huge fan of the file-level closure pattern anyway. It's bad DX and it's never commented, so nobody really understands what it's doing unless they get functional programming to a certain degree. So they just copy and paste and grumble. But I won't argue about it, since theoretically someday we'll be minimizing our code and it'll help there. Maybe we could come up with a way of documenting it clearly and add a comment with a link?

I never really understood the (function ($) { // stuff })(jQuery) stuff until I read JavaScript Module Pattern: In-Depth. If some bit of code is the right thing to do, but hard to understand, then we need some code commenting. I think http://drupal.org/node/224333#javascript_compatibility could use some improvement, for example.

So, let's add some code comments.

Here's the rerolled patch in #11 without code comments.

JohnAlbin’s picture

I just updated http://drupal.org/update/modules/6/7#javascript_compatibility to read:

JavaScript should be compatible with libraries other than jQuery

(issue). JavaScript should be made compatible with libraries other than jQuery by adding a small wrapper around your existing code:

(function ($, Drupal, window, document, undefined) {
  // Original JavaScript code.
})(jQuery, Drupal, this, this.document);

This wrapper is an anonymous closure that provides state throughout the page's lifetime and ensures your code does not unintentionally create/override global variables.

It also explicitly imports the global jQuery variable so that your code can use the local $ variable instead of the jQuery global. This is essential because Drupal loads jQuery with noConflict() compatibility so the jQuery library does not setup the normal $ as a global variable.

The wrapper above also explicitly loads the Drupal, this, and this.document globals so that they can be referenced as Drupal, window, and document, respectively. It is good JavaScript coding practice to explicitly list the globals your code is relying on in this way.

A detailed explanation of this JavaScript pattern can be found in the article JavaScript Module Pattern: In-Depth.

[Edit: I reverted a part of that D6->D7 update since its not actually how D7's code looks. :-( But I moved that exact language to a D8 change record. http://drupal.org/node/1446420 ]

JohnAlbin’s picture

I was going to add a link to the 6/7 upgrade, but that seems odd for D8 change. So I added a change record. http://drupal.org/node/1446420

Here's the patch with the code comment.

nod_’s picture

Thanks for the reroll and all the documentation :D

Couple of things now that it's easy to look at the diff :)
There are changes that should be made globally, not just in drupal.js: #1428534: Use === and !==, #1428524: Replace all $.each() with filtered for loop and the way vars are declared. There are issues for the first two, for the last I guess we have to decide if we what that everywhere or not and if we use only one var on top or allow several var statements.

Ideally I'd go with one var but that's a very easy way to have leaking vars, it's easy to put a ";" instead of "," during the declaration. Since it's dangerous I'd say we go with 1 var per line and multiple variable declaration per line allowed.

The position fixed support addition is dead to me, i'd be happy if that was removed #1439462: Drop our custom $.support.positionFixed and use jQuery's $.support.fixedPosition.

For me this patch should be reduced to doc changes, function check in attachBehaviors, closure definition and check plain perf optimization. Good news is that we all agree on what should be changed :D

nod_’s picture

nod_’s picture

reroll, I left out the === change because it's in another issue linked above.

sun’s picture

Status: Needs review » Needs work
+++ b/core/misc/drupal.js
@@ -3,7 +3,9 @@ var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'locale': {} };
+// JavaScript should be made compatible with libraries other than jQuery by
+// wrapping it in an anonymous closure. See http://drupal.org/node/1446420

Let's remove the See and URL. This does not really need further explanation, and people who want to find out why it exists can grep/pickaxe the commit history.

I'm fine with the remainder of the comment, as long as it is added in drupal.js only.

+++ b/core/misc/drupal.js
@@ -14,25 +16,26 @@ jQuery.noConflict();
- *        ...
+ *        …

@@ -263,7 +270,7 @@ Drupal.formatPlural = function (count, singular, plural, args, options) {
- * @param ...
+ * @param …

Can we revert these, please? We don't use typographic ellipses anywhere else in Drupal's source code base (except of human-readable strings being output).

nod_’s picture

Status: Needs work » Needs review
FileSize
5.88 KB

thanks, reroll

nod_’s picture

woops, forgot the See+url

sun’s picture

Title: clean up drupal.js » Clean up drupal.js
Status: Needs review » Reviewed & tested by the community

Awesome, thank you!

I wanted to mention we need a change notice for this, but the existing https://drupal.org/node/1446420 has been changed and referenced to this issue. I'm not sure whether that's wise. There's tons of JS code written for D7 already, and while the change notification is no functional change, I can see quite some confusion popping up in contrib (à la bug reports along the lines of "Improper JavaScript closure")...

catch’s picture

Title: Clean up drupal.js » Change notification for: Clean up drupal.js
Status: Reviewed & tested by the community » Active

This looks great. I'm going to stick it in the change notification queue just in case, couldn't 100% parse #28.

Committed/pushed to 8.x.

nod_’s picture

don't really get #28 either, change notif seems clear enough to me, can we close this?

nod_’s picture

Status: Active » Fixed

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

nod_’s picture

Title: Change notification for: Clean up drupal.js » Clean up drupal.js