core/includes/common.inc | 82 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/core/includes/common.inc b/core/includes/common.inc index c3cfc15..cd337ee 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2934,6 +2934,14 @@ function drupal_render_page($page) { * the children is typically inserted into the markup generated by the parent * array. * + * An important aspect of rendering is the bubbling of rendering metadata: cache + * tags, attached assets and #post_render_cache metadata all need to be bubbled + * up. That information is needed once the rendering to a HTML string is + * completed: the resulting HTML for the page must know by which cache tags it + * should be invalidated, which (CSS and JavaScript) assets must be loaded, and + * which #post_render_cache callbacks should be executed. A stack data structure + * is used to perform this bubbling. + * * The process of rendering an element is recursive unless the element defines * an implemented theme hook in #theme. During each call to drupal_render(), the * outermost renderable array (also known as an "element") is processed using @@ -2941,6 +2949,8 @@ function drupal_render_page($page) { * - If this element has already been printed (#printed = TRUE) or the user * does not have access to it (#access = FALSE), then an empty string is * returned. + * - If no stack data structure has been created yet, it is done now. Next, + * an empty \Drupal\Core\Render\RenderStackFrame is pushed onto the stack. * - If this element has #cache defined then the cached markup for this * element will be returned if it exists in drupal_render()'s cache. To use * drupal_render() caching, set the element's #cache property to an @@ -2956,6 +2966,12 @@ function drupal_render_page($page) { * - 'expire': Set to one of the cache lifetime constants. * - 'bin': Specify a cache bin to cache the element in. Default is * 'default'. + * When there is a render cache hit, there is no rendering work left to be + * done, so the stack must be updated. The empty (and topmost) frame that + * was just pushed onto the stack is updated with all bubbleable rendering + * metadata from the element retrieved from render cache. Then, this stack + * frame is bubbled: the two topmost frames are popped from the stack, they + * are merged, and the result is pushed back onto the stack. * - If this element has #type defined and the default attributes for this * element have not already been merged in (#defaults_loaded = TRUE) then * the defaults for this type of element, defined in hook_element_info(), @@ -2966,6 +2982,12 @@ function drupal_render_page($page) { * called sequentially to modify the element before rendering. After all the * #pre_render functions have been called, #printed is checked a second time * in case a #pre_render function flags the element as printed. + * If #printed is set, we return early and hence no rendering work is left + * to be done, similarly to a render cache hit. Once again, the empty (and + * topmost) frame that was just pushed onto the stack is updated with all + * bubbleable rendering metadata from the element whose #printed = TRUE. + * Then, this stack frame is bubbled: the two topmost frames are popped from + * the stack, they are merged, and the result is pushed back onto the stack. * - The child elements of this element are sorted by weight using uasort() in * \Drupal\Core\Render\Element::children(). Since this is expensive, when * passing already sorted elements to drupal_render(), for example from a @@ -3025,20 +3047,50 @@ function drupal_render_page($page) { * attribute as a string and the element itself. * - If this element has #prefix and/or #suffix defined, they are concatenated * to #children. + * - The rendering of this element is now complete. The next step will be + * render caching. So this is the perfect time to update the the stack. At + * this point, children of this element (if any), have been rendered also, + * and if there were any, their bubbleable rendering metadata will have been + * bubbled up into the stack frame for the element that is currently being + * rendered. The render cache item for this element must contain the + * bubbleable rendering metadata for this element and all of its children. + * However, right now, the topmost stack frame (the one for this element) + * currently only contains the metadata for the children. Therefore, the + * topmost stack frame is updated with this element's metadata, and then the + * element's metadata is replaced with the metadata in the topmost stack + * frame. This element now contains all bubbleable rendering metadata for + * this element and all its children, so it's now ready for render caching. * - If this element has #cache defined, the rendered output of this element * is saved to drupal_render()'s internal cache. This includes the changes * made by #post_render. - * - If this element (or any of its children) has an array of - * #post_render_cache functions defined, they are called sequentially to - * replace placeholders in the final #markup and extend #attached. - * Placeholders must contain a unique token, to guarantee that e.g. samples - * of placeholders are not replaced also. For this, a special element named - * 'render_cache_placeholder' is provided. + * - If this element has an array of #post_render_cache functions defined, or + * any of its children has (which we would know thanks to the stack having + * been updated just before the render caching step), they are called + * sequentially to replace placeholders in the final #markup and extend + * #attached. Placeholders must contain a unique token, to guarantee that + * e.g. samples of placeholders are not replaced also. + * But, since #post_render_cache callbacks add attach additional assets, the + * correct bubbling of those must once again be taken into account. This + * final stage of rendering should be considered as if it were the parent of + * the current element, because it takes that as its input, and then alters + * its #markup. Hence, just before calling the #post_render_cache callbacks, + * a new empty frame is pushed onto the stack, where all assets #attached + * during the execution of those callbacks will end up in. Then, after the + * execution of those callbacks, we merge that back into the element. * Note that these callbacks run always: when hitting the render cache, when * missing, or when render caching is not used at all. This is done to allow * any Drupal module to customize other render arrays without breaking the * render cache if it is enabled, and to not require it to use other logic * when render caching is disabled. + * - Just before finishing the rendering of this element, this element's stack + * frame (the topmost one) is bubbled: the two topmost frames are popped + * from the stack, they are merged and the result is pushed back onto the + * stack. + * So if this element e.g. was a child element, then a new frame was pushed + * onto the stack element at the beginning of rendering this element, it was + * updated when the rendering was completed, and now we merge it with the + * frame for the parent, so that the parent now has the bubbleable rendering + * metadata for its child. * - #printed is set to TRUE for this element to ensure that it is only * rendered once. * - The final value of #children for this element is returned as the rendered @@ -3115,7 +3167,8 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { _drupal_render_process_post_render_cache($elements); } $elements['#markup'] = SafeMarkup::set($elements['#markup']); - // The render cache item contains all the tags for the subtree. + // The render cache item contains all the bubbleable rendering metadata for + // the subtree. $update_stack($elements); // Render cache hit, so rendering is finished, all necessary info collected! $bubble_stack(); @@ -3146,9 +3199,17 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { } } + // Defaults for bubbleable rendering metadata. + $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); + $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); + $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); + // Allow #pre_render to abort rendering. if (!empty($elements['#printed'])) { - // Early return, bubble the stack! + // The #printed element contains all the bubbleable rendering metadata for + // the subtree. + $update_stack($elements); + // #printed, so rendering is finished, all necessary info collected! $bubble_stack(); return ''; } @@ -3158,11 +3219,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { drupal_process_states($elements); } - // Defaults for bubbleable rendering metadata. - $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); - $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); - $elements['#post_render_cache'] = isset($elements['#post_render_cache']) ? $elements['#post_render_cache'] : array(); - // Get the children of the element, sorted by weight. $children = Element::children($elements, TRUE);