Repository: sveltejs/svelte (JavaScript)
Date: 2017-03-28 16:31:33+00:00
Task type: Bug Fix

Issue:
Functions should be treated as objects for purposes of updating computations
[This example](https://svelte.technology/repl?version=1.13.1&gist=4a539c95737f43bdccd74ab5ebf8d52d) shows the bug (which I did just encounter in a real application, as bizarre as the example looks) — computed values are recomputed if one or more of their dependencies a) are among the `changed` items AND b) the value has changed from its previous value or is a non-primitive value (since objects and arrays could have been mutated).

That doesn't take account of functions, whose return value might be dependent on a closure. So the check needs to be updated:

**Current code:**

```js
function applyComputations ( state, newState, oldState, isInitial ) {
  if ( isInitial || ( 'time' in newState && typeof state.time === 'object' || state.time !== oldState.time ) ) {
    state.getFoo = newState.getFoo = template.computed.getFoo( state.time );
  }
	
  if ( isInitial || ( 'getFoo' in newState && typeof state.getFoo === 'object' || state.getFoo !== oldState.getFoo ) ) {
    state.foo = newState.foo = template.computed.foo( state.getFoo );
  }
}
```

**Better code:**

```js
function differs ( a, b ) {
  return ( a !== b ) || ( a && typeof a === 'object' ) || ( typeof a === 'function' );
}

function applyComputations ( state, newState, oldState, isInitial ) {
  if ( isInitial || ( 'time' in newState && differs( state.time, oldState.time ) ) ) {
    state.getFoo = newState.getFoo = template.computed.getFoo( state.time );
  }
	
  if ( isInitial || ( 'getFoo' in newState && differs( state.getFoo, oldState.getFoo ) ) ) {
    state.foo = newState.foo = template.computed.foo( state.getFoo );
  }
}
```


Issue discussion:
In that example, isn't `getFoo` arguably no longer a pure function? That's not to say there's not still some bug here, or that there's not some other less insane-looking way to reproduce this.

On the other hand I guess in this case burdening everyone with an extra `typeof` to handle this case isn't really all that much of a burden.
> or that there's not some other less insane-looking way to reproduce this.

Ha, that's fair. The place I encountered this was in an app that used D3 scales — rather than generating a new function when the dimensions change, the D3 convention is to call a method of a function that updates the values inside the closure where the function is generated:

https://svelte.technology/repl?version=1.13.1&gist=fcdc653576d0ffb156b4d863cd3e5874

If you shrink the width of the output window you'll see that `barwidth` isn't being recomputed. If you add the `width` parameter, it starts working — but it should consider `xScale` to have 'changed' and not need that extra encouragement.

Solution patch:
diff --git a/src/generators/dom/index.js b/src/generators/dom/index.js
--- a/src/generators/dom/index.js
+++ b/src/generators/dom/index.js
@@ -246,22 +246,23 @@ export default function dom ( parsed, source, options ) {
 
 	if ( computations.length ) {
 		const builder = new CodeBuilder();
+		const differs = generator.helper( 'differs' );
 
 		computations.forEach( ({ key, deps }) => {
 			builder.addBlock( deindent`
-				if ( isInitial || ${deps.map( dep => `( '${dep}' in newState && typeof state.${dep} === 'object' || state.${dep} !== oldState.${dep} )` ).join( ' || ' )} ) {
+				if ( isInitial || ${deps.map( dep => `( '${dep}' in newState && ${differs}( state.${dep}, oldState.${dep} ) )` ).join( ' || ' )} ) {
 					state.${key} = newState.${key} = ${generator.alias( 'template' )}.computed.${key}( ${deps.map( dep => `state.${dep}` ).join( ', ' )} );
 				}
 			` );
 		});
 
 		builders.main.addBlock( deindent`
-			function ${generator.alias( 'applyComputations' )} ( state, newState, oldState, isInitial ) {
+			function ${generator.alias( 'recompute' )} ( state, newState, oldState, isInitial ) {
 				${builder}
 			}
 		` );
 
-		builders._set.addLine( `${generator.alias( 'applyComputations' )}( this._state, newState, oldState, false )` );
+		builders._set.addLine( `${generator.alias( 'recompute' )}( this._state, newState, oldState, false )` );
 	}
 
 	// TODO is the `if` necessary?
@@ -349,7 +350,7 @@ export default function dom ( parsed, source, options ) {
 
 	if ( templateProperties.computed ) {
 		constructorBlock.addLine(
-			`${generator.alias( 'applyComputations' )}( this._state, this._state, {}, true );`
+			`${generator.alias( 'recompute' )}( this._state, this._state, {}, true );`
 		);
 	}
 
diff --git a/src/shared/index.js b/src/shared/index.js
--- a/src/shared/index.js
+++ b/src/shared/index.js
@@ -3,6 +3,10 @@ export * from './methods.js';
 
 export function noop () {}
 
+export function differs ( a, b ) {
+	return ( a !== b ) || ( a && ( typeof a === 'object' ) || ( typeof a === 'function' ) );
+}
+
 export function dispatchObservers ( component, group, newState, oldState ) {
 	for ( var key in group ) {
 		if ( !( key in newState ) ) continue;


Test patch:
diff --git a/test/generator/samples/computed-values-function-dependency/_config.js b/test/generator/samples/computed-values-function-dependency/_config.js
new file mode 100644
--- /dev/null
+++ b/test/generator/samples/computed-values-function-dependency/_config.js
@@ -0,0 +1,10 @@
+export default {
+	html: '<p>2</p>',
+
+	test ( assert, component, target ) {
+		component.set({ y: 2 });
+		assert.equal( component.get( 'x' ), 4 );
+		assert.equal( target.innerHTML, '<p>4</p>' );
+		component.destroy();
+	}
+};
diff --git a/test/generator/samples/computed-values-function-dependency/main.html b/test/generator/samples/computed-values-function-dependency/main.html
new file mode 100644
--- /dev/null
+++ b/test/generator/samples/computed-values-function-dependency/main.html
@@ -0,0 +1,28 @@
+<p>{{x}}</p>
+
+<script>
+	let _x;
+
+	function getX () {
+		return _x;
+	}
+
+	export default {
+		data () {
+			return {
+				y: 1
+			};
+		},
+
+		computed: {
+			xGetter ( y ) {
+				_x = y * 2;
+				return getX;
+			},
+
+			x ( xGetter ) {
+				return xGetter();
+			}
+		}
+	};
+</script>
\ No newline at end of file