The purpose of the plug-in
When the page has a large amount of data, such as thousands of data to render, the DOM will scroll too much and cause the phenomenon of stalling. At this time, this plug-in can dynamically render the DOM of the visual area, and calculate and change the data displayed in the visual area in real time when scrolling.
The principle of
How many items are displayed on the page is determined by the height of the visible area and the height of each item in the items (itemSize, which can be height or width of a horizontal slide). The items that can be displayed are wrapped in a pool array for rendering, and the pool array is dynamically modified as the page scrolls. In order to minimize the cost of scrolling, out-of-range views in the pool will be recycled to the reuse pool. New views in the pool will be taken from the reuse pool first, and those that are not reused will be added.
Data flows through the page
In order to achieve the purpose of dynamic rendering and DOM reuse, the following three pools for storing corresponding items are mainly maintained.
pool
: Displays the current pageView of the pool, which stores the data to be rendered on the current page. That is, the pool is used in tempalte rendering.
<div
v-for="view of pool"
:key="view.nr.id"
:style="ready ? { transform: `translate${direction === 'vertical' ? 'Y' : 'X'}(${view.position}px)` } : null"
class="vue-recycle-scroller__item-view"
:class="{ hover: hoverKey === view.nr.key }"
@mouseenter="hoverKey = view.nr.key"
@mouseleave="hoverKey = null"
>
Copy the code
$_views
Each time addView adds a view, it will not only put the view in the pool, but also put a copy in the views.The data dictionary makes it easy to find the viewWhen the page is scrolling, it will fetch the view between startIndex and endIndex. Each view will go to the views first, which is more efficient than traversing the pool. If the view is found, it means that the current view has been in the visual area, and at this time, it can be directly displayed in the reuse views. If it is not found in views, it means it is a new view, then first go to the reuse pool according to the type, find the reuse, can not find the addView, after the new views will be added.$_unusedViews
: Reuse pool, stores views that are not visible by type. Each scroll first out of view to the unusedViews, after the lost. Do a visual traversal between startIndex and endIndex. When a new view appears, look for it in unusedViews first, and then take it out. If you can’t find it, go addView
The following is the initialization of the data during initialization
created () {
// Record the start index when the refresh is complete
this.$_startIndex = 0
// Record the end index of the flush
this.$_endIndex = 0
// All views displayed on the page correspond to the pool to facilitate quick search
this.$_views = new Map(a)// Reuse pool: store unused views based on their type
this.$_unusedViews = new Map(a)// Indicates whether the stream is rolling
this.$_scrollDirty = false
// Record where the start value was last rolled
this.$_lastUpdateScrollPosition = 0
// In SSR mode, we also prerender the same number of item for the first render
// to avoir mismatch between server and client templates
if (this.prerender) {
this.$_prerender = true
this.updateVisibleItems(false)}},Copy the code
The principle of
The main principle of the whole plug-in is focused on updateVisibleItems(view refresh function), which is triggered by initialization, page scrolling, page resize, and so on. The total process is divided into four steps:
-
Calculate the visual range: After obtaining the scroll information, first calculate the index range of the items to be displayed in the visual area, that is, startIndex and endIndex.
- Gets the start and end values for the currently displayed section and determines whether enough scrolling has been done. If the scroll is small, the items displayed in the visual area do not change and do not need to be refreshed.
GetScroll is calculated based on scrollerTop, etc const scroll = this.getScroll() // Skip update if use hasn't scrolled enough if (checkPositionDiff) { // Check that the current scrolling range does not exceed the itemSize set, that is, there is no more than one view. In this case, there is no need to change the pool, so no update operation is performed this time let positionDiff = scroll.start - this.$_lastUpdateScrollPosition if (positionDiff < 0) positionDiff = -positionDiff if ((itemSize === null && positionDiff < minItemSize) || positionDiff < itemSize) { return { continuous: true,}}}// Refresh the position information after this scroll this.$_lastUpdateScrollPosition = scroll.start // Calculate the offset. The default buffer is 200 and can be customized const buffer = this.buffer scroll.start -= buffer scroll.end += buffer // Variable size mode // Highly variable mode // Since the height of each item is not fixed, you cannot directly use scroll. Start to get startIndex. So through the dichotomy, quickly find the first view that appears in the visible area, that is, startIndex. // Since the calculation attribute has cached all the size records of variable height, the purpose of the binary search is equivalent to finding the index of sizes. The accumulator of the index is less than the accumulator of the index. If the accumulator of index+1 is greater than Scroll. start, it is startIndex that is just slid into the visible area if (itemSize === null) { let h let a = 0 let b = count - 1 // Record the starting point of binary search here let i = ~~(count / 2) let oldI // Searching for startIndex do { oldI = i h = sizes[i].accumulator if (h < scroll.start) { // The minimum value is set to I a = i } else if (i < count - 1 && sizes[i + 1].accumulator > scroll.start) { // Note If both I and I +1 are out of range, set the maximum value to I and continue searching b = i } // Continue to divide i = ~~((a + b) / 2)}while(i ! == oldI) i <0 && (i = 0) startIndex = i // For container style totalSize = sizes[count - 1].accumulator // Searching for endIndex // Find the endIndex that just exceeded for (endIndex = i; endIndex < count && sizes[endIndex].accumulator < scroll.end; endIndex++); if (endIndex === -1) { endIndex = items.length - 1 } else { endIndex++ // Bounds endIndex > count && (endIndex = count) } } else { // Fixed size mode // Fixed height: calculate the startIndex and endIndex of a fixed itemSize based on the scrolling distance startIndex = ~~(scroll.start / itemSize) endIndex = Math.ceil(scroll.end / itemSize) // Bounds startIndex < 0 && (startIndex = 0) endIndex > count && (endIndex = count) totalSize = count * itemSize } } if (endIndex - startIndex > config.itemsLimit) { this.itemsLimitError() } // refresh the total height of items. The totalSize will be given to the height of the outer box in order to create a scrollbar this.totalSize = totalSize Copy the code
- For variable heights, an SIZES table is preferentially maintained for the computed attribute, with the cumulative size of the corresponding index recorded. The purpose of this operation is to get the sum of sizes based on the index, instead of having to recalculate each time.
sizes () { // If itemSize is not provided, enter variable size mode if (this.itemSize === null) { const sizes = { '1': { accumulator: 0}},const items = this.items const field = this.sizeField const minItemSize = this.minItemSize let computedMinSize = 10000 let accumulator = 0 let current for (let i = 0, l = items.length; i < l; i++) { current = items[i][field] || minItemSize if (current < computedMinSize) { computedMinSize = current } accumulator += current sizes[i] = { accumulator, size: current } } // eslint-disable-next-line this.$_computedMinItemSize = computedMinSize return sizes } return[]}Copy the code
-
View reclamation: Traverses the views in the pool and determines that the index of the view is beyond the startIndex and endIndex range. Then the view is reclaimed by the unuseView function and placed in the unusedViews pool. (In this case, putting in the reuse pool is only a reference to the corresponding element in the pool. The number of elements in the pool is not changed, but the attributes of the corresponding element are changed
if (this.$_continuous ! == continuous) {if (continuous) {
// If it is not continuous, the page has a big change, initialize the data
views.clear()
unusedViews.clear()
for (let i = 0, l = pool.length; i < l; i++) {
// Recycle the currently displayed view
view = pool[i]
this.unuseView(view)
}
}
this.$_continuous = continuous
} else if (continuous) {
// This is a continuous slide, traversing the collection pool
for (let i = 0, l = pool.length; i < l; i++) {
view = pool[i]
if (view.nr.used) {
// Update view item index
if (checkItem) {
view.nr.index = items.findIndex(
item= > keyField ? item[keyField] === view.item[keyField] : item === view.item,
)
}
// Check if index is still in visible range
// If index is out of range, then recycle
if (
view.nr.index === -1 ||
view.nr.index < startIndex ||
view.nr.index >= endIndex
) {
this.unuseView(view)
}
}
}
}
Copy the code
The following is the implementation of unuseView:
unuseView (view, fake = false) {
// Put the view into the cache pool according to the category of the view
const unusedViews = this.$_unusedViews
const type = view.nr.type
// Store by type, and reuse by type
let unusedPool = unusedViews.get(type)
if(! unusedPool) { unusedPool = [] unusedViews.set(type, unusedPool) } unusedPool.push(view)if(! fake) {// At this point, set the view reset position (so that the view is not visible) and used to false
view.nr.used = false
view.position = -9999
this.$_views.delete(view.nr.key)
}
}
Copy the code
- Update view: Iterate between startIndex and endIndex, fetching one item at a time, starting to wrap the item and flushing it to the pool.
- Look in the views dictionary according to the item. If found, the current view is still visible, only scrolling, then directly reuse the view.
- If it is not found in views, go to unusedViews to find out whether there is a reusable view. If there is a reusable view, use the reusable view and modify the item, key, index and other attributes of the view. In addition, the corresponding dictionary in views is reconfigured to facilitate subsequent lookup.
- If no view is found in unusedViews, no view is reused. Call addView to add a new view. The view adds an item attribute associated with the items, position attribute to transform style, add used, key, ID, index, and so on. Add a view to the pool and add a dictionary to the views.
let item, type, unusedPool
let v
// Iterate within the visible area
for (let i = startIndex; i < endIndex; i++) {
item = items[i]
const key = keyField ? item[keyField] : item
if (key == null) {
throw new Error(`Key is ${key} on item (keyField is '${keyField}') `)}// if the item is found in the views dictionary, the current view is still visible, but only scrolling, then reuse the view directly.
view = views.get(key)
// If size does not exist, then the height does not exist and will not be added to pool, because it will not be displayed
if(! itemSize && ! sizes[i].size) {if (view) this.unuseView(view)
continue
}
// No view assigned to item
// 3.2 If no view is found in views, go to unusedViews to find whether there is a reusable view. If there is a reusable view, use the reusable view and modify the item, key, index and other attributes of the view. In addition, the corresponding dictionary in views is reconfigured to facilitate subsequent lookup.
if(! view) { type = item[typeField] unusedPool = unusedViews.get(type)if (continuous) {
// Reuse existing view
// Find the views available in the reuse pool according to the type, modify the index, etc
if (unusedPool && unusedPool.length) {
view = unusedPool.pop()
view.item = item
view.nr.used = true
view.nr.index = i
view.nr.key = key
view.nr.type = type
} else {
// If the multiplexing pool does not exist, add it
// 3.3 If no view is found in unusedViews, no view is reused. Call addView to add a new view. The view adds an item attribute associated with the items, position attribute to transform style, add used, key, ID, index, and so on. Add a view to the pool and add a dictionary to the views.
view = this.addView(pool, i, item, key, type)
}
} else {
// Use existing view
// We don't care if they are already used
// because we are not in continous scrolling
// Since it is not continuous sliding, there is no crossover, so do not consider the problem of use occupancy, and directly start from the first of the corresponding reuse pool
v = unusedIndex.get(type) || 0
if(! unusedPool || v >= unusedPool.length) { view =this.addView(pool, i, item, key, type)
this.unuseView(view, true)
unusedPool = unusedViews.get(type)
}
view = unusedPool[v]
view.item = item
view.nr.used = true
view.nr.index = i
view.nr.key = key
view.nr.type = type
unusedIndex.set(type, v + 1)
v++
}
// Put it in the views pool, where it corresponds to the dictionary for subsequent lookup
views.set(key, view)
} else {
// If the current view already exists, re-use it
view.nr.used = true
view.item = item
}
// Update position
// Refresh the view location
if (itemSize === null) {
view.position = sizes[i - 1].accumulator
} else {
view.position = i * itemSize
}
}
// Record the local index
this.$_startIndex = startIndex
this.$_endIndex = endIndex
Copy the code
Add a new view to addView when the reuse pool is not available:
addView (pool, index, item, key, type) {
const view = {
item,
position: 0,}const nonReactive = {
id: uid++,
// The index here corresponds to the index in the incoming source data, which facilitates reordering of views after reuse
index,
used: true,
key,
type,
}
Object.defineProperty(view, 'nr', {
configurable: false.value: nonReactive,
})
// Add a new view to the pool
pool.push(view)
return view
}
Copy the code
- Sort view: After the above processing is complete, the pool may be unordered, because the pool is multiplexed, so you need to sort. Calling the sortViews method will reorder the view according to the index value in the pool.
clearTimeout(this.$_sortTimer)
this.$_sortTimer = setTimeout(this.sortViews, 300)
// Implementation of sortViews
sortViews () {
this.pool.sort((viewA, viewB) = > viewA.nr.index - viewB.nr.index)
}
Copy the code
conclusion
$_unusedViews $_views $_views $_views The use of $_unusedViews makes it not every time to delete the data of the pool to achieve the purpose of rendering, looking back at my usual development, similar to rolling, round casting and other processing methods, a certain range of source data is directly truncated to the pool to achieve the purpose of refreshing, the effect is realized, but there is room for optimization. The use of sizes and $_views is designed to reduce complexity. It is more elegant to pre-store a map, or pre-store an accumulated value, when we are doing things like traversal, findIndex, etc. It also greatly reduces the complexity of traversal logic every time we refresh the view.