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:

committed by
Miško Hevery

parent
622737cee3
commit
b57f7c7776
@ -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",
|
||||
)
|
||||
|
@ -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;
|
||||
|
64
packages/core/test/render3/perf/split_class_list.ts
Normal file
64
packages/core/test/render3/perf/split_class_list.ts
Normal 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();
|
||||
}
|
Reference in New Issue
Block a user