ES6 异步

单线程

JavaScript是一门单线程的语言,被广泛应用于浏览器和页面DOM元素交互,自从Node.js出现后,JavaScript领域由浏览器扩展到服务器,逐渐变成一种通用的计算机程序设计语言。

由于JavaScript的执行环境是单线程的,也就是说JS引擎中负责解析和执行JS代码的线程只有一个,而且一次只能完成一项任务,一个任务执行完毕后才能执行下一个。换句话说,多个任务执行时,当前任务会阻塞其它任务,当前任务也就是主线程。但实际上还有其它线程,如事件触发线程、AJAX请求线程等,这也就引发了同步和异步的问题。

同步异步

JS中的任务可以分为两种同步和异步,同步任务是在主线程上排队执行的任务,只有前一个任务执行完毕后才能执行后一个任务。异步任务是不进入主线程而进入任务队列中的任务,只有任务队列通知主线程某个异步任务才能执行,此时这个任务才会进入到主线程执行。只有执行栈中所有同步任务都执行完毕后系统才会读取任务队列,看看里面的异步任务哪些可以执行...

4933701-78ad79e8421756c1.png
同步执行

由于单线程模式中一次只能执行一个任务,函数调用后必须等待执行结束返回结果才能进行下一个任务。若当前任务执行时间较长,就会导致线程阻塞。也就是说,如果请求的时间较长,而阻塞了后面代码的执行,对于耗时类操作并不适合。

// 同步模式
var x = true;
while(x);//while死循环会堵塞进程
console.log("don't carry out");//这里永远不会执行了
4933701-0cead44684c692a5.png
异步执行

JS虽然是单线程的,但为了避免IO操作阻塞主线程,必须采用回调函数callback的形式将耗时的IO操作委托给其他IO线程进行处理,所以说JS并不是纯粹的单线程,只是有一个主线程在做主循环而已。

异步模式与同步模式相反,可以一起同时执行多个任务,函数调用后不会立即而返回执行的结果。若任务A需要等待,可先执行任务B,等到任务A结果返回后再继续回调。最常见的异步模式就是定时器。

// 定时器是异步的
setTimeout(function(){
  console.log("task A, asynchronous");
}, 0);
console.log("task B, synchronize");

// 定时器延时为0,为什么taskA还是晚于taskB呢?
// 因为定时器是异步的,异步任务会在当前脚本中所有同步任务执行完毕后才会执行。
task B, synchronize
task A, asynchronize

回调函数

在JavaScript的世界中,所有的代码都是单线程执行的,由于这个天生的缺陷,导致JavaScript中所有的网络操作、浏览器事件等都必须是异步执行。那么,JavaScript是如何实现异步编程的呢?

function callback()
{
  console.log("callback");
}
console.log("before");
setTimeout(callback, 1000);//1秒后调用callback函数
console.log("after");

// 脚本执行后控制台输出
before
after
// 1秒后调用callback函数
callback // 异步操作会在未来的某个时间点上触发某个函数调用

回调地狱

JavaScript的异步是采用Callback回调函数来实现的,典型的是AJAX操作。

// 典型的异步操作 AJAX
request.onreadystatechange = function()
{
  if(request.readyState === 4)
  {
    if(request.status === 200)
    {
      return success(request.responseText);
    }
    else
    {
      return fail(request.status);
    }
  }
}

// 回调函数success和fail在AJAX操作中很正常,但不利用代码复用,有没有更好的写法,比如这样呢?
ajaxGet(url).ifSuccess(success).ifFail(fail);
// 这种链式写法先统一执行AJAX逻辑,并且不关心如何处理结果。然后,根据结果是成功还是失败,在未来某个时间调用success或fail函数。

Callback回调函数可以接收外部程序传入的参数,但是却没有办法先外部传值,只能通过下一层的Callback回调函数来使用。当逻辑复杂的时候,Callback回调函数嵌套会变得很深,在这种情况下,参数互相影响导致bug增加,这种Callback回调嵌套被称为Callback回调地狱。如何解决回调嵌套太深而引发的回调地狱的问题呢?

4933701-df82801914207fe9.png
回调地狱

在JavaScript中所有代码都是单线程执行的,由于这个缺陷导致JavaScript中所有网络操作、浏览器事件都必须是异步执行。

Promise

Promise是异步编程的一种解决方案,比传统的回调函数和事件更加合理和强大。

Promise的概念是由CommonJS小组成员在Promises/A规范中提出的。根据Promises/A规范,promise是一个代理对象,promise代理的是一个值,这个值可称之为promise对象的状态值。promise的状态值分为三种分别是pendingresolvedrejected

Promise由社区最早提出并实现,ES6将其写入语言标准并统一了用法,原生提供了Promise对象。

Promise简单来说是一个容器,里面保存着某个将来才会结束的事件(异步操作),从语法上说Promise是一个对象,它可以获取异步操作的消息,Promise提供了统一的API,各种异步操作都可以使用相同的方法进行处理。

Promise对象有两个特点

  • 对象的状态不受外界影响

Promise对象代表了一个异步操作,具有三种状态:进行中pending、已成功fulfilled、已失败rejected。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise名字的由来“承诺”,表示其他手段无法改变。

  • 一旦状态改变将不会再变

Promise对象的状态改变只有两种情况:从pending变为fulfilled、从pending变为rejected。只要这两种情况发生状态就凝固了不会再变了,而且会一直保持这个结果,此时就称为resolved以定型。如果改变已经发生了,再对Promise对象添加回调函数也会立即得到这个结果,这与事件完全不同,事件的特点是如果你错过了它再去监听是得不到结果的。

Promise的构造方法

let promise = new Promise((resolve, reject) => {
  resolve();//异步处理
});

promise的构造函数中会传入一个处理器函数executor,函数具有两个参数分别是resolvereject,当都遭函数执行时,executor函数会立即异步执行。

4933701-37a4d4222b490e42.png
Promise实例一经创建执行器立即执行
const setDelay = (millisecond) => {
  return new Promise( (resolve, reject) => {
    if(typeof millisecond !=="number" ){
      reject(new Error("参数必须是数字类型"));
    }
    setTimeout( () => {
      resolve(`延迟${millisecond}毫秒`);
    }, milliscond);
  })
}

executor函数体内可以编写业务逻辑代码,一般业务了逻辑代码包含正常执行逻辑和异常出错处理,在代理的正常执行逻辑中会调用resolve方法将promise的状态值修改为resolved(retval),在异常出错逻辑中会调用reject(error)promise的状态值修改为rejected。也就是说executor函数执行成功还是失败是可以从promise的状态值中判断的出。

这里需要注意两点

  1. promisepending状态变为resolvedrejected状态只会有一次,一旦变成resolvedrejected状态之后,这个promise的状态就再也不会改变了。
  2. 通过resolve(retval)传入的retval返回值可以是任意值,通过reject(error)传入的error一般会是一个new Error("error")的对象。

promise的状态变化有什么用呢?它的状态可以影响后续then的行为。

promise.then(
  function onFulfilled(value){}
).catch(
  function onRejected(error){}
);

promise的状态是resolved的时候,会调用then方法中的onFulfilled函数,其中参数value值是resolve(retval)传入的。如果是rejected状态会调用catch函数中的onRejected函数,其参数error则是通过rejected(error)传入的。

等价形式

promise.then(
  function onFulfilled(value){},
  function onRejected(error){}
);

then方法带有三个参数成功回调、失败回调、前进回调。一个全新的promise对象从每个then方法的调用中返回。

Promise是抽象的异步处理对象

Promise对象表示未来发生的事件,在创建promise时其参数传入的函数是会被立即执行的,只是其中执行代码可以异步代码。

简单来说,then方法就是把原来的回调写法分离出来,在异步操作执行后,用链式调用的方式执行回调函数。Promise的优势就是在于这个链式调用。

Promise的构造函数接受一个参数是函数,并传入两个参数resolvereject,分别表示异步操作执行成功后的回调函数、异步操作执行失败后的回调函数。按标准来说,resolve是将Promise的状态设置为fullfilledreject是将Promise的状态设置为rejected

4933701-8cd9450ebebbf919.png
promise状态
let cookie = ()=>{
    return new Promise((resolve, reject)=>{
        console.log("cookie begin");
        //使用setTimeout模拟异步操作
        setTimeout(()=>{
            if(true){
                console.log("cookie over");
                resolve("cookie")
            }else{
                reject("reject")
            }
        },1000)
    })
};
let eat = ()=>{
  return new Promise((resolve, reject)=>{
      console.log("eat begin");
      setTimeout(()=>{
            if(true){
                console.log("eat over");
                resolve("eat")
            }else{
                reject("eat")
            }
      },1000)
  })
};
let wash = ()=>{
  return new Promise((resolve, reject)=>{
      console.log("wash begin");
      setTimeout(()=>{
        if(true){
            console.log("wash over");
            resolve("the end")
        }else{
            reject(eat)
        }
      },1000)
  })
};

then

一个promise必须提供一个then方法以访问当前值、最终值、错误原因。

promise.then(onFulfilled, onRejected)

then方法接收两个可选参数onFulfilledonRejected

  • onFulfilled参数:若参数为非函数则忽略

简单来说

  • then方法提供了一个自定义的回调函数,若传入非函数则直接忽略当前then方法。
  • 回调函数中会将上一个then方法中的返回值作为参数供当前then方法调用。
  • then方法执行完毕后需要返回一个新值给下一个then方法调用
  • 每个then只能使用前一个then的返回值

使用resolve方法将Promise对象的状态设置为完成状态,此时then方法就能捕获到变化,并执行“成功”情况的回调。reject方法则是把Promise对象的状态设置为失败,此时then方法执行失败情况的回调。

cookie().then((data)=>{
     console.log(data);
     return eat(data)
 }).then((data)=>{
     console.log(data);
     return wash(data)
 }).then((data)=>{
     console.log(data);
});

简写形式

cookie().then(eat).then(wash).then((data)=>{
    console.log(data);
});

最终输出

cookie
eat begin
eat over
eat
wash begin
wash over
the end

catch

  • 处理失败的情况可以使用then(null, ...),或是使用catch方法。
  • Promises/A规范指出当Promise实例状态修改为reject时,同时该错误会被下一个catch方法指定的回调函数捕获。
cookie().then((data)=>{
    throw new Error("cookie error");
    console.log(data);
    return eat(data)
}).then((data)=>{
    throw new Error("eat error");
    console.log(data);
    return wash(data)
}).then((data)=>{
    throw new Error("wash error");
    console.log(data);
}).catch((error)=>{
    console.log(error)
});
  • cachethen的第二个参数一样,用来指定reject失败的回调。另一方面,当执行resolve的回调时,若抛出异常,此时是不会卡死js,而会进入catch模块。这个错误的捕获是非常有用的,它能帮助我们在开发中识别代码错误。
cookie begin
cookie over
Error: cookie error
    at cookie.then (test.js:43:11)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:169:7)

all

  • Promise提供的all方法可并行执行异步操作,并且在所有异步操作执行完毕后才执行回调。
Promise.all([cookie(), eat(), wash()]).catch((error)=>{
    console.log(error)
});

race

  • Promise提供的race方法与all一样,只不过all是等所有异步操作都完毕后才执行then回调,而race得话,只要有一个异步操作执行完毕,就立即执行then是拿出
Promise.race([cookie(), eat(), wash()]).catch((error)=>{
    console.log(error)
});

古人云:“君子一诺千金”,这种“承诺未来会执行”的对象在JavaScript中称为Promise对象。Promise有各种开源实现,在ES6中被统一规范,并由浏览器直接支持。

// 测试浏览器是否支持ES6的Promise对象
"use strict";
// Promise 对象用于表示一个异步操作的最终状态(完成或失败),以及其返回值。
var obj = new Promise(function(resolve, reject){
  // 执行异步操作...
  setTimeout(function(){
    console.log("execute over");
    resolve(1);// resolve将Promise的状态置为fullfiled
  }, 2000);
});
console.log("ok");

Promise对象是一个代理对象,被代理的值在Promise对象创建时可能是未知的。Promise允许为异步成功和失败分别绑定对应的处理函数(handler),这样异步方法可以像同步方法一样使用返回值,但它并不是立即返回最终执行结果,而是返回一个能代表未来出现的结果的Promise对象。

4933701-06d2b91ea0fe3408.png
promise执行流程

Promise是JavaScript中解决回调地狱的一种方式,也被ECMAScript协会承认,固化为ES6语法中的一部分。

错误处理

(node:616) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): [object Object]
(node:616) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

未处理的承诺拒绝UnhandledPromiseRejectionWarning

错误原因

Promise 的状态变为 rejection 时,没有正确处理,让其一直冒泡propagation,直至被进程捕获。这个 Promise 就被称为 unhandled promise rejection

拒绝警告:不推荐使用未经处理的承诺拒绝。

Bluebird

Bluebird是早期Promise的一种实现,它提供了丰富的接口和语法糖用于降低Promise的使用难度。

Bluebird的安装和引入

$ npm i bluebird
const Promise = require("bluebird");

Promise的创建和使用

使用new Promise传入两个参数resolvereject方法,当Promise调用成功后会执行resolve方法,并封装返回数据。当调用失败时会执行reject方法并封装失败原因。需要注意的时,Promise的返回值只能在链式调用中使用。

async/await

promise调用链看起来比callback方式清晰很多,但仍存在不足之处:

  • 不够简洁,仍然需要创建then的调用链,需创建匿名函数将返回值一层层传递给下一个then调用。
  • 异常不会向上抛出,若某个then中的函数抛出异常,即使没有写catch异常也不会向上抛出,所以在then的调用链外写的try...catch是没有效果的。
  • 代码调试问题,若在某个then的方法中设置断点然后一步步向下走,是不能步进到下一个then的方法的,只能每个then方法中设置断点,然后resume run到下一个断点。

所以ES7提出新的Async/Await标准,async/await应运而生,async是一个函数的修饰符,添加上async关键词的函数会隐式地返回一个promise,函数的返回值将作为promise resolve的值。

await后跟的一定是一个promiseawait只能出现在async函数内,await的语义是必须等到await后面的promise有了返回值才继续执行await的下一行代码。

function readFile(file){
  return new Promise((resolve, reject) => {
    fs.readFile(file, "utf8", (error, content)=>{
      if(error){
        return reject(error);
      }else{
        return resolve(content);
      }
    });
  });
}

async function run(file){
  try{
    let result = await readFile(file);
  }catch(error){
    console.log("read fail");
  }
}

相比promise的写法,async/await写法的好处是

  • 代码简洁明了易于阅读和理解
  • 抛出的异常可以被try...catch捕获
  • 对程序员友好await可以步进到下一行代码

async函数会返回一个Promise对象,当函数执行时一旦遇到await就会先返回,等到触发的异步操作完成才会再接着执行函数体内后面的语句。async函数中可能会有await表达式,它会使async函数暂停执行让出线程即跳出async函数体然后继续执行后续脚本,等待表达式中的Promise解析完成后继续执行async函数并返回解决结果。

async定义异步函数

  • 自动将函数转换为Promise
  • 当调用异步函数时,函数返回值会被resolve处理。
  • 异步函数内部执行在async函数中

await暂停异步函数的执行

  • 当在promise前使用时会等待promise完成并返回promise的结果
  • await只能和promise一起使用,不能和calback一起使用。
  • await只能使用在async函数中

await可以理解为是async wait的缩写,await必须出现在async函数内部,不能单独使用。

async/await并不会取代promise,因为async/await底层依然使用的是promise

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值