likes
comments
collection
share

初识AJAX与Promise

作者站长头像
站长
· 阅读数 15

AJAX入门

什么是AJAX

  • AJAX 代表异步的 JavaScript 和 XML(Asynchronous JavaScript And XML)。简单点说,就是使用 XMLHttpRequest 对象与服务器通信。它可以使用 JSON、XML、HTML 和文本文件等格式发送和接收数据。AJAX 最吸引人的就是它的“异步”特性,也就是说它可以在不重新刷新页面的情况下与服务器通信,交换数据,或更新页面

  • 你可以使用 AJAX 最主要的两个特性做下列事:

    • 在不重新加载页面的情况下发送请求给服务器
    • 接收并使用从服务器发来的数据
  • 总的来说,AJAX是浏览器与服务器进行数据通信的技术

  • 流程图如下:

初识AJAX与Promise

AJAX实现过程

XMLHttpRequest

  • XML(Extensible Markup Language):可扩展标记语言
  • 定义:XMLHttpRequest(XHR)对象用于与服务器交互。通过 XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。这允许网页在不影响用户操作的情况下,更新页面的局部内容。XMLHttpRequest 在 AJAX 编程中被大量使用

AJAX异步交互步骤

实现 Ajax 异步交互需要服务器逻辑进行配合,需要完成以下步骤:

  • 创建 XMLHttpRequest 对象
  • 通过 XMLHttpRequest 对象的 open() 方法与服务端建立连接
  • 构建请求所需的数据内容,并通过 XMLHttpRequest 对象的 send() 方法发送给服务器端
  • 通过 XMLHttpRequest 对象提供的 onreadystatechange 事件监听服务器端你的通信状态
  • (此处 监听 loadend 事件也可)
  • 接受并处理服务端向客户端响应的数据结果
  • 将处理结果更新到 HTML 页面中

open() 方法

  • 通过 XMLHttpRequest 对象的 open() 方法与服务器建立连接

  • 语法:

    xhr.open(method, url, [async][, user][, password])
    
  • 参数说明

    参数说明
    method表示当前的请求方式,常见的有 GETPOST
    url服务端地址
    async布尔值,表示是否异步执行操作,默认为 true
    user可选的用户名用于认证用途;默认为 null
    password可选的密码用于认证用途,默认为 null

send() 方法

  • 通过 XMLHttpRequest 对象的 send() 方法,将客户端页面的数据发送给服务端

  • 语法:

    xhr.send([body]) //body: 在 XHR 请求中要发送的数据体,如果不传递数据则为 null
    

    如果使用GET请求发送数据的时候,需要注意如下:

    • 将请求数据添加到open()方法中的url地址中
    • 发送请求数据中的send()方法中参数设置为null

XMLHttpRequest使用示例:

const xhr = new XMLHttpRequest()
xhr.open('请求方法','请求url') 
//查询参数要在 URL 后面使用 ? 拼接
//例如:http://xxxx.com/xxx/xxx?参数名1=值1&参数名2=值2
xhr.addEventListener('loadend',()=>{
    //响应结果 xhr.response
    console.log(xhr.response)
})
xhr.send()

//若为POST请求,还要设置请求头,请求体携带JSON字符串
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://xxx/api/register')
xhr.addEventListener('loadend', () => {
    console.log(JSON.parse(xhr.response));
})
xhr.setRequestHeader('Content-Type', 'application/json')
    const obj = {
      username: 'xxx',
      password: 'xxx'
    }
xhr.send(JSON.stringify(obj))

AXIOS

本节为Axios常规使用,更多方法请看Axios中文文档

Axios概念

  • Axios,是一个基于 promise 的网络请求库,作用于 node.js 和浏览器中,它是 isomorphic 的(即同一套代码可以运行在浏览器和 node.js 中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用XMLHttpRequest

  • axios 本质上也是对原生 XHR(小黄人) 的封装,只不过它是 Promise 的实现版本,符合最新的ES规范

  • 主要特点:

    • 从浏览器创建 XMLHttpRequests
    • 从 node.js 创建 http 请求
    • 支持 Promise API
    • 拦截请求和响应
    • 转换请求和响应数据
    • 取消请求
    • 自动转换 JSON 数据
    • 客户端支持防御 XSRF
  • 开源API库:BootCDN

Axios使用

  • 语法:

    axios({
        url:'目标资源地址'
    }).then((result) => {
        //对服务器返回的数据做后续处理
    })
    

    引入 axios.js:cdn.jsdelivr.net/npm/axios/d…

    使用 axios 函数:

    1. 传入配置对象
    2. 再用 .then 回调函数接收结果,并做后续处理

常用请求方法

请求方法操作
GET获取数据
POST提交数据
PUT修改数据(全部)
DELETE删除数据
PATCH修改数据(部分)

Get 和 Post 区别:

  • Get 主要用于向服务器获取数据,Post 主要向服务器提交数据
  • Get 传输数据量小,Post 数据量大
  • Get 不安全,数据拼接在 url 后,对用户可见;Post 传输数据对用户不可见

接口文档概念

  • 描述接口的文章(后端提供)
  • 接口:使用 AJAX 和服务器通讯时,使用的 URL、请求方法、参数

Axios查询参数

  • 语法:使用 axios 提供的 params选项(parameter)

  • 注意:axios 在运行时把参数名和值会拼接到 url?参数名=值

  • 示例:

    axios({
        url:'目标资源地址',
        params:{
            参数名:值
        }
    }).then(result => {
        //对服务器返回的数据做后续处理
    })
    

Axios请求配置

相关参数:

  • url:请求的 URL 网址
  • method:请求的方法,GET可以省略(不区分大小写)
  • data:提交数据(请求体参数)

使用示例:

axios({
    url:'目标资源地址',
    method:'请求方法(GET/POST)',
    data:{
        参数名:值
    }
}).then(result => {
    //对服务器返回的数据做后续处理
})

Axios错误处理

  • 语法:在 then 方法的后面,通过点语法调用 catch 方法,传入回调函数并定义形参

  • 示例:

    axios({
        //请求选项
    }).then(result => {
        //处理数据(成功)
    }).catch(error => {
        //处理错误(失败)
    })
    

表单提交插件

  • form-serialize 插件:快速收集表单元素的值

  • 语法:

    const result = serialize(form, { hash: true, empty: true })
    
  • 参数:

    参数说明
    formform 表单对象
    hash设为 true 表示将数据收集成对象,一般用于请求体传参,配合 post 请求;设为 false 表示将数据收集成查询参数的模式:参数名1=参数值1&参数名2=参数值2, 一般用于 get 请求传参
    empty设为 true 表示数据即便是空的也要收集

URL

什么是URL

  • URL (Uniform Resource Locator) 统一资源定位符,或称统一资源定位器、定位地址、URL地址。俗称网页地址(简称地址),是因特网上标准的资源的地址,如同在网络上的门牌。它最初是由蒂姆·伯纳斯-李发明用来作为万维网的地址,现在它已经被万维网联盟编制为因特网标准 RFC 1738
  • 总的来说,URL就是统一资源定位符,简称网址,用于访问网络上的资源
  • URL组成:URL由协议、域名、资源路径三部分组成协议://域名/资源路径
    • 协议:
      • URL使用 http 协议(超文本传输协议),规定浏览器与服务器之间传输数据的格式
    • 域名:
      • 标记服务器在互联网中的方位
    • 资源路径:
      • 标记资源在浏览器下的具体位置

URL查询参数

  • 定义:浏览器提供给服务器的额外信息,让服务器返回浏览器想要的数据
  • 语法:xxxx.com/xxx/xxx?`参数…

HTTP

  • HTTP协议:规定了浏览器发送以及浏览器返回内容的格式

HTTP请求报文

  • 定义:浏览器按照 HTTP 协议要求的格式,发送给服务器的内容

  • 请求报文组成:

    • 请求行:请求方法、URL、协议
    • 请求头:以键值对的格式携带的附加信息,比如:Content-Type
    • 空行:分隔请求头,空行之后的是发送给服务器的资源
    • 请求体:发送的资源
  • 图解:

初识AJAX与Promise

HTTP响应报文

  • 定义:服务器按照 HTTP 协议要求的格式,返回给浏览器的内容

  • 响应报文组成:

    • 响应行(状态行):协议、HTTP 响应状态码、状态信息
    • 响应头:以键值对的格式携带的附加信息,比如:Content-Type
    • 空行:分隔响应头,空行之后的是服务器返回的资源
    • 响应体:返回的资源
  • HTTP响应状态码

    状态码说明
    1xx信息
    2xx成功
    3xx重定向消息
    4xx客户端错误
    5xx服务端错误
  • 常见的 http 响应码

    • 200 - 服务器成功返回网页
    • 400 - 请求错误(请求数据格式错误、账号密码错误)
    • 404 - 请求的网页不存在
    • 503 - 服务不可用
  • 1xx 表示【临时响应】并需要请求者继续执行操作的状态代码

    • 100(继续)请求者应当继续提出请求。服务器返回此代码表示已收到请求的第一部分,正在等待其余部分 101(切换协议)请求者已要求服务器切换协议,服务器已确认并准备切换
  • 2xx 表示【成功】处理了请求的状态代码

    • 200(成功)服务器已成功处理了请求。通常,这表示服务器提供了请求的网页
    • 201(已创建)请求成功并且服务器创建了新的资源
    • 202(已接受)服务器已接受请求,但尚未处理
    • 203(非授权信息)服务器已成功处理了请求,但返回的信息可能来自另一来源
    • 204(无内容)服务器成功处理了请求,但没有返回任何内容
    • 205(重置内容)服务器成功处理了请求,但没有返回任何内容
    • 206(部分内容)服务器成功处理了部分 GET 请求
  • 3xx 表示【要完成请求】,需要进一步操作。通常,这些状态代码用来重定向

    • 300(多种选择)针对请求,服务器可执行多种操作。服务器可根据请求者(user agent)选择一项操作,或提供操作列表供请求者选择
    • 301(永久移动)请求的网页已永久移动到新位置。服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置
    • 302(临时移动)服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
    • 303(查看其他位置)请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码
    • 304(未修改)自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容
    • 305(使用代理)请求者只能使用代理访问请求的网页。如果服务器返回此响应,还表示请求者应使用代理
    • 307(临时重定向)服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
  • 4xx 表示【请求可能出错】,妨碍了服务器的处理

    • 400(错误请求)表示客户端请求的语法错误,服务器无法理解,例如 url 含有非法字符、json 格式有问题
    • 401(未授权)请求要求身份验证,对于需要登录的网页,服务器可能返回此响应
    • 402表示保留,将来使用
    • 403(禁止)表示服务器理解请求客户端的请求,但是拒绝请求
    • 404(未找到)服务器无法根据客户端的请求找到资源(网页)
    • 405(方法禁用)禁用请求中指定的方法
    • 406(不接受)无法使用请求的内容特性响应请求的网页
    • 407(需要代理授权)此状态代码与 401(未授权) 类似,但指定请求者应当授权使用代理
    • 408(请求超时)服务器等候请求时发生超时
    • 409(冲突)服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息
    • 410(已删除)如果请求的资源已永久删除,服务器就会返回此响应
    • 411(需要有效长度)服务器不接受不含有效内容长度标头字段的请求
    • 412(未满足前提条件)服务器未满足请求者在请求中设置的其中一个前提条件
    • 413(请求实体过大)表示响应实在太大。服务器拒绝处理当前请求,请求超过服务器所能处理和允许的最大值
    • 414(请求的 URI 过长)请求的 URI(通常为网址)过长,服务器无法处理
    • 415(不支持的媒体类型)请求的格式不受请求页面的支持
    • 416(请求范围不符合要求)如果页面无法提供请求的范围,则服务器会返回此状态代码
    • 417(未满足期望值)在请求头 Expect 指定的预期内容无法被服务器满足(力不从心)
    • 418表示我是一个茶壶。超文本咖啡馆控制协议,但是并没有被实际的 HTTP 服务器实现
    • 420表示方法失效
    • 422表示不可处理的实体。请求格式正确,但是由于含有语义错误,无法响应
  • 5xx 表示【服务器】在尝试处理请求时发生内部错误。这些错误可能是服务器本身的错误,而不是请求出错

    • 500(服务器内部错误)服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理
    • 501(尚未实施)服务器不具备完成请求的功能。例如,服务器无法识别请求方法时可能会返回此代码
    • 502(错误网关)服务器作为网关或代理,从上游服务器收到无效响应
    • 503(服务不可用)服务器目前无法使用(由于超载或停机维护)。通常,这只是暂时状态
    • 504(网关超时)服务器作为网关或代理,但是没有及时从上游服务器收到请求
    • 505(HTTP 版本不受支持)服务器不支持请求中所用的 HTTP 版本

AJAX进阶

Promise

Promise定义

ECMAScript 6 增加了对 Promises/A+规范的完善支持,即 Promise(期约) 类型。一经推出,Promise 就大受欢迎,成为了主导性的异步编程机制。所有现代浏览器都支持 ES6 期约,很多其他浏览器 API(如fetch()和 Battery Status API)也以期约为基础。此外 Promise 支持链式调用,可以解决回调函数地狱问题(重要作用,后文会讲解)

Promise 对象是一个构造函数,用来生成 Promise 实例,可以包裹一个异步操作

const promise = new Promise(function(resolve, reject) {异步操作});
# Promise 构造函数:Promise(excutor){}
* (1) excutor 函数:执行器 (resolve,reject) => {}
* (2) resolve 函数:内部定义成功时我们调用的函数 value => {}
* (3) reject 函数: 内部定义失败时我们调用的函数 reason => {}
* 说明: excutor 会在 Promise 内部立即同步调用,异步操作会在执行器中执行

Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject,称为函数类型的数据,它们是两个函数,由 JavaScript 引擎提供,不用自己部署。当然这里的 resolve 和 reject 是可以随意命名的,一般来说是这么命名,但要把这里的 resolve 和 reject 与Promise静态方法中的 resolve 和 reject 分开,并不相同!

  • 在异步任务成功时,调用resolve函数,将 Promise 对象的状态从“未完成(pending)”变为“成功(resolved)”,并将异步操作的结果作为参数传递出去

  • 在异步任务失败时,调用reject函数,将 Promise 对象的状态从“未完成(pending)”变为“失败(rejected)”,并将异步操作报出的错误作为参数传递出去

  • Promise 实例生成以后,可以使用 then方法分别指定 resolve 状态和 rejected 状态的回调函数,当然这里的错误接受也可以使用 catch 方法 (本质上也是个语法糖)

    new Promise(function (resolve, reject) {
    	//成功调用 resolve()
        //失败调用 reject()
    }).then(res => {
       //成功执行代码
    }).catch(res => {
       //失败执行代码
    })
    ## Promise 的状态
    * 实例对象中的一个属性 【PromiseState
    ## Promise 对象的值
    * 实例对象中的另一个属性 【PromiseResult
    * 保存着对象【成功/失败】的结果,只能由 resolve 和 reject 修改
    

    总的来说:当 resolvereject 修改 promise 对象状态之后,通过检测 promise 对象的状态,决定执行 then还是 catch 回调方法。在这个过程中:resolvereject 起到的作用是修改对象状态、通知回调函数以及传递回调函数可能需要的参数。 这样做的好处就是:把逻辑判断和回调函数分开处理。通俗来讲,这俩函数就是个干苦力的中间人,无名无姓,连名字都可以随意更改!

  • Promise 只能用以上方法抛出数据信息,使用 return 将毫无效果,但使用 async 声明的异步函数可以使用 return(后文会详细说明)

    new Promise(function (resolve, reject) {
      // resolve('666')
      return '666'
    }).then(res => {
      console.log(res) //无法捕获到任何信息 
    }).catch(err => {
      console.log(err) //无法捕获到任何信息 
    })
    

期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式的媒介。这样看似乎不太好理解,下面引入红宝书中的例子:

try { 
 throw new Error('foo'); 
} catch(e) { 
 console.log(e); // Error: foo 
} 
try { 
 Promise.reject(new Error('bar')); 
} catch(e) { 
 console.log(e); 
} 
// Uncaught (in promise) Error: bar

上述的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队列来处理的。因此,try/catch块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体地说,就是期约的方法

Promise状态

一个Promise对象,必然处于以下几种状态之一:

  • 待定(pending):待定,初始状态,既没有被兑现也没有被拒绝
  • 已兑现(fulfilled):已兑现,表示操作成功完成
  • 已拒绝(rejected):已失败,表示操作失败

对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态

一旦状态改变(从pending变为fulfilled和从pending变为rejected),就不会再变,任何时候都可以得到这个结果,继续修改状态会静默失败

Promise整体流程:

初识AJAX与Promise

Promise实例方法

期约实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理期约成功和失败的结果,连续对期约求值,或者添加只有期约进入终止状态时才会执行的代码

then( )

then()是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

then()方法返回的是一个新的Promise实例,也就是promise能链式书写的原因

  • 语法:

    then(onResolved)
    then(onResolved, onRejected)
    
  • 替代 catch 写法:

    new Promise(function(resolve, reject) {
      reject('出错啦')
    }).then(null,(res) =>{
      console.log(res)
    })
    

两个处理程序参数都是可选的。而且,传给 then()的任何非函数类型的参数都会被静默忽略。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined。这样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代

  • 返回值:当一个 Promise 完成(fulfilled)或者失败(rejected)时,返回函数将被异步调用。具体的返回值依据以下规则返回。如果 then 中的回调函数:

    • 返回了一个值,那么 then 返回的 Promise 将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值

    • 没有返回任何值,那么 then 返回的 Promise 将会成为接受状态,并且该接受状态的回调函数的参数值为 undefined

    • 抛出一个错误,那么 then 返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。

    • 返回一个已经是接受状态的 Promise,那么 then 返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的 Promise 的接受状态回调函数的参数值。(Promise链式调用原理)

      new Promise(function (resolve, reject) {
        resolve('666')
      })
        .then(res => {
          console.log(res)
          return new Promise(function (resolve, reject) {
            resolve('777')
          })
        })
        .then(res => {
          console.log(res)
          return new Promise(function (resolve, reject) {
            resolve('888')
          })
        })
        .then(res => {
          console.log(res)
        })
      
    • 返回一个已经是拒绝状态的 Promise,那么 then 返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的 Promise 的拒绝状态回调函数的参数值

    • 返回一个未定状态(pending)的 Promise,那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的

catch( )

catch()方法是.then(null, onRejected).then(undefined, onRejected)的别名,用于指定发生错误时的回调函数,。这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖

一般来说,使用catch()方法代替then()第二个参数,与then()方法一样。catch()方法返回一个新的实例

  • 语法:

    catch(onRejected)
    
  • 使用示例:

    const someAsyncThing = function () {
      return new Promise(function (resolve, reject) {
        // 下面一行会报错,因为x没有声明
        resolve(x + 2)
      }).catch(res => {
        console.log(res)
      })
    }
    someAsyncThing()
    

finally( )

finally()方法用于给期约添加 onFinally 处理程序,这个处理程序在期约转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道期约的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

换句话说,finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally()方法返回 Promise 实例对象

  • 语法:

    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···})
    

如果给期约添加了多个处理程序,当期约状态变化时,相关处理程序会按照添加它们的顺序依次执行。无论是 then()、catch()还是 finally()添加的处理程序都是如此

let p1 = Promise.resolve(); 
let p2 = Promise.reject(); 
p1.then(() => setTimeout(console.log, 0, 1)); 
p1.then(() => setTimeout(console.log, 0, 2)); 
// 1 
// 2 
p2.then(null, () => setTimeout(console.log, 0, 3)); 
p2.then(null, () => setTimeout(console.log, 0, 4)); 
// 3 
// 4 
p2.catch(() => setTimeout(console.log, 0, 5)); 
p2.catch(() => setTimeout(console.log, 0, 6)); 
// 5 
// 6 
p1.finally(() => setTimeout(console.log, 0, 7)); 
p1.finally(() => setTimeout(console.log, 0, 8)); 
// 7 
// 8

拓展-拒绝期约与拒绝错误处理

注:本节内容为红宝书对应小节内容的提炼,有助于对后文中异步代码报错问题的理解

拒绝期约类似于 throw() 表达式,因为它们都代表一种程序状态,即需要中断或者特殊处理。在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。因此以下这些期约都会以一个错误对象为由被拒绝:

let p1 = new Promise((resolve, reject) => reject(Error('foo'))); 
let p2 = new Promise((resolve, reject) => { throw Error('foo'); }); 
let p3 = Promise.resolve().then(() => { throw Error('foo'); }); 
let p4 = Promise.reject(Error('foo')); 
setTimeout(console.log, 0, p1); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p2); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p3); // Promise <rejected>: Error: foo 
setTimeout(console.log, 0, p4); // Promise <rejected>: Error: foo

期约可以以任何理由拒绝,包括 undefined,但最好统一使用错误对象。这样做主要是因为创建错误对象可以让浏览器捕获错误对象中的栈追踪信息,而这些信息对调试是非常关键的

上述例子中的 4 个错误的栈追踪信息如下:

Uncaught (in promise) Error: foo 
 at Promise (test.html:5) 
 at new Promise (<anonymous>) 
 at test.html:5 
                 
Uncaught (in promise) Error: foo 
 at Promise (test.html:6) 
 at new Promise (<anonymous>) 
 at test.html:6 
                 
Uncaught (in promise) Error: foo 
 at test.html:8 

Uncaught (in promise) Error: foo 
 at Promise.resolve.then (test.html:7)

Promise.resolve().then() 的错误最后才出现,这是因为它需要在运行时消息队列中添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约 (微任务,后文有详解)

此例子引出了一个很重要的现象:

  • 正常情况下,在通过 throw() 关键字抛出错误时,JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令
  • 在 Promise 中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时继续执行同步指令
throw Error('foo'); 
console.log('bar'); // 这一行不会执行
// Uncaught Error: foo

Promise.reject(Error('foo')); 
console.log('bar'); 
// bar 
// Uncaught (in promise) Error: foo
//由此可知:异步错误只能通过异步的 onRejected 处理程序捕获

then()和 catch() 的 onRejected 处理程序在语义上相当于 try/catch。出发点都是捕获错误之后将其隔离,同时不影响正常逻辑执行。为此,onRejected 处理程序的任务应该是在捕获异步错误之后返回一个解决的期约。下面的例子中对比了同步错误处理与异步错误处理:

console.log('begin synchronous execution'); 
try { 
 throw Error('foo'); 
} catch(e) { 
 console.log('caught error', e); 
} 
console.log('continue synchronous execution'); 
// begin synchronous execution 
// caught error Error: foo 
// continue synchronous execution 

new Promise((resolve, reject) => { 
 console.log('begin asynchronous execution'); 
 reject(Error('bar')); 
}).catch((e) => { 
 console.log('caught error', e); 
}).then(() => { 
 console.log('continue asynchronous execution'); 
}); 
// begin asynchronous execution 
// caught error Error: bar 
// continue asynchronous execution

拓展-Promise关键问题

  • 一个 promise 指定多个成功/失败回调函数,都会调用吗?

    //当promise改变为对象状态时都会调用
    let p = new Promise((resolve, reject) => {
        resolve('OK')
    })
    p.then(res => {
        console.log(res)
    })
    p.then(res => {
        console.log(res)
    })
    //此处均会执行
    
  • 改变 promise 状态 和执行回调函数谁先谁后?(此问题本质上就是代码执行顺序问题,后文有详细讲解)

    let p = new Promise((resolve, reject) => {
        //resolve('OK')
        setTimeout(() => {
            resolve('ok')
        },1000) //宏任务,后执行
    })
    p.then(res => {
        console.log(res) //微任务先执行,后文有详细介绍
    })
    
  • promise 异常穿透?

    (1)当使用 promise 的 then 链式调用时,可以在最后指定失败的回调

    (2)前面任何操作出了异常,都会传到最后失败的回调中处理

  • 如何中断 promise 链?

    (1)当使用 promise 的 then 链式调用时,在中间中断,不再调用后面的回调函数

    (2)办法:在回调函数中返回一个 pendding 状态的 promise 对象

    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('ok')
        },1000) 
    })
    p.then(res => {
        console.log(res) //想在此处中断
        return new Promise(() => {}) //返回 pendding 状态的 promise 的对象,中断!
    })
    p.then(res => {
        console.log(res) 
    })
    p.then(res => {
        console.log(res) 
    })
    p.then(res => {
        console.log(res) 
    })
    

Promise静态方法

resolve( )

Promise.resolve():(value) => { }

  • value:成功的数据或 promise 对象

  • 说明:返回一个成功 / 失败的 promise 对象(快速得到一个 promise 对象,如果为值也会被转换为 promise 对象)

  • 示例:

    let p1 = Promise.resolve(520)
    //如果传入的参数为 非 Promise 类型的对象,则返回的结果为成功 promise 对象
    //如果传入的参数为 Promise 对象,则参数的结果决定了 resolve 的结果
    let p2 = Promise.resolve(new Promise((resolve, reject) => {
        reject('error')
    }))
    p2.catch(reason => {
        console.log(reason)
    })//此处不适用catch捕获错误,则浏览器会报错(Uncaught (in promise) error)
    

reject( )

Promise.reject():(reason) => { }

  • reason:失败的原因

  • 说明:返回一个失败的 promise 对象(快速得到一个 promise 对象,如果为值也会被转换为 promise 对象,且永远是一个失败的 promise 对象)

  • 示例:

    let p1 = Promise.reject(520)//失败的 promise 对象
    let p2 = Promise.reject(new Promise((resolve, reject) => {
        resolve('ok')
    }))
    //依旧为失败的 promise 对象
    * Uncaught (in promise) 
    * Promise {<fulfilled>: 'ok'}
      * [[Prototype]]: Promise
      * [[PromiseState]]: "fulfilled"
      * [[PromiseResult]]: "ok"
    

all( )

Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例

  • 语法:

    const p = Promise.all([p1, p2, p3])
    

    接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

    实例p的状态由p1p2p3决定,分为两种:

    • 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数
    • 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

    注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

  • 示例一(返回值组成一个数组,传递给p的回调函数):

    const p1 = new Promise((resolve, reject) => {
      resolve('hello')
    })
      .then((result) => result)
      .catch((e) => e)
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了')
    })
      .then((result) => result)
      .catch((e) => e)
    
    Promise.all([p1, p2])
      .then((result) => console.log(result))
      .catch((e) => console.log(e))
    // ["hello", Error: 报错了]
    
  • 示例二(如果p2没有自己的catch方法,就会调用Promise.all()catch方法):

    const p1 = new Promise((resolve, reject) => {
      resolve('hello')
    }).then((result) => result)
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了')
    }).then((result) => result)
    
    Promise.all([p1, p2])
      .then((result) => console.log(result))
      .catch((e) => console.log(e))
    // Error: 报错了
    

race( )

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

  • 语法:

    const p = Promise.race([p1, p2, p3])
    

    只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变率先改变的 Promise 实例的返回值则传递给p的回调函数

  • 示例:

    const p = Promise.race([
      fetch('/resource-that-may-take-a-while'),
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      }),
    ])
    
    p.then(console.log).catch(console.error)
    

同步代码和异步代码

  • 同步代码: 逐行执行,需原地等待结果后,才继续向下执行

  • 我们应该注意的是,实际上浏览器是按照我们书写代码的顺序一行一行地执行程序的。浏览器会等待代码的解析和工作,在上一行完成后才会执行下一行。这样做是很有必要的,因为每一行新的代码都是建立在前面代码的基础之上。这也使得它成为一个同步程序

  • 异步代码: 调用后耗时,不阻塞代码继续执行(不必原地等待),在将来完成后触发一个回调函数

  • 异步编程技术使你的程序可以在执行一个可能长期运行的任务的同时继续对其他时间做出反应而不必等待任务完成。与此同时,你的程序也将在任务完成后显示结果

  • 示例:

    const result = 0 + 1
    console.log(result)
    setTimeout(() => {
      console.log(2)
    }, 2000)
    document.querySelector('.btn').addEventListener('click', () => {
      console.log(3)
    })
    console.log(4)
    // 运行页面 2s 内点击: 1432
    // 运行页面 2s 后点击: 1423
    

对于同步和异步代码,红宝书上有一个很有意思的现象:

//在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) 
}
// 你可能以为会输出 0、1、2、3、4 
// 实际上会输出 5、5、5、5、5 
//之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的 i 都是同一个变量,因而输出的都是同一个最终值。

//而在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。
//每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) { 
 setTimeout(() => console.log(i), 0) 
}
// 会输出 0、1、2、3、4

产生这种现象的原因主要是 var 定义的变量会有变量提升问题且 变量i 没有块级作用域,于是每次输出的 i 其实都是同一个变量,加上setTimeout( )是异步执行的,于是每次setTimeout( )输出时,输出的是循环后最终的 i 的值。

使用 let 变量没有出现这种问题的主要原因是因为 let 有块级作用域,每次传入 setTimeout( ) 回调函数中的 i 其实是不同的变量 i ,因此最后能输出正确的结果。

回调函数地狱和 Promise 链式调用

回调地狱

异步行为是JavaScript的基础,但在早期JavaScript实现并不理想,只支持定义回调函数来表明异步操作的完成。串联多个异步操作是很常见的问题,通常需要深度嵌套回调函数,但嵌套深度比较大时,代码可读性会变得非常差,整体代码也会变得十分臃肿,这里就形成了回调函数地狱问题(俗称回调地狱)

我们可以对回调地狱进行总结:

  • 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
  • 缺点:可读性差,异常无法捕获,耦合性严重,牵一发动全身

回调地狱问题,对于es6后的JavaScript来说,此种解决方案已经被遗弃,代替的有更好的实现方法,这里只是举例是为了让大家了解JavaScript发展历史中的遗留问题

网上有一张图非常生动的展示了回调地狱的臃肿与可读性差:

初识AJAX与Promise

Promise链式调用

Promise链式调用是解决回调地狱的一种方法。依靠then()方法会返回一个新生成的 Promise 对象特性,继续串联下一环任务,直到结束。then()回调函数中的返回值,会影响新生成的 Promise 对象最终状态和结果。

使用 Promise 链式调用的好处和作用如下:

  • 好处:通过链式调用,解决回调函数嵌套问题
  • 作用:使用 Promise 链式调用,解决回调函数地狱问题

初识AJAX与Promise

Promise使用:每个 Promise 对象中管理一个异步任务,用 then 返回 Promise 对象,串联起来

下面为大家举一个使用 Promise 链式调用的例子:

  • 使用 axios 实现省市区联动

  • 通过 then 返回的 Promise 对象实现 axios 请求同步

    let pname
    axios({ url: '获取省份信息接口地址' })
      .then(res => {
        pname = res.data.list[0]
        document.querySelector('.province').innerHTML = pname
        return axios({ url: '获取省份对应城市信息接口地址', params: { pname } })
      })
      .then(res => {
        const cname = res.data.list[0]
        document.querySelector('.city').innerHTML = cname
        return axios({ url: '获取省市对应地区信息接口地址', params: { pname, cname } })
      })
      .then(res => {
        document.querySelector('.area').innerHTML = res.data.list[0]
      })
    
  • 整个 axios 链式结构如下:

初识AJAX与Promise

async 和 await 使用

async 和 await 本质上是官方推出的 Promise 链式调用的优化语法 (ES8 新规范)。在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值 ,此种用法本质上就是 Promise 链式调用,只不过代码逻辑上形似于同步代码,通过使用 async 和 await 解决回调地狱问题

async 关键字

  • async 关键字用于声明⼀个异步函数(如 async function asyncTask1() {...}
  • async 会⾃动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象
  • promise 对象的结果由 async 函数执行的返回值决定
  • async 函数内部可以使⽤ await

MDN对于 async 函数定义如下:

  • async 函数是使用async关键字声明的函数。async 函数是 AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 promise

async 关键字可以用在函数声明、函数表达式、箭头函数和方法上。**使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。**而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为

  • 例如:

    async function foo() { 
     console.log(1); 
    } 
    foo(); 
    console.log(2); 
    // 1 
    // 2
    //foo()函数仍然会在后面的指令之前被求值
    

与 Promise 不同的是,async 声明的函数可以使用 return 关键字。异步函数如果使用 return 关键字返回了值(如果没有 return 则会返回 undefined),这个值会被 Promise.resolve() 包装成一个期约对象。异步函数始终返回期约对象。在函数外部调用这个函数可以得到它返回的期约。当然,直接返回一个期约对象也是一样的

  • 例如:

    async function foo() { 
     console.log(1); 
     return 3; 
    } 
    // 给返回的期约添加一个解决处理程序
    foo().then(console.log);
    console.log(2); 
    // 1 
    // 2 
    // 3
    
    async function foo() { 
     console.log(1); 
     return Promise.resolve(3); 
    } 
    // 给返回的期约添加一个解决处理程序
    foo().then(console.log); 
    console.log(2); 
    // 1 
    // 2 
    // 3
    

与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约,不过,拒绝期约的错误不会被异步函数捕获(需要使用return)

  • 例如:

    async function foo() { 
     console.log(1); 
     throw 3; 
    } 
    // 给返回的期约添加一个拒绝处理程序
    foo().catch(console.log);
    console.log(2); 
    // 1 
    // 2 
    // 3
    
    async function foo() { 
     console.log(1); 
     Promise.reject(3);  //改成 return Promise.reject(3),则可以正常抛出错误
    } 
    // Attach a rejected handler to the returned promise 
    foo().catch(console.log); 
    console.log(2); 
    // 1 
    // 2 
    // Uncaught (in promise): 3
    

await 关键字

  • await 用于等待异步的功能执行完毕 const result = await someAsyncCall()
  • await 放置在 Promise 调⽤之前,会强制 async 函数中其他代码等待,直到 Promise 完成并返回结果,才会恢复异步函数的执行
  • await 只能在 async 函数内部使⽤
  • await 右侧表达式一般为 promise对象,但也可以是其他值
    • 如果表达式是 promise 对象,await 返回的是 promise 成功的值
    • 如果表达式是其他值,直接将此值作为 await 的返回值
  • 如果 awaitpromise失败了,就会抛出异常,需要通过 try/catch 捕获处理

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别

  • 例如:

    async function foo() { 
     console.log(2); 
    } 
    console.log(1); 
    foo(); 
    console.log(3);
    // 1 2 3
    

JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也被异步求值

  • 红宝书中有一个很经典的例子:(可以先思考一下,看完后文便会豁然开朗)

    async function foo() { 
     console.log(2); 
     console.log(await Promise.resolve(8)); 
     console.log(9); 
    } 
    async function bar() {
     console.log(4); 
     console.log(await 6); 
     console.log(7); 
    } 
    console.log(1); 
    foo(); 
    console.log(3); 
    bar(); 
    console.log(5); 
    // 1 
    // 2 
    // 3 
    // 4 
    // 5 
    // 8 
    // 9 
    // 6 
    // 7
    

相较于 Promise,async/await 有何优势?

  1. 同步化代码的阅读体验(Promise 虽然摆脱了回调地狱,但 then 链式调⽤的阅读负担还是存在的)
  2. 和同步代码更一致的错误处理方式( async/await 可以⽤成熟的 try/catch 做处理,比 Promise 的错误捕获更简洁直观)
  3. 调试时的阅读性, 也相对更友好

上文提到的省市区联动问题,使用 async 和 await 示例如下:

  • 使用 try / catch 来捕获异常
//async也可以加到匿名函数和箭头函数前面
async function getData() {
  try{
    const pObj = await axios({ url: '获取省份信息接口地址' })
    const pname = pObj.data.list[0]
    const cObj = await axios({ url: '获取省份对应城市信息接口地址', params: { pname } })
    const cname = cObj.data.list[0]
    const aObj = await axios({ url: '获取省市对应地区信息接口地址', params: { pname, cname } })
    const areaName = aObj.data.list[0]
    document.querySelector('.province').innerHTML = pname
    document.querySelector('.city').innerHTML = cname
    document.querySelector('.area').innerHTML = areaName
  }catch(e){
    console.log(e)
  }
}

事件循环-EventLoop

事件循环

JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其他语言中的模型截然不同,比如 C 和 Java。事件循环的出现主要是因为JavaScript 是单线程的,为了不阻塞 JS 引擎而设计的一种执行代码的模型。

事件循环定义如下:

  • 定义:执行代码和收集异步任务的模型,在调用栈空闲,反复调用任务队列里回调函数的执行机制,就叫事件循环

为什么要有事件循环:

  • 因为 JavaScript 是单线程的,同一时间只能做同一件事情,意味着有些耗时操作会阻塞代码执行,导致卡死的现象,所以就有了异步任务,而事件循环就是执行同步和异步任务的一种调度机制

在 JavaScript 内,会优先执行同步代码,遇到异步代码便会交给宿主浏览器环境执行,异步有了结果后,把回调函数放入任务队列(ES6前)排队。当调用栈空闲后,会反复调用任务队列里的回调函数,上述过程不断重复就是事件循环。

初识AJAX与Promise

宏任务与微任务

ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务 (宏任务和微任务)。

宏任务

  • 宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合,由 JS 引擎环境执行

常见的微任务有:

  • Promise.then(Promise 本身是同步的,而 then 和 catch 回调函数是异步的)
  • MutaionObserver
  • process.nextTick(Node.js)

微任务

  • 一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前,由浏览器环境执行

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout / setInterval
  • UI rendering / UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)
  • DOM事件
  • Ajax

ES6后执行第一个 script 脚本事件宏任务里面同步代码,遇到 宏任务/微任务 交给宿主环境,有结果的回调函数进入对应队列,当执行栈空闲时,清空微任务队列,再执行下一个宏任务,上述过程不断重复。

初识AJAX与Promise

代码执行顺序面试题

  • JS是异步单线程语言,同步事件执行完才执行事件循环内容(宏任务和微任务)---同步任务 > 微任务 > 宏任务

  • 此外,对于 async 函数,只有从 await 往下才是异步的开始

初识AJAX与Promise

  • 面试题(script宏任务)

    <script>
        console.log(1);
        setTimeout(() => {
            console.log(2);
        }, 0)
        console.log(3);
    </script>
    <script>
        console.log(4);
        setTimeout(() => {
            console.log(5);
        }, 0)
        console.log(6);
    </script>
    <!--输出为:1 3 4 6 2 5 -->
    <!--原理:先执行第一个script宏任务中的同步代码,异步代码放入宿主环境等待执行 -->
    <!--当执行栈空闲时,清空微任务队列,再执行下一个宏任务,上述过程不断重复 -->
    
  • 面试题(Promise相关)

    new Promise((resolve, reject) => {
    	console.log(1)
    	new Promise((resolve, reject) => {
      	  	console.log(2)
        	setTimeout(() => {
        		console.log(3)
       		}, 0)
        	console.log(4)
    	})
    	console.log(5)
    })
    setTimeout(() => {
    	console.log(6)
    }, 1000)
    console.log(7)
    //输出为 1 2 4 5 7 3 6
    //原理:Promise 本身是同步的,而 then 和 catch 回调函数是异步的,此外对于setTimeout来说,延迟时间决定了执行的先后
    //例如:讲promise内部的setTimeout时间改为1000,外部setTimeout改为0,则输出变成 1 2 4 5 7 6 3
    
  • 面试题(setTimeout + Promise执行顺序)

    setTimeout(() => {
        console.log(1)
        new Promise((resolve, reject) => {
            resolve(2)
        }).then(res => {
            console.log(res)
            setTimeout(() => {
            	console.log(3)
            }, 1000)
        })
    }, 0)
    console.log(4)
    setTimeout(() => {
        console.log(5)
    }, 5000)
    console.log(6)
    //输出为:4 6 1 2 3 5
    //原理:微任务先于宏任务执行,Promise本身是同步的
    //对于相同setTimeout按延迟时间排序执行,例如把5000改为500,则执行顺序变成:4 6 1 2 5 3
    
  • 面试题(async await执行机制)

    • await会阻塞其所在表达式中后续表达式的执行 (在和 await 在同一函数内但在 await 后面的代码会被阻塞,形成类似微任务的存在)
    • async 关键字用于声明⼀个异步函数,此外async 会⾃动将常规函数转换成 Promise,返回值也是⼀个 Promise 对象
    async function async1() {
      console.log(1)
      await async2()
      console.log(2)
    }
    async function async2() {
      return new Promise((resolve, reject) => {
        reject(new Error(''))
      })
      .catch(err=>{
         console.log(err);
      })
    }
    console.log(3)
    setTimeout(function () {
      console.log(4)
    }, 0)
    async1()
    new Promise(function (resolve) {
      console.log(5)
      resolve()
    }).then(function () {
      console.log(6)
    })
    console.log(7)
    //输出为: 3 1 5 7 (打印error) 6 2 4
    //原理:对于 async 函数,只有从 await 往下才是异步的开始,当代码运行到await async2()时,第一个微任务console.log(err)进入微任务队列,并在后续运行中,第二个微任务console.log(6)进入微任务队列。当同步代码执行完毕后,微任务队列中的代码开始执行,执行完第一个微任务后,第三个微任务console.log(2),进入微任务队列,全部微任务进入队列并依次按顺序执行
    
    //本题变种
    async function async1() {
      console.log(1)
      await async2()
      console.log(2)
    }
    async function async2() {
      return new Promise((resolve, reject) => {
        reject(new Error(''))
      })
    }
    console.log(3)
    setTimeout(function () {
      console.log(4)
    }, 0)
    async1()
    new Promise(function (resolve) {
      console.log(5)
      resolve()
    }).then(function () {
      console.log(6)
    })
    console.log(7)
    //输出为: 3 1 5 7 6 4 (报错)
    //原理:在异步代码中,如果一个任务执行失败,不会立即抛出异常,而是会被放到任务队列中等待执行。因此,如果在异步代码中使用了 return 语句返回一个错误,这个错误会被放到任务队列中等待执行,直到任务队列中的所有任务都执行完成,才会抛出异常。因此,如果你在 await 后面的函数中返回了一个错误,那么这个错误不会立即抛出异常,而是会在异步代码中执行完成后才被抛出异常。因为async await 的存在,将异步请求等同于变成了同步,函数里面代码是一步一步执行的,前面报错,后面代码将不会执行,于是console.log(2)不会执行
    
  • 面试题(DOM操作)

    document.addEventListener('click', () => {
      let p = new Promise(resolve => resolve(1))
      p.then(result => console.log(result)) //微任务
      console.log(2) 
    })
    document.addEventListener('click', () => {
      let p = new Promise(resolve => resolve(3))
      p.then(result => console.log(result))
      console.log(4)
    })
    
    //正确答案 2 1 4 3
    //原理:给同一元素添加相同的事件监听,会按照添加的顺序执行,因此两个宏任务会依次执行,宏任务执行机制是,先等第一个任务的同步任务和微任务全部执行完,才会执行下一个宏任务,于是会输出2 1 4 3而不是2 4 1 3