mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-05-19 20:01:52 +00:00
fix(jqLite): .data()/.bind() memory leak
Since angular attaches scope/injector/controller into DOM it should clean up after itself. No need to complain about memory leaks, since they can only happened on detached DOM. Detached DOM would only be in tests, since in production the DOM would be attached to render tree and removal would automatically clear memory.
This commit is contained in:
parent
24e7da4f19
commit
ec1c5dfaee
5 changed files with 135 additions and 51 deletions
|
|
@ -74,7 +74,7 @@
|
||||||
* @returns {Object} jQuery object.
|
* @returns {Object} jQuery object.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var jqCache = {},
|
var jqCache = JQLite.cache = {},
|
||||||
jqName = JQLite.expando = 'ng-' + new Date().getTime(),
|
jqName = JQLite.expando = 'ng-' + new Date().getTime(),
|
||||||
jqId = 1,
|
jqId = 1,
|
||||||
addEventListenerFn = (window.document.addEventListener
|
addEventListenerFn = (window.document.addEventListener
|
||||||
|
|
@ -122,15 +122,15 @@ function JQLitePatchJQueryRemove(name, dispatchThis) {
|
||||||
fireEvent = dispatchThis,
|
fireEvent = dispatchThis,
|
||||||
set, setIndex, setLength,
|
set, setIndex, setLength,
|
||||||
element, childIndex, childLength, children,
|
element, childIndex, childLength, children,
|
||||||
fns, data;
|
fns, events;
|
||||||
|
|
||||||
while(list.length) {
|
while(list.length) {
|
||||||
set = list.shift();
|
set = list.shift();
|
||||||
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
|
for(setIndex = 0, setLength = set.length; setIndex < setLength; setIndex++) {
|
||||||
element = jqLite(set[setIndex]);
|
element = jqLite(set[setIndex]);
|
||||||
if (fireEvent) {
|
if (fireEvent) {
|
||||||
data = element.data('events');
|
events = element.data('events');
|
||||||
if ( (fns = data && data.$destroy) ) {
|
if ( (fns = events && events.$destroy) ) {
|
||||||
forEach(fns, function(fn){
|
forEach(fns, function(fn){
|
||||||
fn.handler();
|
fn.handler();
|
||||||
});
|
});
|
||||||
|
|
@ -185,19 +185,35 @@ function JQLiteDealoc(element){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function JQLiteUnbind(element, type, fn) {
|
||||||
|
var events = JQLiteData(element, 'events'),
|
||||||
|
handle = JQLiteData(element, 'handle');
|
||||||
|
|
||||||
|
if (!handle) return; //no listeners registered
|
||||||
|
|
||||||
|
if (isUndefined(type)) {
|
||||||
|
forEach(events, function(eventHandler, type) {
|
||||||
|
removeEventListenerFn(element, type, eventHandler);
|
||||||
|
delete events[type];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (isUndefined(fn)) {
|
||||||
|
removeEventListenerFn(element, type, events[type]);
|
||||||
|
delete events[type];
|
||||||
|
} else {
|
||||||
|
arrayRemove(events[type], fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function JQLiteRemoveData(element) {
|
function JQLiteRemoveData(element) {
|
||||||
var cacheId = element[jqName],
|
var cacheId = element[jqName],
|
||||||
cache = jqCache[cacheId];
|
cache = jqCache[cacheId];
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
if (cache.bind) {
|
if (cache.handle) {
|
||||||
forEach(cache.bind, function(fn, type){
|
cache.events.$destroy && cache.handle({}, '$destroy');
|
||||||
if (type == '$destroy') {
|
JQLiteUnbind(element);
|
||||||
fn({});
|
|
||||||
} else {
|
|
||||||
removeEventListenerFn(element, type, fn);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
delete jqCache[cacheId];
|
delete jqCache[cacheId];
|
||||||
element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
|
element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
|
||||||
|
|
@ -499,8 +515,8 @@ forEach({
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function createEventHandler(element) {
|
function createEventHandler(element, events) {
|
||||||
var eventHandler = function (event) {
|
var eventHandler = function (event, type) {
|
||||||
if (!event.preventDefault) {
|
if (!event.preventDefault) {
|
||||||
event.preventDefault = function() {
|
event.preventDefault = function() {
|
||||||
event.returnValue = false; //ie
|
event.returnValue = false; //ie
|
||||||
|
|
@ -530,8 +546,12 @@ function createEventHandler(element) {
|
||||||
return event.defaultPrevented;
|
return event.defaultPrevented;
|
||||||
};
|
};
|
||||||
|
|
||||||
forEach(eventHandler.fns, function(fn){
|
forEach(events[type || event.type], function(fn) {
|
||||||
|
try {
|
||||||
fn.call(element, event);
|
fn.call(element, event);
|
||||||
|
} catch (e) {
|
||||||
|
// Not much to do here since jQuery ignores these anyway
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove monkey-patched methods (IE),
|
// Remove monkey-patched methods (IE),
|
||||||
|
|
@ -548,7 +568,7 @@ function createEventHandler(element) {
|
||||||
delete event.isDefaultPrevented;
|
delete event.isDefaultPrevented;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
eventHandler.fns = [];
|
eventHandler.elem = element;
|
||||||
return eventHandler;
|
return eventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,61 +583,45 @@ forEach({
|
||||||
dealoc: JQLiteDealoc,
|
dealoc: JQLiteDealoc,
|
||||||
|
|
||||||
bind: function bindFn(element, type, fn){
|
bind: function bindFn(element, type, fn){
|
||||||
var bind = JQLiteData(element, 'bind');
|
var events = JQLiteData(element, 'events'),
|
||||||
|
handle = JQLiteData(element, 'handle');
|
||||||
|
|
||||||
|
if (!events) JQLiteData(element, 'events', events = {});
|
||||||
|
if (!handle) JQLiteData(element, 'handle', handle = createEventHandler(element, events));
|
||||||
|
|
||||||
if (!bind) JQLiteData(element, 'bind', bind = {});
|
|
||||||
forEach(type.split(' '), function(type){
|
forEach(type.split(' '), function(type){
|
||||||
var eventHandler = bind[type];
|
var eventFns = events[type];
|
||||||
|
|
||||||
|
if (!eventFns) {
|
||||||
if (!eventHandler) {
|
|
||||||
if (type == 'mouseenter' || type == 'mouseleave') {
|
if (type == 'mouseenter' || type == 'mouseleave') {
|
||||||
var mouseenter = bind.mouseenter = createEventHandler(element);
|
|
||||||
var mouseleave = bind.mouseleave = createEventHandler(element);
|
|
||||||
var counter = 0;
|
var counter = 0;
|
||||||
|
|
||||||
|
events.mouseenter = [];
|
||||||
|
events.mouseleave = [];
|
||||||
|
|
||||||
bindFn(element, 'mouseover', function(event) {
|
bindFn(element, 'mouseover', function(event) {
|
||||||
counter++;
|
counter++;
|
||||||
if (counter == 1) {
|
if (counter == 1) {
|
||||||
mouseenter(event);
|
handle(event, 'mouseenter');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
bindFn(element, 'mouseout', function(event) {
|
bindFn(element, 'mouseout', function(event) {
|
||||||
counter --;
|
counter --;
|
||||||
if (counter == 0) {
|
if (counter == 0) {
|
||||||
mouseleave(event);
|
handle(event, 'mouseleave');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventHandler = bind[type];
|
|
||||||
} else {
|
} else {
|
||||||
eventHandler = bind[type] = createEventHandler(element);
|
addEventListenerFn(element, type, handle);
|
||||||
addEventListenerFn(element, type, eventHandler);
|
events[type] = [];
|
||||||
}
|
}
|
||||||
|
eventFns = events[type]
|
||||||
}
|
}
|
||||||
eventHandler.fns.push(fn);
|
eventFns.push(fn);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
unbind: function(element, type, fn) {
|
unbind: JQLiteUnbind,
|
||||||
var bind = JQLiteData(element, 'bind');
|
|
||||||
if (!bind) return; //no listeners registered
|
|
||||||
|
|
||||||
if (isUndefined(type)) {
|
|
||||||
forEach(bind, function(eventHandler, type) {
|
|
||||||
removeEventListenerFn(element, type, eventHandler);
|
|
||||||
delete bind[type];
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (isUndefined(fn)) {
|
|
||||||
removeEventListenerFn(element, type, bind[type]);
|
|
||||||
delete bind[type];
|
|
||||||
} else {
|
|
||||||
arrayRemove(bind[type].fns, fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceWith: function(element, replaceNode) {
|
replaceWith: function(element, replaceNode) {
|
||||||
var index, parent = element.parentNode;
|
var index, parent = element.parentNode;
|
||||||
|
|
|
||||||
15
src/ngMock/angular-mocks.js
vendored
15
src/ngMock/angular-mocks.js
vendored
|
|
@ -1526,6 +1526,20 @@ angular.mock.e2e = {};
|
||||||
angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock];
|
angular.mock.e2e.$httpBackendDecorator = ['$delegate', '$browser', createHttpBackendMock];
|
||||||
|
|
||||||
|
|
||||||
|
angular.mock.clearDataCache = function() {
|
||||||
|
var key,
|
||||||
|
cache = angular.element.cache;
|
||||||
|
|
||||||
|
for(key in cache) {
|
||||||
|
if (cache.hasOwnProperty(key)) {
|
||||||
|
var handle = cache[key].handle;
|
||||||
|
|
||||||
|
handle && angular.element(handle.elem).unbind();
|
||||||
|
delete cache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
window.jstestdriver && (function(window) {
|
window.jstestdriver && (function(window) {
|
||||||
/**
|
/**
|
||||||
|
|
@ -1550,6 +1564,7 @@ window.jasmine && (function(window) {
|
||||||
var spec = getCurrentSpec();
|
var spec = getCurrentSpec();
|
||||||
spec.$injector = null;
|
spec.$injector = null;
|
||||||
spec.$modules = null;
|
spec.$modules = null;
|
||||||
|
angular.mock.clearDataCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getCurrentSpec() {
|
function getCurrentSpec() {
|
||||||
|
|
|
||||||
|
|
@ -291,11 +291,37 @@ describe('jqLite', function() {
|
||||||
expect(element.data()).toEqual({meLike: 'turtles', youLike: 'carrots', existing: 'val'});
|
expect(element.data()).toEqual({meLike: 'turtles', youLike: 'carrots', existing: 'val'});
|
||||||
expect(element.data()).toBe(oldData); // merge into the old object
|
expect(element.data()).toBe(oldData); // merge into the old object
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('data cleanup', function() {
|
||||||
|
it('should remove data on element removal', function() {
|
||||||
|
var div = jqLite('<div><span>text</span></div>'),
|
||||||
|
span = div.find('span');
|
||||||
|
|
||||||
|
span.data('name', 'angular');
|
||||||
|
span.remove();
|
||||||
|
expect(span.data('name')).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove event listeners on element removal', function() {
|
||||||
|
var div = jqLite('<div><span>text</span></div>'),
|
||||||
|
span = div.find('span'),
|
||||||
|
log = '';
|
||||||
|
|
||||||
|
span.bind('click', function() { log+= 'click;'});
|
||||||
|
browserTrigger(span);
|
||||||
|
expect(log).toEqual('click;');
|
||||||
|
|
||||||
|
span.remove();
|
||||||
|
|
||||||
|
browserTrigger(span);
|
||||||
|
expect(log).toEqual('click;');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('attr', function() {
|
describe('attr', function() {
|
||||||
it('shoul read write and remove attr', function() {
|
it('should read write and remove attr', function() {
|
||||||
var selector = jqLite([a, b]);
|
var selector = jqLite([a, b]);
|
||||||
|
|
||||||
expect(selector.attr('prop', 'value')).toEqual(selector);
|
expect(selector.attr('prop', 'value')).toEqual(selector);
|
||||||
|
|
@ -667,7 +693,7 @@ describe('jqLite', function() {
|
||||||
var jWindow = jqLite(window).bind('hashchange', function() {
|
var jWindow = jqLite(window).bind('hashchange', function() {
|
||||||
log = 'works!';
|
log = 'works!';
|
||||||
});
|
});
|
||||||
eventFn({});
|
eventFn({type: 'hashchange'});
|
||||||
expect(log).toEqual('works!');
|
expect(log).toEqual('works!');
|
||||||
dealoc(jWindow);
|
dealoc(jWindow);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
40
test/ngMock/angular-mocksSpec.js
vendored
40
test/ngMock/angular-mocksSpec.js
vendored
|
|
@ -148,6 +148,7 @@ describe('ngMock', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('$log', function() {
|
describe('$log', function() {
|
||||||
var $log;
|
var $log;
|
||||||
beforeEach(inject(['$log', function(log) {
|
beforeEach(inject(['$log', function(log) {
|
||||||
|
|
@ -229,6 +230,7 @@ describe('ngMock', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('defer', function() {
|
describe('defer', function() {
|
||||||
var browser, log;
|
var browser, log;
|
||||||
beforeEach(inject(function($browser) {
|
beforeEach(inject(function($browser) {
|
||||||
|
|
@ -341,6 +343,44 @@ describe('ngMock', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('angular.mock.clearDataCache', function() {
|
||||||
|
function keys(obj) {
|
||||||
|
var keys = [];
|
||||||
|
for(var key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) keys.push(key);
|
||||||
|
}
|
||||||
|
return keys.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should remove data', function() {
|
||||||
|
expect(angular.element.cache).toEqual({});
|
||||||
|
var div = angular.element('<div></div>');
|
||||||
|
div.data('name', 'angular');
|
||||||
|
expect(keys(angular.element.cache)).not.toEqual([]);
|
||||||
|
angular.mock.clearDataCache();
|
||||||
|
expect(keys(angular.element.cache)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deregister event handlers', function() {
|
||||||
|
expect(keys(angular.element.cache)).toEqual([]);
|
||||||
|
|
||||||
|
var div = angular.element('<div></div>');
|
||||||
|
|
||||||
|
div.bind('click', angular.noop);
|
||||||
|
div.bind('mousemove', angular.noop);
|
||||||
|
div.data('some', 'data');
|
||||||
|
expect(keys(angular.element.cache).length).toBe(1);
|
||||||
|
|
||||||
|
angular.mock.clearDataCache();
|
||||||
|
expect(keys(angular.element.cache)).toEqual([]);
|
||||||
|
expect(div.data('some')).toBeUndefined();
|
||||||
|
|
||||||
|
div.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('jasmine module and inject', function(){
|
describe('jasmine module and inject', function(){
|
||||||
var log;
|
var log;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ afterEach(function() {
|
||||||
var count = 0;
|
var count = 0;
|
||||||
forEachSorted(jqCache, function(value, key){
|
forEachSorted(jqCache, function(value, key){
|
||||||
count ++;
|
count ++;
|
||||||
delete jqCache[key];
|
|
||||||
forEach(value, function(value, key){
|
forEach(value, function(value, key){
|
||||||
if (value.$element) {
|
if (value.$element) {
|
||||||
dump('LEAK', key, value.$id, sortedHtml(value.$element));
|
dump('LEAK', key, value.$id, sortedHtml(value.$element));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue