Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

fix+perf($parse): allow watching array/object literal values, disable deep watch for one-way bindings #15301

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -3508,21 +3508,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (optional && !attrs[attrName]) break;

parentGet = $parse(attrs[attrName]);
var deepWatch = parentGet.literal;
var isLiteral = parentGet.literal;

var initialValue = destination[scopeName] = parentGet(scope);
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);

removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
if (oldValue === newValue) {
if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) {
if (oldValue === initialValue || (isLiteral && equals(oldValue, initialValue))) {
return;
}
oldValue = initialValue;
}
recordChanges(scopeName, newValue, oldValue);
destination[scopeName] = newValue;
}, deepWatch);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep watching never happens, but we still have a variable named deepWatch. Isn't it a bit misleading?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK so let's change deepWatch to isLiteral.


removeWatchCollection.push(removeWatch);
break;
Expand Down
8 changes: 4 additions & 4 deletions src/ng/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -1786,13 +1786,13 @@ function $ParseProvider() {
}
}

function expressionInputDirtyCheck(newValue, oldValueOfValue) {
function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) {

if (newValue == null || oldValueOfValue == null) { // null/undefined
return newValue === oldValueOfValue;
}

if (typeof newValue === 'object') {
if (typeof newValue === 'object' && !compareObjectIdentity) {

// attempt to convert the value to a primitive type
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
Expand Down Expand Up @@ -1821,7 +1821,7 @@ function $ParseProvider() {
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
Expand All @@ -1841,7 +1841,7 @@ function $ParseProvider() {

for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
Expand Down
76 changes: 74 additions & 2 deletions test/ng/compileSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4259,6 +4259,78 @@ describe('$compile', function() {
});


it('should trigger `$onChanges` for literal expressions when expression input value changes (simple value)', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add tests verifying that $onChanges is not called when a property of the input value changes?

var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };

angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop1': '<' }
});

module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop1="[val]"></c1>')($rootScope);

$rootScope.$apply('val = 1');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [1]})});

$rootScope.$apply('val = 2');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [1], currentValue: [2]})});
});
});


it('should trigger `$onChanges` for literal expressions when expression input value changes (complex value)', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };

angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop1': '<' }
});

module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop1="[val]"></c1>')($rootScope);

$rootScope.$apply('val = [1]');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})});

$rootScope.$apply('val = [2]');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[2]]})});
});
});


it('should trigger `$onChanges` for literal expressions when expression input value changes instances, even when equal', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };

angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop1': '<' }
});

module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop1="[val]"></c1>')($rootScope);

$rootScope.$apply('val = [1]');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [undefined], currentValue: [[1]]})});

$rootScope.$apply('val = [1]');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: [[1]], currentValue: [[1]]})});
});
});


it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() {
var log = [];
function TestController() { }
Expand Down Expand Up @@ -5676,7 +5748,7 @@ describe('$compile', function() {
}));


it('should deep-watch array literals', inject(function() {
it('should watch input values to array literals', inject(function() {
$rootScope.name = 'georgios';
$rootScope.obj = {name: 'pete'};
compile('<div><span my-component ow-ref="[{name: name}, obj]">');
Expand All @@ -5690,7 +5762,7 @@ describe('$compile', function() {
}));


it('should deep-watch object literals', inject(function() {
it('should watch input values object literals', inject(function() {
$rootScope.name = 'georgios';
$rootScope.obj = {name: 'pete'};
compile('<div><span my-component ow-ref="{name: name, item: obj}">');
Expand Down
68 changes: 68 additions & 0 deletions test/ng/parseSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3117,6 +3117,74 @@ describe('parser', function() {
scope.$digest();
expect(objB.value).toBe(scope.input);
}));

it('should support watching literals', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };

scope.$watch('{val: val}', listener);

scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual({val: 1});

scope.$apply('val = []');
expect(callCount).toBe(2);
expect(lastVal).toEqual({val: []});

scope.$apply('val = []');
expect(callCount).toBe(3);
expect(lastVal).toEqual({val: []});

scope.$apply('val = {}');
expect(callCount).toBe(4);
expect(lastVal).toEqual({val: {}});
}));

it('should only watch the direct inputs to literals', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };

scope.$watch('{val: val}', listener);

scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual({val: 1});

scope.$apply('val = [2]');
expect(callCount).toBe(2);
expect(lastVal).toEqual({val: [2]});

scope.$apply('val.push(3)');
expect(callCount).toBe(2);

scope.$apply('val.length = 0');
expect(callCount).toBe(2);
}));

it('should only watch the direct inputs to nested literals', inject(function($parse) {
var lastVal = NaN;
var callCount = 0;
var listener = function(val) { callCount++; lastVal = val; };

scope.$watch('[{val: [val]}]', listener);

scope.$apply('val = 1');
expect(callCount).toBe(1);
expect(lastVal).toEqual([{val: [1]}]);

scope.$apply('val = [2]');
expect(callCount).toBe(2);
expect(lastVal).toEqual([{val: [[2]]}]);

scope.$apply('val.push(3)');
expect(callCount).toBe(2);

scope.$apply('val.length = 0');
expect(callCount).toBe(2);
}));
});

describe('locals', function() {
Expand Down