Skip to content

07-异步操作

异步操作

统计信息:字数 8138 阅读17分钟

2021-11-18 2023-04-08

概念

1、单线程模式

JS 语言只在一个线程上运行,JS在某时刻只能执行一个任务,其他任务必须在后面排队等待。实际上 JS V8 引擎宏观上具有多线程,单个 JS 脚本文件只能在一个线程(主线程)上运行,其他线程在后台配合。

单线程的原因:浏览器线程主要和 DOM 相关,如果多个线程同时更改 DOM,就造成了不确定性,所以早期使用单线程完成。

单线程好处:设计简单,执行环境单纯,线程不会冲突。

单线程缺点:读写外部数据,或者发送请求,可能脚本长时间停滞(浏览器假死),此时 CPU 还在闲置,界面是空白。

H5规定:一个JS脚本可以创建多个线程,子线程完全被主线程控制,子线程不能控制DOM(本质单线程没变)。

2、同步任务和异步任务

程序中的任务分成**同步任务和异步任务**;同步任务是主线程中的任务,必须等上一个任务完成后再执行下一个任务。异步任务不在主线程中,进入**任务队列**。当主线程的一个状态变化后(例如ajax接到数据),任务队列中的任务会以回调函数的形式进入主线程执行,不会产生堵塞。Ajax可以实现同步任务和异步任务。如果是同步任务,需要等请求返回后执行后面的代码;如果是异步任务,在请求的同时,会执行后面的代码;请求响应后,主线程再调用回调函数。

3、任务队列和事件循环

JS 程序中具有一个主线程,还有若干任务队列。主线程首先执行同步任务,执行完同步任务后,就会查看任务队列中的异步任务,如果满足条件,异步任务进入主线程变成同步任务执行。

事件循环:JS 引擎不断检查异步事件;当同步事件进行完,JS引擎会检查任务队列中的异步任务并执行,直到全部任务执行结束,对应程序执行结束。

4、异步操作的模式

回调函数:简单,但是不同程序高度耦合,多个回调函数嵌套很复杂,每个任务对应一个回调函数。

function f1() {
  //
}
function f2(callback) {
  //
  callback();
}
f2(f1);
// 把回调函数以参数的形式传入同步任务函数中

事件驱动:可以绑定多个事件,每个事件可以执行多个回调函数,去耦合,模块化。整个程序如果是事件驱动,那么主线程不明确。

function f1(){
  setTimeout(function() {
    f1.trigger('onclick');
  }, 1000);
}

f1.on('click', f2);

发布订阅(观察者模式,publish-subscribe pattern, observer pattern)

$.subscribe('done', f2); // f2 函数向信号中心$订阅 done 信号

function f1 (){
  setTimeout(function() {
    $.publish('done');
    // f1 执行完毕后,向信号中心 $ 发布done信号(从而引发f2函数执行)
  });
}

function f2() {
  // do something
  $.unsubscribe('done');
  // 函数执行完毕后,取消订阅
}

发布订阅模式比事件驱动模式好,可以查看消息中心获取信号数量,信号订阅者,从整体上监控程序的运行

5、多个异步任务的流程控制

如果多个回调函数嵌套,结构复杂难以维护。可以采用串行和并行执行。

串行执行:使用一个串行函数,多个任务依次执行。缺点耗时。

并行执行:同时发布异步任务,优点省时间,缺点耗性能。

串行和并行结合:设置一个变量存放异步任务的结果和当前的数量,设置一个当前运行的最大值,通过性能控制任务运行的数量。

定时器

主要是 setTimeout setInterval clearTimeout, clearInterval,可以向任务队列添加定时任务。

setTimeout 函数返回一个变量(递增的整数),用于清除定时器。函数默认传入两个参数,分别是异步执行的函数和间隔时间,可选参数是回调函数的变量(ab);函数内部的作用于是 window,this 的指向发生变化,注意。

let that = this;
let a = setTimeout(function(a, b) {
  // that.fucntion...
}, 1000, a, b);

setInterval 函数的参数、返回值相同,可以定时执行某个函数,不会主动停止。缺点:间隔不包括函数运行的时间,如果函数执行时间较长,那么后一次函数也会准时执行。

在滚动函数或者渐变函数中,最好的办法是设置 setTimeout 并在函数内部设置下一次时间间隔函数。

清除所有定时器

(function() {
  // 每轮事件循环检查一次
  var gid = setInterval(clearAllTimeouts, 0);

  function clearAllTimeouts() {
    var id = setTimeout(function() {}, 0);
    while (id > 0) {
      if (id !== gid) {
        clearTimeout(id);
      }
      id--;
    }
  }
})();

(function(){
  var gid = setInterval(clearAllTimeouts, 0); // 事件循环时检查一次
  function clearAllTimeouts() {
    var id = setTimeout(function() {}, 0); // 首先创建一个定时器,获取定时器最大值
    while (id > 0) {
      // 清除全部的定时器
      if (id !== gid) {
        clearTimeout(id);
      }
      id--;
    }
  }
})();

防抖

$('textarea').on('keydown', debounce(ajaxAction));

function debounce(fn) {
  var timer;
  var that = this;
  var args = arguments; // 传入的参数
  clearTimeout(timer); // 首先清除一下上次的定时器
  timer = setTimeout(function() {
    fn.apply(this, args);
  }, 2000);
}

定时器原理

把当前事件移出事件队列,等固定的时间后执行这个事件。

不足:如果当前主线程执行的时间比较长,超过了定期器的时间间隔,那么主线程任务执行完后会立即执行定时器内容(可能多个interval同时触发),即时间间隔 + JS 执行的时间 = 设置的时间。

setTimeout(fn , 0) 的特殊用法:表示当前主线程任务执行完毕后,执行现在的程序(优化性能),不同浏览器执行的时间不同。例如,在事件冒泡中,首先触发子元素的回调函数,然后触发父元素的回调函数。在子元素上使用setTimeout(0)即可改变执行的顺序。例如2,DOM渲染的速度比JS执行的慢,可以在JS中使用这个函数,让DOM渲染结束后再执行JS代码。

在 react 中,setTimeout 的特殊用法:生命周期函数中,componentDidMount componentDidUpdate 是组件已经加载或者组件已经更新(但是此时界面 DOM 可能没有更新)在这两个生命周期中获取 dom 的属性,可能是不正确的。所以可以设置定时器,然后时间设置0,表示当前JS执行后(即DOM渲染完成后),再执行对应的代码,这样可以正确获取 DOM 的属性。

Promise对象🐛

Promise 是异步操作和回调函数的很好的解决方案,这里简单介绍(主要在ES6)

异步任务返回一个 Promise 实例,这个实例具有 then 的方法,可以处理下一步的回调函数。Promise 简化了回调函数的写法。

function f1 (resolve, reject) {
  // deal with async event
}

let a = new Promise(f1);
a.then((res) => {
  console.log(res);
  // deal with response
});

传统写法和 Promise 写法,回调函数变成链式函数

f1(function(value1) {
  f2(value1, function(value2) {
    //
  });
});

(new Promise(f1)).then(f2).then(f3)...

Promise 实例对象的状态(对应ajax请求对象的状态),结果可能是两个,成功或者失败(成功返回 result, 失败返回 error,如果发出异步那么结果一定会发生,所以称为承诺)

Promise.prototype.then 可以为异步事件添加回调函数,例如下面的图片加载

var preloadImage = function(path) {
  return new Promise(function(resolve, reject) {
    var image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

preloadImage('https://example.com/my.jpg').then((e) => {
  document.body.append(e.target)
}).then(() => {
  console.log('加载成功')
});

Last update: November 9, 2024