x-speedSheet 源码解读
整体架构
渲染
初始渲染
1 调用SpeadSheet构造函数
1.1 主要是根据传入的options创建出dom元素插入到传入的id元素中,有点类似react把虚拟dom元素创建出来之后,插入到页面设置的id元素上。
1.2 初始化一些数据,比如this.data 就是表示代理了整个sheet 数据的,有点类似设置了整个sheet数据的proxy。
1.3 this.sheet 表示整个sheet的初始化和事件的初始化,同时把this.data 也挂载到this.sheet.data上面,同时把event和table等数据表示挂载到上面。比如下图就是this.sheet的数据结构。

constructor(selectors, options = {}) {
let targetEl = selectors;
// 默认显示bottombar
this.options = { showBottomBar: true, ...options };
// sheetIndex 类似excel下面的几个表格的索引
this.sheetIndex = 1;
// 所有的数据,也就是每个sheet在数组中一个索引
this.datas = [];
if (typeof selectors === "string") {
// 获取容器
targetEl = document.querySelector(selectors);
}
this.bottombar = this.options.showBottomBar
? new Bottombar(
() => {
const d = this.addSheet();
this.sheet.resetData(d);
},
(index) => {
const d = this.datas[index];
this.sheet.resetData(d);
},
() => {
this.deleteSheet();
},
(index, value) => {
this.datas[index].name = value;
}
)
: null;
// #
// addSheet(name, active)
// 功能 添加多表
// @param name string 名称
// @param active boolean 默认为 true
// this.data 表示一张表的所有数据代理
this.data = this.addSheet();
// h就是创建一个对象,该对象的属性el表示创建的dom元素
const rootEl = h("div", `${cssPrefix}`).on("contextmenu", (evt) =>
evt.preventDefault()
);
// 把我们创建的元素插入到浏览器的dom元素上
targetEl.appendChild(rootEl.el);
// 创建表,this.data表示 DataProxy 代理类
this.sheet = new Sheet(rootEl, this.data);
if (this.bottombar !== null) {
rootEl.child(this.bottombar.el);
}
}
2 Sheet类
通过在构造函数中的逻辑可以发现,几乎所有的初始化整个views层和初始化事件等逻辑都在Sheet类中
在源码中this.sheet = new Sheet(rootEl, this.data); rootEl是一个div的数据结构,想象成是类似react的viralDOM表示就可以了,不过会比viralDOM简单很多。this.data就是在构造函数里边创建的整个sheet中的data的proxy代理对象。
在Sheet的构造函数中也是做了一些初始化的工作,比如创建toolbar对象,print 打印的对象,创建select的时候div的对象,创建edit的时候div的对象等,等到创建完这些对象之后,然后调用事件初始化函数初始化事件,调用render函数渲染。
constructor(targetEl, data) {
// 使用发布订阅模式
this.eventMap = createEventEmitter();
const { view, showToolbar, showContextmenu } = data.settings;
// el 表示真实的dom元素
this.el = h("div", `${cssPrefix}-sheet`);
// 创建toolbar
this.toolbar = new Toolbar(data, view.width, !showToolbar);
// 创建打印
this.print = new Print(data);
targetEl.children(this.toolbar.el, this.el, this.print.el);
// sheetDataProxy 代理
this.data = data;
// 创建表格
this.tableEl = h("canvas", `${cssPrefix}-table`);
// 创建缩放
this.rowResizer = new Resizer(false, data.rows.height);
this.colResizer = new Resizer(true, data.cols.minWidth);
// 创建scrollbar
this.verticalScrollbar = new Scrollbar(true);
this.horizontalScrollbar = new Scrollbar(false);
// 创建editor div对象
this.editor = new Editor(
formulas,
() => this.getTableOffset(),
data.rows.height
);
// 创建data检测
this.modalValidation = new ModalValidation();
// 创建右键点击的菜单栏
this.contextMenu = new ContextMenu(() => this.getRect(), !showContextmenu);
// 创建selected,也就是在表格中点击之后的对象
this.selector = new Selector(data);
// 创建遮罩层,通过这里获取到事件
this.overlayerCEl = h("div", `${cssPrefix}-overlayer-content`).children(
this.editor.el,
this.selector.el
);
this.overlayerEl = h("div", `${cssPrefix}-overlayer`).child(
this.overlayerCEl
);
// 创建排序
this.sortFilter = new SortFilter();
// 把创建的dom元素都插入到this.el中
this.el.children(
this.tableEl,
this.overlayerEl.el,
this.rowResizer.el,
this.colResizer.el,
this.verticalScrollbar.el,
this.horizontalScrollbar.el,
this.contextMenu.el,
this.modalValidation.el,
this.sortFilter.el
);
// 创建table对象
this.table = new Table(this.tableEl.el, data);
// 初始化事件
sheetInitEvents.call(this);
// 调用表格的render画出表格
sheetReset.call(this);
// 设置选择的表格的cell的坐标为0,0
selectorSet.call(this, false, 0, 0);
}
3 Table
Table的构造函数没有做过多的东西,很简单就是初始化了一些变量,this.draw就是一个画的函数,一个this.data是前面的sheetDataProxy。
这里的主要逻辑在Table的render函数里边
render函数里边就是一些比较简单的使用canvas的渲染逻辑,基本比较简单。
render() {
// resize canvas
const { data } = this;
const { rows, cols } = data;
// fixed width of header 固定宽度
const fixedWidth = cols.indexWidth;
// fixed height of header 固定高度
const fiexedHeight = rows.height;
this.draw.resize(data.viewWidth(), data.viewHeight());
this.clear();
const viewRange = data.viewRange();
// renderAll.call(this, viewRange, data.scroll);
const tx = data.freezeTotalWidth();
const ty = data.freezeTotalHeight();
const { x, y } = data.scroll;
// 1
renderContentGrid.call(this, viewRange, fixedWidth, fiexedHeight, tx, ty);
renderContent.call(this, viewRange, fixedWidth, fiexedHeight, -x, -y);
renderFixedHeaders.call(
this,
"all",
viewRange,
fixedWidth,
fiexedHeight,
tx,
ty
);
// 渲染左侧栏
renderFixedLeftTopCell.call(this, fixedWidth, fiexedHeight);
const [fri, fci] = data.freeze;
if (fri > 0 || fci > 0) {
// 2
if (fri > 0) {
const vr = viewRange.clone();
vr.sri = 0;
vr.eri = fri - 1;
vr.h = ty;
renderContentGrid.call(this, vr, fixedWidth, fiexedHeight, tx, 0);
renderContent.call(this, vr, fixedWidth, fiexedHeight, -x, 0);
renderFixedHeaders.call(
this,
"top",
vr,
fixedWidth,
fiexedHeight,
tx,
0
);
}
// 3
if (fci > 0) {
const vr = viewRange.clone();
vr.sci = 0;
vr.eci = fci - 1;
vr.w = tx;
renderContentGrid.call(this, vr, fixedWidth, fiexedHeight, 0, ty);
renderFixedHeaders.call(
this,
"left",
vr,
fixedWidth,
fiexedHeight,
0,
ty
);
renderContent.call(this, vr, fixedWidth, fiexedHeight, 0, -y);
}
// 4
const freezeViewRange = data.freezeViewRange();
renderContentGrid.call(
this,
freezeViewRange,
fixedWidth,
fiexedHeight,
0,
0
);
renderFixedHeaders.call(
this,
"all",
freezeViewRange,
fixedWidth,
fiexedHeight,
0,
0
);
renderContent.call(this, freezeViewRange, fixedWidth, fiexedHeight, 0, 0);
// 5
renderFreezeHighlightLine.call(this, fixedWidth, fiexedHeight, tx, ty);
}
}
到这里基本上使用x_spreadsheet('#x-spreadsheet-demo',options)的流程基本上就结束了。
此时的ui图如下图所示:

loadData
-
当完成了初始的渲染之后,然后我们可以使用loadData来加载数据,来完成表格中加载数据的过程。
可以先看下调用使用如下data,调用loadData达到的效果。
const rows = {
len: 80,
1: {
cells: {
0: { text: 'testingtesttestetst' },
2: { text: 'testing' },
},
},
2: {
cells: {
// 这里的style: 0 表示data中的styles的第一个元素
0: { text: 'render', style: 0 },
1: { text: 'Hello' },
2: { text: 'haha', merge: [1, 1] },
}
},
8: {
cells: {
// 这里的style: 0 表示data中的styles的第一个元素
8: { text: 'border test', style: 0 },
}
}
};
var xs = x_spreadsheet('#x-spreadsheet-demo', {
showToolbar: true,
showGrid: true,
showBottomBar: true,
col: {
len: 50,
width: 100,
// indexWidth: 60,
minWidth: 20,
},
row: {
len: 20,
},
extendToolbar: {
left: [
{
tip: 'Save',
icon: saveIcon,
onClick: (data, sheet) => {
console.log('click save button:', data, sheet)
}
}
],
right: [
{
tip: 'Preview',
el: previewEl,
onClick: (data, sheet) => {
console.log('click preview button:', data)
}
}
],
}
})
.loadData([{
freeze: 'B3',
styles: [
{
bgcolor: '#f4f5f8',
textwrap: true,
color: '#900b09',
border: {
top: ['thin', '#0366d6'],
bottom: ['thin', '#0366d6'],
right: ['thin', '#0366d6'],
left: ['thin', '#0366d6'],
},
},
],
merges: [
'C3:D4',
],
cols: {
len: 10,
2: { width: 200 },
},
rows,
})
效果图如下

-
这里的关键是通过调用dataSheetProxy的resetData来重新渲染,然后调用到了table的resetData,然后table调用render来重新渲染。到此基本的渲染基本上就完成了。
事件系统
表格的点击事件
-
通过createEventEmitter 注册事件, 文件目录(src/component/event.js),这里的注册事件很简单,就是普通的发布订阅模式
-
通过最外层的遮罩层监听事件函数,比如单击,比如双击,双击的时候,显示编辑框。