深入剖析Promise对象

概述

  • 单线程 js 异步方案

单线程模式工作的原因

  • 最早js这门语言就是运行在浏览器端的脚本语言,目的是为了实现页面上的动态交互,而实现页面交互的核心就是dom操作,这也就决定了他必须使用单线程模型,否则会出现很复杂的线程同步问题
  • 我们可以试想我们在js当中同时有多个线程一起工作,其中有一个线程修改了某一个dom,而另外一个线程同时删除了这个元素,此时我们的浏览器就无法确定以那一个线程的工作结果为准,所以为了避免这种线程同步的问题,从一开始js就被设计为了单线程模式工作,这也就成为了这门语言最核心的特性之一
  • 这里所说的单线程就是在js的执行环境中负责执行代码的线程只有一个,可以想象为,在我们的内部,只有一个人对我们的代码去执行任务,因为只有一个人,所以同时只能执行一个任务,如果说有多个任务的话就必须要排队,依次完成
  • 这种模式最大的优点就是:更安全、更简单,缺点也同样很明显,如果说我们遇到某一个特别耗时的任务,后边的任务就必须要去排队等待这个任务的结束,这也就导致我们整个程序的执行会被拖延,出现假死的情况
  • 为了解决耗时拖延假死这种问题,js将任务的执行模式分为了两种
    • 同步(synchronous)
    • 异步(asynchronous)
  • 我们接下来要学习与一部相关的内容,主要由以下几点
    • 同步模式与异步模式
      • 在表象上的差异
      • 存在的意义
      • js单线程如何实现的异步模式
    • 事件循环与消息队列
    • 异步编程的几种方式
    • Promise异步方案、宏任务/微任务队列
    • Generator异步方案、Async/Await语法糖

同步模式(Synchronous)

  • 指的就是我们代码当中的任务依次执行,后一个任务必须等待前一个任务结束才能执行,程序执行的顺序与我们代码的书写顺序完全一致,也就是这种方式会比较简单
  • 在单线程模式下,我们大多数任务会以同步模式去运行
  • 示例
console.log('global begin')

function bar () {
    console.log('bar task')
}

function foo () {
    console.log('foo task')
}

console.log('global end')
  • 这是一个纯同步模式下的执行情况,所以特别容易理解,因为他整个执行过程非常符合我们的阅读逻辑或者思考逻辑
  • 但是这种排队执行的机制,存在一个很严重的问题,如果其中某一个任务或者具体点说就是其中的某一行代码,执行的时间过长,那他后边的任务就会被延迟,这种延迟我们一般称之为阻塞,这种阻塞对于用户而言就意味着界面会卡顿,或者卡死,所以必须要有异步模式来解决我们程序当中那些无法避免的耗时操作
  • 例如我们在浏览器端的ajax操作,或者在node.js当中的大文件读写,都会需要使用到异步模式去执行,从而避免我们的代码被卡死

异步模式(Asynchronous)

  • 不同于同步模式的执行方式,异步模式的API是不会等待这个任务的结束才会执行下一个任务,对于耗时操作,都是开启过后就立即往后执行下一个任务
  • 耗时任务的后续逻辑,我们会通过回调函数的方式定义,在内部我们这个耗时任务完成过后就会自动执行我们这里传入的回调函数
  • 异步模式对于js非常重要,如果没有这种模式的话,我们单线程的js语言就无法同时处理大量耗时任务
  • 对于开发者而言,单线程模式下面的异步,他最大的难点就是代码执行的顺序混乱,不会像同步代码一样通俗易懂,因为它的执行顺序相对的会比较跳跃,对于这个问题更多的是要理解和习惯,最好的办法就是:多看、多练、多思考
  • 示例
console.log('global begin')

setTimeout(function timer1 () {
    console.log('timer1 invoke)
}, 1800)

setTimeout(function timer2 () {
    console.log('timer2 invoke')

    setTimeout(function inner () {
        console.log('inner invoke')
    }, 1000)
}, 1000)

console.log('global end')
  • 整个过程是通过内部的消息队列和事件循环去实现的
  • 在js中线程的某一个时刻,发起了一个异步调用,然后紧接着继续往后执行其它任务
  • 此时异步调用线程会单独执行异步任务,执行完之后会将这个任务的回调放入消息队列
  • js主线程完成所有的任务过后,会再依次执行我们消息队列里的任务
  • 特别注意:js他确实是单线程的,而我们的浏览器他并不是单线程的,更具体一点来说,我们通过js调用的某些内部API他并不是单线程的
  • 例如:倒计时器 — 它内部就会有一个单独的线程负责倒计时,在时间到了之后会将我们的回调放入消息队列
  • 也就是说,这样的事情,他是有一个单独的线程去做的,我们所说的单线程指的是执行我们代码的那个线程
  • 也就是说,内部的API他们会用单独的线程去执行这些等待的操作
  • 除此以外,我们这里说的同步也好异步也好,肯定不是指我们写代码的方式,而是说我们运行环境提供的API到底是以同步模式还是异步模式的方式工作
    • 对于同步模式的API,它的特点就是执行完这个任务才会继续往下走,例如console.log()
    • 对于异步模式的API,他就是下达任务开启的指令就会继续往下执行,代码是不会在这一行等待任务结束的,例如setTimeout()

回调函数

  • 所有异步编程的根基
  • 可以理解为我想要做的事情,我明确的知道这件事情我应该怎么做,但是我并不知道这件事情所依赖的任务什么时候才能完成,所以说最好的办法就是,把这件事情的步骤写到一个函数当中,交给任务的执行者,那这个任务的执行者,他是知道这个任务什么时候结束的,他就可以在任务结束过后帮你去执行,你想要做的事情,那这个想要做的事情其实就可以理解为回调函数
    • 这么说可能会比较抽象,具体一点:比如说我想给我的桌子重新刷一遍漆,我已经明确知道我应该怎么去刷,但是我没有油漆,我需要让你帮我去买一桶油漆,那你去买油漆肯定是需要一定时间的,而我又会有其他的事情要做,所以我不能一直等着你,那我会选择把这个桌子应该怎么刷的步骤写到一个纸条上,然后一起交给你,过后我就去忙别的事,你买完油漆回来就可以按照我纸条上的步骤给桌子刷漆
  • 这个例子当中我其实就相当于异步任务的调用者,而你就相当于具体的异步任务执行者,我给你的纸条就相当于我这个调用者所定义的回调函数
  • 那我们在拿程序当中的ajax请求为例:当我们调用ajax操作,目的就是为了拿到请求结果过后去做一些事情,例如我们将其显示到界面上,但是这个请求什么时候能完成我们并不知道,所以说我们需要把得到结果要去执行的任务定义到一个函数中,然后内部的ajax请求到数据过后,它会自动执行这个任务
  • 这种由调用者定义,交给执行者执行的函数,称之为回调函数
  • 具体的用法也很简单,就是把函数作为参数传递罢了
  • 示例
function foo (callback) {
    setTimeout(function () {
        callback()
    }, 3000)
}

foo(function () {
    console.log('这就是一个回调函数')
    console.log('调用者定义这个函数,执行者执行这个函数')
    console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})
  • 只不过这种方式的代码,相对来讲特别不利于阅读,而且整个过程执行顺序会非常混乱

Promise

  • 一种更优的异步编程统一方案
  • 回调函数可以说是js所有异步编程的根基,但是我们直接使用去完成复杂的异步流程就无法避免大量的回调函数嵌套,这也就会导致我们常说的回调地狱问题
  • 为了避免回调地狱的问题 CommonJS 社区提出了Promise 的规范
    • 目的就是为了异步编程去提供一种更合理更强大的统一解决方案,在ES5中被标准化,成为语言规范
  • 所谓promise实际上就是一个对象,用来去表示一个异步任务,最终结束过后,他是成功还是失败
    • 就像是内部对外界做出一个承诺,一开始这个承诺是一个待定的状态 Pending,最后有可能成功 fulfilled,也有可能失败 Rejected
  • 承诺状态明确过后,不管是成功还是失败,都会有相对应的任务会被自动执行,而且这种承诺会有很明显的特点,一点明确了结果过后,不可更改
  • 例如:你需要我去帮你发送一次ajax请求,其实就可以理解为,我承诺帮你请求一个地址,这个请求有可能成功,然后调用 onFulfiled 的回调,如果失败就会调用 onRejected 的回调

Promise的基本用法

const promise = new Promise(function (resolve, reject) {
    // "兑现" 承诺的逻辑
    // resolve(100)    // 承诺达成

    reject(new Error('promise rejected'))   // 承诺失败
})

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

console.log('end')

Promise使用案例

function ajax (url) {
    return new Promise (function (resolve, reject) {
        let 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)
})

Promise常见误区

  • 嵌套使用的方式是使用promise最常见的错误,正确的做法是借助于 promise then 方法链式调用的特点的特点尽量去保证我们异步任务的扁平化

Promise链式调用

  • 其实Promise最大的优势就是可以链式调用,这样就能最大程度的去避免回调嵌套
  • 每一个then方法他实际上都是在为上一个then返回的promise对象添加状态明确过后的回调,那这些promise会依次执行,这里添加的这些回调函数也就是从前到后依次执行
  • 示例
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

function ajax (url) {
    return new Promise (function (resolve, reject) {
        let 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()
    })
}

// var promise = ajax('/api/users.json')

// var pro = promise.then(
//     function onFulfilled (value) {
//         console.log('onFulfiled', value)
//     },
//     function onRejected (error) {
//         console.log('onRejected', error)
//     }
// )

// console.log(pro === promise)

ajax('api/users.json')
    .then(function (value) {
        console.log(11111)
        /**
         * 这里手动return一个ajax后实际上下一个then方法,自动回到这里来
         * 
         * 这样就可以比避免一些不必要的回调嵌套了
         * 而且以此类推,如果说有多个连续的任务,我们就可以使用这种链式调用的方式,去避免回调的嵌套
         * 
         * 尽量的保证我们代码的扁平化
         * 
         * 如果说我们的回调当中返回的不是一个promise,而是一个普通的值
         * 那这个值,会作为当前的then方法,返回一个promise中的值,在下一个then方法中
         * 我们接受的这个回调参数他实际上拿到的就是这样一个值
         * 
         * 如果我们的回调当中,没有返回任何的值,默认返回的就应该是一个undefined
         * 
         * 
         * */ 
        return ajax('api/urls.json')
    })
    .then(function (value) {
        console.log(22222)
    })
    .then(function (value) {
        console.log(33333)
    })
    .then(function (value) {
        console.log(44444)
    })
    .then(function (value) {
        console.log(55555)
    })
  • 我们也可以在then的回调当中,手动返回一个promise对象
  • 总结
    • Promise对象的then方法会返回一个全新的promise对象
    • 后面的then方法就是在为上一个then返回的promise注册回调
    • 前面then方法中回调函数的返回值会作为后面的then方法回调的参数
    • 如果回调中返回的是promise,那么后面的then方法的回调会等待它的结束

Promise 异常处理

  • Promise执行失败,会返回onRejected这个回调函数
  • 如果是在Promise执行的过程中,出现了异常,或者是我们手动抛出了一个异常,那onRejected也会被执行
  • 所以onRejected回调,它实际上就是为Promise当中的异常,去做一些处理,在Promise失败了,或者出现异常时,他都会被执行
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

function ajax (url) {
    return new Promise (function (resolve, reject) {
        // foo()    // (node:7960) UnhandledPromiseRejectionWarning: ReferenceError: foo is not defined
        // throw new Error()   // (node:11528) UnhandledPromiseRejectionWarning: Error
        let 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 onFulfilled (value) {
        console.log('onFulfiled', value)
    },
    function onRejected (error) {
        console.log('onRejected', error)
    }
)
  • 我们也可以使用promise的cache方法来注册onRejected
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;

function ajax (url) {
    return new Promise (function (resolve, reject) {
        let 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 onFulfilled (value) {
        console.log('onFulfiled', value)
    })
    .catch(function onRejected (error) {
        console.log('onRejected', error)
    })
  • cache方法其实就是then方法的一个别名,因为我们调用它其实就相当于调用了then方法,我们第一个参数传递了一个undefined,相对来说用cache方法来指定失败回调更为常见一些,因为这种方式会更适合于链式调用,具体原因我们从下边代码聊起

......

ajax('/api/users.json').then(
    function onFulfilled (value) {
        console.log('onFulfiled', value)
    },
    function onRejected (error) {
        console.log('onRejected', error)
    }
)

ajax('/api/users.json')
    .then(function onFulfilled (value) {
        console.log('onFulfiled', value)
    })
    .catch(function onRejected (error) {
        console.log('onRejected', error)
    })
  • 从表象上看,catch和then都是一样的,都能够捕获到promise在执行中的异常
  • 但是差异并不是没有
    • 每一个then返回的都是全新的promise对象,我们在后边通过链式调用的方式调用的cache,它实际上是在给前边then方法返回的promise对象去指定失败的回调,并不是直接去给第一个promise对象所指定的,只不过因为这是同一个promise链条,所以前边promise上的异常,会一直被往后传递,所以我们在这里才能够捕获到第一个promise的异常
    • 而通过then方法的第二个参数去指定的失败回调函数,那他只是给第一个promise对象指定的,也就是说他只能够捕获到这个promise对象的异常
    • 具体在表象上的差异就是,如果我们在then方法当中返回了第二个promise而且这个promise在执行过程当中,出现了异常,那我们使用then的第二个参数注册的失败会掉它是捕获不到第二个promise的异常的,因为它只是给第一个promise对象注册的失败回调
  • 具体我们可以做以下尝试
ajax('/api/users.json').then(
    function onFulfilled (value) {
        console.log('onFulfiled', value)
        return ajax('/errerrerr')   // 捕获不到
    },
    function onRejected (error) {
        console.log('onRejected', error)
    }
)

ajax('/api/users.json')
    .then(function onFulfilled (value) {
        console.log('onFulfiled', value)
        return ajax('/errerrerr')   // 正常捕获
    })
    .catch(function onRejected (error) {
        console.log('onRejected', error)
    })
  • 原因就是cache的失败回调是注册在then方法返回的promise上,那这个对象是失败的,所以他能捕获到
  • 所以在链式调用下,我们推荐使用cache去分开指定成功与失败的回调
  • 因为promise链条上,任何一个异常,都会被向后传递,直至被捕获,也就是说,这种方式更像是给整个promise链条注册的失败回调,所以它相对来说他更痛有一些
  • 除此之外,我们也可以在全局对象上去注册一个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 对象
})
  • 但是在全局中捕获是不太合适的,更合适的做法应该是在代码中明确捕获每一个可能的异常,而不是丢给全局统一处理

Promise的静态方法

  • Promise.resolve()
    • 快速的把一个值转换为一个promise对象
Promise.resolve('foo')
    .then(function (value) {
        console.log(value)
    })

new Promise(function (resolve, reject) {
    resolve('foo')
})

var promise = ajax('/api/user.json')
var promise2 = Promise.resolve(promise)

console.log(promise2 === promise)   // ture
/**
 * 会返回一个状态为onFulfilled的状态对象,也就是成功的状态对象
 * 这里的foo字符串,会作为这个promise所返回的值
 * 也就是说在他的onFulfilled得回调当中,拿到的参数就是foo这样的字符串
 * 
 * 这种方式完全等价于通过 new Promise对象的这种方式
 * 然后我们在执行函数当中直接resolve这个字符串
 * 
 * 另外,这个方法如果接受到的是另一个promise对象,那这个promise对象会被原样返回
 * */ 
  • Promise.reject()
    • 快速的创建一个一定失败的promise对象
// 快速的创建一个一定失败的promise对象
Promise.reject(new Error('rejected'))
    .catch(function (err) {
        console.log(err)
    })

Promise并行处理

Promise.all()

  • 前边介绍的操作都是通过 promise 去串联执行多个异步任务,也就是一个任务结束过后再去开启下一个任务
  • 相比于传统回调的方式,promise 提供了更扁平的异步编程体验,如果我们需要同时并行执行多个异步任务,promise 也可以提供更为完善的体验
    • 例如我们经常要在一个页面中请求多个接口,如果这几个需求之间没有相互的依赖,我们最好是同时去请求,避免我们依次请求消耗更多的时间
    • 这种并行请求其实很容易去实现,我们只需要单独去调用 ajax 函数即可,但是如何判断所有请求都已经结束的时机
    • 传统做法是去定义一个计数器,没结束一次请求让这个计数器去累加一次,直到计数器数量和任务数量相等时就表示所有任务都结束了
    • 这种方法会异常的麻烦,而且还要考虑出现异常的情况
    • 这是我们如果使用 promise 的 all() 就会方便很多
      • 该方法可以将多个 promise 合并为一个 promise 统一去管理
  • 示例
var promise = Promise.all([
    ajax('/api/users.json'),
    ajax('/api/users.json')
])

promise.then(function (values) {
    console.log(values)
}).catch(function (error) {
    console.log(error)
})
  • 注意:
    • 在这个任务当中,只有在两个任务都成功请求结束了,我们这里 Promise 才会成功结束,如果说其中有任何一个任务失败,这个 Promise 就会以失败结束
    • 这是一种很好的同步执行多个 Promise 的方式,这里我们可以在综合串联和并行的两种方式
    • Promise.all() 等待所有任务结束才结束

Promise.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)
})

Promise执行时序 / 宏任务 vs 微任务

  • 正如我们一开始所介绍到的,即便说我们 Promise 当中并没有任何的异步操作,那他的回调函数仍然会进入到回调队列当中去排队,也就是说我们必须要等待当前所有的同步代码执行完成,过后才会去执行 Promise 当中的回调,当然,这句话其实不是特别的严谨,我们可以通过代码来看一下
console.log('global start')

Promise.resolve()
    .then(() => {
        console.log('promise')
    })

console.log('global end')

/**
 * global start
 * global end
 * promise
 * */ 

//  ==========================================

// console.log('global start')

Promise.resolve()
    .then(() => {
        console.log('promise 1')
    })
    .then(() => {
        console.log('promise 2')
    })
    .then(() => {
        console.log('promise 3')
    })
    .then(() => {
        console.log('promise 4')
    })

console.log('global end')

/**
 * global start
 * global end
 * promise 1
 * promise 2
 * promise 3
 * promise 4
 * */ 

 //  ==========================================

console.log('global start')

setTimeout(() => {
    console.log('setTimeout')
}, 0)

Promise.resolve()
    .then(() => {
        console.log('promise 1')
    })
    .then(() => {
        console.log('promise 2')
    })
    .then(() => {
        console.log('promise 3')
    })
    .then(() => {
        console.log('promise 4')
    })

console.log('global end')

/**
 * global start
 * global end
 * promise 1
 * promise 2
 * promise 3
 * promise 4
 * setTimeout
 * */ 
  • 按照我们之前对js中异步执行过程中的分析
  • 我们使用的setTimeout() 传入的回调函数,他会立即进入到回调队列当中去排队,因为延迟时间是0
  • 进入到回调队列过后就会等待下一轮的执行
  • 此时按照之前的分析,setTimeout应该是先进入回调队列当中,然后才是 Promise
  • 所以应该先打印setTimeout,然后才是 promise ,但是实际上打印的顺序是promise在前,setTimeout在后
  • 原因在于 promise 的异步执行时序会有点特殊,在搞明白之前我们先看一个生活中的一个场景
  • 假设我现在去银行柜台办理存款业务,办理完成后我突然又想在办理一个信用卡,这个时候我肯定直接告诉银行柜员我的需求,银行柜员为了提高效率,以及客户的体验,他肯定不会让我重新去排队,他如果能够帮我办理的话,肯定就一起帮我办理了,这种行为其实并不属于插队,只是我在完成主要任务过后,临时多了一些小需求,这个例子中,我在柜台排队办理业务,就像是Js中回调队列等待执行的那些任务一样,那我们队伍当中的每一个人都对应着回调队列当中的一个任务
  • 也有很多人会把这种回调队列中的任务称之为宏任务,其实就是一个说法而已
  • 而宏任务的执行过程当中,有可能会临时加上一些额外的需求,对于这些额外的任务可以选择作为新的宏任务进入到队列中排队
  • 例如我们使用的setTimeout,也可以选择像刚刚办理信用卡那样,作为当前任务的微任务,这也是我们在js这个领域里边的一个说法
  • 其实就是直接在我当前任务结束过后立即执行,而不是到整个队伍的末尾再重新排队,这就是宏任务和微任务之间的差异
  • promise就是作为微任务执行的,所以他会在本轮调用的末尾去自动执行,这也是为什么先打印 promise 后打印 setTimeout 的原因
  • 微任务的概念其实是后来才被引入到js当中的,它的目的就是提高我们应用的响应能力
  • 就像我们在生活中,这个柜台只允许我们重新排队,不允许我们在办理过程当中加一些新需求的话对于我们的效率其实会有一个大大的降低
  • 目前绝大多数异步调用都是作为宏任务执行,而 promise & MutationObserver & node 中的 process.nextTick 的对象
  • 他们都会作为微任务,直接在本轮调用的末尾执行
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值