异步操作

本文围绕JavaScript异步操作展开。介绍了单线程模型、同步与异步任务、任务队列和事件循环机制,包括浏览器和Node中的不同情况。阐述了异步操作的模式、流程控制方式,如串行、并行及结合。还讲解了定时器功能,以及Promise对象的状态、构造、方法等,指出其优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

异步

概述


单线程模型

JavaScript是单线程模式,同时只能执行一个任务。
js引擎有多线程,每个js脚本只能在一个线程上运行(主线程),其他线程在后台配合。
当IO操作耗时很长时, CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop)。
为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

同步任务和异步任务

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。

  • 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务,异步任务不具有“堵塞”效应。

任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供多个任务队列(task queue),里面是各种需要当前程序处理的异步任务。
引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)

event Loop

掘金小册

谷歌大神

宏任务按照宏任务队列执行,浏览器可以在不同宏任务执行之间进行渲染
微任务按照微任务队列执行,执行的时机:
    如果没有js在执行列,将会在每个callback后执行
    或者在每个宏任务的结尾执行。

Event Loop的执行顺序:

  • 首先执行同步代码,这属于宏任务(macrotask)
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行(微任务)
  • 执行所有的微任务(microtask),在这期间加入的微任务会加入到微任务队列尾部,并在之后执行。
  • 执行完所有微任务后,如有必要会渲染页面
  • 开始下一轮event loop,执行宏任务重的异步代码(setTimeOut)

微任务包括:

  • process.nextTick (Node独有)
  • promise
  • MutationObserver

宏任务:

  • script
  • setTimeOut
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

Node 中的 event loop

node中的 event loop 和浏览器中的弯曲是不同的东西
Node中的 event loop 分为 6 个阶段,会按照顺序反复运行。当进入相应的阶段之后,会从对应的回调队列中取出函数去执行。当该阶段的回调函数队列为空或者数量到达系统设定的阈值之后,进行状态切换,进入下一阶段。
在这里插入图片描述

异步操作的模式

回调函数

回调函数的优点是简单、容易理解和实现
缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling)

事件监听

事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。
缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。

发布、订阅

异步操作的流程控制

串行执行

可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function series(item) {
  if(item) {
    async( item, function(result) {
      results.push(result);
      return series(items.shift());
    });
  } else {
    return final(results[results.length - 1]);
  }
}

series(items.shift());
并行执行

流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

items.forEach(function(item) {
  async(item, function(result){
    results.push(result);
    if(results.length === items.length) {
      final(results[results.length - 1]);
    }
  })
});

如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。

并行与串行的结合

所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;

function async(arg, callback) {
  console.log('参数为 ' + arg +' , 1秒后返回结果');
  setTimeout(function () { callback(arg * 2); }, 1000);
}

function final(value) {
  console.log('完成: ', value);
}

function launcher() {
  while(running < limit && items.length > 0) {
    var item = items.shift();
    async(item, function(result) {
      results.push(result);
      running--;
      if(items.length > 0) {
        launcher();
      } else if(running == 0) {
        final(results);
      }
    });
    running++;
  }
}

launcher();

定时器


JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成

setTimeOut()
  • 如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。
var x = 1;

var obj = {
  x: 2,
  y: function () {
    console.log(this.x);
  }
};

setTimeout(obj.y, 1000) // 1

可以将obj.y放入一个函数来解决这个问题

setTimeInterval()

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

  • 一个常见用途是实现轮询
var hash = window.location.hash;
var hashWatcher = setInterval(function() {
  if (window.location.hash != hash) {
    updatePage();
  }
}, 1000);
clearTimeout(),clearInterval()
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);

clearTimeout(id1);
clearInterval(id2);

setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1。利用这一点,可以写一个函数,取消当前所有的setTimeout定时器。

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

  function clearAllTimeouts() {
    var id = setTimeout(function() {}, 0);
    while (id > 0) {
      if (id !== gid) {
        clearTimeout(id);
      }
      id--;
    }
  }
})();
debounce(去抖)函数
$('textarea').on('keydown', debounce(ajaxAction, 2500));

function debounce(fn, delay){
  var timer = null; // 声明计时器???
  return function() {
    var context = this;
    var args = arguments;
    clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(context, args);
    }, delay);
  };
}
运行机制

setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
如果本轮事件循环过长,没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。

setTimeout(f, 0)

setTimeout(f, 0)中的 f 会在下一轮事件循环一开始就执行。

应用
  • 一大应用是,可以调整事件的发生顺序。
  • 另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发。只有用setTimeout改写,上面的代码才能发挥作用
//以下代码将会达不到目的
document.getElementById('input-box').onkeypress = function (event) {
  this.value = this.value.toUpperCase();
}

//用setTimeout改写
document.getElementById('input-box').onkeypress = function() {
  var self = this;
  setTimeout(function() {
    self.value = self.value.toUpperCase();
  }, 0);
}

Promise对象


Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
Promise 是一个对象,也是一个构造函数。
Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。

// 传统写法
step1(function (value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        // ...
      });
    });
  });
});

// Promise 的写法
(new Promise(step1))
  .then(step2)
  .then(step3)
  .then(step4);
Promise 对象的状态

Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。

  • 异步操作未完成(pending)
  • 异步操作成功(fulfilled)
  • 异步操作失败(rejected)

上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。
这三种的状态的变化途径只有两种。

  • 从“未完成”到“成功”
  • 从“未完成”到“失败”

一旦状态发生变化,就凝固了,不会再有新的状态变化,这也意味着,Promise 实例的状态变化只可能发生一次。
因此,Promise 的最终结果只有两种。

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。
Promise 构造函数

JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例。

var promise = new Promise(function (resolve, reject) {
  // ...

  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});
Promise.prototype.then()

then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
then方法可以链式使用。

p1
  .then(step1)
  .then(step2)
  .then(step3)
  .then(
    console.log,
    console.error
  );

最后一个then方法,回调函数是console.log和console.error,用法上有一点重要的区别。

  • console.log只显示step3的返回值
  • console.error可以显示p1、step1、step2、step3之中任意一个发生的错误。

举例来说,如果step1的状态变为rejected,那么step2和step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性。

then() 用法辨析
// 写法一 f3回调函数的参数,是f2函数的运行结果
f1().then(function () {
  return f2();
}).then(f3);

// 写法二  f3回调函数的参数是undefined
f1().then(function () {
  f2();
}).then(f3);

// 写法三  f3回调函数的参数,是f2函数返回的函数的运行结果
f1().then(f2())
	.then(f3);

// 写法四  写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。
f1().then(f2)
	.then(f3);
实例:图片加载
var preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    var image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
小结

Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。
Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态(传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。)
Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑

微任务

Promise 的回调函数属于异步任务,会在同步任务之后执行,但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。

setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function (resolve, reject) {
  resolve(2);
}).then(console.log);

console.log(3);
// 3
// 2
// 1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值