Merge pull request #4306 from Polymer/2.0-preview-debouncer-classes

Clean up Async and Debouncer interfaces with ES6
This commit is contained in:
Kevin Schaaf 2017-02-24 14:52:09 -08:00 committed by GitHub
commit af5eae716c
3 changed files with 125 additions and 195 deletions

View File

@ -10,106 +10,118 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<link rel="import" href="boot.html">
<script>
(function(global) {
(function () {
'use strict';
Polymer.Async = {};
/** @typedef {{run: function(function(), number=):number, cancel: function(number)}} */
let AsyncInterface; // eslint-disable-line no-unused-vars
/**
* A timer with the async interface.
* @implements {AsyncInterface}
*/
Polymer.Async.timeOut = {
after: function(delay) {
return delay === 0 ? Polymer.Async.timeOut :
{
run: function(fn) {
return global.setTimeout(fn, delay);
},
cancel: global.clearTimeout.bind(global)
}
let timeOut = {
run(fn, delay = 0) {
return window.setTimeout(fn, delay);
},
run: global.setTimeout.bind(global),
cancel: global.clearTimeout.bind(global)
};
/**
* requestAnimationFrame with the async interface.
*/
Polymer.Async.animationFrame = {
run: global.requestAnimationFrame.bind(global),
cancel: global.cancelAnimationFrame.bind(global)
};
/**
* requestIdleCallback with the async interface.
*/
Polymer.Async.idlePeriod = {
run(fn) {
return global.requestIdleCallback ? global.requestIdleCallback(fn) : global.setTimeout(fn, 16);
},
cancel(timer) {
return global.cancelIdleCallback ? global.cancelIdleCallback(timer) : global.clearTimeout(timer);
cancel(handle) {
window.clearTimeout(handle);
}
};
/**
* Micro task with the async interface.
* @param {number} wait
* @return {!AsyncInterface}
*/
Polymer.Async.microTask = {
_currVal: 0,
_lastVal: 0,
_callbacks: [],
_twiddleContent: 0,
_twiddle: document.createTextNode(''),
timeOut.after = function (wait) {
let after = {
run(fn) {
return window.setTimeout(fn, wait);
},
cancel: timeOut.cancel
}
return after;
};
/**
* requestAnimationFrame with the async interface.
* @implements {AsyncInterface}
*/
let animationFrame = {
run(fn) {
return window.requestAnimationFrame(fn);
},
cancel(handle) {
return window.cancelAnimationFrame(handle);
}
};
/**
* requestIdleCallback with the async interface.
* @implements {AsyncInterface}
*/
let idlePeriod = window.requestIdleCallback ? {
run(fn) {
return window.requestIdleCallback(fn);
},
cancel(handle) {
return window.cancelIdleCallback(handle);
}
} : timeOut.after(16);
/**
* Micro task with the async interface.
* @implements {AsyncInterface}
*/
class MicroTask {
constructor() {
this._currVal = 0;
this._lastVal = 0;
this._callbacks = [];
this._twiddleContent = 0;
this._twiddle = document.createTextNode('');
new MutationObserver(() => {
this._atEndOfMicrotask();
}).observe(this._twiddle, { characterData: true });
}
run(callback) {
this._twiddle.textContent = this._twiddleContent++;
this._callbacks.push(callback);
return this._currVal++;
},
}
cancel(handle) {
const idx = handle - this._lastVal;
if (idx >= 0) {
if (!this._callbacks[idx]) {
throw 'invalid async handle: ' + handle;
throw `invalid async handle: ${handle}`;
}
this._callbacks[idx] = null;
}
},
}
_atEndOfMicrotask() {
const len = this._callbacks.length;
for (let i=0; i<len; i++) {
for (let i = 0; i < len; i++) {
let cb = this._callbacks[i];
if (cb) {
try {
cb();
} catch(e) {
// Clear queue up to this point & start over after throwing
i++;
this._callbacks.splice(0, i);
this._lastVal += i;
this._twiddle.textContent = this._twiddleContent++;
throw e;
} catch (e) {
setTimeout(() => { throw e });
}
}
}
this._callbacks.splice(0, len);
this._lastVal += len;
},
flush() {
this._observer.takeRecords();
this._atEndOfMicrotask();
}
}
/** @type {Object<string, !AsyncInterface>} */
Polymer.Async = {
timeOut,
animationFrame,
idlePeriod,
microTask: new MicroTask()
};
Polymer.Async.microTask._observer = new window.MutationObserver(function microTaskObserver() {
Polymer.Async.microTask._atEndOfMicrotask();
});
Polymer.Async.microTask._observer.observe(Polymer.Async.microTask._twiddle, {characterData: true});
})(this);
</script>
})();
</script>

View File

@ -15,30 +15,31 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
(function() {
'use strict';
/** @constructor */
Polymer.Debouncer = function Debouncer() {
this._asyncModule = null;
this._callback = null;
this._timer = null;
this.flush = this.flush.bind(this);
};
/** @typedef {{run: function(function(), number=):number, cancel: function(number)}} */
let AsyncModule; // eslint-disable-line no-unused-vars
Polymer.mixin(Polymer.Debouncer.prototype, {
class Debouncer {
constructor() {
this._asyncModule = null;
this._callback = null;
this._timer = null;
}
/**
* Sets the scheduler; that is, a module with the Async interface,
* a callback and optional arguments to be passed to the run function
* from the async module.
*
* @param {{run: function, cancel: function}} asyncModule
* @param {function} callback
* @param {Array=}
* @param {!AsyncModule} asyncModule
* @param {function()} callback
*/
setConfig(asyncModule, cb) {
this._asyncModule = asyncModule;
this._callback = cb;
this._timer = this._asyncModule.run(this.flush);
},
this._timer = this._asyncModule.run(() => {
this._timer = null;
this._callback()
});
}
/**
* Cancels an active debouncer and returns a reference to itself.
*/
@ -47,8 +48,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
this._asyncModule.cancel(this._timer);
this._timer = null;
}
},
}
/**
* Flushes an active debouncer and returns a reference to itself.
*/
@ -57,8 +57,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
this.cancel();
this._callback();
}
},
}
/**
* Returns true if the debouncer is active.
*
@ -67,26 +66,26 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
isActive() {
return this._timer != null;
}
});
/**
* Creates a debouncer if no debouncer is passed as a parameter
* or it cancels an active debouncer otherwise.
*
* @param {Polymer.Debouncer?} debouncer
* @param {{run: function, cancel: function}} asyncModule
* @param {function} cb
* @return {Polymer.Debouncer}
* @param {!AsyncModule} asyncModule
* @param {function()} cb
* @return {!Debouncer}
*/
Polymer.Debouncer.debounce = function debounce(debouncer, asyncModule, cb) {
if (debouncer instanceof Polymer.Debouncer) {
debouncer.cancel();
} else {
debouncer = new Polymer.Debouncer();
static debounce(debouncer, asyncModule, cb) {
if (debouncer instanceof Debouncer) {
debouncer.cancel();
} else {
debouncer = new Debouncer();
}
debouncer.setConfig(asyncModule, cb);
return debouncer;
}
debouncer.setConfig(asyncModule, cb);
return debouncer;
};
}
Polymer.Debouncer = Debouncer;
})();
</script>

View File

@ -12,6 +12,18 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
<head>
<meta charset="utf-8">
<script src="../../../webcomponentsjs/webcomponents-lite.js"></script>
<script>
var capturedErrors = [];
var captureEnabled = false;
window.addEventListener('error', function(e) {
if (captureEnabled) {
capturedErrors.push(e);
e.stopImmediatePropagation();
e.preventDefault();
return true;
}
});
</script>
<script src="../../../web-component-tester/browser.js"></script>
<link rel="import" href="../../src/utils/async.html">
</head>
@ -72,8 +84,8 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
});
});
test('Errors are thrown but the queue is continued in' +
' another microtask.', function(done) {
test('Errors are thrown but the queue is continued', function(done) {
captureEnabled = true;
var callCount1 = 0;
var callCount2 = 0;
var callCount3 = 0;
@ -101,112 +113,19 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
Polymer.Async.microTask.run(callback2);
Polymer.Async.microTask.run(callback3);
Polymer.Async.microTask.run(callback4);
// Manually flush the queue so the error can be caught.
assert.throws(function() {
Polymer.Async.microTask.flush();
});
// Only `callback1` and `callback2` were called during the last flush.
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 0);
assert.equal(callCount4, 0);
assert.equal(callCount5, 0);
// Manually flush the queue so the error can be caught.
assert.throws(function() {
Polymer.Async.microTask.flush();
});
// Only `callback3` was called during the last flush.
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 1);
assert.equal(callCount4, 0);
assert.equal(callCount5, 0);
// All callbacks have been called by the next task.
setTimeout(function() {
captureEnabled = false;
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 1);
assert.equal(callCount4, 1);
assert.equal(callCount5, 1);
assert.equal(capturedErrors.length, 2);
capturedErrors.length = 0;
done();
});
}, 100);
});
test('`flush` synchronously runs all functions queued with microtask' +
' timing.', function() {
var callCount1 = 0;
var callCount2 = 0;
var callCount3 = 0;
var callback1 = function() {
callCount1++;
};
var callback2 = function() {
callCount2++;
};
var callback3 = function() {
callCount3++;
};
Polymer.Async.microTask.run(callback1);
Polymer.Async.microTask.run(callback2);
Polymer.Async.microTask.run(callback3);
Polymer.Async.microTask.flush();
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 1);
});
test('`flush` rethrows any error thrown by a dequeued callback and leaves' +
' any remaining callbacks in the queue.', function(done) {
var callCount1 = 0;
var callCount2 = 0;
var callCount3 = 0;
var callback1Error = new Error("callback1Error");
var callback1 = function() {
callCount1++;
throw callback1Error;
};
var callback2Error = new Error("callback2Error");
var callback2 = function() {
callCount2++;
throw callback2Error;
};
var callback3 = function() {
callCount3++;
};
Polymer.Async.microTask.run(callback1);
Polymer.Async.microTask.run(callback2);
Polymer.Async.microTask.run(callback3);
try {
Polymer.Async.microTask.flush();
assert.fail();
} catch (err) {
assert.equal(err, callback1Error);
}
// `callback1` throws, so items later in the queue were not dequeued or
// called during the flush.
assert.equal(callCount1, 1);
assert.equal(callCount2, 0);
assert.equal(callCount3, 0);
try {
Polymer.Async.microTask.flush();
assert.fail();
} catch (err) {
assert.equal(err, callback2Error);
}
// `callback2` throws, so items later in the queue were not dequeued or
// called during the flush.
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 0);
setTimeout(function() {
// `callback3` was called in another microtask.
assert.equal(callCount1, 1);
assert.equal(callCount2, 1);
assert.equal(callCount3, 1);
done();
});
});
});
suite('Cancelling micro tasks', function() {