资讯专栏INFORMATION COLUMN

Promise必知必会

muzhuyu / 1221人阅读

摘要:而真正的回调函数,是在的外面被调用的,也就是和中调用方法返回的是一个新的实例注意,不是原来那个实例。

前端开发中经常会进行一些异步操作,常见的异步有:

网络请求:ajax

IO操作: readFile

定时器:setTimeout

博客地址

回调

最基础的异步解决方案莫过于回调函数了

前端经常会在成功时和失败时分别注册回调函数

const req = new XMLHttpRequest();
req.open("GET", URL, true);
req.onload = function () {
    // 成功的回调
    if (req.status === 200) {
        console.log(req.statusText)
    }
};
req.onerror = function () {
    // 失败的回调
    console.log(req.statusText)
};
req.send();

node的异步api,则通常只注册一个回调函数,通过约定的参数来判断到底是成功还是失败:

const fs = require("fs");
fs.readFile("input.txt", function (err, data) {
    // 回调函数
    // 第一个参数是err,如果有err,则表示调用失败
   if (err) {
       return console.error(err);
   }
   console.log("异步读取: " + data.toString());
});

回调的异步解决方案本身也简单易懂,但是它有一个致命的缺点:无法优雅的控制异步流程

什么意思?

单个异步当然可以很简单的使用回调函数,但是对于多个异步操作,就会陷入回调地狱中

// 请求data1成功后再请求data2,最后请求data3
const ajax = $.ajax({
    url: "data1.json",
    success: function(data1) {
        console.log(data1);
        $.ajax({
            url: "data2.json",
            success: function(data2) {
                console.log(data2);
                $.ajax({
                    url: "data3.json",
                    success: function(data3) {
                        console.log(data3);

                    }
                })
            }
        })
    }
})

这种要按顺序进行异步流程控制的场景,回调函数就显得捉襟见肘了。这时,Promise的异步解决方案就被提了出来。

Promise

当初在学Promise时,看得我真是一脸懵逼,完全不明白这货到底怎么用。其实,Promise的api要分成两部分来理解:

Promise构造函数:resolve reject (改变内部状态)

Promise对象: then catch (流程控制)

Promise对象

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)

初始时,该对象状态为pending,之后只能变成fulfilled和rejected其中的一个

then方法有两个参数,分别对应状态为fulfilled和rejected时的回调函数,其中第二个参数可选

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

通常我们会省略then的第二个参数,而改用catch来注册状态变为rejected时的回调函数

promise.then(function(value) {
  // success
}).catch(function(error) {
  // failure
});
Promise构造函数

Promise对象怎么生成的呢?就是通过构造函数new出来的。

const promise = new Promise(function(resolve, reject) {
    
});

Promise构造函数接收一个函数作为参数,这个函数可以接收两个参数:resolve和reject

resolve, reject是两个函数,由JavaScript引擎提供,不用自己编写

前面我们说过,Promise对象有三种状态,初始时为pending,之后可以变成fulfilled或者rejected,那怎么改变状态呢?答案就是调用resolve或者reject

调用resolve时,状态变成fulfilled,表示异步已经完成;调用reject时,状态变成rejected,表示异步失败。

回调和Promise的对比

其实这里就是Promise最难理解的地方了,我们先看下例子:

回调函数封装

function getURL(URL, success, error) {
    const req = new XMLHttpRequest();
    req.open("GET", URL, true);
    req.onload = function () {
        if (req.status === 200) {
            success(req.responseText);
        } else {
            error(new Error(req.statusText));
        }
    };
    req.onerror = function () {
        error(new Error(req.statusText));
    };
    req.send();
}
const URL = "http://httpbin.org/get";
getURL(URL, function onFulfilled(value) {
    console.log(value);
}, function onRejected(error) {
    console.error(error);
})

Promise封装

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}

const URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

两段代码最大的区别就是:

用回调函数封装的getURL函数,需要明显的传给它成功和失败的回调函数,success和error的最终调用是在getURL里被调用的

用Promise封装的getURL函数,完全不关心成功和失败的回调函数,它只需要在ajax成功时调用resolve(),告诉promise对象,你现在的状态变成了fulfilled,在ajax失败时,调用reject()。而真正的回调函数,是在getURL的外面被调用的,也就是then和catch中调用

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}

const URL = "http://httpbin.org/get";
const URL2 = "http://deepred5.com/cors.php?search=ntr";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
    // 返回了一个新的Promise对象
    return getURL(URL2)
}).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

这段代码就充分说明了Promise对于流程控制的优势:读取URL的数据后再读取URL2,没有了之前的回调地狱问题。

Promise应用

Promise经常用于对函数的异步流程封装

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};
const fs = require("fs")
const path = require("path") 
const readFilePromise = function (fileName) {
    return new Promise((resolve, reject) => {
        fs.readFile(fileName, (err, data) => {
            if (err) {
                reject(err)
            } else {
                resolve(data.toString())
            }
        })
    })
}

结合上面几个例子,我们可以看出Promise封装代码的基本套路:

const methodPromise = function() {
    return new Promise((resolve, reject) => {
        // 异步流程
        if (/* 异步操作成功 */){
            resolve(value);
        } else {
            reject(error);
        }
    })
}
Promise.race Promise.all

Promise.all 接收一个promise对象的数组作为参数,当这个数组里的所有promise对象全部变为resolve的时候,它才会去调用then方法,如果其中有一个变为rejected,就直接调用catch方法

传给then方法的是一个数组,里面分别对应promise返回的结果

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        const req = new XMLHttpRequest();
        req.open("GET", URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}

Promise.all([getURL("http://deepred5.com/cors.php?search=ntr"), getURL("http://deepred5.com/cors.php?search=rbq")])
.then((dataArr) => {
    const [data1, data2] = dataArr;
}).catch((err) => {
    console.log(err)
})

Promise.race类似,只不过只要有一个Promise变成resolve就调用then方法

Promise.resolve Promise.reject
Promise.resolve(42); 
// 等价于
new Promise(function(resolve){
    resolve(42);
});

Promise.reject(new Error("出错了"))
// 等价于
new Promise(function(resolve,reject){
    reject(new Error("出错了"));
});
Promise.resolve(42).then(function(value){
    console.log(value);
});

Promise.reject(new Error("出错了")).catch(function(error){
    console.error(error);
});

Promise.resolve方法另一个作用就是将thenable对象转换为promise对象

const promise = Promise.resolve($.ajax("/json/comment.json"));// => promise对象
promise.then(function(value){
   console.log(value);
});

thenable对象指的是具有then方法的对象:

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});
异常捕获

理想状态下,Promise可以通过catch捕获到异常,但是如果我们没有使用catch,那么虽然控制台会打印错误,但是这次错误并不会终止脚本执行


上述代码只会打印2


打印1和2

解决方法:
window有一个unhandledRejection事件,专门监听未捕获的reject错误

window.onunhandledrejection = function(e) {
    console.log(e.reason);
}
const promise = new Promise((resolve, reject) => {
    const a = b.c.d;
    resolve("ok");
})
promise.then(data => {
    console.log(data)
})
参考

ECMAScript 6 入门

JavaScript Promise迷你书

深入理解 JavaScript 异步

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96737.html

相关文章

  • 前端必知必会HTTP请求系列(二)简单一点的HTTP协议

    摘要:通过请求和响应的交换达成通信协议中已经规定了请求是从客户端发出,最后由服务端响应这个请求并返回。随后的字符串指明了请求访问的资源对象。协议自身不对请求和响应之间的通信状态进行保存,也就是说这个级别。从前发送请求后需等待并受到响应。 showImg(https://segmentfault.com/img/bVbmDsG?w=1024&h=538); http协议用户客户端和服务器之间的...

    xbynet 评论0 收藏0
  • 前端必知必会HTTP请求系列(三)HTTP报文内的http信息

    摘要:报文用于协议交互的信息被称为报文。现在出现的各种首部字段及状态码稍后会阐述。状态码响应报文包含了多个范围的内容使用。如果服务器无法响应范围请求,则会返回状态码和完整的实体内容。 showImg(https://segmentfault.com/img/bVbthNL?w=900&h=500); http报文 用于HTTP协议交互的信息被称为HTTP报文。请求端的http报文叫做请求报文...

    Invoker 评论0 收藏0
  • [ 学习路线 ] 2015 前端(JS)工程师必知必会 (2)

    摘要:转自前端外刊评论非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下上次我写前端工程师必知必会已经是三年前了,那是我写过最火的文章了。测试的第二大障碍是工具。 转自:前端外刊评论 非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下........ 上次我写《前端工程师必知必会》已经是三年前了,那是我写过最火的文章了。三年了,我仍然会在Twitter上...

    stefan 评论0 收藏0
  • [ 学习路线 ] 2015 前端(JS)工程师必知必会 (2)

    摘要:转自前端外刊评论非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下上次我写前端工程师必知必会已经是三年前了,那是我写过最火的文章了。测试的第二大障碍是工具。 转自:前端外刊评论 非常感谢,翻译的很好,受益很多,转到此处让前端小伙伴们也惊呆下........ 上次我写《前端工程师必知必会》已经是三年前了,那是我写过最火的文章了。三年了,我仍然会在Twitter上...

    liaorio 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<