Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Commit 11f6227

Browse files
committed
Merge pull request #407 from firebase/kato-403
Optimize our $bindTo code base
2 parents 5a1b733 + 250b5fa commit 11f6227

File tree

6 files changed

+311
-118
lines changed

6 files changed

+311
-118
lines changed

Gruntfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ module.exports = function(grunt) {
7272
jshint : {
7373
options : {
7474
jshintrc: '.jshintrc',
75-
ignores: ['src/polyfills.js'],
75+
ignores: ['src/lib/polyfills.js']
7676
},
7777
all : ['src/**/*.js']
7878
},

src/FirebaseObject.js

Lines changed: 155 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
* <code>$firebase( firebaseRef, {objectFactory: NewFactory}).$asObject();</code>
2525
*/
2626
angular.module('firebase').factory('$FirebaseObject', [
27-
'$parse', '$firebaseUtils', '$log',
28-
function($parse, $firebaseUtils, $log) {
27+
'$parse', '$firebaseUtils', '$log', '$interval',
28+
function($parse, $firebaseUtils, $log, $interval) {
2929
/**
3030
* This constructor should probably never be called manually. It is used internally by
3131
* <code>$firebase.$asObject()</code>.
@@ -38,18 +38,19 @@
3838
* @constructor
3939
*/
4040
function FirebaseObject($firebase, destroyFn, readyPromise) {
41-
// IDE does not understand defineProperty so declare traditionally
42-
// to avoid lots of IDE warnings about invalid properties
41+
// These are private config props and functions used internally
42+
// they are collected here to reduce clutter in console.log and forEach
4343
this.$$conf = {
4444
promise: readyPromise,
4545
inst: $firebase,
46-
bound: null,
46+
binding: new ThreeWayBinding(this),
4747
destroyFn: destroyFn,
4848
listeners: []
4949
};
5050

5151
// this bit of magic makes $$conf non-enumerable and non-configurable
5252
// and non-writable (its properties are still writable but the ref cannot be replaced)
53+
// we declare it above so the IDE can relax
5354
Object.defineProperty(this, '$$conf', {
5455
value: this.$$conf
5556
});
@@ -66,10 +67,10 @@
6667
* @returns a promise which will resolve after the save is completed.
6768
*/
6869
$save: function () {
69-
var notify = this.$$notify.bind(this);
70-
return this.$inst().$set($firebaseUtils.toJSON(this))
70+
var self = this;
71+
return self.$inst().$set($firebaseUtils.toJSON(self))
7172
.then(function(ref) {
72-
notify();
73+
self.$$notify();
7374
return ref;
7475
});
7576
},
@@ -122,43 +123,7 @@
122123
$bindTo: function (scope, varName) {
123124
var self = this;
124125
return self.$loaded().then(function () {
125-
//todo split this into a subclass and shorten this method
126-
//todo add comments and explanations
127-
if (self.$$conf.bound) {
128-
$log.error('Can only bind to one scope variable at a time');
129-
return $firebaseUtils.reject('Can only bind to one scope variable at a time');
130-
}
131-
132-
var unbind = function () {
133-
if (self.$$conf.bound) {
134-
self.$$conf.bound = null;
135-
off();
136-
}
137-
};
138-
139-
// expose a few useful methods to other methods
140-
var parsed = $parse(varName);
141-
var $bound = self.$$conf.bound = {
142-
update: function() {
143-
var curr = $firebaseUtils.parseScopeData(self);
144-
parsed.assign(scope, curr);
145-
},
146-
get: function () {
147-
return parsed(scope);
148-
},
149-
unbind: unbind
150-
};
151-
152-
$bound.update();
153-
scope.$on('$destroy', $bound.unbind);
154-
155-
// monitor scope for any changes
156-
var off = scope.$watch(varName, function () {
157-
var newData = $firebaseUtils.toJSON($bound.get());
158-
self.$inst().$set(newData);
159-
}, true);
160-
161-
return unbind;
126+
return self.$$conf.binding.bindTo(scope, varName);
162127
});
163128
},
164129

@@ -195,9 +160,7 @@
195160
var self = this;
196161
if (!self.$isDestroyed) {
197162
self.$isDestroyed = true;
198-
if (self.$$conf.bound) {
199-
self.$$conf.bound.unbind();
200-
}
163+
self.$$conf.binding.destroy();
201164
$firebaseUtils.each(self, function (v, k) {
202165
delete self[k];
203166
});
@@ -234,17 +197,25 @@
234197
this.$destroy(err);
235198
},
236199

200+
/**
201+
* Called internally by $bindTo when data is changed in $scope.
202+
* Should apply updates to this record but should not call
203+
* notify().
204+
*/
205+
$$scopeUpdated: function(newData) {
206+
// we use a one-directional loop to avoid feedback with 3-way bindings
207+
// since set() is applied locally anyway, this is still performant
208+
return this.$inst().$set($firebaseUtils.toJSON(newData));
209+
},
210+
237211
/**
238212
* Updates any bound scope variables and notifies listeners registered
239213
* with $watch any time there is a change to data
240214
*/
241215
$$notify: function() {
242-
var self = this;
243-
if( self.$$conf.bound ) {
244-
self.$$conf.bound.update();
245-
}
216+
var self = this, list = this.$$conf.listeners.slice();
246217
// be sure to do this after setting up data and init state
247-
angular.forEach(self.$$conf.listeners, function (parts) {
218+
angular.forEach(list, function (parts) {
248219
parts[0].call(parts[1], {event: 'value', key: self.$id});
249220
});
250221
},
@@ -296,6 +267,137 @@
296267
return $firebaseUtils.inherit(ChildClass, FirebaseObject, methods);
297268
};
298269

270+
/**
271+
* Creates a three-way data binding on a scope variable.
272+
*
273+
* @param {FirebaseObject} rec
274+
* @returns {*}
275+
* @constructor
276+
*/
277+
function ThreeWayBinding(rec) {
278+
this.subs = [];
279+
this.scope = null;
280+
this.name = null;
281+
this.rec = rec;
282+
}
283+
284+
ThreeWayBinding.prototype = {
285+
assertNotBound: function(varName) {
286+
if( this.scope ) {
287+
var msg = 'Cannot bind to ' + varName + ' because this instance is already bound to ' +
288+
this.name + '; one binding per instance ' +
289+
'(call unbind method or create another $firebase instance)';
290+
$log.error(msg);
291+
return $firebaseUtils.reject(msg);
292+
}
293+
},
294+
295+
bindTo: function(scope, varName) {
296+
function _bind(self) {
297+
var sending = false;
298+
var parsed = $parse(varName);
299+
var rec = self.rec;
300+
self.scope = scope;
301+
self.varName = varName;
302+
303+
function equals(rec) {
304+
var parsed = getScope();
305+
var newData = $firebaseUtils.scopeData(rec);
306+
return angular.equals(parsed, newData) &&
307+
parsed.$priority === rec.$priority &&
308+
parsed.$value === rec.$value;
309+
}
310+
311+
function getScope() {
312+
return $firebaseUtils.scopeData(parsed(scope));
313+
}
314+
315+
function setScope(rec) {
316+
parsed.assign(scope, $firebaseUtils.scopeData(rec));
317+
}
318+
319+
var scopeUpdated = function() {
320+
var send = $firebaseUtils.debounce(function() {
321+
rec.$$scopeUpdated(getScope())
322+
['finally'](function() { sending = false; });
323+
}, 50, 500);
324+
if( !equals(rec) ) {
325+
sending = true;
326+
send();
327+
}
328+
};
329+
330+
var recUpdated = function() {
331+
if( !sending && !equals(rec) ) {
332+
setScope(rec);
333+
}
334+
};
335+
336+
// $watch will not check any vars prefixed with $, so we
337+
// manually check $priority and $value using this method
338+
function checkMetaVars() {
339+
var dat = parsed(scope);
340+
if( dat.$value !== rec.$value || dat.$priority !== rec.$priority ) {
341+
scopeUpdated();
342+
}
343+
}
344+
345+
// Okay, so this magic hack is um... magic. It increments a
346+
// variable every 50 seconds (counterKey) so that whenever $digest
347+
// is run, the variable will be dirty. This allows us to determine
348+
// when $digest is invoked, manually check the meta vars, and
349+
// manually invoke our watcher if the $ prefixed data has changed
350+
(function() {
351+
// create a counter and store it in scope
352+
var counterKey = '_firebaseCounterForVar'+varName;
353+
scope[counterKey] = 0;
354+
// update the counter every 51ms
355+
// why 51? because it must be greater than scopeUpdated's debounce
356+
// or protractor has a conniption
357+
var to = $interval(function() {
358+
scope[counterKey]++;
359+
}, 51, 0, false);
360+
// watch the counter for changes (which means $digest ran)
361+
self.subs.push(scope.$watch(counterKey, checkMetaVars));
362+
// cancel our interval and clear var from scope if unbound
363+
self.subs.push(function() {
364+
$interval.cancel(to);
365+
delete scope[counterKey];
366+
});
367+
})();
368+
369+
setScope(rec);
370+
self.subs.push(scope.$on('$destroy', self.unbind.bind(self)));
371+
372+
// monitor scope for any changes
373+
self.subs.push(scope.$watch(varName, scopeUpdated, true));
374+
375+
// monitor the object for changes
376+
self.subs.push(rec.$watch(recUpdated));
377+
378+
return self.unbind.bind(self);
379+
}
380+
381+
return this.assertNotBound(varName) || _bind(this);
382+
},
383+
384+
unbind: function() {
385+
if( this.scope ) {
386+
angular.forEach(this.subs, function(unbind) {
387+
unbind();
388+
});
389+
this.subs = [];
390+
this.scope = null;
391+
this.name = null;
392+
}
393+
},
394+
395+
destroy: function() {
396+
this.unbind();
397+
this.rec = null;
398+
}
399+
};
400+
299401
return FirebaseObject;
300402
}
301403
]);

0 commit comments

Comments
 (0)