I recently used position: sticky when modifying the style in Blogsue. Without further ado, let’s get to the main point.

define

Position: sticky is a new value of the CSS position property. As its name suggests, it “sticks” to your browser window. There are many applications for this presentation. For example, there is such a scene on the right side of Zhihu: when the user keeps scrolling down, the column (advertisement) on the right side is fixed and will not disappear in the user interface. For meituan on mobile, the screen box above also needs to be fixed on the left.

As with waterfall flow and Colum-count before it, these widely used typesetting formats will eventually have a native implementation. Specific use here is not opened, can consult MDN:https://developer.mozilla.org/zh-CN/docs/Web/CSS/position

Polyfill – stickyfill

Position: sticky as a new feature, compatibility problem has been an insurmountable hurdle. You can see that the entire IE series is not supported:

position: sticky

  • Stickyfill doesn’t support the X axis
  • So stickyfill limits the element to the parent element, which means that when the parent element leaves the screen, that element also leaves the screen.

How to use stickyFill

In stickyFill repo, the author describes how polyfill can be used:

<div class="sticky">.</div>
Copy the code
.sticky {
    position: -webkit-sticky;
    position: sticky;
    top: 0;
}
Copy the code

Then apply the polyfill:

var elements = document.querySelectorAll('.sticky');
Stickyfill.add(elements);
Copy the code

Pollyfill acts as a “patch” and ideally just introduces its code into the project without doing anything afterwards. Promise’s polyfill, for example, creates the Promise class directly under Global, which we simply import and will automatically help us prepare. But can stickyfill do that? In theory, yes. Because stickyfill just walks through the DOM tree to find all the DOM nodes with sticky attributes and then adds rules to them. But in practice, because traversing the DOM tree is too costly, stickyfill takes a back seat and lets us choose which nodes we want to traverse.

Source analysis

Now that we’ve seen how to use stickyfill, you can see that stickyfill is hosting the elements that we need to process, using the power of javascript to simulate position: sticky. Let’s take a look at how stickyfill manages and handles elements. Due to article length limitations, this article only covers a few core methods. The following source code has been condensed for clarity:

Package default variable && managed element custom class

The stickyFill module presets some classes and variables:

// Here stickies is the inventory of all managed nodes in the array
const stickies = [];

// Store the top and left values of the latest state
const scroll = {
    top: null.left: null
};

/ / Sticky class
// All nodes confirmed to be maintained will be wrapped by this class
class Sticky {
    constructor (node) {
        // Error detection
        if(! (nodeinstanceof HTMLElement))
            throw new Error('First argument must be HTMLElement');
        // Prevent repeat occurrences of the same DOM node
        if (stickies.some(sticky= > sticky._node === node))
            throw new Error('Stickyfill is already applied to this node');
        
        // The DOM node of wrap
        this._node = node;
        // Store the current state of the DOM node, with three values:
        // start: The node is displayed normally
        // middle: The node is in fixed state
        // end: This node slides to the bottom of the parent node and will be near the bottom edge of the parent node
        this._stickyMode = null;
        // Whether the object takes effect.
        this._active = false;
        // Put it in the instance queue for management
        stickies.push(this);
        // The refresh function performs initial processing on the node and activates it
        this.refresh();
    }
    / /...
}
Copy the code

Global initialization function

Here Stickyfill does a good job of listening for rolling events and checking the runtime environment during the global initialization phase:

function init () {
    // Avoid repeated initialization
    if (isInitialized) {
        return;
    }
    isInitialized = true;

    // Define the processing logic required by the onScroll event, as can be seen from pageXOffset/pageYOffset to determine the scrolling distance
    function checkScroll () {
        if (window.pageXOffset ! = scroll.left) { scroll.top =window.pageYOffset;
            scroll.left = window.pageXOffset;
            // If the current left value is all times, we need to refresh all elements
            // Why refresh? Because stickyfill only supports sticky up and down
            // The right/left values are fixed based on the browser window
            // Refresh the managed node here
            // See the following "Three states of A Sticky DOM node (core)"
            Stickyfill.refreshAll();
        }
        else if (window.pageYOffset ! = scroll.top) { scroll.top =window.pageYOffset;
            scroll.left = window.pageXOffset;

            // If the height changes, the state refresh function is executed
            stickies.forEach(sticky= > sticky._recalcPosition());
        }
    }

    checkScroll();
    window.addEventListener('scroll', checkScroll);

    // Refresh the node when the screen size changes or the orientation of the phone screen changes
    window.addEventListener('resize', Stickyfill.refreshAll);
    window.addEventListener('orientationchange', Stickyfill.refreshAll);

    // Define a circulator where sticky._fastcheck () is the main function
    // Check whether the position of the element itself and its parent has changed, and refresh the node if it has changed
    // The main function is to keep track of your refresh when you use JS to manipulate elements
    // The timing here is 500ms. My opinion is for performance reasons
    let fastCheckTimer;
    function startFastCheckTimer () {
        fastCheckTimer = setInterval(function () {
            stickies.forEach(sticky= > sticky._fastCheck());
        }, 500);
    }
    function stopFastCheckTimer () {
        clearInterval(fastCheckTimer);
    }
    // Check page hiding
    // window.hidden This value indicates whether the page is hidden
    // For performance reasons, stickyFill will cancel the fastCheckTimer when the page is hidden
    let docHiddenKey;
    let visibilityChangeEventName;
    // Compatible with both formats with or without prefixes
    if ('hidden' in document) {
        docHiddenKey = 'hidden';
        visibilityChangeEventName = 'visibilitychange';
    }
    else if ('webkitHidden' in document) {
        docHiddenKey = 'webkitHidden';
        visibilityChangeEventName = 'webkitvisibilitychange';
    }
    if (visibilityChangeEventName) {
        if (!document[docHiddenKey]) startFastCheckTimer();
        document.addEventListener(visibilityChangeEventName, () => {
            if (document[docHiddenKey]) {
                stopFastCheckTimer();
            }
            else{ startFastCheckTimer(); }}); }else startFastCheckTimer();
}
Copy the code

The element management

We know from the API that the way to add elements to stickyfill is stickyfill. AddOne (Element) and stickyFill. Add (elementList) :

addOne (node) {
    // Check whether it is a Node Node
    if(! (nodeinstanceof HTMLElement)) {
        if (node.length && node[0]) node = node[0];
        else return;
    }
    // This is to avoid hosting multiple times
    for (var i = 0; i < stickies.length; i++) {
        if (stickies[i]._node === node) return stickies[i];
    }
    // Return the instance
    return new Sticky(node);
},
// Pass the array method
// Similar to addOne
add (nodeList) {
    // ...
},
Copy the code

Element state transition

So how does stickyfill figure out what state the node is in?

Three states of DOM nodes in the Sticky class

We know that in the STCIkyFill library (note that this is different from the current specification) :

  • position: stickyWhen an element is originally positioned in the interface, it’s likeposition: absoluteThe same.
  • When an element moves into a situation where it’s supposed to be hidden, likeposition: fixedThe same.
  • When the element reaches the bottom of the parent element, it sticks to the bottom of the parent element until it disappears. Just likeposition: absolute; bottom: 0The same.
Detailed explanation of conversion method

As we saw in the method above, StickyFill filters the elements we want to host and stores them in the Stickies array after wrapping the Sricky class. At the same time, we also know the three representations of the display form of elements in Sticky. Thus, we introduce three states of DOM nodes in Sticky class and the corresponding style definition and transformation mode of each state. A private method in the Sticky class _recalcPosition:

    _recalcPosition () {
        // Exit if the element is invalid
        if (!this._active || this._removed) return;
        // Get the current state of the element
        const stickyMode = scroll.top <= this._limits.start
            ? 'start'
            : scroll.top >= this._limits.end? 'end': 'middle';
        // Exit in the same state to avoid repeated operations
        if (this._stickyMode == stickyMode) return;

        switch (stickyMode) {
            // Start state: absolute
            // Then define the top/right/left values
            case 'start':
                extend(this._node.style, {
                    position: 'absolute'.left: this._offsetToParent.left + 'px'.right: this._offsetToParent.right + 'px'.top: this._offsetToParent.top + 'px'.bottom: 'auto'.width: 'auto'.marginLeft: 0.marginRight: 0.marginTop: 0
                });
                break;
            // Use fixed to "stick" elements to the interface
            // Then define the top/right/left values
            case 'middle':
                extend(this._node.style, {
                    position: 'fixed'.left: this._offsetToWindow.left + 'px'.right: this._offsetToWindow.right + 'px'.top: this._styles.top,
                    bottom: 'auto'.width: 'auto'.marginLeft: 0.marginRight: 0.marginTop: 0
                });
                break;
            // State of the element attached to the bottom of the parent element, using absolute
            // Also set bottom to 0
            case 'end':
                extend(this._node.style, {
                    position: 'absolute'.left: this._offsetToParent.left + 'px'.right: this._offsetToParent.right + 'px'.top: 'auto'.bottom: 0.width: 'auto'.marginLeft: 0.marginRight: 0
                });
                break;
        }
        // Save the current state
        this._stickyMode = stickyMode;
    }
Copy the code

Other tips

There are some interesting tricks inside StickyFill to optimize your code:

Check whether sticky is native

In stickyfill, we use a variable seppuku to determine whether the system supports position: sticky.

let seppuku = false;
const isWindowDefined = typeof window! = ='undefined';

// The module cannot be used without 'window' or 'window.getcomputedstyle'
if(! isWindowDefined || !window.getComputedStyle) seppuku = true;
// Check whether native 'position: sticky' is supported
// Create a test DOM node and then sticky its style.potision with all possible values
// Then fetch style.position again to see if the DOM element recognizes the value
// This is part of DOM knowledge. When we give the set value of node.style, we will automatically check the input value and store it if it is correct
// This is the difference between Node.xxx and Node.setattribute
else {
    const testNode = document.createElement('div');

    if([' '.'-webkit-'.'-moz-'.'-ms-'].some(prefix= > {
            try {
                testNode.style.position = prefix + 'sticky';
            }
            catch(e) {}

            returntestNode.style.position ! =' ';
        })
    ) seppuku = true;
}
Copy the code
Clone nodes to avoid repeated manipulation of real DOM nodes

In the real world, the nodes we want to host can be very complex and large. Then we can get a lot of computation when we get the style attribute for it. Here stickyfill optimizes performance by creating a new simple div with no content and then copying the shape and style of the original Node into it:

// Create a Clone node
const clone = this._clone = {};
clone.node = document.createElement('div');

// Copy the style of the original node to the clone node
extend(clone.node.style, {
    width: nodeWinOffset.right - nodeWinOffset.left + 'px'.height: nodeWinOffset.bottom - nodeWinOffset.top + 'px'.marginTop: nodeComputedProps.marginTop,
    marginBottom: nodeComputedProps.marginBottom,
    marginLeft: nodeComputedProps.marginLeft,
    marginRight: nodeComputedProps.marginRight,
    cssFloat: nodeComputedProps.cssFloat,
    padding: 0.border: 0.borderSpacing: 0.fontSize: '1em'.position: 'static'
});
// Insert into the interface
// Because the node is absolute, insert it directly before the node and then cover it
// The user's presentation will not change
referenceNode.insertBefore(clone.node, node);
clone.docOffsetTop = getDocOffsetTop(clone.node);
Copy the code

conclusion

In general, stickyfill works by listening for the window.onScroll event to transition the three possible states of an element.

Refer to the link

  • https://github.com/wilddeer/stickyfill
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/position
  • https://css-tricks.com/position-sticky-2/
  • https://juejin.cn/post/6844903503001829390