refactor(render): don’t store DOM nodes but store strings for big ProtoViews.

Also inserts comment nodes before/after projected nodes so that text nodes don’t get merged when we serialize/deserialize them.

Closes #3356
First part of #3364
This commit is contained in:
Tobias Bosch
2015-07-29 17:41:09 -07:00
parent c08403935f
commit 0dbdd5cd3c
14 changed files with 250 additions and 40 deletions

View File

@ -238,6 +238,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
void setChecked(InputElement el, bool isChecked) {
el.checked = isChecked;
}
Comment createComment(String text) {
return new Comment(text);
}
TemplateElement createTemplate(String html) {
var t = new TemplateElement();
// We do not sanitize because templates are part of the application code
@ -341,6 +344,9 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
Node importIntoDoc(Node node) {
return document.importNode(node, true);
}
Node adoptNode(Node node) {
return document.adoptNode(node);
}
bool isPageRule(CssRule rule) => rule is CssPageRule;
bool isStyleRule(CssRule rule) => rule is CssStyleRule;
bool isMediaRule(CssRule rule) => rule is CssMediaRule;

View File

@ -153,6 +153,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
setValue(el, value: string) { el.value = value; }
getChecked(el): boolean { return el.checked; }
setChecked(el, value: boolean) { el.checked = value; }
createComment(text: string): Comment { return document.createComment(text); }
createTemplate(html): HTMLElement {
var t = document.createElement('template');
t.innerHTML = html;
@ -238,6 +239,7 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
return document.importNode(toImport, true);
}
adoptNode(node: Node): any { return document.adoptNode(node); }
isPageRule(rule): boolean { return rule.type === CSSRule.PAGE_RULE; }
isStyleRule(rule): boolean { return rule.type === CSSRule.STYLE_RULE; }
isMediaRule(rule): boolean { return rule.type === CSSRule.MEDIA_RULE; }

View File

@ -69,6 +69,7 @@ export class DomAdapter {
setValue(el, value: string) { throw _abstract(); }
getChecked(el): boolean { throw _abstract(); }
setChecked(el, value: boolean) { throw _abstract(); }
createComment(text: string): any { throw _abstract(); }
createTemplate(html): HTMLElement { throw _abstract(); }
createElement(tagName, doc = null): HTMLElement { throw _abstract(); }
createTextNode(text: string, doc = null): Text { throw _abstract(); }
@ -110,6 +111,7 @@ export class DomAdapter {
hasShadowRoot(node): boolean { throw _abstract(); }
isShadowRoot(node): boolean { throw _abstract(); }
importIntoDoc /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); }
adoptNode /*<T extends Node>*/ (node: Node /*T*/): Node /*T*/ { throw _abstract(); }
isPageRule(rule): boolean { throw _abstract(); }
isStyleRule(rule): boolean { throw _abstract(); }
isMediaRule(rule): boolean { throw _abstract(); }

View File

@ -184,6 +184,7 @@ class Html5LibDomAdapter implements DomAdapter {
setChecked(el, bool value) {
throw 'not implemented';
}
createComment(String text) => new Comment(text);
createTemplate(String html) => createElement('template')..innerHtml = html;
createElement(tagName, [doc]) {
return new Element.tag(tagName);
@ -292,6 +293,9 @@ class Html5LibDomAdapter implements DomAdapter {
importIntoDoc(node) {
throw 'not implemented';
}
adoptNode(node) {
throw 'not implemented';
}
bool isPageRule(rule) {
throw 'not implemented';
}

View File

@ -77,6 +77,9 @@ export class Parse5DomAdapter extends DomAdapter {
return res;
}
elementMatches(node, selector: string, matcher = null): boolean {
if (!selector || selector === '*') {
return true;
}
var result = false;
if (selector && selector.charAt(0) == "#") {
result = this.getAttribute(node, 'id') == selector.substring(1);
@ -252,6 +255,7 @@ export class Parse5DomAdapter extends DomAdapter {
setValue(el, value: string) { el.value = value; }
getChecked(el): boolean { return el.checked; }
setChecked(el, value: boolean) { el.checked = value; }
createComment(text: string): Comment { return treeAdapter.createCommentNode(text); }
createTemplate(html): HTMLElement {
var template = treeAdapter.createElement("template", 'http://www.w3.org/1999/xhtml', []);
var content = parser.parseFragment(html);
@ -447,6 +451,7 @@ export class Parse5DomAdapter extends DomAdapter {
hasShadowRoot(node): boolean { return isPresent(node.shadowRoot); }
isShadowRoot(node): boolean { return this.getShadowRoot(node) == node; }
importIntoDoc(node): any { return this.clone(node); }
adoptNode(node): any { return node; }
isPageRule(rule): boolean {
return rule.type === 6; // CSSRule.PAGE_RULE
}

View File

@ -12,6 +12,8 @@ export const EVENT_TARGET_SEPARATOR = ':';
export const NG_CONTENT_ELEMENT_NAME = 'ng-content';
export const NG_SHADOW_ROOT_ELEMENT_NAME = 'shadow-root';
const MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE = 20;
var CAMEL_CASE_REGEXP = /([A-Z])/g;
var DASH_CASE_REGEXP = /-([a-z])/g;
@ -57,8 +59,7 @@ export class ClonedProtoView {
export function cloneAndQueryProtoView(pv: DomProtoView, importIntoDocument: boolean):
ClonedProtoView {
var templateContent = importIntoDocument ? DOM.importIntoDoc(DOM.content(pv.rootElement)) :
DOM.clone(DOM.content(pv.rootElement));
var templateContent = pv.cloneableTemplate.clone(importIntoDocument);
var boundElements = queryBoundElements(templateContent, pv.isSingleElementFragment);
var boundTextNodes = queryBoundTextNodes(templateContent, pv.rootTextNodeIndices, boundElements,
@ -140,3 +141,45 @@ export function prependAll(parentNode: Node, nodes: Node[]) {
lastInsertedNode = node;
});
}
export interface CloneableTemplate { clone(importIntoDoc: boolean): Node; }
export class SerializedCloneableTemplate implements CloneableTemplate {
templateString: string;
constructor(templateRoot: Element) { this.templateString = DOM.getInnerHTML(templateRoot); }
clone(importIntoDoc: boolean): Node {
var result = DOM.content(DOM.createTemplate(this.templateString));
if (importIntoDoc) {
result = DOM.adoptNode(result);
}
return result;
}
}
export class ReferenceCloneableTemplate implements CloneableTemplate {
constructor(public templateRoot: Element) {}
clone(importIntoDoc: boolean): Node {
if (importIntoDoc) {
return DOM.importIntoDoc(DOM.content(this.templateRoot));
} else {
return DOM.clone(DOM.content(this.templateRoot));
}
}
}
export function prepareTemplateForClone(templateRoot: Element): CloneableTemplate {
var root = DOM.content(templateRoot);
var elementCount = DOM.querySelectorAll(root, '*').length;
var firstChild = DOM.firstChild(root);
var forceSerialize =
isPresent(firstChild) && DOM.isCommentNode(firstChild) ? DOM.nodeValue(firstChild) : null;
if (forceSerialize == 'nocache') {
return new SerializedCloneableTemplate(templateRoot);
} else if (forceSerialize == 'cache') {
return new ReferenceCloneableTemplate(templateRoot);
} else if (elementCount > MAX_IN_MEMORY_ELEMENTS_PER_TEMPLATE) {
return new SerializedCloneableTemplate(templateRoot);
} else {
return new ReferenceCloneableTemplate(templateRoot);
}
}

View File

@ -5,6 +5,8 @@ import {RenderProtoViewRef, ViewType, ViewEncapsulation} from '../../api';
import {DOM} from 'angular2/src/dom/dom_adapter';
import {prepareTemplateForClone, CloneableTemplate} from '../util';
export function resolveInternalDomProtoView(protoViewRef: RenderProtoViewRef): DomProtoView {
return (<DomProtoViewRef>protoViewRef)._protoView;
}
@ -25,12 +27,12 @@ export class DomProtoView {
var isSingleElementFragment = fragmentsRootNodeCount.length === 1 &&
fragmentsRootNodeCount[0] === 1 &&
DOM.isElementNode(DOM.firstChild(DOM.content(rootElement)));
return new DomProtoView(type, rootElement, viewEncapsulation, elementBinders, hostAttributes,
rootTextNodeIndices, boundTextNodeCount, fragmentsRootNodeCount,
isSingleElementFragment);
return new DomProtoView(type, prepareTemplateForClone(rootElement), viewEncapsulation,
elementBinders, hostAttributes, rootTextNodeIndices, boundTextNodeCount,
fragmentsRootNodeCount, isSingleElementFragment);
}
constructor(public type: ViewType, public rootElement: Element,
constructor(public type: ViewType, public cloneableTemplate: CloneableTemplate,
public encapsulation: ViewEncapsulation,
public elementBinders: List<DomElementBinder>,
public hostAttributes: Map<string, string>, public rootTextNodeIndices: number[],

View File

@ -251,6 +251,7 @@ function appendComponentNodesToHost(hostProtoView: ClonedProtoView, binderIdx: n
function projectMatchingNodes(selector: string, contentElement: Element, nodes: Node[]): Node[] {
var remaining = [];
DOM.insertBefore(contentElement, DOM.createComment('['));
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var matches = false;
@ -265,6 +266,7 @@ function projectMatchingNodes(selector: string, contentElement: Element, nodes:
remaining.push(node);
}
}
DOM.insertBefore(contentElement, DOM.createComment(']'));
DOM.remove(contentElement);
return remaining;
}

View File

@ -77,6 +77,8 @@ export function stringifyElement(el): string {
if (!ListWrapper.contains(_singleTagWhitelist, tagName)) {
result += `</${tagName}>`;
}
} else if (DOM.isCommentNode(el)) {
result += `<!--${DOM.nodeValue(el)}-->`;
} else {
result += DOM.getText(el);
}