0

I want to invoke jquery.animate directly to change the effect of a div, but found it doesn't have any effect.

Instead, I need to put it inside a setTimeout(..., 0) to make it work.

I wonder why do I need to do this, and is it the best approach?

Live demo

http://jsbin.com/docahu/2/edit

Or here:

var FooView = Backbone.View.extend({
  id: 'foo',
});

var BarView = Backbone.View.extend({
  
  render: function() {
    $("#foo").animate({width: '200px'}); 

    // !!! HERE !!!
    setTimeout(function() {
       $("#foo").animate({height: '100px'}); 
    }, 0);

    return this;
  }
  
});


var fooView = new FooView();
var barView = new BarView();

var combinedView = $(fooView.render().el).append(barView.render().el);

$(document.body).append(combinedView);
#foo {
  width: 50px;
  height: 50px;
  background-color: blue;
  color: white;
}
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Jquery animate delay problem in backbone render" />
<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//jashkenas.github.io/underscore/underscore-min.js"></script>
<script src="//jashkenas.github.io/backbone/backbone-min.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  
</body>
</html>

You can see height is changed but the width is not.


PS:

Also I found $(document).ready() is also working:

$(document).ready(function() {
   $("#foo").animate({height: '100px'}); 
});

Which one is better to use?

Freewind
  • 193,756
  • 157
  • 432
  • 708

4 Answers4

2

It's because it's trying to animate the width before the element is in the DOM. If you put a selector in that position, you'll probably find it's not getting anything.

Doing a timeout of 0 (so javascript finishes rendering the things THEN tries the animation) or waiting for the document to finish rendering fixes that

Things happen in this order:

  1. You render your view, but it's unattached to the DOM
  2. the width animation runs. Nothing happens because '#foo' isn't on the DOM.
  3. you attach it to the dom.
  4. your height animation runs. It works because '#foo' is in the DOM.
Jeff Ling
  • 1,032
  • 9
  • 12
  • It's because it lets javascript finish running the render functions and appending to DOM, THEN it does the animation. Added to answer. – Jeff Ling Dec 20 '14 at 10:01
1

Well seems like it works by chance. The reason the first one is not working is probably because the object are still not loaded on the screen. The second one is working because after the timer was dispatched and ended (this does not really take 0 time) the page was loaded by another thread on the computer. So the overhead of creating the timer and calling back the procedure is apparently enough to finish loading the page.

You should use $(document).ready to make sure it is always called after the document is fully loaded, because like I said, it is now working by chance, and a different browser\machine may not run any of the two (or both).

Background: JavaScript starts getting executed while the page is loaded, and the DOM is build at the same time (just at the time the HTML and JavaScript text is downloaded). So if you reference DOM objects from JavaScript code like you are doing now, you get a race condition where the outcome is not defined. To avoid that there is the $(document).ready callback.

Edit

See this question. Also the Udacity course is really cool to understand what is going on.

Community
  • 1
  • 1
Ramzi Khahil
  • 4,932
  • 4
  • 35
  • 69
  • Sorry I just realised I've used `$(document).ready(...)` removed `setTimeout`, now fix it – Freewind Dec 20 '14 at 10:08
  • body is empty so `ready` will be over before `render` even starts – charlietfl Dec 20 '14 at 10:09
  • I agree in principle with @charlietfl. Even if the page were not empty, and even as you say @Kahil, that JS begins to execute on `pageLoad`, I don't see how putting the `jQuery.animate` on the `$(document).ready` avoids a potential race condition. For example, the view that he is rendering, a future view were the OP wants the same functionality, may be rendered at any arbitrary time after the DOM is ready. Putting the jQuery listener may answer the OP's question but raise other problems. – seebiscuit Dec 21 '14 at 02:57
  • I disagree. In case the asker did not want to load the div right away s/he would point that out in the question. While all you said about the threading is true, it is not relevant to the question in my eyes (even though I considered adding it to the background). Any way, suggesting the setTimeout with 0 cannot solve the problem, because different threads execute the JS and load the HTML. So even if one thread does the JS, you know nothing about the state of the HTML. That is why the `ready` was invented in the first place. I will come back to you with some references. – Ramzi Khahil Dec 21 '14 at 07:28
  • For now I can reference you to [this](http://api.jquery.com/ready/). But I have something better explaining the process. I just have to find it again... – Ramzi Khahil Dec 21 '14 at 07:31
  • @Kahil First of all, thank you for the reference to the the articles. Good read. I did a little more research into this question and I found that we're essentially both right! Why? Because the way JSBin works, look for the console.logs here http://jsbin.com/bukekotodo/2/edit, and toggle the test variable at the end to see the difference between simply placing the `animate` last or using `$(document).ready`. Also, please see my updated answer. – seebiscuit Dec 21 '14 at 21:36
0

Updated

TL;DR

Using $(document).ready is equivalent to placing the JavaScript at the end of the document…

JSBin (which is were the OP posted his sample code) will execute the JavaScript after all the HTML elements render, but before the $(document).ready event. Binding the jQuery.animate in $(document).ready is the same as firing it anytime after the view is appended to the DOM.

The simple and stable solution is to simply invoke

$("#foo").animate({width: '200px'});

on the last line of the OP's code. (Read the end of the Answer to see a more formal way of binding the jQuery.animate function)

To answer the OP's original quesiton: setTimeout() works in your case because of the way async functions are queue in the JavaScript runtime. If the <body> has already been loaded, then using setTimeout(0) the way the OP does, will have the same effect as placing the animation binding in $(document).ready.


Why setTimeout(0) works

The first thing to understand is that JavaScript is not a multi-threaded framework. While you certainly can invoke non-synchronous functions, asynchronous, async functions don't actually run parallel to the synchronous operations. Instead, async functions are queued to run as soon as the runtime is free.

For example, take the following three synchronous functions.

function1();
function2();
function3();

As you'd expect function1will fire first, followed, in order, byfunction1, function2thenfunction3. However, if I place function1` in an asynchronous call

setTimeout(function1,0);
function2();
function3();

then function1 will be placed on a queue, leaving function2 and then function3 to fire. As soon as the event loop is finished function1 is invoked. That is, it fires last! You can see this in action in this fiddle.

In the OP's example, setTimeout(function() { $("#foo").animate({height: '100px'});}, 0); was fired immediately after the runtime executed $(document.body).append(combinedView); and so jQuery was able to find the #foo element, so technically this is a correct way to do what the OP wants. This is true because of the way JSBin works. That is, it loads the JavaScript from its JavaScript Module after the DOM has loaded (but before the $(document).ready event).

Edited:

Do not use the $(document).ready function...in general

I think there's some confusion regarding how $(document).ready function solves the OP's problem. Most of the confusion probably stems from the complexity of how different web page elements (HTML, CSS, JavaScript) affect the rendering of the DOM.

There is a main parsing and rendering thread used by browsers. This is where your HTML is processed, your CSS stylesheets are fetched and parsed, and your JavaScript is fetched/parsed. All of these operations are executed as they are encountered and will be blocking (unless async or defer is specified in your <link>/<script> tags).

The order in which you pace all of these tags is essential. If your script tag is written at the top of your document (say within the <head> tag) it will be executed before any HTML is injected into the DOM.

In essence, using $(document).ready is equivalent to placing the JavaScript at the end of the document… Since JSBin (which is were the OP posted his sample code) will execute the JavaScript after all the HTML elements render, but before the $(document).ready function, binding the jQuery.animate in $(document).ready is the same as firing it anytime after the view is appended to the DOM.

Instead, the simple and stable solution is to simply invoke

$("#foo").animate({width: '200px'});

after both the fooView and the barView have been attached to the DOM. Or more formally:

var BarView = Backbone.View.extend({
  
  render: function() {
    // process your html here

    return this;
  }

  bindTransitions: function {
    $("#foo").animate({width: '200px', height: '100px'}); 
  }
  
});


var fooView = new FooView();
var barView = new BarView();

var combinedView = $(fooView.render().el).append(barView.render().el);

$(document.body).append(combinedView);

barView.bindTransitions();
Community
  • 1
  • 1
seebiscuit
  • 4,905
  • 5
  • 31
  • 47
  • I think you are wrong about the setTimeout with 0 time. In my opinion it can never be good practice for this purpose. Please see the links I put in my answer (after the "edit"), and my comments to you there. – Ramzi Khahil Dec 21 '14 at 11:00
0

If you've properly scoped your backbone view, you should be able to reference the element that is currently in memory when you are trying to change the width or height (pre-render).

You can do this by doing something similar to: this.$el.find(#foo") to obtain access / manipulate to your markup before it is added to the DOM.

Mad-Chemist
  • 487
  • 6
  • 18