异步执行之setTimeout

如果要对一个十分巨大的数组进行遍历,进行复杂的逻辑计算,再将数据渲染到浏览器,通常会这样书写代码。

for (let i = 0; i < 2000; i++) {
  process(i) //逻辑处理
} 

但是,当页面刷新的时候,如果数据量很大,或者逻辑运算复杂,会发现页面要等待很久才能加载出来,这是因为在process逻辑处理的时候,页面一直处于阻塞状态,导致迟迟不能显示。

要解决阻塞的问题,就是要想办法让出UI线程的控制权,使得UI可以更新。我们可以让这些数据的处理处于异步的过程中,从而不会影响页面UI主线程导致阻塞。

setTimeout可以用来实现代码异步的目的,举个例子:

setTimeout(() => {
  console.log('world')
})
console.log('hello')

最终会先输出hello,再输出world,这是因为setTimeout中的任务,js会将其添加到任务队列的末尾,当队列空闲时才会执行,所以会在最后打印world,在之前关于setTimeout这篇文章中也有介绍。

所以可以将最上面的代码改写为如下方式(由于setTimeout定时器的精度问题,一般会将延时设置为25毫秒)

let array = []
for (let i = 0; i < 2000; i++) {
  array.push(i)
}

let todo = array.concat() //数组拷贝
setTimeout(function () {
  console.log(todo.shift())
  if (todo.length > 0) {
    setTimeout(arguments.callee, 25)
  }
}, 25)

改写后,数据循环处理代码不会阻塞浏览器渲染了。但是,问题又来了,上面每处理一次数据都要延迟25毫秒,假如每处理数组中的一项需要1毫秒,如果每个定时器只处理一项,且在两次处理之间产生25毫秒的延迟,那么2000条数据就是2000*(25+1) = 52000,也就是52秒。这更不能容忍了,由于拆分了太多零散的定时任务,导致了延时过长,要解决这个问题,可以考虑将它们进行分组,比如,每100条数据进行一个分组,那么2000/100*25 + 2000=2500,那么2000条的数据处理延时就变成了2500毫秒,相比之前有了很大的提升。

let array = []
for (let i = 0; i < 1000; i++) {
  array.push(i)
}

const chunkItems = _.chunk(array, 100) //lodash方法

chunkItems.forEach(chunkItem => {
  let todo = chunkItem.concat()
  setTimeout(function () {
    console.log(todo.shift())
    if (todo.length > 0) {
      setTimeout(arguments.callee, 25)
    }
  }, 25)
})

改写之后,相比之前,渲染速度有了很大的提升,但是,还有改进的余地,以100条数据进行拆分也不一定十分合理,我们可以用代码运行时间检测来判断是否需要将逻辑执行加入setTimeout队列,以代码运行50毫秒时间为基准,如果数组单条逻辑处理执行时间超过50毫秒,就添加到队列,如果不到就直接执行。

function timedProcessArray(items, process, callback) {
  const todo = items.concat()

  setTimeout(function () {
    const start = +new Date()
    do {
      process(todo.shift())
    } while (todo.length > 0 && (+new Date() - start < 50))

    if (todo.length > 0) {
      setTimeout(arguments.callee, 25)
    } else {
      callback(items)
    }
  })
}

在函数中添加了一个do-while循环,它在每个数组条目处理完成后检测执行时间。

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注