ES6 Async

单线程

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

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

同步异步

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

同步执行

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

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

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回调地狱。如何解决回调嵌套太深而引发的回调地狱的问题呢?

回调地狱

在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函数会立即异步执行。

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

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)
});
const clubs = [
  {id:1, owner:"joe", status:1},
  {id:2, owner:"lvy", status:0},
  {id:3, owner:"ben", status:1}
];
const players = [
  {id:1, username:"joe", account:124314},
  {id:2, username:"charly", account:424614},
  {id:3, username:"mary", account:994334},
];
const getClubById = id=>new Promise((resolve, reject)=>{
   setTimeout(()=>{
       const club = clubs.find(item=>item.id === id);
       if(club){
           resolve(club);
       }else{
           reject(Error("no club found"));
       }
   }, 1000);
});
const getPlayerById = id => new Promise((resolve, reject)=>{
   setTimeout(()=>{
       const player = players.find(item=>item.id === id);
       if(player){
           resolve(player);
       }else{
           reject(Error("no player found"));
       }
   }, 1000);
});

Promise.all([getClubById(1), getPlayerById(2)]).then(response=>{
    const [club, player] = response;
    console.log(club, player);
}).catch(error=>console.error(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对象。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,697评论 1 56
  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,693评论 0 5
  • 目录:Promise 的含义基本用法Promise.prototype.then()Promise.prototy...
    BluesCurry阅读 1,488评论 0 8
  • 前言 编程语言很多的新概念都是为了更好的解决老问题而提出来的。这篇博客就是一步步分析异步编程解决方案的问题以及后续...
    李向_c52d阅读 1,064评论 0 2
  • 推荐大家使用的CSS书写规范、顺序 css书写顺序 1. 位置属性(position, top, right, z...
    ITzc阅读 416评论 0 1