Freewind
Freewind

Reputation: 198218

Why I need to use `setTimeout()` to make `jquery.animate` work in a backbone view?

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?

Upvotes: 0

Views: 571

Answers (4)

Mad-Chemist
Mad-Chemist

Reputation: 487

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.

Upvotes: 0

seebiscuit
seebiscuit

Reputation: 5053

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();

Upvotes: 0

Ramzi Khahil
Ramzi Khahil

Reputation: 5052

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.

Upvotes: 1

Jeff Ling
Jeff Ling

Reputation: 1032

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.

Upvotes: 2

Related Questions