进阶学习6:JavaScript异步编程——Promise、链式调用、异常处理、静态方法、并行执行、执行时序、宏任务微任务理解

目录

五、Promise

1.前言

2.概念

3.基本用法

4.使用Promise封装ajax函数

5.Promise链式调用

常见误区

理解promise的then方法

链式调用样例 

链式调用总结

6.Promise异常处理

7.Promise静态方法

8.Promise 并行执行

Promise.all() 

Promise.race()

9.Promise执行时序 宏任务/微任务


五、Promise

1.前言

回调函数是异步模式的根基,但是如果我们直接使用传统的回调方式去完成复杂的异步流程,就无法避免大量回调函数嵌套,就会变成回调地域的问题。于是最早CommonJS社区提出了Promise的规范,后来ES2015中杯标准化,成为一种语言规范,

2.概念

所谓Promise就是一个对象,用来去表示一个异步任务最终在结束过后是成功还是失败。在状态明确过后(就是成功或者失败后)都会有对应的任务自动被执行。一旦明确结果之后,再也不会改变。

3.基本用法

//DEMO1
// Promise 基本演示
const promise = new Promise(function(resolve, reject){
    // 这里属于兑现承诺
    // resolve(100) //承诺达成
    reject('promise rejected')//承诺失败
})

promise.then(function (value) {
    console.log('resolved', value)
},function (error) {
    console.log('rejected', error)
})

console.log('end')

输出结果,可以看到promise里的部分再end输出之后才执行。 

4.使用Promise封装ajax函数

首先先把环境搞起来,老师大概默认大家都用过webpack了_(:з」∠)_中间真的搞了我好一会才终于跑成功。

npm install webpack -g

npm install webpack-dev-server -g

npm install webpack-cli@3 -g

npm install html-webpack-plugin

然后添加一个webpack.config.js文件夹,项目结构如下:

webpack.config.js中的内容:

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  mode: 'none',
  stats: 'none',
  devtool: 'source-map',
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

然后api文件夹里面创建一个users.json,用来模拟api数据

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  mode: 'none',
  stats: 'none',
  devtool: 'source-map',
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

输入webpack-dev-server Promise.js运行项目

具体Promise.js里面写的就是我们封装ajax函数的代码,成功的话返回json里面的内容

//DEMO2
// Promise 使用案例 封装Ajax函数
function ajax (url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function () {
            if(this.status == 200){
                resolve(this.response)
            }else{
                reject(new Error(this.statusText))
            }
        }
        xhr.send()
    })
}

ajax('/api/users.json').then(function(res){
    console.log(res)
},function(error){
    console.log(error)
})
console.log('END')

运行结果: 

假如我们将地址改为一个没有的文件

ajax('/api/nothing.json').then(function(res){
    console.log(res)
},function(error){
    console.log(error)
})
console.log('END')

就会打印一个Error: Not Found

5.Promise链式调用

常见误区

嵌套使用的方式是使用Promise最常见的误区,最好是借助Promise then方法的链式调用特点,尽量保证异步任务扁平化。

理解promise的then方法

//DEMO2
// Promise 使用案例 封装Ajax函数
function ajax (url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest()
        xhr.open('GET', url)
        xhr.responseType = 'json'
        xhr.onload = function () {
            if(this.status == 200){
                resolve(this.response)
            }else{
                reject(new Error(this.statusText))
            }
        }
        xhr.send()
    })
}

// ajax('/api/nothing.json').then(function(res){
//     console.log(res)
// },function(error){
//     console.log(error)
// })
console.log('END')

let promise = ajax('/api/users.json')
let promise2 = promise.then(
    function onFulfilled(res){
        console.log('onFulfilled', res)
    },function onRejected(error){
        console.log('onRejected', error)
    }
)
console.log('promise',promise)
console.log('promise2',promise2)
console.log(promise === promise2)

在上面的代码基础上修改看一下,可以看到其实我们的promise2,也就是promise的then方法返回的是一个全新的Promise对象

链式调用样例 

每个then方法实际上都是为上一个then方法返回的Promise对象去添加状态明确过后的回调,这些会从前到后依次执行。这样可以用链式调用的方式避免嵌套。

ajax('/api/users.json')
.then(function(value){
    console.log(1)
    return value
})
.then(function(value){
    console.log(2)
    console.log(value)
    return 'foo'
})
.then(function(value){
    console.log(3)    
    console.log(value)

})
.then(function(value){
    console.log(4)
    console.log(value)
})

链式调用总结

  • promise对象then方法,返回了全新的promise对象。可以再继续调用then方法,如果return的不是promise对象,而是一个值,那么这个值会作为resolve的值传递,如果没有值,默认是undefined
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

6.Promise异常处理

.catch()(推荐)

promise中如果有异常,都会调用reject方法,还可以使用.catch()

使用.catch方法更为常见,因为更加符合链式调用

//DEMO4 Promise异常处理
ajax('/api/users.json').then(
    function onFulfilled(res){
        console.log('onFulfilled', res)
    },function onRejected(error){
        console.log('onRejected', error)
    }
)
//用catch方法更常见,更适合链式调用
//表现上来看,效果是一样的,但是仔细对比差异还是很大的,catch其实是给前面的then指定失败的回调,而不是直接给第一个promise
//而上面那种onRejected只是给第一个promise的异常
ajax('/api/users.json').then(function onFulfilled(value) {
    console.log('onFulfilled',value)
})
.catch(function onRejected (error){
    console.log('onRejected',error)
})

.catch形式和前面then里面的第二个参数的形式,两者异常捕获的区别:

  • .catch()是对上一个.then()返回的promise进行处理,不过第一个promise的报错也顺延到了catch中,而then的第二个参数形式,只能捕获第一个promise的报错,如果当前then的resolve函数处理中有报错是捕获不到的。

所以.catch是给整个promise链条注册的一个失败回调。推荐使用。

对上面代码稍微做了下修改,在then中添加了异常。

//DEMO4 Promise异常处理
ajax('/api/users.json').then(
    function onFulfilled(res){
        console.log('onFulfilled1', res)
        return ajax('error-url')
    },function onRejected(error){
        console.log('onRejected1', error)
    }
)
//用catch方法更常见,更适合链式调用
//表现上来看,效果是一样的,但是仔细对比差异还是很大的,catch其实是给前面的then指定失败的回调,而不是直接给第一个promise
//而上面那种onRejected只是给第一个promise的异常
ajax('/api/users.json').then(function onFulfilled(value) {
    console.log('onFulfilled2',value)
    return ajax('error-url')
})
.catch(function onRejected (error){
    console.log('onRejected2',error)
})

从打印的结果里面可以看到,第一种方式then中的异常是没有被捕获到的,而第二种方式catch捕获到了

此外,还可以在全局对象上注册一个unhandledrejection事件,处理那些代码中没有被手动捕获的promise异常,当然并不推荐使用

更合理的是:在代码中明确捕获每一个可能的异常,而不是丢给全局处理

// 浏览器
window.addEventListener('unhandledrejection', event => {
  const { reason, promise } = event
  console.log(reason, promise)

  //reason => Promise 失败原因,一般是一个错误对象
  //promise => 出现异常的Promise对象

  event.preventDefault()
}, false)

// node
process.on('unhandledRejection', (reason, promise) => {
  console.log(reason, promise)

  //reason => Promise 失败原因,一般是一个错误对象
  //promise => 出现异常的Promise对象
})

7.Promise静态方法

Promise.resolve()

//DEMO5 Promise.resolve()
Promise.resolve('foo')

new Promise(function(resolve, reject){
    resolve('foo')
})
//上面两个是相同的
var promise = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
console.log(promise === promise2) //===>true
// 这也是想等的

Promise.resolve({
    then: function (onFulfilled, onRejected) {
        onFulfilled('foo')
    }
}).then(function(value){
    console.log(value)
})

Promise.reject(new Error('rejected')).catch(function(error){
    console.log(error)
})

输出结果

8.Promise 并行执行

比如一个页面同时请求几个接口

这时候,在我的代码当中再加一个posts.json文件和urls.json作为第二个接口,项目结构如图

posts.json:

[
  {
    "title": "Hello world",
    "body": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
  },
  {
    "title": "25 forever",
    "body": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
  }
]

urls.json

{
    "users": "/api/users.json",
    "posts": "/api/posts.json"
}

Promise.all() 

//DEMO6 并行
//Promise.all方法接受的是一个数组,数组中每一个元素都是一个Promise对象
var promise = Promise.all([
    ajax('/api/users.json'),
    ajax('/api/posts.json'),
])
//只有里面的Promise.all里面的对象一个个全部完成之后,promise才会完成,才会调用then,如果其中有一个任务失败了,那么promise也失败会调用catch
promise.then(function (values) {
    console.log(values)
}).catch(function (error){
    console.log(error)
})
//返回的结果是两次的结果的array

ajax('/api/urls.json')
  .then(value => {
    const urls = Object.values(value)
    const tasks = urls.map(url => ajax(url))
    return Promise.all(tasks)
  })
  .then(values => {
    console.log(values)
  })


Promise.race()

只会等待第一个结束的任务,其他就失败了

//DEMO7 并行 race()
// Promise.race 实现超时控制
const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('timeout')), 500)
})

Promise.race([
  request,
  timeout
])
.then(value => {
  console.log(value)
})
.catch(error => {
  console.log(error)
})

9.Promise执行时序 宏任务/微任务

首先我们运行一下以下代码

//DEMO8
// 微任务
console.log('global start')
Promise.resolve()
  .then(() => {
    console.log('promise')
  })
  .then(() => {
    console.log('promise 2')
  })
  .then(() => {
    console.log('promise 3')
  })
console.log('global end')

输出结果应该和之前的理解没有什么问题

于是我们再加上setTimeout

//DEMO8
// 微任务
console.log('global start')
// setTimeout 的回调是 宏任务,进入回调队列排队
setTimeout(() => {
  console.log('setTimeout')
}, 0)
// Promise 的回调是 微任务,本轮调用末尾直接执行
Promise.resolve()
  .then(() => {
    console.log('promise')
  })
  .then(() => {
    console.log('promise 2')
  })
  .then(() => {
    console.log('promise 3')
  })
console.log('global end')

和之前想象中的结果不太一样,按照之前学习的逻辑setTimeout 应该是会在promise之前执行,但是实际上setTimeout在最后。

在解释为什么之前,再引入两个概念

宏任务也就是回调队列里面的任务,宏任务执行过程中可以临时加上一些额外的需求,这些临时的需求可以选择作为一个新的宏任务进队列中排队。也可以作为微任务,直接在当前任务结束过后立即执行。

Promise的回调会作为微任务执行,而SetTimeout的回调函数则是作为宏任务重新排队。

目前绝大部分异步调用都是作为宏任务执行的,而Promise和MutationObserver还有process.nextTick都会作为微任务在本轮的末尾直接就执行了。

其他参考资料:

1.拉勾网 《大前端训练营》课程

  • 4
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值