SOURCE

console 命令行工具 X clear

                    
>
console
class ListView extends HTMLElement {
    constructor() {
        super();
        this.shadow = this.attachShadow({ mode: "closed" });
        this.adpater = null;
        this.container = document.createElement("div");
        this.container.style.cssText = `overflow: hidden;width: 100%; 
        height: 100%;position: relative;`;
        this.content = document.createElement("div");
        this.content.style.cssText = `width: 100%; height: 100%;
        position:absolute;left: 0;top: 0;user-select: none;`;
        this.container.appendChild(this.content);
        this.scrollbar = document.createElement("div");
        this.scrollbar.style.cssText = `position: absolute; right: 0; 
        top: 0; height: 100%;user-select: none;`;
        this.scrollbarThumb = document.createElement("div");
        this.scrollbarThumb.style.cssText = `width: 100%; background: #ccc;cursor: 
        pointer;position: absolute;top: 0; left: 0;user-select: none;
        box-sizing: border-box;`;
        this.scrollbar.appendChild(this.scrollbarThumb);
        this.container.appendChild(this.scrollbar);
        this.shadow.appendChild(this.container);
        this._firstRow = null;
        this._lastRow = null;
        this._elements = [];
        this._scrollTop = 0;
        this._pending = false;
        this._keepThumbActive = false;
        this._isMouseOnScrollbarThumb = false;
        this._observer = new ResizeObserver((es) => {
            this.schedule();
        });
        this._observer.observe(this.container);
        this._scrolbarSize = 15;
        Object.defineProperty(this, "scrollbarSize", {
            enumerable: true,
            configurable: true,
            get() {
                return this._scrolbarSize;
            },
            set(val) {
                if (typeof val != "number" || !val) {
                    return;
                }
                val = Math.max(0, val);
                this._scrolbarSize = val;
                this.scrollbar.style.width = val + "px";
                this.content.style.width = `calc(100% - ${val}px)`;
            }
        });
        this.scrollbarSize = 10;

        let _self = this;
        function captureMouse(e) {
            let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
            let thumbOffsetY = e.clientY - thumbRect.top;
            let rect = _self.scrollbar.getBoundingClientRect();
            function onMouseMove(e) {
                let totalHeight = _self.adpater.getItemCount() * _self.adpater.getItemHeight();
                let thumbOffsetTop = e.clientY - thumbOffsetY - rect.top;
                let maxOffset = rect.height - thumbRect.height;
                thumbOffsetTop = Math.min(maxOffset, Math.max(0, thumbOffsetTop));
                _self.scrollbarThumb.style.top = thumbOffsetTop + "px";
                _self._scrollTop = thumbOffsetTop * (totalHeight - rect.height) / maxOffset;
                _self.schedule();
            }
            function onMouseUp(e) {
                document.removeEventListener("mousemove", onMouseMove);
                document.removeEventListener("mouseup", onMouseUp);
                _self._keepThumbActive = false;
                if(!_self._isMouseOnScrollbarThumb) {
                    _self.scrollbarThumb.style.background = "#ccc";
                }
            }
            document.addEventListener("mousemove", onMouseMove);
            document.addEventListener("mouseup", onMouseUp);
            _self._keepThumbActive = true;
        }
        this.scrollbarThumb.addEventListener("mousedown", function (e) {
            e.stopImmediatePropagation();
            captureMouse(e);
        });
        this.scrollbarThumb.addEventListener("mouseenter", function (e) {
            _self.scrollbarThumb.style.background = "#999";
            _self._isMouseOnScrollbarThumb = true;
        });
        this.scrollbarThumb.addEventListener("mouseleave", function (e) {
            _self._isMouseOnScrollbarThumb = false;

            if(_self._keepThumbActive) {
                return;
            }
            _self.scrollbarThumb.style.background = "#ccc";
        });

        let scrollFinished = true;
        let animationScroll = true;
        let timer = null;
        let deltaY = 0;

        function startAnimationScroll() {
            _self.setScrollTopBy(deltaY);

            let isNegative = deltaY < 0;
            let newDeltaY = Math.abs(deltaY) - 0.5;
            deltaY = isNegative ? -newDeltaY : newDeltaY;
            if (newDeltaY <= 0) {
                deltaY = 0;
                scrollFinished = true;
                cancelAnimationFrame(timer);
            } else {
                timer = requestAnimationFrame(startAnimationScroll);
            }
        }
        function startAnimationWheelScroll() {
            if (!scrollFinished) {
                return;
            }
            scrollFinished = false;
            startAnimationScroll();
        }
        this.container.addEventListener("wheel", function (e) {
            let isTouch = Math.abs(e.deltaY) < 125;
            if (animationScroll && !isTouch) {
                deltaY = e.deltaY < 0 ? -15 : 15;
                startAnimationWheelScroll();
            } else {
                _self.setScrollTopBy(e.deltaY);
            }
            let maxScrollTop = _self.getScrollHeight() - _self.container.getBoundingClientRect().height;
            if(_self._scrollTop > 0 && _self._scrollTop < maxScrollTop) {
                e.preventDefault();
            }
        });

        function captureMouse2(e) {
            let timer = null;
            let rect = _self._rect;
            let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
            let isMouseOnScrollbarTrack = true;
            let dir = e.clientY > thumbRect.bottom ? 1 : 0;
            function onMouseUp() {
                document.removeEventListener("mouseup", onMouseUp);
                document.removeEventListener("mousemove", onMouseMove);
                _self.scrollbar.removeEventListener("mouseenter", onMouseEnter);
                _self.scrollbar.removeEventListener("mouseleave", onMouseLeave);
                cancelAnimationFrame(timer);
            }
            function onMouseEnter() {
                isMouseOnScrollbarTrack = true;
            }
            function onMouseLeave() {
                isMouseOnScrollbarTrack = false;
            }
            function onMouseMove(ev) {
                e = ev;
            }
            document.addEventListener("mouseup", onMouseUp);
            document.addEventListener("mousemove", onMouseMove);
            _self.scrollbar.addEventListener("mouseenter", onMouseEnter);
            _self.scrollbar.addEventListener("mouseleave", onMouseLeave);

            function startAutoScroll() {
                let thumbRect = _self.scrollbarThumb.getBoundingClientRect();
                let dir2 = e.clientY > thumbRect.bottom ? 1 : (e.clientY < thumbRect.top ? 0 : -1);

                if (dir2 == dir) {
                    if (isMouseOnScrollbarTrack) {
                        let max = 25, min = 1;
                        let offset = Math.floor(Math.random() * (max - min)) + min;
                        let delta = dir2 == 0 ? -offset : (dir2 == 1 ? offset : 0);
                        _self.setScrollTopBy(delta);
                    }
                }
                timer = requestAnimationFrame(startAutoScroll);
            }
            startAutoScroll();
        }
        this.scrollbar.addEventListener("mousedown", function (e) {
            captureMouse2(e);
        });
    }
    getScrollHeight() {
        return this.adpater ? this.adpater.getItemCount() * this.adpater.getItemHeight() : 0;
    }
    setAdapter(adapter) {
        this.adpater = adapter;
        this.schedule();
    }
    update() {
        if (!this.adpater) {
            return;
        }

        let itemHeight = this.adpater.getItemHeight();
        let itemCount = this.adpater.getItemCount();
        let totalHeight = itemCount * itemHeight;
        let rect = this._rect || this.container.getBoundingClientRect();
        let thumbHeight = Math.max(20, rect.height / totalHeight * rect.height);
        this.scrollbarThumb.style.height = thumbHeight + "px";
        this.content.style.top = -(this._scrollTop % this.adpater.getItemHeight()) + "px";

        let firstRow = Math.floor(this._scrollTop / itemHeight);
        let lastRow = Math.min(itemCount - 1, Math.ceil((this._scrollTop + rect.height) / itemHeight));

        if (firstRow === this._firstRow && lastRow === this._lastRow) {
            return;
        }
        this._firstRow = firstRow;
        this._lastRow = lastRow;

        let counter = 0;
        for (let i = firstRow; i <= lastRow; ++i) {
            let elem = this.adpater.getElement(i, this._elements[counter]);
            this._elements[counter] = elem;
            ++counter;
        }
        this._elements.length = counter;
        this.content.replaceChildren(...this._elements);
    }
    schedule(changes) {
        if (this._pending) {
            return;
        }
        this._pending = true;
        requestAnimationFrame(() => {
            this.update();
            this._pending = false;
            this._changes = 0;
        });
    }
    setScrollTop(scrollTop) {
        if (!this.adpater || typeof scrollTop != "number") {
            return;
        }
        let scrollHeight = this.adpater.getItemHeight() * this.adpater.getItemCount();
        let rect = this._rect || this.container.getBoundingClientRect();
        let maxScrollTop = scrollHeight - rect.height;
        scrollTop = Math.min(Math.max(0, scrollTop), maxScrollTop);
        this._scrollTop = scrollTop;
        let thumbHeight = this.scrollbarThumb.getBoundingClientRect().height;
        let thumbOffsetTop = scrollTop * (rect.height - thumbHeight) / (scrollHeight - rect.height);
        this.scrollbarThumb.style.top = thumbOffsetTop + "px";
        this.schedule();
    }
    setScrollTopBy(delta) {
        this.setScrollTop(this._scrollTop + delta);
    }
}
customElements.define("list-view", ListView);

let listView = document.querySelector("list-view");
listView.setAdapter({
    getItemHeight() {
        return 70;
    },
    getItemCount() {
        return 200000;
    },
    getElement(i, elem) {
        if (!elem) {
            elem = document.createElement("div");
            elem.style.height = elem.style.lineHeight = this.getItemHeight() + "px";
            elem.style.boxSizing = "border-box";
            elem.style.padding = "10px";
            let img = document.createElement("img");
            img.src = "https://cn.bing.com/th?id=OPAC.wi8BGRAzJverPQ474C474&o=5&pid=21.1&w=160&h=160&qlt=100&dpr=1.3";
            img.style.cssText = "height: 100%; float: left;";
            elem.appendChild(img);

            let price = document.createElement("div");
            price.classList.add("price");
            price.style.cssText = `float: left; margin-left: 10px;`;
            elem.appendChild(price);
        }
        elem.querySelector(".price").textContent = "连衣裙" + (i + 1);
        return elem;
    }
});
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=, initial-scale=">
	<meta http-equiv="X-UA-Compatible" content="">
	<title></title>
</head>
<body>
	<div id="container">
        <list-view></list-view>
    </div>
</body>
</html>
#container {
    position: absolute;
    width: 300px;
    height: 300px;
    box-shadow: 0 0 3px #aaa;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
}
body,html {
    overflow: hidden;
}