1 the introduction
BI platform is a very important platform-level product of Ali Data Center team. To ensure a good experience of report editing and browsing, performance optimization is essential.
Current BI tool is generally report forms, to know the report form is not just the chart component, filter condition associated with these components and the complexity of relationship any filter condition can lead to changes in their related items to fetch and rendering component, is a large amount of data and statements, a form component loading millions of magnitude of the data, Rendering on demand is a must in order to maintain a normal display with such a large amount of data.
Here not ListView infinite scroll, on-demand rendering model because the layout of the report have free streaming layout, the layout of magnet and three sets of layout, each layout style difference is very big, can’t use a fixed formula to calculate the component is visible, so we choose to initialize components full volume rendering, prevent components within the first screen rendering. Since the data has not been obtained under the initial conditions, full rendering will not cause performance problems, which is the premise of the establishment of this scheme.
So TODAY I will introduce how to use DOM to determine whether components are visible in the canvas of this technical solution, from the point of view of architecture design and code abstraction, step by step decomposition, not only hope you can easily understand how to achieve this technical solution, but also hope you can master the tricks of this, learn to draw infertile examples.
2. Intensive reading
Using the React framework as an example, the thinking path for rendering on demand is as follows:
Get component active state -> block rerendering of non-active components.
Here I choose to start with the results, thinking first about how to block component rendering, and then working out how to write the function that determines whether the component is visible.
Block component rerendering
We need a RenderWhenActive component that supports an active parameter. This layer is transparent when Active is true and blocks all renderings when active is false.
To be more specific, the effect looks like this:
- In inActive, any changes to props will not cause the component to render.
- When switching from inActive to Active, the props previously applied to the component takes effect immediately.
- If the switch to Active does not change, the re-render should not be triggered.
- Switching from Active to inActive should not trigger rendering and immediately blocks subsequent rerenders.
Function Component can’t do this right now. We still need to use the Class Component shouldComponentUpdate to do this, because the Class Component blocks rendering. The latest props is stored, and the Function Component has no internal state at all and can’t do this yet.
We can easily do this by writing a RenderWhenActive component:
class RenderWhenActive extends React.Component {
public shouldComponentUpdate(nextProps) {
return nextProps.active;
}
public render() { return this.props.children } } Copy the code
Gets the active status of components
Before we think further, let’s not get down to the details of how to tell if a component is displayed, but how to call it, assuming that we already have such a function.
We obviously need a custom Hook: useActive determines whether the component is active and passes the value returned by active to RenderWhenActive components:
const ComponentLoader = ({ children }) = > {
const active = useActive();
return <RenderWhenActive active={active}>{children}</RenderWhenActive>;
};
Copy the code
In this way, any component rendered by the rendering engine with ComponentLoader is capable of rendering on demand.
Implement useActive
Now that the component and Hook side flow is fully connected, we can focus on how to implement the useActive Hook.
Using the Hooks API, you can use the useEffect to determine if the component is Active after rendering and use the useState to store this state:
export function useActive(domId: string) {
// All elements default to unActive
const [active, setActive] = React.useState(false);
React.useEffect((a)= > {
const visibleObserve = new VisibleObserve(domId, "rootId", setActive); visibleObserve.observe(); return (a)= > visibleObserve.unobserve(); }, [domId]); return active; } Copy the code
The active state of all components is false when initialized, however this state in shouldComponentUpdate does not block the first rendering, so the component’s DOM node initialization is still rendered.
The custom Class VisibleObserve is registered in the useEffect phase. It is used to listen to whether the component DOM node is visible in its parent node rootId and is thrown by a third callback when the status changes. Here setActive is the third parameter. You can change the active status of the current component in time.
VisibleObserve this function has the observe and unobserve apis to start and cancel the listen, respectively. Using the useEffect destruction function to perform a return callback, the listening and destruction mechanism is complete.
The next step is to implement the core VisibleObserve function that listens for components to be visible.
Preparations to listen for components to be visible
Before you implement VisibleObserve, how many ways can you implement it? You probably have a lot of weird scenarios in mind. Yes, there are many ways to determine whether a component is visible in a container, and even if the best solution is found in terms of functionality, there is no perfect solution from a compatibility point of view, so this is a function with many implementation possibilities, and different solutions for different browser versions is the best strategy.
One way to handle this is to make an abstract class that all the actual methods inherit and implement, so that we have multiple sets of “different implementations of the same API” that can be switched between different scenarios.
Use the abstract create an abstract class AVisibleObserve, realize two public constructors and affirms the important function to observe and unobserve:
/ * ** An abstract class that listens for elements to be visible* /
abstract class AVisibleObserve {
/ * ** Listen for the DOM ID of the element* / protected targetDomId: string; / * ** Visible range root node DOM ID* / protected rootDomId: string; / * ** Active change callback* / protected onActiveChange: (active? : boolean) = > void; constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { this.targetDomId = targetDomId; this.rootDomId = rootDomId; this.onActiveChange = onActiveChange; } / * ** Start listening* / abstract observe(): void; / * ** Cancel listening* / abstract unobserve(): void; } Copy the code
So we can implement multiple solutions. After a bit of thinking, we only need two solutions. One is a stupid method of polling detection implemented by setInterval, and the other is a new method implemented by advanced API IntersectionObserver of the browser. Since the latter has compatibility requirements, the former is implemented as a bottom-all solution.
Therefore, we can define two sets of corresponding methods:
class IntersectionVisibleObserve extends AVisibleObserve {
constructor(/ * * /) { super(targetDomId, rootDomId, onActiveChange);
}
observe() { // balabala.. } unobserve() { // balabala.. } } class SetIntervalVisibleObserve extends AVisibleObserve { constructor(/ * * /) { super(targetDomId, rootDomId, onActiveChange); } observe() { // balabala.. } unobserve() { // balabala.. } } Copy the code
Finally, make a general class as a call entry:
/ * *Listen to whether the element is visible to the general class* /
export class VisibleObserve extends AVisibleObserve {
/ * ** Actual VisibleObserve class* / private actualVisibleObserve: AVisibleObserve = null; constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange); // Select different Observe solutions according to browser API compatibility if ('IntersectionObserver' in window) { // The latest IntersectionObserve scheme this.actualVisibleObserve = new IntersectionVisibleObserve(targetDomId, rootDomId, onActiveChange); } else { // Compatible SetInterval scheme this.actualVisibleObserve = new SetIntervalVisibleObserve(targetDomId, rootDomId, onActiveChange); } } observe() { this.actualVisibleObserve.observe(); } unobserve() { this.actualVisibleObserve.unobserve(); } } Copy the code
In the constructor, we can determine whether the current browser supports IntersectionObserver. However, the instance created in any scheme inherits from AVisibleObserve, so we can store it with the uniform actualVisibleObserve member variable.
The observe and unobserve phases can override the implementation of a concrete class, Direct call this. ActualVisibleObserve. Observe () and enclosing actualVisibleObserve. Unobserve () the two apis.
The idea here is that the parent class cares about the interface layer API, and the subclass cares about how the API is implemented based on that interface.
Let’s take a look at how low specs (compatible) and high specs (native) are implemented.
Listening component visible – Compatible version
In compatible versions, you need to define an additional member variable called interval to store the SetInterval reference, which is unobserved when clearInterval is used.
The judging visible function is abstracted into the judgeActive function. The core idea is to judge whether there is an inclusion relation between two rectangles (container and component to be judged). If the inclusion is true, it represents visible; if the inclusion is not true, it is not visible.
Here is the full implementation function:
class SetIntervalVisibleObserve extends AVisibleObserve {
/ * ** the Interval reference* /
private interval: number;
/ * ** Check whether the interval is visible* / private checkInterval = 1000; constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange); } / * ** Determines whether the element is visible* / private judgeActive() { // Get rect of root component const rootComponentDom = document.getElementById(this.rootDomId); if(! rootComponentDom) { return; } // Root component rect const rootComponentRect = rootComponentDom.getBoundingClientRect(); // Get the current component recT const componentDom = document.getElementById(this.targetDomId); if(! componentDom) { return; } // The current component rect const componentRect = componentDom.getBoundingClientRect(); // Determine whether the current component is visible to the root component // Sum of the lengths const sumOfWidth = Math.abs(rootComponentRect.left - rootComponentRect.right) + Math.abs(componentRect.left - componentRect.right); // Add the width const sumOfHeight = Math.abs(rootComponentRect.bottom - rootComponentRect.top) + Math.abs(componentRect.bottom - componentRect.top); // Sum of lengths + double spacing (cross spacing is negative) const sumOfWidthWithGap = Math.abs( rootComponentRect.left + rootComponentRect.right - componentRect.left - componentRect.right, ); // Total width + double spacing (cross spacing is negative) const sumOfHeightWithGap = Math.abs( rootComponentRect.bottom + rootComponentRect.top - componentRect.bottom - componentRect.top, ); if (sumOfWidthWithGap <= sumOfWidth && sumOfHeightWithGap <= sumOfHeight) { / / in the interior this.onActiveChange(true); } else { / / in the external this.onActiveChange(false); } } observe() { // Check if the element is visible once when listening this.judgeActive(); this.interval = setInterval(this.judgeActive, this.checkInterval); } unobserve() { clearInterval(this.interval); } } Copy the code
Based on the container rootDomId and component targetDomId, we can get the corresponding DOM instance and call getBoundingClientRect to get the position and width of the corresponding rectangle.
The algorithm idea is as follows:
Make the container root and the component Component.
- Calculates the sum of root and Component lengths
sumOfWidth
Sum of and widthsumOfHeight
. - Calculate the sum of root and Component lengths + double the spacing
sumOfWidthWithGap
Plus the sum of the widths plus twice the spacingsumOfHeightWithGap
. sumOfWidthWithGap - sumOfWidth
Is the transverse gap distance,sumOfHeightWithGap - sumOfHeight
The difference of is the transverse gap distance, and both values are negative in the interior.
The key is that, from a horizontal perspective, the following formula can be understood as the sum of the widths + twice the width spacing:
// Sum of lengths + double spacing (cross spacing is negative)
const sumOfWidthWithGap = Math.abs(
rootComponentRect.left +
rootComponentRect.right -
componentRect.left -
componentRect.right ); Copy the code
While sumOfWidth is the sum of the widths, the difference between them is the double spacing value, and a positive number means there is no intersection horizontally. When the intersection of the vertical and horizontal is negative, it is either crossed or contained internally.
Whether the listening component is visible – native version
It would be easier if the browser supports IntersectionObserver. Here is the complete code:
class IntersectionVisibleObserve extends AVisibleObserve {
/ * ** IntersectionObserver instance* /
private intersectionObserver: IntersectionObserver;
constructor(targetDomId: string, rootDomId: string, onActiveChange: (active? : boolean) => void) { super(targetDomId, rootDomId, onActiveChange); this.intersectionObserver = new IntersectionObserver( changes= > { if (changes[0].intersectionRatio > 0) { onActiveChange(true); } else { onActiveChange(false); // Because virtual DOM updates lead to actual DOM updates, it is also triggered here. If dom is lost, listen again if (!document.body.contains(changes[0].target)) { this.intersectionObserver.unobserve(changes[0].target); this.intersectionObserver.observe(document.getElementById(this.targetDomId)); } } }, { root: document.getElementById(rootDomId), }, ); } observe() { if (document.getElementById(this.targetDomId)) { this.intersectionObserver.observe(document.getElementById(this.targetDomId)); } } unobserve() { this.intersectionObserver.disconnect(); } } Copy the code
IntersectionRatio > 0 can be used to determine whether the element appears in the parent container. If intersectionRatio === 1, it indicates that the component appears in the container completely. In this case, it is required that all parts appear active.
One thing to note is that this judgment is different from the SetInterval, virtual DOM DOM instance may be updated, due to the React to IntersectionObserver. Observe after listening DOM element is destroyed, causing subsequent failure monitoring, So add the following code when the element is hidden:
// Because virtual DOM updates lead to actual DOM updates, it is also triggered here. If dom is lost, listen again
if (!document.body.contains(changes[0].target)) {
this.intersectionObserver.unobserve(changes[0].target);
this.intersectionObserver.observe(document.getElementById(this.targetDomId));
}
Copy the code
- When an element is judged to be out of the viewable area, it also contains elements that are destroyed.
- So by
body.contains
Determines if the element is destroyed, and if so, listens again for a new DOM instance.
3 summary
To sum up, the logic of rendering on demand is not applicable only to rendering engines, but it can be quite intrusive to add to code written directly for ProCode scenes.
Perhaps on-demand rendering in the visual area could be done inside a front-end development framework, not a standard framework feature, but not entirely a business feature either.
This time around, what would be a better design if handwritten React code could render on demand?
React Render on Demand · Issue #254 · dt-fe/weekly
If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.
Pay attention to the front end of intensive reading wechat public account
Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)