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很好,请使用他。❀❀❀❀

相关推荐
fruge3 分钟前
前端正则表达式实战合集:表单验证与字符串处理高频场景
前端·正则表达式
baozj8 分钟前
🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
前端·javascript·vue.js
用户40993225021216 分钟前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端117 分钟前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试19 分钟前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机30 分钟前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
molly cheung1 小时前
FetchAPI 请求流式数据 基本用法
javascript·fetch·请求取消·流式·流式数据·流式请求取消
疯狂踩坑人1 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia1 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc
街尾杂货店&1 小时前
CSS - transition 过渡属性及使用方法(示例代码)
前端·css