最近一个同学反馈,导入了一个带有公式的excel表以后,公式有部分不能生效,因为我们之前发现导入的Excel后,确实有这样子的问题,所以首先需要确认的是本身在luckysheet上做的公式是否是支持的。结果确认后发现本身也不行,所以问题已经确定是在luckysheet上,而不是excel导入的问题了。
分析
我们先来简化的看下,到底是什么样的公式是存在问题的,这里面其实涉及到的矩阵的转置。
首先我们看上图,上图的逻辑就是把横向的矩阵数据转置成了纵向,所以一旦源矩阵的数据如果发生变化的话,对应转置举证的数据也会发生变化。
下来我们增加新的引用关系, 如下图:
在某个单元格中去引用转置后的举证的其中一个单元格, 所以理论上我们希望会得到这样子的一个结果,就是一旦我修改了原单元格中的矩阵数据,相应转置的矩阵数据也会发生变化,同时引用了转置举证的单元格数据也要随着去更新。但是实际的结果是并没有。我们看下最后的结果现象
从上图就可以看出来更新了原矩阵的值后,下方选中的单元格数据并没有发生变化。遇到这样子的情况我们首先需要先确认一个事情即这个问题是因为举证转置导致的 还是本身引用的引用导致的这个问题。
所以我们重新做一个验证,如下图:
有如上的三者之前的关系,验证发现,通过修改原数据以后,选中的单元格数据其实是会发生更新的,所以我们大体可以确认一个问题即这个问题可能更多是跟矩阵转置有关系。
那现在我们就要排查下,问题出在哪个地方了。
首先我们得找到入口的地方,看了下代码逻辑后,终于发现了,在更新某个单元格后,如果该单元格只是纯文本的更新 没有其他公式引用的话,就会调用到 execFunctionGroup 方法。
所以我们需要看下 execFunctionGroup 他的具体逻辑是如何的。
这块的逻辑其实比较复杂 我们找到其中重点一个地方来看下
Object.keys(formulaObjects).forEach((key) => {
let formulaObject = formulaObjects[key];
arrayMatch(formulaObject.formulaArray, formulaObjects, updateValueOjects, function (childKey) {
if (childKey in formulaObjects) {
let childFormulaObject = formulaObjects[childKey];
formulaObject.chidren[childKey] = 1;
childFormulaObject.parents[key] = 1;
}
if (!isForce && childKey in updateValueOjects) {
updateValueArray.push(formulaObject);
}
});
if (isForce) {
updateValueArray.push(formulaObject);
}
});
从这个地方的调试发现,经过公式引用图那块的逻辑处理后,我们的 I4 那块的方法就被过滤掉了。所以问题很大概率就出现在那块了。我们详细看下这块的逻辑吧。
我们发现那块的逻辑又调用到了 arrayMatch 这个方法。
let arrayMatch = function (formulaArray, formulaObjects, updateValueOjects, func) {
for (let a = 0; a < formulaArray.length; a++) {
let range = formulaArray[a];
let cacheKey = "r" + range.row[0] + "" + range.row[1] + "c" + range.column[0] + "" + range.column[1] + "index" + range.sheetIndex;
if (cacheKey in arrayMatchCache) {
let amc = arrayMatchCache[cacheKey];
amc.forEach((item) => {
func(item.key, item.r, item.c, item.sheetIndex);
});
}
else {
let functionArr = [];
for (let r = range.row[0]; r <= range.row[1]; r++) {
for (let c = range.column[0]; c <= range.column[1]; c++) {
let key = "r" + r + "c" + c + "i" + range.sheetIndex;
func(key, r, c, range.sheetIndex);
if ((formulaObjects && key in formulaObjects) || (updateValueOjects && key in updateValueOjects)) {
functionArr.push({
key: key,
r: r,
c: c,
sheetIndex: range.sheetIndex
});
}
}
}
if (formulaObjects || updateValueOjects) {
arrayMatchCache[cacheKey] = functionArr;
}
}
}
}
我们发现这里会将每个公式的formulaArray 的区域都取出来,然后判断是否该区域内的节点是否在公式列表中,如果在的话,说明其实是存在有引用的引用的关系的。这个时候其实会在对应节点的parents中将key做赋值的操作的, 但是现在的问题就出现 我们上面截图的地方,r3c8i1 并没有出现在formulaObjects 中。因为他只有我们刚才截图中的r2c7i1 以及 r7c7i1 这两个,所以这里也就是为什么我们新的一个单元格的数据没有被更新的原因了,因为它根本就没有被计算进来。
解决
其实我们希望我们的 r3c8i1 能够被算到 r2c7i1 中因为它本身就是属于这个矩阵转置中的一部分。
我们又观察了下转置的矩阵的单元格,发现我选中其中一个单元格的时候其实他是有方法能够识别出来整个矩阵的区间的,如果是这样子的话 我是不是可以通过我这个节点,找到他真正的具有公式的单元格,再来就能够满足前面分析说的 r3c8i1 能够被算到 r2c7i1 这样子的情况了。
认真又看了下代码,还真的找到了这样子的一个逻辑,即
dynamicArray.js
function dynamicArrayHightShow(r, c) {
let dynamicArray = Store.luckysheetfile[getSheetIndex(Store.currentSheetIndex)]["dynamicArray"] == null ? [] : Store.luckysheetfile[getSheetIndex(Store.currentSheetIndex)]["dynamicArray"];
let dynamicArray_compute = dynamicArrayCompute(dynamicArray);
....
其实我们要的就是这个 dynamicArray_compute 这个值是什么,我们来放进去看下
我们得到了这样子的一个结果,这就有点思路了,因为我们刚才说过 我们没办法讲 r3c8i1 映射到 r2c7i1 现在通过这个对象就可以计算到了呀, 因为key中的r以及c实际上就是真正带有公式的单元格。
所以我们需要改造下前面 execFunctionGroup 说到的那块逻辑
Object.keys(formulaObjects).forEach((key) => {
let formulaObject = formulaObjects[key];
arrayMatch(formulaObject.formulaArray, formulaObjects, updateValueOjects, function (childKey, r, c, sheetIndex) {
if (childKey in formulaObjects || ((r + "_" + c) in dynamicArray_compute && ("r" + dynamicArray_compute[(r + "_" + c)].r + "c" + dynamicArray_compute[(r + "_" + c)].c + "i" + sheetIndex) in formulaObjects)) {
if ((r + "_" + c) in dynamicArray_compute) {
childKey = "r" + dynamicArray_compute[(r + "_" + c)].r + "c" + dynamicArray_compute[(r + "_" + c)].c + "i" + sheetIndex;
}
let childFormulaObject = formulaObjects[childKey];
formulaObject.chidren[childKey] = 1;
childFormulaObject.parents[key] = 1;
}
if (!isForce && childKey in updateValueOjects) {
updateValueArray.push(formulaObject);
}
});
if (isForce) {
updateValueArray.push(formulaObject);
}
});
如下我们再来验证下
现在数据是会发生变化了,但是更新竟然总是会延迟上一个数据。这个地方就很奇怪了。
所以我们只能重新断点跟下去看下什么情况了。最后发现最后的逻辑都进入到了
[execfunction](https://github.com/mengshukeji/Luckysheet/blob/master/src/global/formula.js#L5768) 这个方法中
execfunction: function (txt, r, c, index, isrefresh, notInsertFunc) {
let _this = this;
let _locale = locale();
let locale_formulaMore = _locale.formulaMore;
if (txt.indexOf(_this.error.r) > -1) {
return [false, _this.error.r, txt];
}
if (!_this.checkBracketNum(txt)) {
txt += ")";
}
if (index == null) {
index = Store.currentSheetIndex;
}
Store.calculateSheetIndex = index;
let fp = $.trim(_this.functionParserExe(txt));
if ((fp.substr(0, 20) == "luckysheet_function." || fp.substr(0, 22) == "luckysheet_compareWith")) {
_this.functionHTMLIndex = 0;
}
if (!_this.testFunction(txt, fp) || fp == "") {
tooltip.info("", locale_formulaMore.execfunctionError);
return [false, _this.error.n, txt];
}
let result = null;
window.luckysheetCurrentRow = r;
window.luckysheetCurrentColumn = c;
window.luckysheetCurrentIndex = index;
window.luckysheetCurrentFunction = txt;
let sparklines = null;
try {
....
result = new Function("return " + fp)();
if (typeof (result) == "string") {
result = result.replace(/\x7F/g, '"');
}
if (fp.indexOf("SPLINES") > -1) {
sparklines = result;
result = "";
}
}
catch (e) {
let err = e;
console.log(e, fp);
err = _this.errorInfo(err);
result = [_this.error.n, err];
}
....
return [true, result, txt];
}
我们看到前面的代码逻辑,关键的地方其实就是 result = new Function("return " + fp)(); 所以我们需要断点到这个看下I4 的获取结果是什么。
确实这里的data 中的v 就是要更新的值,但是这个值其实是未更新前的值 并不是最新的值。 所以可能是矩阵转置结果更新慢导致了这样子的问题。
我们又得最后看下数据的更新逻辑是啥
let v = _this.execfunction(calc_funcStr, formulaCell.r, formulaCell.c, formulaCell.index);
_this.groupValuesRefreshData.push({
"r": formulaCell.r,
"c": formulaCell.c,
"v": v[1],
"f": v[2],
"spe": v[3],
"index": formulaCell.index
});
从上面我们发现这个结果的数据更新其实不是立即更新的 是每个单元格都计算完结果以后,丢到一个groupValuesRefreshData 数组中,最后统一去触发更新了,这里就不难理解 可能就会有我们刚才说的那个问题了,所以我们可以尝试去这样处理,一旦方法执行完以后,就立即去更新单元格的数据是不是更好点,所以我们尝试这么去改。
直接增加如下的逻辑,在execfunction 后
let v = _this.execfunction(calc_funcStr, formulaCell.r, formulaCell.c, formulaCell.index);
let item = {
"r": formulaCell.r,
"c": formulaCell.c,
"v": v[1],
"f": v[2],
"spe": v[3],
"index": formulaCell.index
}
_this.groupValuesRefreshData.push(item);
_this.execFunctionGlobalData[formulaCell.r + "_" + formulaCell.c + "_" + formulaCell.index] = {
v: v[1],
f: v[2]
};
console.log(_this.groupValuesRefreshData)
let luckysheetfile = getluckysheetfile();
let file = luckysheetfile[getSheetIndex(item.index)];
let data = file.data;
if (data == null) {
continue;
}
let updateValue = {};
if (item.spe != null) {
if (item.spe.type == "sparklines") {
updateValue.spl = item.spe.data;
}
else if (item.spe.type == "dynamicArrayItem") {
file.dynamicArray = _this.insertUpdateDynamicArray(item.spe.data);
}
}
updateValue.v = item.v;
updateValue.f = item.f;
setcellvalue(item.r, item.c, data, updateValue);
我们看下最后的结果
如上就做到实时同步了,但是这个其实有个弊端的,因为本身作者统一更新的设计肯定是有一定道理的,也许是为了性能更优吧,所以这块的处理其实并不是那么的优雅,所以只是想到暂时这么解决这个问题。
|