JavaScript 异步编程

异步的概念

异步(Asynchronous, async)是与同步(Synchronous, sync)相对的概念。

在传统的单线程编程中,程序的执行是按照控制流从前向后顺序执行的。而异步的概念则是不保证同步,即程序的执行可能不按照原有的序列依次执行。

简单来说就是:同步是按照代码顺序执行的,异步不按照代码顺序执行。

什么时候使用异步编程

因为同步是阻塞的,如果我们在执行一个耗时的操作,如请求一个数据导出接口,那我们浏览器需要等到接口数据返回才能继续向下进行,用户体验很不好。

所以通常我们会在执行耗时操作的时候,发起一个子线程,通过异步发起的执行操作,这样主线程能继续完成其他工作。

但是子线程会带来一个问题:主线程无法知道子线程是否已经执行完成了,如果后续需要处理一些事情,我们无法将它合并到主线程中去。

为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。

JavaScript Promise

为了更优雅的书写复杂的异步任务,ECMAScript 6 提供了 Promise 这个类。

构造 Promise

new Promise(function (resolve, reject) {
    // 要做的事情...
});

这个对象看起来平平无奇,但是却能实现神奇的功能。

例如,要实现一个分三次输出字符串,第一次间隔 1 秒,第二次间隔 2 秒,第三次间隔 3 秒的功能,原先的代码:

// 函数瀑布
setTimeout(function () {
    console.log("First");
    setTimeout(function () {
        console.log("Second");
        setTimeout(function () {
            console.log("Third");
        }, 3000);
    }, 2000);
}, 1000);

使用 Promise 的实现方式:

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 2000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

将原本嵌套的逻辑,变成了顺序的,提高了可读性和可维护性。

Promise 构造函数

Promise 构造函数接受一个函数作为参数,该函数是同步的并且会被立即执行,称为起始函数。

起始函数包含两个参数 resolve 和 reject,分别表示 Promise 成功和失败的状态。

Promise 构造函数返回一个 Promise 对象,该对象具有以下几个方法:

  • then:用于处理 Promise 成功状态的回调函数。

  • catch:用于处理 Promise 失败状态的回调函数。

  • finally:无论 Promise 是成功还是失败,都会执行的回调函数。

注意以上三个方法的参数都需要是一个函数。

下面是一个使用 Promise 构造函数创建 Promise 对象的例子:

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    if (Math.random() < 0.5) {
      resolve('success');
    } else {
      reject('error');
    }
  }, 1000);
});
 
promise.then(result => {
  console.log(result);
}).catch(error => {
  console.log(error);
});

在构造函数执行时,起始函数就立即开始执行,setTimeout 函数指定 1 秒后会随机执行 resolve 或 reject 方法。

而 resolve 方法会执行 then 方法的参数,reject 方法中会执行 catch 方法的参数。

如果仅执行上半段,而不调用 then 和 catch,如果是走到 reject 的话,会报错 Uncaught (in promise) error。

then 函数支持多次调用,会按传入的顺序依次执行方法,有任何异常都会直接跳到 catch 序列

new Promise(function (resolve, reject) {
    console.log(1111);
    resolve(2222);
}).then(function (value) {
    console.log(value);
    return 3333;
}).then(function (value) {
    console.log(value);
    throw "An error";
}).catch(function (err) {
    console.log(err);
});

resolve() 中可以放置一个参数用于向下一个 then 传递一个值,then 中的函数也可以返回一个值传递给下一个 then。但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作,即替换了原先的 Promise 对象。

但是请注意以下两点:

  1. resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;

  2. resolve 和 reject 并不能够使起始函数停止运行,别忘了 return。

Promise 函数

上面举例的函数比瀑布函数还要长,我们可以将他封装成一个方法。

这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。

function print(delay, message) {
	return new Promise(function (resolve, reject) {
		setTimeout(function() {
			console.log(message);
			resolve();
		}, delay);
	});
}

// 实现
print(1000,'first')
  .then(function() {
	return print(2000, 'second');
  })
  .then(function() {
    print(3000, 'thrid');
  });

// 注意区别 情形一:then 方法的参数必须是一个函数
print(1000,'first')
  .then(function() {
	return print(2000, 'second');
  })
  .then(
    print(1000, 'thrid') // 这个 print 不在 function 内
  );
// 结果是 second 与 third 会同时打印出来,上述方法等价于
print(1000, 'first')
  .then(function() {
    print(2000, 'third');
  })
  .then(
    new Promise(function (resolve, reject) {
	  setTimeout(function() {
	    console.log('second');
	      resolve();
	  }, 3000);
	})
  );
// 即 then 里面的 Promise 的初始函数从整个语句声明的时候就开始执行了。

// 注意区别 情形二:第一个 then 里面的 print 没有被 return,没有返回自己的 Promise
print(1000, 'first')
  .then(function() {
    print(2000, 'second'); // 这里没有 return
	console.log('second finish');
  })
  .then(function() {
    print(3000, 'third');
  });
// 结果是 third 在第 4 秒被打印出来,而不是在第 6 秒,因为第二个 print 没有被 return,则后一个 then 还是第一个 print 创建的 Promise 的方法。
// 因为第一个 then 先执行了一个异步操作(new Promise)然后打印了 second finish,可以看到 second finish 紧随着 first 就被打印出来了,所以第一个 then 很快就结束了,第二个 then 开始执行。

常见问题

Q:什么时候适合用 Promise 而不是传统回调函数?
A:当需要连续、顺序地使用异步方法的时候,比如调用有先后逻辑关联的接口时,就适合使用 Promise。

Q:Promise 是一种将异步转换为同步的方法吗?
A:完全不是,Promise 只是一种更良好的编程风格。

Q:什么时候我们需要再写一个 then 而不是在当前的 then 接着编程?
A:当你需要再进行一次异步操作的时候,同时记得要 return 这个新的 Promise。

异步函数

异步函数(async function)是 ECMAScript 2017 标准的规范。

之前的 Promise 函数的实现可以变得更加简洁:

// 原先
print(1000, "First").then(function () {
    return print(2000, "Second");
}).then(function () {
    print(3000, "Third");
});

// 改进
async function asyncFunc() {
    await print(1000, "First");
    await print(2000, "Second");
    await print(3000, "Third");
}
asyncFunc();

异步函数 async function 可以将异步操作变得像同步一样简单。

异步函数中可以使用 await 指令,await 指令后必须跟着一个 Promise,异步函数会在这个 Promise 运行中暂停,直到其运行结束再继续运行。

异步函数实际上原理与 Promise 原生 API 的机制是一模一样的,只不过更便于程序员阅读。

异常处理机制将通过 try-catch 块实现:

async function asyncFunc() {
    try {
        await new Promise(function (resolve, reject) {
            throw "Some error"; // 或者 reject("Some error")
        });
    } catch (err) {
        console.log(err);
        // 会输出 Some error
    }
}
asyncFunc();

await 语句也可以正常返回 resolve 的值:

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
}
asyncFunc();

深入理解 Promise 对象

Promise 对象有以下两个特点

  1. 对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
  • pending:初始状态。

  • fulfilled:操作成功完成。

  • rejected:操作失败。

只有异步操作的结果,可以决定当前是哪一种状态,其他任何操作都无法更改这个状态。

  1. 一旦状态改变,就不会再变,任何时候都可以得到这个结果。可能的状态流向:pending->fulfilled,pending->rejected,只有这两种,一旦发送就一直保持不再变化。

如果在改变发生后再对 Promise 对象添加回调函数,仍然可以立刻生效。这与 Event 的错过了就监听不到了不同。

function delay(delay, messgae) {
	return new Promise((resolve, reject) => {
		setTimeout(()=>{
			console.log('delay ' + delay + ' Promise');
			resolve(messgae);
		}, delay);
	});
}

let d1 = delay(1000, 'layback');

// 打印出 delay 1000 Promise 之后再执行
d1.then((msg) => {
	console.log(msg);
});
d1.then((msg) => {
	console.log(msg+'2');
});
// 可以发现都可以打印出结果,分别为 layback,layback2

Promise 优缺点

有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

then 的链式操作

then 方法可以接受参数,参数来源有两种:1. promise 中 resolve 方法传来的参数;2. 前一个 then 方法 return 的结果,如果前一个 then 方法返回的是 Promise 对象,则后一个 then 会到该 Promise 有了运行结果,才会进一步调用。

这种设计使得嵌套的异步操作从“横向发展”转变为“向下发展”。

catch 方法

Promise.prototype.catch 方法是 Promise.prototype.then(null, rejection) 的别名,用于指定发生错误时的回调函数。

Promise.all 方法与 Promise.race 方法

Promise.all 方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。新 Promise 的状态由多个 Promise 实例共同决定。

var p = Promise.all([p1,p2,p3]);
  • 当 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成数组,传递给 p 的回调函数。

  • 只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

function delay(delay, messgae) {
	return new Promise((resolve, reject) => {
		setTimeout(()=>{
			console.log('delay ' + delay + ' Promise');
			if(delay != 5000) {
				resolve(messgae);
			} else {
				reject('delay too long');
			}
		}, delay);
	});
}

var pros = [{delay: 1000, msg: '1S'},{delay: 3000, msg: '3S'},{delay: 10000, msg: '10S'}].map((item)=>{ return delay(item.delay, item.msg);})
var prosErr = [{delay: 1000, msg: '1S'},{delay: 3000, msg: '3S'},{delay: 5000, msg: '5S'},{delay: 10000, msg: '10S'}].map((item)=>{ return delay(item.delay, item.msg);})


Promise.all(pros).then(function(msg) {
  console.log(msg);
}).catch(function(err){
  console.log(err);
});

从打印结果可以看出,Promise.all 成功时会等到所有的 Promise 对象都得到结果后,执行 resolve 得到全部 resolve 参数的一个数组。失败时,会在 reject 发生的时候就执行 catch,并且不会影响其他的异步操作继续进行(仍然打印出了 delay 10000 Promise(即前面说的无法取消))。

Promise.race 方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。但是,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的返回值。

Promise.resolve 方法与 Promise.reject 方法

如果 Promise.resolve 方法的参数,不是具有 then 方法的对象(又称 thenable 对象),则返回一个新的 Promise 对象,且它的状态为fulfilled。

var p = Promise.resolve('Hello');
 
p.then(function (s){
  console.log(s)
});
// Hello // p 的状态为 fulfilled, then方法会立刻执行。

Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。Promise.reject方法的参数reason,会被传递给实例的回调函数。

var p = Promise.reject('出错了');
 
p.then(null, function (s){
  console.log(s)
});
// 或
p.catch((err)=>{console.log(err);})
// 出错了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值