xinglie / xinglie.github.io

blog
https://xinglie.github.io
153 stars 22 forks source link

magix中的界面异步更新 #58

Open xinglie opened 4 years ago

xinglie commented 4 years ago

分片更新函数

在前端如果一个操作非常费时,则会造成界面的假死,通常我们要把这种操作转换成使用类似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就能感知出卡顿。所以我们该如何确保这个值动态化?

我们可以转换思路,换成执行这些函数,一旦执行时间超出一定值时,则进行中断休息,比如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部分,子节点更新和自身更新。伪代码如下

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则不需要。

这样我们在更新节点较多的界面时,浏览器并不会假死,而在性能好的机器上,我们可以看到节点同步更新,而在性能差的机器上,我们可能会看到节点一层层的更新掉。

更新算法

这里使用了和Vue类似的更新算法。 从两头向中间比较,先比较两个开头的节点是否一致,再比较两个末尾的节点是否一致。以及两个列表的头和尾比较,最后再使用一定的策略更新中间节点。

最后再删除或添加多余的节点

细节可参考这篇文章 https://www.cnblogs.com/wind-lanyan/p/9061684.html

当然,这里面最重要的是前面提到的分片更新函数,这样在更新上万个节点时,也不会造成浏览器的假死

多次更新

当前区块如果已经进入异步更新,则此时如果再发出一条或多条更新的指令,则每次均会把数据置为最新,然后再设置一个需要再次更新的标识位。

区块的异步更新完成后,检查是否有再次更新的标识,如果有,则再进入一次更新。

这样做的好处是可以合并异步更新中多次的更新指令为一次,缺点是在异步更新中如果再发出更新的指令,理想的状态是直接停止上次的更新,然后进入下一个更新状态。但这个方案需要对界面和虚拟dom实时对应,相应的操作也会变多,故先采用这个折中的比较省力的方案上