正在加载中

最后更新于 2019年06月14日

ui线程

每个浏览器用于执行js代码和更新ui的进程就只有一个,这个被称为‘ui线程’,它基于一个简单的队列系统,也就是根据代码的先后循序将代码插入队列并执行。

一般来说,当用户点击一个按钮botton的时候,就会触发ui线程,他会创建两个任务并添加到队列中,第一个任务是更新ui按钮,它需要改变外观来表示它被点击了,然后再触发onclick事件,如果你对这个事件进行了调用其他函数,就会执行所调用的函数。

事实上,大多数浏览器会在js代码运行时停止把新的任务加入ui队列中去,也就是说当用户点击一个事件,这个事件所调用的函数运行时间过长,那么在运行的这段时间中用户再点击其他的事件是没有交互的,产生了一种假死的状态。

所以,一个js代码的运行它必须要快,以避免对用户体验造成不良影响。

浏览器限制

浏览器本身是有限制的,它会从js代码运行的行数和运行的时间进行限制,比如当运行超过500万条语句时浏览器会弹出警告,并停止脚本运行,甚至还有之前的栈大小限制,还有就是运行时间,当一个js文件开始运行时,浏览器会开始计时,如果这个脚本运行超过规定的时间,那么浏览器就会弹出提示,当前脚本运行时间过长,是否停止脚本运行。

但对于普通用户来说,他们可能并不知道这个脚本指的是什么,也就会处于是停止还是不停止运行的两难选择,为此,我们要从一开始就要避免出现这种问题。

多久才算久

100毫秒以内是最佳的时间

使用定时器让出时间片段

即便我们极力避免js的运行时间不超过100ms,但是难免还是会有大量的计算占用时间,所以我们可以通过定时器来一段一段的运行,每一段都有空出一些时间让其他代码运行。

定时器的两种方式:

  1. setTimeout()
  2. setInterval()

通过对定时器设置延迟时间来达到延迟调用的效果。

延迟的时间怎么计算的?

定时间被运行后开始计算,而不是他的上一级函数运行后,如:

function set() {
    one();
    setTimeout(function(){},250)

}

250ms的计算是从setTimeout运行后开始计算,而不是set()运行后。

如果你在一个事件中使用一个定时器,那么事件中运行的函数最好要快一些,如果事件函数运行超过的250ms,也就是你设置的延时时间,那么可能会在事件结束之前定时器就被运行了。

定时器的精度

事实上js的定时器并不是很精准,你设置延迟250毫秒并不意味着就一定是在250毫秒后运行,浏览器都在试图尽可能的精准,但往往会过快或者过慢几毫秒,因此,定时器不能用于测量实际的时间。

在windows系统中,定时器的步进值是15ms,也就是说,设置为10ms的延迟往往会是15ms后运行,或者低于15ms视为0,为此,最小的延迟值应该要是15或者30,起码要保证还有15ms的间隔。

注意,大部分浏览器在延迟小于10的时候,表现都不太一样。

使用定时间处理数组

常用的循环代码:

for(var i =0,len=item.length;i<len;i++) {
  process(item[i]);
}

这是一个常用的循环代码,每次循环传入数组item的一个值,然后运行process()方法,假设这里的process运行时间过长,就会造成页面的假死,如果item数组庞大,也会如此。

为此我们就可以使用延迟调用来空出一部分时间给其他代码运行。

但是使用延迟调用要明确两个因素:

  1. 处理的过程是否必须同步
  2. 数据是否要按顺序处理

如果以上两点都不需要考虑,那么就可以使用该方法,因为延迟调用是异步的,然后有可能在不断延迟中,上一个还没运行完下一个就调用了。

function set(item) {
  var arr = item.concat();
  setTimeout(function(){
      process(arr.shift());
      if(arr.length > 0) {
        setTimeout(arguments.callee,25)
      }else {
        callback();
      }
  },25)
}

然而事实上arguments.callee已经不推荐使用,因为每次递归都会重新创建arguments对象,这个代价是很昂贵的,我们可以使用非匿名函数。

function set(item) {
  var arr = item.concat();
  setTimeout(function fn(){
      process(arr.shift());
      if(arr.length > 0) {
        setTimeout(fn,25)
      }else {
        callback();
      }
  },25)
}

这里插个题外话

一道面试题。接受参数n=5,不用for循环输出数组【1,2,3,4,5】

function set(num) {
  var arr = [];
  var i = 0;
  return (function fn() {
      arr.unshift(num--);
      if(num > 0) {
          fn();
      }
      return arr;
  })()
  
}

我们还可以将定时器封装成一个可以多次调用的函数

function processArray(items,process,callback) {
  var todo = itmes.concat();

  setTimeoue(function fn(){
    process(todo.shift());
  },25)

  if(todo.length > 0) {
     setTimeoue(fn,25);
  }else {
    callback();
  }

}

函数支持传三个参数,依次是需要传入的参数数组集合,调用的函数,回调的函数

分割任务

如果一个函数任务里面有几个子任务,且每个子任务都是相对独立的,但是一起的话运行时间过长,于是我们可以采用上面的那种办法

原方法:

function sava(id) {
  open(id);
  write(id);
  close(id);

  //上面都完成了
  updataUI(id);
}

改为:

function sava(id) {
  var tasks = [open,write,close];

  setTimeout(function fn(){
    var task = tasks.shift();
    task(id);
    if(tasks.length > 0) {
        setTimeout(fn(),25);
    }else {
        updataUI(id);
    }
  },25)
}

记录代码的运行时间

如果我们需要每次运行的函数过多,那么无限的setTimeout反倒成为了累赘,因为是延迟调用,那么自然会造成反应的延迟,特别是数量过多的情况,为此我们可以想到之前的‘达夫设备’,一个setTimeout运行多个函数

那么怎么在极短的时间里运行多个函数呢?

一般来说,一个js代码的运行不超过100ms最佳,那么我们既然要优化,自然不能用最大值,所以我们可以采用50ms。

function timed(items,process,callback){
  var todo = items.concat();

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

    if(todo.length > 0 ) {
      setTimeout(fn(),25);
    }else {
      callback();
    }

  },25)

}

当定时间内的函数运行时先创建一个时间,也就是当前时间,然后再do-while循环,第二次循环的时候判断todo还有没有值,并且第二次判断的时间减去刚开始的时间要不超过50ms才可以继续循环,如果不满足,重新延迟调用。

定时器的性能

定时器延迟的时间越短,且量越多,就会造成浏览器的ui线程堵塞,所以在延迟短的定时器中,尽量少的使用,但是超过1秒或以上的延迟其实不怎么会造成堵塞,所以在使用定时器的时候可以根据情况自行分配好延迟的时间。

workers

workers是新引入的一个api接口,它可以创建一个独立的线程来运行js,ie10及以上才支持,新版浏览器都支持,所以在不考虑到兼容性的问题是,workers是一个处理大量运算脚本的最佳选择。

workers创建时需要传入一个参数,这个参数就是需要运行的js文件路径,在workers中,有一个只读的navigator对象,还有一些js的对象,但是要注意,在workers中不要对ui进行操作,因为他是独立于ui线程的,所以操作可能会带来错误或者其他问题,workers本身用于独立计算那些,并且通过message事件和postMessage()相互通信。

也就是说js首先会创建一个workers环境,new Workers(‘xxx.js’);

然后创建一个onmessage事件,通过event对象的data属性获取到从workers传来的参数,页面也可以通过postMessage()向workers传参。

在workers环境中也创建一个onmessage事件用于监听页面传来的参数,也是event对象的data获取数据。

向页面传参也是postMessage()方法。

var worker = new Workers('xxx.js');

worker.onmessage = function(event) {
     collback(event.data);
} 

worker.postMessage('参数');

//workers

self.onmessage = function(event) {
    process(event.data);
}

worker.postMessage('计算好参数传给页面');

在workers中也可以再载入其他js文件,载入的后js文件在workers中运行,使用importScripts()方法

//workers中
imporScripts('xxx.js','sss.js');
  • weixiao kaixin tushetou jingkong deyi fanu liezui liuhan daku ganga bishi nanguo lihai qian yiwen numu tu yi haixiu se fadai minyan hehe henkaixin huaji biyiyan kuanghan maimeng shui xiaku penqi zhangzui pen aini ye niu laji ok chigua renshi kongbu shuai xiaoxiese touxiao huaixiao jingnu chihuai kaisang xiaoku koubi zhuangbi lianhong kanbujian shafa zhijing xiangjiao dabian yaowan redjing lazhu rizhi duocang chixigua hejiu xixi xiaopen goukun xiaobuchu shenme wusuowei guancha lajing chouyan xiaochi bie zhadanzui zhadanxiao
  1. YIR

    发表于:
    来自 Google Chrome 76 in windows 10

    我在想这篇文章的封面是什么相机。

    1. YIR

      发表于:
      来自 Google Chrome 76 in windows 10
      @YIR

      嗷,一刷新就没了。

    1. 木灵鱼儿

      发表于:
      来自 FireFox 68 in windows 10
      @YIR

      随机的图片啊

登录