概述
默认情况下,注册在 UI 线程上的自定义函数会通过 Calc Worker 与 UI 线程之间的往返通信完成计算。通过重写 supportCalcWorker() 并返回 true,SpreadJS 会将函数的计算逻辑序列化后直接放到 Calc Worker 中执行,从而减少线程切换带来的开销。
本 Demo 使用两个逻辑相同的 SHA-256 哈希函数进行对比:
HASH:默认仍在主线程完成计算
HASHINWORKER:直接在 Calc Worker 中执行计算
示例会分别在两个工作簿中填充 1000 个公式单元格,并展示两种实现方式的计算耗时差异。
实现思路
引入 spread-sheets-calcworker 插件,启用 Calc Worker 支持
定义异步自定义函数 HASH
基于 HASH 派生 HASHINWORKER,并实现 supportCalcWorker()
分别初始化两个工作簿,填充相同的数据和公式
首次计算及点击按钮重新计算时,统计各自耗时并显示结果
代码解析
让自定义函数直接在 Calc Worker 中执行
当 supportCalcWorker() 返回 true 时,SpreadJS 会把该自定义函数的计算逻辑发送到 Calc Worker 中执行,而不是在 Worker 中再回调 UI 线程。
在异步函数中执行哈希计算
这里使用浏览器提供的 crypto.subtle API 计算 SHA-256 哈希值,并通过 context.setAsyncResult() 返回结果。
统计重新计算耗时
按钮点击后触发完整重算,并借助 waitForAllCalculations() 等待异步计算结束,再输出总耗时。
限制说明
需要在 evaluate 或 evaluateAsync 内部定义所有相关逻辑
因为 SpreadJS 会调用 customFunction.evaluate.toString() 或 customFunction.evaluateAsync.toString(),再将函数源码发送到 Calc Worker 中执行,所以计算依赖的逻辑不能依赖外部闭包变量。
Calc Worker 中无法访问 Workbook 和 Worksheet 对象
在计算线程里不能直接使用 GC.Spread.Sheets、spread.calculate、spread.suspendCalcService 等工作簿相关 API,只能使用计算上下文和 Web Worker 可用的原生能力。
运行效果
左侧工作簿使用 HASHINWORKER,直接在 Calc Worker 中计算
右侧工作簿使用 HASH,通过默认方式回到主线程计算
两侧都会显示首次计算耗时
点击“重新计算”按钮后,可以反复比较两种实现方式的性能差异
相关 API
AsyncFunction
用于定义异步自定义函数。
supportCalcWorker
返回 true 后,表示该自定义函数支持直接在 Calc Worker 中执行。
waitForAllCalculations
等待当前工作簿中的所有异步计算完成,适合用于性能统计和串联后续逻辑。
window.onload = function () {
var spreadLeft = new GC.Spread.Sheets.Workbook(document.getElementById("ssLeft"), { sheetCount: 1 });
var spreadRight = new GC.Spread.Sheets.Workbook(document.getElementById("ssRight"), { sheetCount: 1 });
spreadLeft.options.incrementalCalculation = true;
spreadRight.options.incrementalCalculation = true;
initSpread(spreadLeft, spreadRight);
};
async function initSpread(spreadLeft, spreadRight) {
function HashFunction() {
}
HashFunction.prototype = new GC.Spread.CalcEngine.Functions.AsyncFunction("HASH", 1, 1);
HashFunction.prototype.defaultValue = function () {
return "计算中...";
};
HashFunction.prototype.evaluateAsync = function (context, value) {
var text = value == null ? "" : String(value);
var encoder = new TextEncoder();
var data = encoder.encode(text);
crypto.subtle.digest("SHA-256", data).then(function (hashBuffer) {
var hashArray = Array.from(new Uint8Array(hashBuffer));
var hashHex = hashArray.map(function (b) {
return b.toString(16).padStart(2, "0");
}).join("");
context.setAsyncResult(hashHex.substring(0, 6));
});
};
function HashInWorkerFunction() {
}
HashInWorkerFunction.prototype = new HashFunction();
HashInWorkerFunction.prototype.name = "HASHINWORKER";
HashInWorkerFunction.prototype.supportCalcWorker = function () {
return true;
};
GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASH", new HashFunction());
GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("HASHINWORKER", new HashInWorkerFunction());
var sheetLeft = spreadLeft.getActiveSheet();
var sheetRight = spreadRight.getActiveSheet();
await spreadLeft.waitForAllCalculations();
await spreadRight.waitForAllCalculations();
setupSheet(sheetLeft, "HASHINWORKER", true);
setupSheet(sheetRight, "HASH", false);
var startLeft = performance.now();
sheetLeft.resumeCalcService();
await spreadLeft.waitForAllCalculations();
var timeLeft = (performance.now() - startLeft).toFixed(2);
sheetLeft.setValue(1, 0, "计算耗时:" + timeLeft + " ms");
var startRight = performance.now();
sheetRight.resumeCalcService();
await spreadRight.waitForAllCalculations();
var timeRight = (performance.now() - startRight).toFixed(2);
sheetRight.setValue(1, 0, "计算耗时:" + timeRight + " ms");
function bindRecalc(spread, sheet) {
var isRecalculating = false;
spread.bind(GC.Spread.Sheets.Events.ButtonClicked, async function () {
if (isRecalculating) {
return;
}
isRecalculating = true;
sheet.setValue(1, 0, "计算中...");
var start = performance.now();
spread.calculate();
await spread.waitForAllCalculations();
var time = (performance.now() - start).toFixed(2);
sheet.setValue(1, 0, "计算耗时:" + time + " ms");
isRecalculating = false;
});
}
bindRecalc(spreadLeft, sheetLeft);
bindRecalc(spreadRight, sheetRight);
}
var names = [
"Alice Johnson", "Bob Smith", "Charlie Brown", "Diana Prince", "Edward Norton",
"Fiona Apple", "George Martin", "Helen Troy", "Ivan Petrov", "Julia Roberts",
"Kevin Hart", "Laura Palmer", "Michael Scott", "Nancy Drew", "Oscar Wilde",
"Patricia Moore", "Quincy Adams", "Rachel Green", "Steven King", "Tina Turner"
];
var emails = [
"alice@example.com", "bob@example.com", "charlie@example.com", "diana@example.com", "edward@example.com",
"fiona@example.com", "george@example.com", "helen@example.com", "ivan@example.com", "julia@example.com",
"kevin@example.com", "laura@example.com", "michael@example.com", "nancy@example.com", "oscar@example.com",
"patricia@example.com", "quincy@example.com", "rachel@example.com", "steven@example.com", "tina@example.com"
];
function setupSheet(sheet, funcName, isWorker) {
sheet.suspendPaint();
sheet.suspendCalcService();
sheet.options.allowCellOverflow = true;
var headerColor = isWorker ? "#51cf66" : "#ff6b6b";
var headerForeColor = "#ffffff";
var lightBg = isWorker ? "#d3f9d8" : "#ffe0e0";
var dataRows = 1000;
var headerRow = 0;
var timeRow = 1;
var tableHeaderRow = 2;
var dataStartRow = 3;
sheet.setRowCount(dataStartRow + dataRows);
sheet.setColumnCount(4);
sheet.setColumnWidth(0, 50);
sheet.setColumnWidth(1, 150);
sheet.setColumnWidth(2, 190);
sheet.setColumnWidth(3, 140);
sheet.addSpan(headerRow, 0, 1, 4);
sheet.setValue(headerRow, 0, isWorker ? "启用 supportCalcWorker()" : "未启用 supportCalcWorker()");
sheet.getCell(headerRow, 0)
.backColor(headerColor)
.foreColor(headerForeColor)
.font("bold 14px Microsoft YaHei")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center)
.vAlign(GC.Spread.Sheets.VerticalAlign.center);
sheet.setRowHeight(headerRow, 36);
sheet.addSpan(timeRow, 0, 1, 3);
sheet.setValue(timeRow, 0, "计算中...");
sheet.getCell(timeRow, 0)
.backColor(lightBg)
.font("bold 16px Consolas")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center)
.vAlign(GC.Spread.Sheets.VerticalAlign.center);
sheet.setRowHeight(timeRow, 30);
var btnCellType = new GC.Spread.Sheets.CellTypes.Button();
btnCellType.text("重新计算");
btnCellType.buttonBackColor(lightBg);
sheet.setCellType(timeRow, 3, btnCellType);
sheet.getCell(timeRow, 3)
.backColor(lightBg)
.hAlign(GC.Spread.Sheets.HorizontalAlign.center)
.vAlign(GC.Spread.Sheets.VerticalAlign.center);
var headers = ["#", "姓名", "邮箱", funcName + "()"];
for (var c = 0; c < headers.length; c++) {
sheet.setValue(tableHeaderRow, c, headers[c]);
sheet.getCell(tableHeaderRow, c)
.backColor("#f1f3f5")
.font("bold 12px Microsoft YaHei")
.hAlign(GC.Spread.Sheets.HorizontalAlign.center);
}
sheet.setRowHeight(tableHeaderRow, 26);
for (var i = 0; i < dataRows; i++) {
var row = dataStartRow + i;
sheet.setValue(row, 0, i + 1);
sheet.setValue(row, 1, names[i % names.length]);
sheet.setValue(row, 2, emails[i % emails.length]);
sheet.setFormula(row, 3, "=" + funcName + "(B" + (row + 1) + "&C" + (row + 1) + ")");
if (i % 2 === 1) {
sheet.getRange(row, 0, 1, 4).backColor("#fafafa");
}
}
sheet.resumePaint();
}
<!doctype html>
<html style="height:100%;font-size:14px;">
<head>
<meta name="spreadjs culture" content="zh-cn" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/styles/gc.spread.sheets.excel2013white.css">
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets/dist/gc.spread.sheets.all.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-calc-worker/dist/gc.spread.sheets.calcworker.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/zh/purejs/node_modules/@grapecity-software/spread-sheets-resources-zh/dist/gc.spread.sheets.resources.zh.min.js" type="text/javascript"></script>
<script src="$DEMOROOT$/spread/source/js/license.js" type="text/javascript"></script>
<script src="app.js" type="text/javascript"></script>
<link rel="stylesheet" type="text/css" href="styles.css">
</head>
<body>
<div class="sample-tutorial">
<div class="spread-panel">
<div id="ssLeft" class="sample-spreadsheets"></div>
</div>
<div class="spread-panel">
<div id="ssRight" class="sample-spreadsheets"></div>
</div>
</div>
</body>
</html>
body {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
font-family: "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
}
.sample-tutorial {
position: relative;
height: 100%;
overflow: hidden;
display: flex;
}
.spread-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.spread-panel:first-child {
border-right: 2px solid #e0e0e0;
}
.sample-spreadsheets {
flex: 1;
overflow: hidden;
}