promise的深入理解

回调为什么不能完美完成异步

  1. 复杂的函数追踪与我们大脑平时的思考方式不一致
scss 复制代码
AudioListener('click',function handler(evt){
    setTimeout(() => {
        ajax('http://some.url',function res(text){
            if(text = 'text'){
                text()
            }
        })
    }, 500);
})

这段代码我们很容易想到是先执行click函数,进而调用定时器,再发送ajax请求。但其实这是一种十分偶然的情况,异步的过程中会有相当多的噪音,我们需要在这几个函数之间跳来跳去

  1. 信任问题
javascript 复制代码
//A
ajax("...",function(){})
//B

我们知道A和B发生在现在,并且是在JavaScript主程序的控制下,而//C则会延迟到将来发生,并且是在第三方的控制下(在本例中是ajax,即当ajax收到浏览器返回的请求时,就会通知JavaScript引擎执行C代码)。它有一个这样的思路:有时候ajax(也就是交付你回调的第三方)不是你编写的代码,也不在你的直接控制之下。多数情况下是个第三方提供的工具。

我们把这称为控制反转,也就是把自己程序一部分的执行权交给某个第三方。这就是回调的最大问题,它会导致信任链的完全断裂

  • 多次调用时的信任问题

  • 当通过ajax多次发送请求时,由于response(data)一直可以变化,导致我们可能同时接收到成功和失败的结果

  • 当完全没有调用这个回调函数的信任问题 当ajax一直没有接收到值时,回调一直没有办法被调用。可以通过设置计时器的超时来取消这个事件

  • 调用过早或过晚导致的信任问题

javascript 复制代码
function result(data){
    console.log(a);
}
var a = 0
ajax("..",result)
a++

这段代码可能输出0(同步回调调用,可以通过async设置)或者1(异步回调调用)。

总结:promise信任问题

  • 调用回调过早
  • 调用回调过晚或者不调用
  • 调用回调次数过多或者过少
  • 未能传递所需的环境和参数
  • 吞掉可能出现的错误和异常

promise解决信任问题

1.调用过早

造成这个问题的原因,一般是一个任务可能同步完成,可能异步完成,即ajax收到数据之后,就会让ja主引擎执行代码,导致可能某个重要的关键步骤还没执行。但promise就不必担心这个问题,因为即使是立刻完成的promise,也只是会将其加入任务队列,再通过事件循环,先执行完所有同步任务再来执行异步任务

2.调用过晚

一旦promise的状态发生了改变,对应的回调就会被加到任务队列中,这里的回调一定会通过事件循环机制触发,并且这些回调不会影响或者延误其他回调的调用

javascript 复制代码
p.then(function(){
    p.then(function(){
        console.log('c');
    })
    console.log('a');
})
p.then(function(){
    console.log('b');
})
//a b c

这里的c不会打断或抢占b,这就是promise的运作方式

javascript 复制代码
var p3 = new Promise((resolve, reject) => {
    resolve('B')
})

var p1 = new Promise((resolve, reject) => {
    resolve(p3)
})

p2 = new Promise((resolve, reject) => {
    resolve('A ')
})

p1.then(function(v){
    console.log(v);
})

p2.then(function(v){
    console.log(v);
})
//A B

这里不是B A的原因是,p1不是用立即值而是用另一个promise决议,后者本身决议值为B。规定的行为是将p3展开到p1,这是一个异步的展开,也就是resolve(p3)会进入任务队列,因此p1的then回调排在p2的后面

3.回调未调用

首先,没有任何东西(包括JavaScript错误)能阻止promise向你通知它的决议。如果你对promise注册了一个完成回调和一个拒绝回调,那么promise决议时一定会调用其中一个

但如果promise本身永远不会被决议呢?promise也提供了解决方案,那就是一种叫做竞态的高级抽象机制

javascript 复制代码
function timeoutPromise(delay){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject('timeout')
        }, delay);
    })
}

Promise.race([
    foo(),
    timeoutPromise(3000)
]).then(
    function(){
        //foo及时完成
    },
    function(err){
        //或者foo被拒绝,或者只是没有按时完成
        //查看err是哪种情况
    }
)

4.调用次数过多或者过少的问题

一般正确次数是1。过少则是调用0次,和之前说的未调用的情况是一样。

过多的情况则很容易解释。promise只能决议一次,后续对他状态的改变都是无用的,由于只能被决议一次,那么通过then注册的回调自然只能执行一次

5.未能传递参数/环境值

promise至多只能有一个决议值,如果没有用任何值显式决议,那么这个值就是undefined,并且无论这个值是什么,都会传递给注册的回调 并且resolve()和reject()只能有一个参数,当传递多个参数时,第一个参数之后的参数都会被忽略

6.吞掉错误和异常

如果promise在创建过程或者决议过程中的任何一个时间出现JavaScript异常错误,那么这个错误就会被捕捉,并且会使这个promise被拒绝

javascript 复制代码
var p = new Promise((resolve, reject) => {
    foo.bar() //foo未定义,所以会出错
    resolve(23) //永远不会到达这里
})

p.then(
    function fulfilled(){
        //永远不会到达这里
    },
    function rejected(){
        //err将会是一个TypeError异常对象来自foo.bar()这一行
    }
)

这里有效解决了链式回调的风险,链式回调可能出错了会引起同步响应,成功了才是异步响应。而promise会把JavaScript异常也变成了异步反应。那么如果是then中注册的回调报错了呢?

javascript 复制代码
var p = new Promise((resolve, reject) => {
    resolve(23) 
})

p.then(
    function fulfilled(){
        foo.bar()
        console.log('aaa');//永远不会到达这里
    },
    function rejected(){
        //永远不会到达这里
    }
)

看起来我们并没有侦听到这个错误,但实际上不是这样的,p.then()调用本身返回了另一个promise,正是这个promise会因TypeScript异常而被拒绝。为什么不直接调用我们定义的错误处理函数呢?因为这样违背了promise的基本原则,即promise一旦决议就不可再变,p的决议已经是resolve(23)了,再查看promise的决议时,不能因为处理函数出错了而调用rejected函数

是可信任的Promise吗?

我们会遇到无法识别调用的函数是否是Promise,还是只是一个thenable,因此我们可以使用Promise.resolve()将参数转化成一个真正的promise。如果向Promise.resolve()传递一个非promise对象,就会得到这个值填充的Promise对象,如果传递的是Promise对象,那么就返回这个Promise对象本身

javascript 复制代码
//不要这样做
foo(42)
.then(function(v){
    console.log(v);
})

//而是要这样做
Promise.resolve(foo(42))
.then(function(v){
    console.log(v);
})

链式流

我们可以将多个Promise连接到一起,以表达一系列异步步骤。这种方法可以实现的关键是因为以下两个Promise固有行为特征

  1. 每次对Promise调用then方法,它都会创建并返回一个新的Promise,我们可以将其链接起来
  2. 不管then方法中的回调返回的值是什么,它都会被自动设置为该then方法返回的Promise(第1点中的Promise)的状态

我们可以通过then中显示地返回Promis,控制每一步是异步还是同步

javascript 复制代码
var p = Promise.resolve(21)

p.then(function(v){
    console.log(v);
    
    return new Promise((resolve, reject) => {
        resolve(v * 2)
    })
})
.then(function(v){
    console.log(v);
})

错误处理

错误处理我们一般会想到try...catch,但是try...catch无法捕捉到异步代码的出错,它能捕捉到的异常必须是线程执行已经进入 try catch,但 try catch 未执行完的时候抛出来的 ,而异步代码是交由事件线程,当事件循环将异步事件交给js引擎时,大概率try catch已经执行完毕,因此无法处理异步代码。(参考trycatch 不能捕获运行时异常_面试官:用一句话描述 JS 异常是否能被 try catch 捕获到 ?... - 掘金 (juejin.cn)

Promise的错误处理采用了分离回调风格,一个回调用于完成情况,另一个用于拒绝情况

javascript 复制代码
var p = Promise.resolve(42)

p.then(function fulfilled(msg){
  // 数字没有String函数,所以会抛出错误
  console.log(msg.toLowerCase());
},function rejected(err){
  //永远不会到达这里
})

这段代码发生错误的时候我们没有通知,因为这个错误应该是由p.then()方法提供的,但是我们此例中没有捕捉。当然,我们在代码的最后加上.catch(handleErrors),但是如果传入的handleErrors也可能失败。任何Promise链的最后一步,不管是什么,总是存在着未被查看的Promise中出现未捕获错误的可能性

Promise模式

1.Promise.all([])

在promise链中,任意时刻都只能有一个异步任务正在进行,步骤2只能等待步骤1完成才能进行,但是如果要同时执行两个或更多步骤,就要用到这种机制了,门是要等待两个或更多并发/并行的任务都完成才能继续,在Promise API中,这种模式叫做Promise.all([])

javascript 复制代码
let p1 = request(url1)
let p2 = request(url2)

Promise.all([p1,p2])
.then(function (){},function (){})

Promise.all需要一个参数,是一个数组,可以是Promise、thenable或者立刻值,但是列表中的每一个值都需要通过Promise.resolve()过滤,如果数组是空的,那么Promise.all返回的主Promise就会立刻完成 。Promise.all返回的值是所有传入的Promise的完成消息组成的数组,与指定的顺序一致,和代码的完成顺序无关

Promise.all返回的主Promise在且仅在所有的成员Promise都完成后才会完成,如果这些Promise中有任何一个被拒绝,主Promise就会被立刻拒绝,并丢弃来自其他所有Promise的全部成果。因此要永远记住要为Promise.all返回的Promise关联一个拒绝处理函数

2.Promise.race([])

但有时候只想响应第一个跨过终点线的Promise,而抛弃其他Promise,这种模式叫做,在promise中被称为竞态

Promise.race接受单个数组参数,这个数组可以是一个或多个Promise、thenable或者立即值组成,但是立即值没有意义,因为显然列表中的第一个会获胜,就好像一个选手直接在终点开始比赛一样

与Promise.all类似,一旦有任何一个Promise决议为完成,Promise.race就会完成,一旦决议为拒绝,那么他就会拒绝

和Promise.all不同,如果传入的是一个空数组,Promise将永远不会决议,而不是立刻决议

并发迭代

有时候需要在一列Promise中迭代,并且对Promise都执行某个任务,非常类似于同步数组中的map,但是这些工具也可以有异步版本

javascript 复制代码
if(!Promise.map)(
    Promise.map = function(vals,cb){
        // 返回一个等待所有map的promise的新Promise
        // 其实就是通过Promise.all方法,将数组里的每一个值到执行一遍函数,cb是传入具体的函数操作d
        return Promise.all(
            vals.map(function(val){
                return new Promise(function(resolve){
                    cb(val,resolve)
                })
            })
        )
    }
)

var p1 = Promise.resolve(21)
var p1 = Promise.resolve(42)
var p1 = Promise.resolve('Ops')

Promise.map([p1,p2,p3],function(pr,done){
    Promise.resolve(pr) //将传进来的数组Promise化,确认是Promise对象
    .then(
        function(){
            function(v){
                done(v*2)
            }
        },
        done
    )
})
.then(
    function(vals){
        console.log(vals); // [42,84,"Oops"]
    }
)

Promise的局限性

1. 顺序错误处理问题

这等同于try catch存在的局限,在Promise链中,我们如果没有为then写上reject函数,则会在链上一直传播,即便最后加了catch(handleError),你也没有办法保证handleError没有错误

2.单一值

Promise只能有一个完成值或一个拒绝理由,在复杂的情景中,这是一种局限。当然,可以通过将返回的多个值封装在数组或者对象中,但更建议通过Promise.all的方式封装,符合'每个Promise一个值'的理念

3.单决议

Promise最本质的一个特征,就是只能被决议一次,但这样会造成问题

javascript 复制代码
 var p = new Promise((resolve, reject) => {
    click("#mybtn",resolve)
 })

 p.then(function(evt){
    var btnID = evt.currentTarget.id
    request(url + btnID)
 })
 .then(function(text){
    console.log(text);
})

这样的代码在点击第2次的时候就不会成功,因为Promise p已经被决议了,因此第二次的决议就会被忽略。因此我们要为每个事件的发生创建一整个新的Promise链

javascript 复制代码
click("#mybtn",function(evt){
    var btnID = evt.currentTarget.id
    request(url + btnID)
    .then(function(text){
        console.log(text);
    })
})

这种方法可以工作,因为针对这个按钮的每个点击事件都会启动一整个新的promise。然而在事件处理函数中定义整个promise链,这很丑陋

3.无法取消的Promise

一旦创建了Promise并为其注册了完成或者拒绝处理函数,如果出现某种情况使得这个任务悬而未决的话,你也没有办法从外部阻止他的进程

考虑到前面的Promise超时场景

less 复制代码
var p = foo(42)

Promise.race([
    p,
    timeoutPromise(3000)
])
.then(
    doSomeThing,
    handleError
)

p.then(function(){
    //即使在超时的情况下也会发生
})

这个'超时'相对于Promise p是外部的,所以p是本身还是会继续运行

单独的一个Promise并不是一个真正的流程控制机制,相比之下,集合在一起的Promise链才是一个流程控制的表达,因此取消定义在这个抽象层次上是更合适的,也正因为如此,单个Promise的取消总是让人很别扭

4.Promise的性能

把基于回调的异步任务链与Promise链中需要移动的部分数量进行比较,很显然,Promise进行的动作更多一些,比如任务队列,这自然会慢一点,但是牺牲的一点性能,Promise提供信任性,对回调地狱的避免以及可组合性,这是非常值得的。Promise很好,请使用他。❀❀❀❀

相关推荐
云草桑10 分钟前
逆向工程 反编译 C# net core
前端·c#·反编译·逆向工程
布丁椰奶冻16 分钟前
解决使用nvm管理node版本时提示npm下载失败的问题
前端·npm·node.js
Leyla42 分钟前
【代码重构】好的重构与坏的重构
前端
影子落人间1 小时前
已解决npm ERR! request to https://registry.npm.taobao.org/@vant%2farea-data failed
前端·npm·node.js
世俗ˊ1 小时前
CSS入门笔记
前端·css·笔记
子非鱼9211 小时前
【前端】ES6:Set与Map
前端·javascript·es6
6230_1 小时前
git使用“保姆级”教程1——简介及配置项设置
前端·git·学习·html·web3·学习方法·改行学it
想退休的搬砖人1 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js
加勒比海涛2 小时前
HTML 揭秘:HTML 编码快速入门
前端·html
啥子花道2 小时前
Vue3.4 中 v-model 双向数据绑定新玩法详解
前端·javascript·vue.js