Open xinglie opened 4 years ago
在前端如果一个操作非常费时,则会造成界面的假死,通常我们要把这种操作转换成使用类似setTimeout等方法来让浏览器有机会响应界面,虽然这样做可能总体耗时更长,比如
setTimeout
for(let i=0;i<bigArray.length;i++){ bigArray[i]=longProcessTime(bigArray[i]); }
这样的一个循环假设非常耗时,则我们可以转换成
let task=(start,max)=>{ let items=bigArray.slice(start,start+max); for(let i=0;i<items.length;i++){ bigArray[i+start]=longProcessTime(items[i]); } if(start+max<bigArray.length){ setTimeout(()=>{ task(start+max,max); }); } }; task(0,100);
在这个里面,有一个不好界定的值100,有些性能好的机器可能一次处理300个都不带卡的,有些可能超过50就能感知出卡顿。所以我们该如何确保这个值动态化?
100
300
50
我们可以转换思路,换成执行这些函数,一旦执行时间超出一定值时,则进行中断休息,比如32ms,这样在同样的单位时间内,性能好的机器可以多执行一次函数,性能差的机器,执行的函数少一些。
32ms
我们可以抽象出这样的一个分片执行函数的功能,示意代码如下
let CallIndex = 0; let CallList = []; let CallBreakTime = 32; let StartCall = () => { let last = Date_Now(), next, args, context; try { while (CallBreakTime) { next = CallList[CallIndex - 1]; context = CallList[CallIndex]; args = CallList[CallIndex + 1]; CallIndex += 4; if (next) { if (next != Noop) { if (IsArray(args)) { next.apply(context, args); } else { next.call(context, args); } } if (Date_Now() - last > CallBreakTime && CallList.length > CallIndex) { Timeout(StartCall); console.log(`[CF] take a break of ${CallList.length} at ${CallIndex}`); break; } } else { CallList.length = CallIndex = 0; break; } } } catch (ex) { Mx_Cfg.error(ex); Timeout(StartCall); } }; let CallFunction = (fn, args?, context?, id?) => { if (!CallIndex) { CallIndex = 1; Timeout(StartCall); } if (id) { for (let i = CallList.length - 1; i >= CallIndex; i -= 4) { if (CallList[i] == id) { CallList[i - 3] = Noop; console.log('ignore id', id); } } } CallList.push(fn, context, args, id); };
然后我们可以
for(let i=0;i<bigArray.length;i++){ CallFunction(idx=>{ bigArray[idx]=longProcessTime(bigArray[idx]); },i); }
这样就能达到在性能好的机器上多执行,性能差的机器上少执行,总体来讲不会造成界面假死的情况。
节点更新分2部分,子节点更新和自身更新。伪代码如下
2
let updateChildren=(children)=>{ for(let c of children){ CallFunction(updateNode,c); } }; let updateNode=(child)=>{ //update child let children=child.childNodes; updateChildren(children); };
在这个地方,我们也不要把任意的函数都放在CallFunction里执行,我们把比如更新节点的动作放到CallFunction里,而updateChildren则不需要。
CallFunction
updateChildren
这样我们在更新节点较多的界面时,浏览器并不会假死,而在性能好的机器上,我们可以看到节点同步更新,而在性能差的机器上,我们可能会看到节点一层层的更新掉。
这里使用了和Vue类似的更新算法。 从两头向中间比较,先比较两个开头的节点是否一致,再比较两个末尾的节点是否一致。以及两个列表的头和尾比较,最后再使用一定的策略更新中间节点。
Vue
最后再删除或添加多余的节点
细节可参考这篇文章 https://www.cnblogs.com/wind-lanyan/p/9061684.html
当然,这里面最重要的是前面提到的分片更新函数,这样在更新上万个节点时,也不会造成浏览器的假死
当前区块如果已经进入异步更新,则此时如果再发出一条或多条更新的指令,则每次均会把数据置为最新,然后再设置一个需要再次更新的标识位。
区块的异步更新完成后,检查是否有再次更新的标识,如果有,则再进入一次更新。
这样做的好处是可以合并异步更新中多次的更新指令为一次,缺点是在异步更新中如果再发出更新的指令,理想的状态是直接停止上次的更新,然后进入下一个更新状态。但这个方案需要对界面和虚拟dom实时对应,相应的操作也会变多,故先采用这个折中的比较省力的方案上
分片更新函数
在前端如果一个操作非常费时,则会造成界面的假死,通常我们要把这种操作转换成使用类似
setTimeout
等方法来让浏览器有机会响应界面,虽然这样做可能总体耗时更长,比如这样的一个循环假设非常耗时,则我们可以转换成
在这个里面,有一个不好界定的值
100
,有些性能好的机器可能一次处理300
个都不带卡的,有些可能超过50
就能感知出卡顿。所以我们该如何确保这个值动态化?我们可以转换思路,换成执行这些函数,一旦执行时间超出一定值时,则进行中断休息,比如
32ms
,这样在同样的单位时间内,性能好的机器可以多执行一次函数,性能差的机器,执行的函数少一些。我们可以抽象出这样的一个分片执行函数的功能,示意代码如下
然后我们可以
这样就能达到在性能好的机器上多执行,性能差的机器上少执行,总体来讲不会造成界面假死的情况。
节点更新
节点更新分
2
部分,子节点更新和自身更新。伪代码如下在这个地方,我们也不要把任意的函数都放在
CallFunction
里执行,我们把比如更新节点的动作放到CallFunction
里,而updateChildren
则不需要。这样我们在更新节点较多的界面时,浏览器并不会假死,而在性能好的机器上,我们可以看到节点同步更新,而在性能差的机器上,我们可能会看到节点一层层的更新掉。
更新算法
这里使用了和
Vue
类似的更新算法。 从两头向中间比较,先比较两个开头的节点是否一致,再比较两个末尾的节点是否一致。以及两个列表的头和尾比较,最后再使用一定的策略更新中间节点。最后再删除或添加多余的节点
细节可参考这篇文章 https://www.cnblogs.com/wind-lanyan/p/9061684.html
当然,这里面最重要的是前面提到的分片更新函数,这样在更新上万个节点时,也不会造成浏览器的假死
多次更新
当前区块如果已经进入异步更新,则此时如果再发出一条或多条更新的指令,则每次均会把数据置为最新,然后再设置一个需要再次更新的标识位。
区块的异步更新完成后,检查是否有再次更新的标识,如果有,则再进入一次更新。
这样做的好处是可以合并异步更新中多次的更新指令为一次,缺点是在异步更新中如果再发出更新的指令,理想的状态是直接停止上次的更新,然后进入下一个更新状态。但这个方案需要对界面和虚拟dom实时对应,相应的操作也会变多,故先采用这个折中的比较省力的方案上