do dirty check in _set, so we can easily skip unnecessary computations later (#768)

pull/769/head
Rich Harris 7 years ago
parent 1d77a45fdc
commit 0f7e87c804

@ -74,10 +74,9 @@ export default function dom(
});
const builder = new CodeBuilder();
const computationBuilder = new CodeBuilder();
if (computations.length) {
const computationBuilder = new CodeBuilder();
computations.forEach(({ key, deps }) => {
if (generator.readonly.has(key)) {
// <:Window> bindings
@ -88,48 +87,18 @@ export default function dom(
generator.readonly.add(key);
const condition = `isInitial || ${deps
.map(
dep =>
`( '${dep}' in newState && @differs( state.${dep}, oldState.${dep} ) )`
)
.join(' || ')}`;
const statement = `state.${key} = newState.${key} = @template.computed.${key}( ${deps
const condition = `isInitial || ${deps.map(dep =>
`( '${dep}' in changed )`
).join(' || ')}`;
const statement = `if ( @differs( ( state.${key} = @template.computed.${key}( ${deps
.map(dep => `state.${dep}`)
.join(', ')} );`;
.join(', ')} ) ), oldState.${key} ) ) changed.${key} = true;`;
computationBuilder.addConditionalLine(condition, statement);
});
builder.addBlock(deindent`
function @recompute ( state, newState, oldState, isInitial ) {
${computationBuilder}
}
`);
}
const _set = deindent`
${options.dev &&
deindent`
if ( typeof newState !== 'object' ) {
throw new Error( 'Component .set was called without an object of data key-values to update.' );
}
${Array.from(generator.readonly).map(
prop =>
`if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );`
)}
`}
var oldState = this._state;
this._state = @assign( {}, oldState, newState );
${computations.length &&
`@recompute( this._state, newState, oldState, false )`}
@dispatchObservers( this, this._observers.pre, newState, oldState );
${block.hasUpdateMethod && `this._fragment.update( newState, this._state );`}
@dispatchObservers( this, this._observers.post, newState, oldState );
`;
if (hasJs) {
builder.addBlock(`[✂${parsed.js.content.start}-${parsed.js.content.end}✂]`);
}
@ -174,7 +143,7 @@ export default function dom(
? `@proto `
: deindent`
{
${['destroy', 'get', 'fire', 'observe', 'on', 'set', 'teardown']
${['destroy', 'get', 'fire', 'observe', 'on', 'set', '_set', 'teardown']
.map(n => `${n}: @${n === 'teardown' ? 'destroy' : n}`)
.join(',\n')}
}`;
@ -190,7 +159,7 @@ export default function dom(
? `@assign( @template.data(), options.data )`
: `options.data || {}`};
${generator.metaBindings}
${computations.length && `@recompute( this._state, this._state, {}, true );`}
${computations.length && `this._recompute( {}, this._state, {}, true );`}
${options.dev &&
Array.from(generator.expectedProperties).map(
prop =>
@ -259,9 +228,20 @@ export default function dom(
@assign( ${prototypeBase}, ${proto});
${name}.prototype._set = function _set ( newState ) {
${_set}
};
${options.dev && deindent`
${name}.prototype._checkReadOnly = function _checkReadOnly ( newState ) {
${Array.from(generator.readonly).map(
prop =>
`if ( '${prop}' in newState && !this._updatingReadonlyProperty ) throw new Error( "Cannot set read-only property '${prop}'" );`
)}
};
`}
${computations.length ? deindent`
${name}.prototype._recompute = function _recompute ( changed, state, oldState, isInitial ) {
${computationBuilder}
}
` : (!sharedPath && `${name}.prototype._recompute = @noop;`)}
${templateProperties.setup && `@template.setup( ${name} );`}
`);

@ -462,7 +462,7 @@ export default function preprocess(
generator.blocks.push(block);
preprocessChildren(generator, block, state, node, false, [], true, null);
block.hasUpdateMethod = block.dependencies.size > 0;
block.hasUpdateMethod = true;
return { block, state };
}

@ -13,7 +13,7 @@ export default function visitMustacheTag(
const name = node._state.name;
const value = block.getUniqueName(`${name}_value`);
const { snippet } = block.contextualise(node.expression);
const { dependencies, snippet } = block.contextualise(node.expression);
block.addVariable(value);
block.addElement(
@ -26,9 +26,13 @@ export default function visitMustacheTag(
true
);
block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) {
${name}.data = ${value};
}
`);
if (dependencies.length) {
const changedCheck = dependencies.map(dependency => `'${dependency}' in changed`).join(' || ');
block.builders.update.addBlock(deindent`
if ( ( ${changedCheck} ) && ${value} !== ( ${value} = ${snippet} ) ) {
${name}.data = ${value};
}
`);
}
}

@ -26,25 +26,23 @@ export function differs(a, b) {
return a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
export function dispatchObservers(component, group, newState, oldState) {
export function dispatchObservers(component, group, changed, newState, oldState) {
for (var key in group) {
if (!(key in newState)) continue;
if (!(key in changed)) continue;
var newValue = newState[key];
var oldValue = oldState[key];
if (differs(newValue, oldValue)) {
var callbacks = group[key];
if (!callbacks) continue;
var callbacks = group[key];
if (!callbacks) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
for (var i = 0; i < callbacks.length; i += 1) {
var callback = callbacks[i];
if (callback.__calling) continue;
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
callback.__calling = true;
callback.call(component, newValue, oldValue);
callback.__calling = false;
}
}
}
@ -133,6 +131,34 @@ export function set(newState) {
this._root._lock = false;
}
export function _set(newState) {
var oldState = this._state,
changed = {},
dirty = false;
for (var key in newState) {
if (differs(newState[key], oldState[key])) changed[key] = dirty = true;
}
if (!dirty) return;
this._state = assign({}, oldState, newState);
this._recompute(changed, this._state, oldState, false);
dispatchObservers(this, this._observers.pre, changed, newState, oldState);
this._fragment.update(changed, this._state);
dispatchObservers(this, this._observers.post, changed, newState, oldState);
}
export function _setDev(newState) {
if (typeof newState !== 'object') {
throw new Error(
'Component .set was called without an object of data key-values to update.'
);
}
this._checkReadOnly(newState);
_set.call(this, newState);
}
export function callAll(fns) {
while (fns && fns.length) fns.pop()();
}
@ -144,7 +170,9 @@ export var proto = {
observe: observe,
on: on,
set: set,
teardown: destroy
teardown: destroy,
_recompute: noop,
_set: _set
};
export var protoDev = {
@ -154,5 +182,7 @@ export var protoDev = {
observe: observeDev,
on: onDev,
set: set,
teardown: destroyDev
teardown: destroyDev,
_recompute: noop,
_set: _setDev
};

@ -1,7 +1,7 @@
{{#each things as thing}}
<Visibility bind:isVisible="visibilityMap[thing]">
<p>{{thing}} ({{visibilityMap[thing]}})</p>
</Visibility>
</Visibility>
{{/each}}
<script>

@ -0,0 +1,24 @@
export default {
data: {
x: 1,
y: 2
},
html: `
<p>1</p>
<p>2</p>
`,
test(assert, component) {
global.count = 0;
component.set({ x: 3 });
assert.equal(global.count, 0);
component.set({ x: 4, y: 5 });
assert.equal(global.count, 1);
component.set({ x: 5, y: 5 });
assert.equal(global.count, 1);
}
};

@ -0,0 +1,13 @@
<p>{{x}}</p>
<p>{{myHelper(y)}}</p>
<script>
export default {
helpers: {
myHelper(value) {
global.count += 1;
return value;
}
}
};
</script>
Loading…
Cancel
Save