前言
作为使用React前端开发,可能vue也一样,应该会经常遇到长列表的功能需求,如果不做性能优化,那会导致渲染性能低,带来网页卡顿的问题。常见的解决方案应该是使用虚拟列表,只渲染可见的部分。但是对于虚拟列表的功能,经常不仅仅只需要渲染,还会有拖拽排序,多选组合等功能,这会带来很多的业务复杂度。首先,因为很少会自己出造轮子,所以光相关组件库的引用,就需要虚拟列表库,拖拽库等,因为虚拟列表无法渲染树结构,所以还需要讲树结构展平,然后还需要维护父子关系。那除了使用虚拟列表,还有哪些高性能渲染长列表的方式呢?这里介绍一种使用canvas画布绘制长列表的方法。先看下成品的效果,
这个是我们编辑器项目的canvas列表效果,这里的icon和滚动条也是使用canvas绘制的,使用体验跟dom体验相差无几,甚至应该比虚拟列表的体验好。
如果你的项目恰好也是一个基于canvas的产品,那使用canvas应该也是更好的选择。
接下来,本文会教大家封装一个开源的canvas列表的组件,这篇文章主要讲下列表的渲染以及滚动条的实现
技术分析
1. 渲染部分
每个列表项就是矩形,矩形里面绘制一个icon,绘制一个文字,如果是父元素,那需要绘制一个文件夹图标和一个折叠图标
滚动条也是一个矩形,在弄个圆角美观点
2. 交互部分
点击的时候需要知道点击的时候需要点击了哪个部分
需要拖拽功能
3. 技术选型
为了更快的实现上面的功能,选个canvas库进行开发,常见的canvas库有fabricjs,konvajs,pixijs
fabricjs文档不友好,
pixijs是基于webgl的,性能比较好,但是demo 没适合这个组件的
konvajs 文档挺好,官方demo 提供了很多这个组件需要的功能
所以这个组件选择基于konvajs进行开发
实现步骤
数据结构
常见的树结构
export type TreeItemOption = {
label: string;
id: string | number;
data?: any;
children?: TreeItemOption[];
};
canvas列表类
export class CanvasTree {
private _stage: Stage;
private _layer: Layer;
private scrollBar: Rect;
private totalHeight = 30;
private _renderIndex = 0;
constructor(container: HTMLElement, private _data: TreeItemOption[] = []) {
this._stage = new Stage({
container: container as HTMLDivElement,
width: container.clientWidth,
height: container.clientHeight,
});
const layer = new Layer();
this._layer = layer;
this._stage.add(layer);
// 渲染滚动条
const rect = new Rect({
name: SCROLL_BAR_NAME,
height: 100,
width: SCROLL_BAR_WIDTH,
cornerRadius: 5,
fill: "#ccc",
ID: SCROLL_BAR_NAME,
x: this._stage.width() - SCROLL_BAR_WIDTH,
hitStrokeWidth: 10,
y: 0,
draggable: true,
dragBoundFunc: (pos) => {
return {
x: this.scrollBar.x(),
y: clamp(pos.y, 0, this.Height - this.scrollBar.height()),
};
},
});
layer.add(rect);
this.scrollBar = rect;
this.render();
this.register();
}
get Width() {
return this._stage.width();
}
get Height() {
return this._stage.height();
}
}
以上代码主要是初始化konvajs的画布场景,场景中加入一个层,层里面添加一个rect 作为滚动条。滚动条固定在画布的最右边,拖拽的时候固定x坐标,y的范围限制在画布内。
滚动条的高度为 (场景高度/列表总高度)*场景高度
绘制列表项
renderNodeItem(item: TreeItemOption, depth: number) {
const x = depth * 30;
const y = this._renderIndex * 30;
const group = new Group({
x,
y,
});
const background = new Rect({
name: "Backgroud",
width: this.Width,
height: 30,
stroke: "black",
strokeWidth: 1,
});
group.add(background);
const text = new Text({
text: item.label,
x: 30,
height: 30,
verticalAlign: "middle",
});
group.add(text);
this._layer.add(group);
this._renderIndex++;
if (item.children) {
for (const element of item.children) {
this.renderNodeItem(element, depth + 1);
}
}
}
列表项使用Group渲染,每个Group 加入Rect作为背景。使用·renderIndex
记录项的索引,每个项的y值根据索引决定,每个项的x值由项所在的深度决定
监听滚动行为
this._stage.on("wheel", (e) => {
if (this.totalHeight <= this.Height) return;
e.evt.preventDefault();
const direction = -Math.sign(e.evt.deltaY);
if (direction < 0 && -this._stage.y() + this.Height >= this.totalHeight) {
return;
}
if (direction > 0 && this._stage.y() >= 0) {
return;
}
let y = this._stage.y();
y += direction * (itemHeight + 1) * (Math.abs(e.evt.deltaY) / 100);
if (direction < 0 && -y + this.Height >= this.totalHeight) {
y = this.Height - this.totalHeight;
}
if (direction > 0 && y >= 0) {
y = 0;
}
this._stage.y(y);
this.updateScrollBar(y);
});
updateScrollBar(y: number) {
if (this.totalHeight <= this.Height) return;
const scale = Math.abs(y) / this.totalHeight;
const dist = scale * this.Height;
this.scrollBar.y(-y + dist);
}
监听场景的 wheel事件,如果场景的y 值在有效范围内(即在0 到列表总高度之间,总高度就是列表项数*列表项高度),那更新场景的y值,让场景上下滑动,同时需要更新滚动条的位置。
滚动条的位置 根据滚动高度和总列表高度的比例决定
拖拽滚动条
scrollY2StageY() {
const y = this.scrollBar.y();
const stageY = Math.abs(this._stage.y());
const dist = y - stageY;
const scale = dist / this.Height;
this._stage.y(-this.totalHeight * scale);
this.updateScrollBar(-this.totalHeight * scale);
}
将滚动条的y值 转换为场景的y值,然后在根据最新的场景高度,更新下滚动条的位置
效果
完整代码
请前往代码仓库查看
总结
以上基于konvajs实现了长列表的渲染以及对滚动条的模拟,下一步讲实现列表项的拖动,编组等。
参考资料
– Konvajs
原文链接:https://juejin.cn/post/7326268986797113381 作者:ZoeLee