JavaScript 异步

JavaScript 异步

JS三座大山:原型原型链、作用域闭包、同步异步。
异步: callback→Promise→async/await 

callback与异步

function a(callback) 
{
    alert("执行parent函数a!"); 
    alert("开始调用回调函数"); 
    callback(); 
    alert("结束回调函数"); 
}

function b(){ 
    alert("执行回调函数b"); 
} 

function test() 
{ 
   a(b);
   a(function() { 
        alert("执行匿名回调函数"); 
   }); 
}
test();

执行parent函数a!
开始调用回调函数
执行回调函数b
结束回调函数

执行parent函数a!
开始调用回调函数
执行匿名回调函数
结束回调函数

简单的说,回调就是把一个函数作为形参进行传递。

function a(log){
    console.log('执行a');
    setTimeout(function(){
        log('setTimeout');
    }, 0);
}

function b(){
    console.log('执行b');
}

function log(str){
  console.log(str);
}
a(log);
b();

执行顺序:
执行a
执行b
setTimeout

js是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,后面的任务必须等到前面的任务执行完毕后才能执行,如果有一个比较耗时的操作,比如http请求,文件io,这样的效率是不高的,所以就需要异步的形式。

其他语言遇到这种比较耗时的任务往往是开一个线程来处理,但js本身就是单线程的,js对这种任务的处理就是将这个任务挂载起来,等耗时任务完成后再把回调函数添加到执行队列尾部,所以,在刚刚这个例子中,虽然延迟时间为0,但还是最后才打印 setTimeout 。

JS中所有的任务可以分为两种:同步任务和异步任务。

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务:不进入主线程,而进入任务队列中的任务,只有任务队列通知主线程,某个异步任务可以执行了,这个任务才会进入主线程执行。
  • 事件循环(Event Loop):只有执行栈中的所有同步任务都执行完毕,系统才会读取任务队列,看看里面的异步任务哪些可以执行,然后那些对应的异步任务,结束等待状态,进入执行栈,开始执行。

js的回调函数与异步 
JS 异步(callback→Promise→async/await)

Promise

ES6给我们提供了一个原生的构造函数 Promise  , Promise  代表了一个异步操作,可以将异步对象和回调函数脱离开来,通过 .then 方法在这个异步操作上绑定回调函数, Promise 可以让我们通过链式调用的方法去解决回调嵌套的问题。

有三种状态: pending (进行中)、 fulfilled (已成功)和 rejected (已失败)

promise 对象的两个重要方法: resolve/reject
1) resolve 方法可以使 Promise 对象的状态改变为成功,同时传递一个参数用于后续成功后的操作。
2) reject 方法可以将 Promise 对象的状态改变为失败,同时将错误信息传递到后续错误处理的操作。

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject 。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 resolved 时调用,第二个回调函数是 Promise 对象的状态变为 rejected 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

如果代码异步操作抛出错误,会调用 catch 方法指定的回调函数,处理这个错误,而且 then 方法指定的回调函数,如果运行中抛出错误,也会被 catch 捕获。 Promise 对象的错误具有"冒泡"性质,会一直向后传递,直到被捕获为止,也就是说,错误总是会被下一个 catch 语句捕获。

Promise.prototype.catch 方法是 .then(null, rejection) 或 .then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。 catch 方法返回的还是一个 Promise 对象,因此后面还可以接着调用 then 方法。

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

理解Promise用法的关键点: 

  1. then 方法是 Promise 实例的方法,即 Promise.prototype 上的,它的作用是为 Promise 实例添加状态改变时的回调函数,这个方法的第一个参数是 resolved 状态的回调函数,第二个参数(可选)是 rejected 状态的回调函数。
  2. 链式中的第二个 then 开始,它们的 resolve 中的参数,是前一个 then 中 resolve 的 return 语句的返回值。
  3. 关于执行顺序: Promise 在实例化的时候就会执行,也就是如果 Promise 的实例化语句中函数 console.log输出语句,它会比 then 中的先执行。 Promise.all 中传入的Promise对象的数组(假设为 p1 、 p2 ),即使 p2 的运行速度比 p1 快, Promise.all 方法仍然会按照数组中的顺序将结果返回。

Promise的缺点: 

  1. 当处于未完成状态时,无法确定目前处于哪一阶段。
  2. 如果不设置回调函数, Promise 内部的错误不会反映到外部。
  3. 无法取消 Promise ,一旦新建它就会立即执行,无法中途取消。

Promise 对象 

async/await

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async:定义异步函数
1)自动把函数转换为 Promise 
2)当调用异步函数时,函数返回值会被 resolve 处理
3)异步函数内部可以使用 await 

await:暂停异步函数的执行
1)当使用在 Promise 前面时, await 等待 Promise 完成,并返回 Promise 的结果
2) await 只能和 Promise 一起使用,不能和callback一起使用
3) await 只能用在 async 函数中

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 1000);

console.log('test');
//test
//1秒后hello world

只要在函数名之前加上 async 关键字,就表明这个函数内部有异步操作。这个异步操作返回一个 Promise 对象,前面用 await 关键字注明。函数执行的时候,一旦遇到 await ,就会先执行 await 后面的表达式中的内容(异步),不再执行函数体后面的语句,接着去执行后面的任务。等到异步操作执行完毕后,再自动返回到函数体内,继续执行函数体后面的语句。

async function getABC() {
  let A = await getValueA(); // getValueA 花费 2 秒
  let B = await getValueB(); // getValueA 花费 4 秒
  let C = await getValueC(); // getValueA 花费 3 秒

  return A*B*C;
}

//await 把异步变成了同步。
async function getABC() {
  // Promise.all() 允许同时执行所有的异步函数
  let results = await Promise.all([ getValueA, getValueB, getValueC ]); 

  return results.reduce((total,value) => total * value);
}
//函数总耗时为 4 秒(getValueB 的耗时)

async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"

上面代码中,函数 getTitle 内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行 then 方法里面的 console.log 。

如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject 。

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了

async 函数 
1 分钟读完《10 分钟学会 JavaScript 的 Async/Await》