Skip to content

Commit

Permalink
fix(render): update sheduler refactor for performance boost
Browse files Browse the repository at this point in the history
Move from recursion to interation with deferred  invalidate event
  • Loading branch information
smalluban committed May 16, 2019
1 parent 9c5093a commit 0ed17e4
Show file tree
Hide file tree
Showing 9 changed files with 120 additions and 73 deletions.
2 changes: 1 addition & 1 deletion docs/core-concepts/descriptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ You can use `connect` to attach event listeners, initialize property value (usin
myElement.addEventListener('@invalidate', () => { ...});
```

When property cache value invalidates, change detection dispatches `@invalidate` custom event (composed and bubbling). You can listen to this event and observe changes in the element properties. It is dispatched implicit when you set new value by the assertion or explicit by calling `invalidate` in `connect` callback. The event type was chosen to avoid name collision with those created by the custom elements authors.
When property cache value invalidates, change detection dispatches `@invalidate` custom event (bubbling). You can listen to this event and observe changes in the element properties. It is dispatched implicit when you set new value by the assertion or explicit by calling `invalidate` in `connect` callback. The event type was chosen to avoid name collision with those created by the custom elements authors.

If the third party code is responsible for the property value, you can use `invalidate` callback to update it and trigger event dispatch. For example, it can be used to connect to async web APIs or external libraries:

Expand Down
4 changes: 3 additions & 1 deletion src/children.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { deferred } from './utils';

function walk(node, fn, options, items = []) {
Array.from(node.children).forEach((child) => {
const hybrids = child.constructor.hybrids;
Expand All @@ -24,7 +26,7 @@ export default function children(hybridsOrFn, options = { deep: false, nested: f

const childEventListener = ({ target }) => {
if (!set.size) {
Promise.resolve().then(() => {
deferred.then(() => {
const list = host[key];
for (let i = 0; i < list.length; i += 1) {
if (set.has(list[i])) {
Expand Down
15 changes: 12 additions & 3 deletions src/define.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import property from './property';
import render from './render';

import * as cache from './cache';
import { dispatch, pascalToDash } from './utils';
import { dispatch, pascalToDash, deferred } from './utils';

/* istanbul ignore next */
try { process.env.NODE_ENV } catch(e) { var process = { env: { NODE_ENV: 'production' } }; } // eslint-disable-line

const dispatchSet = new Set();

function dispatchInvalidate(host) {
dispatch(host, '@invalidate', { bubbles: true, composed: true });
if (!dispatchSet.size) {
deferred.then(() => {
dispatchSet.forEach(target => dispatch(target, '@invalidate', { bubbles: true }));
dispatchSet.clear();
});
}

dispatchSet.add(host);
}

const defaultMethod = (host, value) => value;
Expand Down Expand Up @@ -73,7 +82,7 @@ if (process.env.NODE_ENV !== 'production') {
const updateQueue = new Map();
update = (Hybrid, lastHybrids) => {
if (!updateQueue.size) {
Promise.resolve().then(() => {
deferred.then(() => {
walkInShadow(document.body, (node) => {
if (updateQueue.has(node.constructor)) {
const hybrids = updateQueue.get(node.constructor);
Expand Down
20 changes: 5 additions & 15 deletions src/parent.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
const map = new WeakMap();

document.addEventListener('@invalidate', (event) => {
const set = map.get(event.composedPath()[0]);
if (set) set.forEach(fn => fn());
});

function walk(node, fn) {
let parentElement = node.parentElement || node.parentNode.host;

Expand All @@ -28,18 +21,15 @@ export default function parent(hybridsOrFn) {
get: host => walk(host, fn),
connect(host, key, invalidate) {
const target = host[key];
const cb = (event) => {
if (event.target === target) invalidate(false);
};

if (target) {
let set = map.get(target);
if (!set) {
set = new Set();
map.set(target, set);
}

set.add(invalidate);
target.addEventListener('@invalidate', cb);

return () => {
set.delete(invalidate);
target.removeEventListener('@invalidate', cb);
invalidate();
};
}
Expand Down
84 changes: 54 additions & 30 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,74 @@
import { shadyCSS } from './utils';
import { deferred, shadyCSS } from './utils';

const map = new WeakMap();
const cache = new WeakMap();
const FPS_THRESHOLD = 1000 / 60; // 60 FPS ~ 16,67ms time window

let queue = [];
let index = 0;
let startTime = 0;

const FPS_THRESHOLD = 1000 / 60; // 60 FPS ~ 16,67ms time window

export function update() {
try {
if (!startTime) {
startTime = performance.now();
}

for (; index < queue.length; index += 1) {
const target = queue[index];

export function update(index = 0, startTime = 0) {
if (startTime && (performance.now() - startTime > FPS_THRESHOLD)) {
requestAnimationFrame(() => update(index));
} else {
const target = queue[index];
const nextTime = performance.now();

if (!target) {
shadyCSS(shady => queue.forEach(t => shady.styleSubtree(t)));
queue = [];
} else {
if (map.has(target)) {
const key = map.get(target);
const prevUpdate = cache.get(target);
try {
const nextUpdate = target[key];
if (nextUpdate !== prevUpdate) {
cache.set(target, nextUpdate);
nextUpdate();
if (!prevUpdate) shadyCSS(shady => shady.styleElement(target));
const nextUpdate = target[key];

if (nextUpdate !== prevUpdate) {
cache.set(target, nextUpdate);
nextUpdate();
if (!prevUpdate) {
shadyCSS(shady => shady.styleElement(target));
} else {
shadyCSS(shady => shady.styleSubtree(target));
}
} catch (e) {
update(index + 1, nextTime);
throw e;
}

if (index + 1 < queue.length && (performance.now() - startTime) > FPS_THRESHOLD) {
throw queue;
}
}
update(index + 1, nextTime);
}

queue = [];
index = 0;
deferred.then(() => { startTime = 0; });
} catch (e) {
index += 1;
requestAnimationFrame(update);
deferred.then(() => { startTime = 0; });

if (e !== queue) throw e;
}
}

function addToQueue(event) {
const target = event.composedPath()[0];
if (target === event.currentTarget) {
if (!queue[0]) {
requestAnimationFrame((() => update()));
}
if (queue.indexOf(target) === -1) {
queue.push(target);
if (event.target === event.currentTarget && map.has(event.target)) {
if (!startTime) {
if (!queue.length) {
requestAnimationFrame(update);
} else {
queue.splice(index, 0, event.target);
return;
}
} else if (!queue.length) {
if ((performance.now() - startTime) > FPS_THRESHOLD) {
requestAnimationFrame(update);
} else {
deferred.then(update);
}
}

queue.push(event.target);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export function stringifyElement(element) {
}

export const IS_IE = 'ActiveXObject' in window;

export const deferred = Promise.resolve();
4 changes: 2 additions & 2 deletions test/spec/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe('children:', () => {
});
}));

it('updates parent computed property', done => tree((el) => {
it('updates parent computed property', done => tree(el => resolveTimeout(() => {
expect(el.customName).toBe('one');
el.children[0].customName = 'four';
let called = false;
Expand All @@ -88,7 +88,7 @@ describe('children:', () => {
expect(el.customName).toBe('four');
done();
});
}));
})));
});

describe('function condition', () => {
Expand Down
27 changes: 18 additions & 9 deletions test/spec/parent.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,17 +86,26 @@ describe('parent:', () => {
expect(el.parent).toBe(null);
}));

it('updates child computed property', () => directParentTree((el) => {
const spy = jasmine.createSpy('event callback');
const child = el.children[0];
it('updates child computed property', done => directParentTree((el) => {
Promise.resolve().then(() => {
const spy = jasmine.createSpy('event callback');
const child = el.children[0];

expect(el.customProperty).toBe('value');
expect(child.computed).toBe('value other value');

expect(el.customProperty).toBe('value');
expect(child.computed).toBe('value other value');
child.addEventListener('@invalidate', spy);

child.addEventListener('@invalidate', spy);
el.customProperty = 'new value';
el.customProperty = 'new value';

expect(child.computed).toBe('new value other value');
expect(spy).toHaveBeenCalledTimes(1);
Promise.resolve().then(() => {
Promise.resolve().then(() => {
expect(spy).toHaveBeenCalledTimes(1);
expect(child.computed).toBe('new value other value');

done();
});
});
});
}));
});
35 changes: 23 additions & 12 deletions test/spec/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,13 @@ describe('render:', () => {
});

test('<test-render-throws-in-render></test-render-throws-in-render>')(() => {
expect(() => {
update();
}).toThrow();
fn = () => {};
done();
Promise.resolve().then(() => {
expect(() => {
update();
}).toThrow();
fn = () => {};
done();
});
});
});

Expand All @@ -126,11 +128,13 @@ describe('render:', () => {
});

test('<test-render-throws-in-callback></test-render-throws-in-callback>')(() => {
expect(() => {
update();
}).toThrow();
fn = () => {};
done();
Promise.resolve().then(() => {
expect(() => {
update();
}).toThrow();
fn = () => {};
done();
});
});
});

Expand Down Expand Up @@ -239,11 +243,18 @@ describe('render:', () => {
}
});

it('uses styleElement and styleSubtree', done => tree(() => resolveTimeout(() => {
it('uses styleElement on first paint', done => tree(() => resolveTimeout(() => {
expect(window.ShadyCSS.styleElement).toHaveBeenCalled();
expect(window.ShadyCSS.styleSubtree).toHaveBeenCalled();
done();
})));

it('uses styleSubtree on sequential paint', done => tree(el => resolveRaf(() => {
el.value = 1;
return resolveTimeout(() => {
expect(window.ShadyCSS.styleSubtree).toHaveBeenCalled();
done();
});
})));
});

describe('options object with shadowRoot option', () => {
Expand Down

0 comments on commit 0ed17e4

Please # to comment.