专栏名称: 寒东设计师
前端工程师
目录
相关文章推荐
中核集团  ·  中核集团审计中心揭牌成立! ·  9 小时前  
中核集团  ·  报名开启!这场马拉松与非洲同行! ·  2 天前  
雨生云计算  ·  【广告】KreadoAI 联盟计划正式开启! ·  2 天前  
谷哥大叔  ·  也许是最后一期线下课了 ·  3 天前  
樊登读书  ·  抢疯了!3大王牌经典买1得2!马上下架!! ·  3 天前  
51好读  ›  专栏  ›  寒东设计师

浅入浅出Promise

寒东设计师  · 掘金  ·  · 2018-07-05 04:03

正文

阅读 55

浅入浅出Promise

Promise 是我最喜欢的es6语法,也是面试中最容易问到的部分。那么怎么做到在使用中得心应手,在面试中脱颖而出呢?
先来个面试题做做:

面试题:用Promise封装一下原生ajax

面试官经常会让手写一个Promise封装,写出下面这一版就行了(想了解更多的可自行扩展):


function ajaxMise(url, method, data, async, timeout) {
    var xhr = new XMLHttpRequest()
    return new Promise(function (resolve, reject) {
        xhr.open(method, url, async);
        xhr.timeout = options.timeout;
        xhr.onloadend = function () {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
                resolve(xhr);
            else
                reject({
                    errorType: 'status_error',
                    xhr: xhr
                })
        }
        xhr.send(data);
        //错误处理
        xhr.onabort = function () {
            reject(new Error({
                errorType: 'abort_error',
                xhr: xhr
            }));
        }
        xhr.ontimeout = function () {
            reject({
                errorType: 'timeout_error',
                xhr: xhr
            });
        }
        xhr.onerror = function () {
            reject({
                errorType: 'onerror',
                xhr: xhr
            })
        }
    })
}

Promise简介

Promise 是一个对象,保存着未来将要结束的事件。她有两个特征,引用阮一峰老师的描述就是:

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

Promise基本用法
let promise1 = new Promise(function (resolve, reject){
    setTimeout(function (){
        resolve('ok') //将这个promise置为成功态(fulfilled),会触发成功的回调
    },1000)
})
promise1.then(fucntion success(val) {
    console.log(val) //一秒之后会打印'ok'
})
最简单代码实现一个Promise
class PromiseM {
    constructor (process) {
        this.status = 'pending'
        this.msg = ''
        process(this.resolve.bind(this), this.reject.bind(this))
        return this
    }
    resolve (val) {
        this.status = 'fulfilled'
        this.msg = val
    }
    reject (err) {
        this.status = 'rejected'
        this.msg = err
    }
    then (fufilled, reject) {
        if(this.status === 'fulfilled') {
            fufilled(this.msg)
        }
        if(this.status === 'rejected') {
            reject(this.msg)
        }
    }

}
//测试代码
var mm=new PromiseM(function(resolve,reject){
    resolve('123');
});
mm.then(function(success){
    console.log(success);
},function(){
    console.log('fail!');
});

Micro-task / event loop

上面提到 Promise 和事件的不同,除此之外还有一个重要不同,就是 Promise 创建是 micro-task 。再看一道面试题:

面试题:写出下面代码的输出顺序

console.log('script start');

setTimeout(function () {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});

console.log('script end');

正确答案是:'script start'、'script end'、'promise1'、'promise2'、'setTimeout'。原因就是:

  • setTimeout(或者事件)注册的是一个task,由Event Loop控制
  • Promise注册的是一个micro-task

Event Loop 是js的一个重要机制,就是遇到事件或者 setTimeout 等就会把对应的回调函数放入一个事件队列(task queue),等到主程序执行完毕就依次把队列里的函数压入栈中执行。可以参考阮一峰老师的 JavaScript 运行机制详解:再谈Event Loop ,不过貌似老师的网站被攻击还没有恢复。
但是 Promise 不是上面的机制,她创建的是一个微任务(micro-task), micro-task 的执行总是在当前执行栈结束和下一个 task 执行之前,顺序就是“当前执行栈” -> “micro-task” -> “task queue中取一个回调” -> “micro-task” -> ... (不断消费task queue) -> “micro-task”,总之就是当前执行栈为空时,就到了一个 micro-task 的检查点。
下面是 micro-task 的定义:

Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.

Promise 注册的是 micro-task ,所以上面题目中:主线程中'script start'、'script end'先打印,然后清空微任务队列,'promise1'、'promise2'打印,然后取出 task queue 中的回调执行,'setTimeout'打印。

为什么出现promise

Promise 提供了对js异步编程的新的解决方案,因为我们一直使用的回调函数其实是存在很大问题,只是限制于js的单线程等原因不得不大量书写。当然 Promise 并不是完全摆脱回调,她只是改变了传递回调的位置。那么传统的回调存在什么问题呢?

嵌套

这里所说的嵌套是指大量的回调函数会使得代码难以读懂和修改,试想一个这个场景:让你把下面的url4的调用提到url2之前。你需要非常小心的剪切代码,并且笨拙的粘贴,result4这个参数你还不敢修改,因为这要额外花费很多功夫并且存在风险。

$.ajax('url1',function success(result1){
    $.ajax('url2',function success(result2){
        $.ajax('url3',function success(result3){
            $.ajax('url4',function success(result4){
                //……
            })
        })
    })
})

当然,上面的问题有点戏剧成分,现实中极少出现这种难搞的情况。与此相比,回调函数带来的思维上的难以理解是更致命的,因为我们的大脑更喜欢同步的逻辑,这也是为什么 await 关键字那么受欢迎的原因。
我记得有一次我给后端的同学做JS新特性分享的时候,说到 await 关键字,有个人惊呼:“哇!这个不错啊,这就可以像写java一样写代码了”。

信任

除去书写的不优雅和维护的困难以外,回调函数其实还存在信任问题。
事实上回调函数不一定会像你期望的那样被调用。因为控制权不在你的手上。这种问题被称作“控制反转”。例如下面的例子:

$.ajax('xxxxxx',function success(result1){
    //比如成功之后我会操作数据库记录结算金额
})

上面是 jQuery 中的 ajax 调用,我们期望在某些事件结束后,让第三方(jQ)帮我们执行我的程序(回调)。
那么,我们和第三方之间并没有一个契约或者规范可以遵循,除非你把你想使用的第三方库通读一遍,保证它做了你想做的事,但事实上你很难确定。即使在自己的代码中,或者自己编写的工具,我们都很难做到百分之百信任。

Promise解决方案

Promise 是一个规范,尝试以一种更加友好的方式书写代码。 Promise 对象接受一个函数作为参数,函数提供两个参数:

  • resolve:将 promise 从未完成切换到成功状态,也就是上面提到的从 pending 切换到 fufilled , resolve 可以传递参数,下一级 promise 中的成功函数会接收到它
  • reject:将 promise 从未完成切换到失败状态,即从 pending 切换到 rejected
let promise1 = new Promise(function(reslove, reject){
    //reslove或者reject或者出错
})
promise1.then(fufilled, rejected).then().then() //这是伪代码
promise1.then(fufilled, rejected)//可以then多次

function fufilled(data) {
    console.log(data)
}
function rejected(e){
    console.log(e)
}

正如上面提到的两个特征,一旦状态改变,这个 Promise 就已经完成决议(不会再更改),并且返回一个新的 Promise ,可以链式调用。并且可以注册多个 then 方法,他们同时决议并且互不影响。这种设计明显比回调函数要优雅的多,也更易于理解和维护。那么在信任问题上她又有哪些改善呢?
Promise 通过通知的机制将“控制反转”的关系又“反转”回来。回调是我传递给第三方一个函数,期望它在事件发生时帮我执行,而 Promise 是在大家都遵循规范的前提下,我会在事件发生时得到通知,这时我决定做一些事(执行一些函数)。看到了吧,这是有本质差异的。
此外,回调函数还有以下信任问题, Promise 也都做了相关约束:

  • 回调调用过早
  • 回调调用过晚(或者没有调用)
  • 调用次数太多
  • 没有把参数成功传递给你的回调
  • 吐掉了错误或者异常
过早或者过晚

一个 Promise 回调一定会在当前栈执行完毕和下一个异步时机点上调用,即使像下面这样的同步 resolve 代码也会异步执行,而你传给工具库的回调函数却可能被同步执行(调用过早)或者被忘记执行(或者过晚)。







请到「今天看啥」查看全文


推荐文章
中核集团  ·  中核集团审计中心揭牌成立!
9 小时前
雨生云计算  ·  【广告】KreadoAI 联盟计划正式开启!
2 天前
谷哥大叔  ·  也许是最后一期线下课了
3 天前
投中网  ·  马云的新宠
7 年前
程序员大咖  ·  还没对象吗?要不要给你介绍
7 年前