mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Avoid recursion when materializing DOM (Fix #3028)
This way we don't get a stack overflow when materializing nested Views. Certain browser/OS combinations seem to have particularly low budgets (especially Firefox/Windows apparently). Verified by running https://github.com/mxab/meteor-call-stack-exceed on Chrome/Mac. Nesting limit used to be about 160, but now you get unlimited nesting (tried up to 10,000, which renders in about 7-8 seconds). Tested for correctness by running all package tests.
This commit is contained in:
@@ -7,49 +7,79 @@
|
|||||||
// - `intoArray`: the array of DOM nodes and DOMRanges to push the output
|
// - `intoArray`: the array of DOM nodes and DOMRanges to push the output
|
||||||
// into (required)
|
// into (required)
|
||||||
// - `parentView`: the View we are materializing content for (optional)
|
// - `parentView`: the View we are materializing content for (optional)
|
||||||
|
//
|
||||||
|
// Returns `intoArray`, which is especially useful if you pass in `[]`.
|
||||||
Blaze._materializeDOM = function (htmljs, intoArray, parentView) {
|
Blaze._materializeDOM = function (htmljs, intoArray, parentView) {
|
||||||
|
// In order to use fewer stack frames, materializeDOMInner can push
|
||||||
|
// tasks onto `workStack`, and they will be popped off
|
||||||
|
// and run, last first, after materializeDOMInner returns. The
|
||||||
|
// reason we use a stack instead of a queue is so that we recurse
|
||||||
|
// depth-first, doing newer tasks first.
|
||||||
|
var workStack = [];
|
||||||
|
materializeDOMInner(htmljs, intoArray, parentView, workStack);
|
||||||
|
|
||||||
|
// A "task" is either an array of arguments to materializeDOM or
|
||||||
|
// a function to execute. If we only allowed functions as tasks,
|
||||||
|
// we would have to generate the functions using _.bind or close
|
||||||
|
// over a loop variable, either of which is a little less efficient.
|
||||||
|
while (workStack.length) {
|
||||||
|
// Note that running the workStack task may push new items onto
|
||||||
|
// the workStack.
|
||||||
|
var task = workStack.pop();
|
||||||
|
if (typeof task === 'function') {
|
||||||
|
task();
|
||||||
|
} else {
|
||||||
|
// assume array
|
||||||
|
materializeDOMInner(task[0], task[1], task[2], workStack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return intoArray;
|
||||||
|
};
|
||||||
|
|
||||||
|
var materializeDOMInner = function (htmljs, intoArray, parentView, workStack) {
|
||||||
if (htmljs == null) {
|
if (htmljs == null) {
|
||||||
// null or undefined
|
// null or undefined
|
||||||
return intoArray;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (typeof htmljs) {
|
switch (typeof htmljs) {
|
||||||
case 'string': case 'boolean': case 'number':
|
case 'string': case 'boolean': case 'number':
|
||||||
intoArray.push(document.createTextNode(String(htmljs)));
|
intoArray.push(document.createTextNode(String(htmljs)));
|
||||||
return intoArray;
|
return;
|
||||||
case 'object':
|
case 'object':
|
||||||
if (htmljs.htmljsType) {
|
if (htmljs.htmljsType) {
|
||||||
switch (htmljs.htmljsType) {
|
switch (htmljs.htmljsType) {
|
||||||
case HTML.Tag.htmljsType:
|
case HTML.Tag.htmljsType:
|
||||||
intoArray.push(materializeTag(htmljs, parentView));
|
intoArray.push(materializeTag(htmljs, parentView, workStack));
|
||||||
return intoArray;
|
return;
|
||||||
case HTML.CharRef.htmljsType:
|
case HTML.CharRef.htmljsType:
|
||||||
intoArray.push(document.createTextNode(htmljs.str));
|
intoArray.push(document.createTextNode(htmljs.str));
|
||||||
return intoArray;
|
return;
|
||||||
case HTML.Comment.htmljsType:
|
case HTML.Comment.htmljsType:
|
||||||
intoArray.push(document.createComment(htmljs.sanitizedValue));
|
intoArray.push(document.createComment(htmljs.sanitizedValue));
|
||||||
return intoArray;
|
return;
|
||||||
case HTML.Raw.htmljsType:
|
case HTML.Raw.htmljsType:
|
||||||
// Get an array of DOM nodes by using the browser's HTML parser
|
// Get an array of DOM nodes by using the browser's HTML parser
|
||||||
// (like innerHTML).
|
// (like innerHTML).
|
||||||
var nodes = Blaze._DOMBackend.parseHTML(htmljs.value);
|
var nodes = Blaze._DOMBackend.parseHTML(htmljs.value);
|
||||||
for (var i = 0; i < nodes.length; i++)
|
for (var i = 0; i < nodes.length; i++)
|
||||||
intoArray.push(nodes[i]);
|
intoArray.push(nodes[i]);
|
||||||
return intoArray;
|
return;
|
||||||
}
|
}
|
||||||
} else if (HTML.isArray(htmljs)) {
|
} else if (HTML.isArray(htmljs)) {
|
||||||
for (var i = 0; i < htmljs.length; i++) {
|
for (var i = htmljs.length-1; i >= 0; i--) {
|
||||||
Blaze._materializeDOM(htmljs[i], intoArray, parentView);
|
workStack.push([htmljs[i], intoArray, parentView]);
|
||||||
}
|
}
|
||||||
return intoArray;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (htmljs instanceof Blaze.Template) {
|
if (htmljs instanceof Blaze.Template) {
|
||||||
htmljs = htmljs.constructView();
|
htmljs = htmljs.constructView();
|
||||||
// fall through to Blaze.View case below
|
// fall through to Blaze.View case below
|
||||||
}
|
}
|
||||||
if (htmljs instanceof Blaze.View) {
|
if (htmljs instanceof Blaze.View) {
|
||||||
intoArray.push(Blaze._materializeView(htmljs, parentView));
|
Blaze._materializeView(htmljs, parentView, workStack, intoArray);
|
||||||
return intoArray;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +87,7 @@ Blaze._materializeDOM = function (htmljs, intoArray, parentView) {
|
|||||||
throw new Error("Unexpected object in htmljs: " + htmljs);
|
throw new Error("Unexpected object in htmljs: " + htmljs);
|
||||||
};
|
};
|
||||||
|
|
||||||
var materializeTag = function (tag, parentView) {
|
var materializeTag = function (tag, parentView, workStack) {
|
||||||
var tagName = tag.tagName;
|
var tagName = tag.tagName;
|
||||||
var elem;
|
var elem;
|
||||||
if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag))
|
if ((HTML.isKnownSVGElement(tagName) || isSVGAnchor(tag))
|
||||||
@@ -118,13 +148,20 @@ var materializeTag = function (tag, parentView) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var childNodesAndRanges = Blaze._materializeDOM(children, [], parentView);
|
if (children.length) {
|
||||||
for (var i = 0; i < childNodesAndRanges.length; i++) {
|
var childNodesAndRanges = [];
|
||||||
var x = childNodesAndRanges[i];
|
// push this function first so that it's done last
|
||||||
if (x instanceof Blaze._DOMRange)
|
workStack.push(function () {
|
||||||
x.attach(elem);
|
for (var i = 0; i < childNodesAndRanges.length; i++) {
|
||||||
else
|
var x = childNodesAndRanges[i];
|
||||||
elem.appendChild(x);
|
if (x instanceof Blaze._DOMRange)
|
||||||
|
x.attach(elem);
|
||||||
|
else
|
||||||
|
elem.appendChild(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// now push the task that calculates childNodesAndRanges
|
||||||
|
workStack.push([children, childNodesAndRanges, parentView]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return elem;
|
return elem;
|
||||||
|
|||||||
@@ -281,7 +281,46 @@ Blaze._createView = function (view, parentView, forExpansion) {
|
|||||||
Blaze._fireCallbacks(view, 'created');
|
Blaze._fireCallbacks(view, 'created');
|
||||||
};
|
};
|
||||||
|
|
||||||
Blaze._materializeView = function (view, parentView) {
|
var doFirstRender = function (view, initialContent) {
|
||||||
|
var domrange = new Blaze._DOMRange(initialContent);
|
||||||
|
view._domrange = domrange;
|
||||||
|
domrange.view = view;
|
||||||
|
view.isRendered = true;
|
||||||
|
Blaze._fireCallbacks(view, 'rendered');
|
||||||
|
|
||||||
|
var teardownHook = null;
|
||||||
|
|
||||||
|
domrange.onAttached(function attached(range, element) {
|
||||||
|
view._isAttached = true;
|
||||||
|
|
||||||
|
teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown(
|
||||||
|
element, function teardown() {
|
||||||
|
Blaze._destroyView(view, true /* _skipNodes */);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// tear down the teardown hook
|
||||||
|
view.onViewDestroyed(function () {
|
||||||
|
teardownHook && teardownHook.stop();
|
||||||
|
teardownHook = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return domrange;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Take an uncreated View `view` and create and render it to DOM,
|
||||||
|
// setting up the autorun that updates the View. Returns a new
|
||||||
|
// DOMRange, which has been associated with the View.
|
||||||
|
//
|
||||||
|
// The private arguments `_workStack` and `_intoArray` are passed in
|
||||||
|
// by Blaze._materializeDOM. If provided, then we avoid the mutual
|
||||||
|
// recursion of calling back into Blaze._materializeDOM so that deep
|
||||||
|
// View hierarchies don't blow the stack. Instead, we push tasks onto
|
||||||
|
// workStack for the initial rendering and subsequent setup of the
|
||||||
|
// View, and they are done after we return. When there is a
|
||||||
|
// _workStack, we do not return the new DOMRange, but instead push it
|
||||||
|
// into _intoArray from a _workStack task.
|
||||||
|
Blaze._materializeView = function (view, parentView, _workStack, _intoArray) {
|
||||||
Blaze._createView(view, parentView);
|
Blaze._createView(view, parentView);
|
||||||
|
|
||||||
var domrange;
|
var domrange;
|
||||||
@@ -298,20 +337,16 @@ Blaze._materializeView = function (view, parentView) {
|
|||||||
var htmljs = view._render();
|
var htmljs = view._render();
|
||||||
view._isInRender = false;
|
view._isInRender = false;
|
||||||
|
|
||||||
Tracker.nonreactive(function doMaterialize() {
|
if (! c.firstRun) {
|
||||||
var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view);
|
Tracker.nonreactive(function doMaterialize() {
|
||||||
if (c.firstRun || ! Blaze._isContentEqual(lastHtmljs, htmljs)) {
|
// re-render
|
||||||
if (c.firstRun) {
|
var rangesAndNodes = Blaze._materializeDOM(htmljs, [], view);
|
||||||
domrange = new Blaze._DOMRange(rangesAndNodes);
|
if (! Blaze._isContentEqual(lastHtmljs, htmljs)) {
|
||||||
view._domrange = domrange;
|
|
||||||
domrange.view = view;
|
|
||||||
view.isRendered = true;
|
|
||||||
} else {
|
|
||||||
domrange.setMembers(rangesAndNodes);
|
domrange.setMembers(rangesAndNodes);
|
||||||
|
Blaze._fireCallbacks(view, 'rendered');
|
||||||
}
|
}
|
||||||
Blaze._fireCallbacks(view, 'rendered');
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
lastHtmljs = htmljs;
|
lastHtmljs = htmljs;
|
||||||
|
|
||||||
// Causes any nested views to stop immediately, not when we call
|
// Causes any nested views to stop immediately, not when we call
|
||||||
@@ -323,25 +358,37 @@ Blaze._materializeView = function (view, parentView) {
|
|||||||
});
|
});
|
||||||
}, undefined, 'materialize');
|
}, undefined, 'materialize');
|
||||||
|
|
||||||
var teardownHook = null;
|
// first render. lastHtmljs is the first htmljs.
|
||||||
|
var initialContents;
|
||||||
domrange.onAttached(function attached(range, element) {
|
if (! _workStack) {
|
||||||
view._isAttached = true;
|
initialContents = Blaze._materializeDOM(lastHtmljs, [], view);
|
||||||
|
domrange = doFirstRender(view, initialContents);
|
||||||
teardownHook = Blaze._DOMBackend.Teardown.onElementTeardown(
|
initialContents = null; // help GC because we close over this scope a lot
|
||||||
element, function teardown() {
|
} else {
|
||||||
Blaze._destroyView(view, true /* _skipNodes */);
|
// We're being called from Blaze._materializeDOM, so to avoid
|
||||||
});
|
// recursion and save stack space, provide a description of the
|
||||||
});
|
// work to be done instead of doing it. Tasks pushed onto
|
||||||
|
// _workStack will be done in LIFO order after we return.
|
||||||
// tear down the teardown hook
|
// The work will still be done within a Tracker.nonreactive,
|
||||||
view.onViewDestroyed(function () {
|
// because it will be done by some call to Blaze._materializeDOM
|
||||||
teardownHook && teardownHook.stop();
|
// (which is always called in a Tracker.nonreactive).
|
||||||
teardownHook = null;
|
initialContents = [];
|
||||||
});
|
// push this function first so that it happens last
|
||||||
|
_workStack.push(function () {
|
||||||
|
domrange = doFirstRender(view, initialContents);
|
||||||
|
initialContents = null; // help GC because of all the closures here
|
||||||
|
_intoArray.push(domrange);
|
||||||
|
});
|
||||||
|
// now push the task that calculates initialContents
|
||||||
|
_workStack.push([lastHtmljs, initialContents, view]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return domrange;
|
if (! _workStack) {
|
||||||
|
return domrange;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expands a View to HTMLjs, calling `render` recursively on all
|
// Expands a View to HTMLjs, calling `render` recursively on all
|
||||||
|
|||||||
Reference in New Issue
Block a user