refactor(ivy): Add style reconciliation algorithm (#34004)

This change introduces class/style reconciliation algorithm for DOM elements.
NOTE: The code is not yet hooked up, it will be used by future style algorithm.

Background:
Styling algorithm currently has [two paths](https://hackmd.io/@5zDGNGArSxiHhgvxRGrg-g/rycZk3N5S)
when computing how the style should be rendered.
1. A direct path which concatenates styling and uses `elemnent.className`/`element.style.cssText` and
2. A merge path which uses internal data structures and uses `element.classList.add/remove`/`element.style[property]`.

The situation is confusing and hard to follow/maintain. So a future PR will remove the merge-path and do everything with
direct-path. This however breaks when some other code adds class or style to the element without Angular's knowledge.
If this happens instead of switching from direct-path to merge-path algorithm, this change provides a different mental model
whereby we always do `direct-path` but the code which writes to the DOM detects the situation and reconciles the out of bound write.

The reconciliation process is as follows:
1. Detect that no one has modified `className`/`cssText` and if so just write directly (fast path).
2. If out of bounds write did occur, switch from writing using `className`/`cssText` to `element.classList.add/remove`/`element.style[property]`.
   This does require that the write function computes the difference between the previous Angular expected state and current Angular state.
   (This requires a parser. The advantage of having a parser is that we can support `style="width: {{exp}}px" kind of bindings.`)
   Compute the diff and apply it in non destructive way using `element.classList.add/remove`/`element.style[property]`

Properties of approach:
- If no out of bounds style modification:
  - Very fast code path: Just concatenate string in right order and write them to DOM.
  - Class list order is preserved
- If out of bounds style modification detected:
  - Penalty for parsing
  - Switch to non destructive modification: `element.classList.add/remove`/`element.style[property]`
  - Switch to alphabetical way of setting classes.

PR Close #34004
This commit is contained in:
Misko Hevery
2019-11-22 20:40:29 -08:00
committed by Miško Hevery
parent 622737cee3
commit b57f7c7776
15 changed files with 1110 additions and 4 deletions

View File

@ -9,6 +9,7 @@ ts_library(
),
deps = [
"//packages/core",
"//packages/core/src/util",
"@npm//@types/jasmine",
"@npm//@types/node",
],
@ -215,3 +216,16 @@ ng_benchmark(
name = "duplicate_map_based_style_and_class_bindings",
bundle = ":duplicate_map_based_style_and_class_bindings_lib",
)
ng_rollup_bundle(
name = "split_class_list_lib",
entry_point = ":split_class_list.ts",
deps = [
":perf_lib",
],
)
ng_benchmark(
name = "split_class_list",
bundle = ":split_class_list_lib",
)

View File

@ -65,7 +65,7 @@ export function createBenchmark(benchmarkName: string): Benchmark {
if (!runAgain) {
// tslint:disable-next-line:no-console
console.log(
` ${formatTime(iterationTime_ms)} (count: ${profile.sampleCount}, iterations: ${profile.iterationCount})`);
` ${formatTime(profile.bestTime)} (count: ${profile.sampleCount}, iterations: ${profile.iterationCount})`);
}
}
iterationCounter = profile.iterationCount;

View File

@ -0,0 +1,64 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {processClassToken, splitClassList} from '@angular/core/src/render3/styling/class_differ';
import {createBenchmark} from './micro_bench';
const benchmark = createBenchmark('split_class_list');
const splitTime = benchmark('String.split(" ")');
const splitRegexpTime = benchmark('String.split(/\\s+/)');
const splitClassListTime = benchmark('splitClassList');
const LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const CLASSES: string[] = [LETTERS];
for (let i = 0; i < LETTERS.length; i++) {
CLASSES.push(LETTERS.substring(0, i) + ' ' + LETTERS.substring(i, LETTERS.length));
}
let index = 0;
let changes = new Map<string, boolean|null>();
let parts: string[] = [];
while (splitTime()) {
changes = clearArray(changes);
const classes = CLASSES[index++];
parts = classes.split(' ');
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part !== '') {
processClassToken(changes, part, false);
}
}
if (index === CLASSES.length) index = 0;
}
const WHITESPACE = /\s+/m;
while (splitRegexpTime()) {
changes = clearArray(changes);
const classes = CLASSES[index++];
parts = classes.split(WHITESPACE);
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part !== '') {
processClassToken(changes, part, false);
}
}
if (index === CLASSES.length) index = 0;
}
while (splitClassListTime()) {
changes = clearArray(changes);
splitClassList(CLASSES[index++], changes, false);
if (index === CLASSES.length) index = 0;
}
benchmark.report();
function clearArray(a: Map<any, any>): any {
a.clear();
}