现在的前端Promise已是无处不在,重要性已不必再说,---必须掌握!!!
在实际使用中,有非常多的场景我们不能立即知道应该如何继续往下执行。最常见的时ajax请求:
let url = 'http://---';
let result;
let XHR = new AML HttpRequest();
XHR.open('GET',url,true);
XHR.send();
XHR.onreadystatechange = function(){
if(XHR.readyState == 4 && XHR.staus == 200){
result = XHR.response;
}
}
在原生的ajax中,利用了onreadystatechange事件,当事件触发并符合一定的条件时,才能拿到想要的数据,之后才能处理数据。
这样看上去并没有什么麻烦,但是如果这时候,我们还需要另外一个ajax请求,这个新的ajax请求的参数要从上一个请求的返回结果,这个时候不得不等待上一个接口请求完成之后,在请求下一个接口。
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);
// 伪代码
var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}
当出现第三个,或者更多请求时,我们的代码就变成了一场灾难--『回调地狱』。
而Promise就是用来解决这个问题的。
当然,除了回调地域之外,还有一个非常重要的需求:为了代码更加具有可读性和可维护性,我能需要将数据请求于数据处理明确的区分开来。
当我们想要确保某代码在谁谁之后之星,我们可以利用函数调用栈,将我们想要执行的代码放入回调函数中。
//一个简单封装
function want(){
console.log('这是你想要执行的代码');
}
function fn(want){
console.log('这里表示执行了一大堆代码')
want && want();
}
function fn(want);
利用回调函数封装,是我们在出血JavaScript时常使用的技能。
如果浏览器已经支持了原生Promise对象,那么我们知道,浏览器的js引擎已经有了Promise队列,这样就可以利用Promise将任务放在它的队列中去。
function want(){
console.log('这是你想要执行的代码');
}
function fn(want){
console.log('这里表示执行了一大堆各种代码');
return new Promise(resolve,reject){
if(typeof want == 'function'){
resolve();
}else{
rejuct('TypeError:'+want+'不是一个函数')
}
}
}
fn(want).then(function(want){
want();
})
fn('1234).catch(funcdtion(err){
console.log(err);
})
虽然看上去变得更复杂了,但是代码变得更加健壮,处理了错误输入的情况。
为了更好的扩展Promise的引用,这里需要先看一下promise的基础知识。
一、Promise对象有三种状态
它们分别是:
pending:等待中,或进行中,表示还没有结果
resolved(Fulfilled):已经完成,表示得到了我们想要的结果,可以继续往下执行。
rejected:也表示得到了结果,但是由于结果并非我们所愿,因此拒绝执行。
这三种状态不受外界影响,而且状态只能从pending变成resolved或者rejected,并且不可逆。
在Promise对象的构造函数中,将第一个函数作为第一个参数,而这个函数就是用来处理Promise状态变化的.
new Promise(function(resove,rejuxt){
if(true){
resolve();
}
if(false){
reject();
}
})
上边的resolve和reject都是一个函数,它们的作用分别是将状态修改为resolved和rejected。
二、Promise对象中的then方法,
可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态执行,第二个参数接收rejuct状态的执行。
function fn(num){
return new Promise(function(resolve,reject){
if (typeof num == 'number'){
resolve();
}else{
reject();
}
}).then(function(){
console.log('参数是一个number值');
},function(){
console.log('参数不是一个number值');
})
}
then方法的执行结果也会返回一个Promise对象,因此我们可以进行then的链式执行。这也是解决回调地域的主要方式。
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
})
.then(function() {
console.log('参数是一个number值');
})
.then(null, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
then(null,function(){})就等同于catch(function(){})
三、Promise中的数据传递
var fn = function(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve(num);
} else {
reject('TypeError');
}
})
}
fn(2).then(function(num) {
console.log('first: ' + num);
return num + 1; //return结果作为resolve的返回结果
})
.then(function(num) {
console.log('second: ' + num);
return num + 1;
})
.then(function(num) {
console.log('third: ' + num);
return num + 1;
});
// 输出结果
first: 2
second: 3
third: 4
了解了这些基础知识后,我们来利用Promise的只是,对最开始的ajax的例子进行一个简单的封装。看看回会是什么样子呢?
let url = ''
//封装一个get请求的方法
function getJSON(url){
let XHR = new XMLHttpRequest();
XHR.open('GET',url,true);
XHR.onreadystatechange = function(){
if(XHR.readyState == 4 && XHR.staus == 200){
try{
let response = JSON.parse(XHR.responseText);
resolve(response);
}catch(e){
reject(e);
}
} else{
reject(new Error(XHR.statusText));
}
}
}
为了健壮性,处理了很多可能出现的异常,总之,就是正确的返回结果,就resolve一下,错误的返回结果,就reject一下。并且利用上面的参数传递的方式,将正确结果或者错误信息通过他们的参数传递出来。
现在所有的库几乎都将ajax请求利用Promise进行了封装,因此我们在使用jQuery等库中的ajax请求时,都可以利用Promise来让我们的代码更加优雅和简单。这也是Promise最常用的一个场景,因此我们一定要非常非常熟悉它,这样才能在应用的时候更加灵活。
四、Promise.all
当有一个ajax请求,它的参数需要另外2个甚至更多请求都有返回结果之后才能确定,那么这个时候,就需要用到Promise.all来帮助我们应对这个场景。
Promise.all接收一个Promise对象组成的数组作为参数,当这个数组所有的Promise对象状态都变成resolved或者rejected的时候,它才会去调用then方法。
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var url1 = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-06-10';
function renderAll() {
return Promise.all([getJSON(url), getJSON(url1)]);
}
renderAll().then(function(value) {
// 建议大家在浏览器中看看这里的value值
console.log(value);
})
五、 Promise.race
与Promise.all相似的是,Promise.race都是以一个Promise对象组成的数组作为参数,不同的是,只要当数组中的其中一个Promsie状态变成resolved或者rejected时,就可以调用.then方法了。而传递给then方法的值也会有所不同,大家可以再浏览器中运行下面的例子与上面的例子进行对比。
function renderRace() {
return Promise.race([getJSON(url), getJSON(url1)]);
}
renderRace().then(function(value) {
console.log(value);
})
应用:
第一个引用场景,有效的将ajax的数据请求和数据处理分别放在不同的模块进行管理。
首先,将所有url放在一个模块里统一处理。
// libs/API.js
define(function() {
return {
dayInfo: 'https://hq.tigerbrokers.com/fundamental/finance_calendar/get_day/2017-04-03',
typeInfo: 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-03-26/2017-04-15'
}
})
在实际开发中,url并不是直接通过字符串就能直接确认的,某些url还需要通过参数拼接等,这个时候需要我们灵活处理。
第二步,将所有的数据请求放在同一个模块中统一处理。
// libs/request.js
define(function(require) {
var API = require('API');
// 因为jQuery中的get方法也是通过Promise进行了封装,最终返回的是一个Promise对象,因此这样我们就可以将数据请求与数据处理放在不同的模块
// 这样我们就可以使用一个统一的模块来管理所有的数据请求
// 获取当天的信息
getDayInfo = function() {
return $.get(API.dayInfo);
}
// 获取type信息
getTypeInfo = function() {
return $.get(API.typeInfo);
};
return {
getDayInfo: getDayInfo,
getTypeInfo: getTypeInfo
}
});
在这个模块中,我们还可以对拿到的数据进行一些你需要的过滤处理,确保最终返回给下一个模块的数据是能够直接使用的。
第三步:就是拿到数据并且处理数据了。
// components/calendar.js
define(function(require) {
var request = require('request');
// 拿到数据之后,需要处理的组件,可以根据数据渲染出需求想要的样式
// 当然这里为了简化,就仅仅只是输出数据就行了,在实际中,拿到数据之后还要进行相应的处理
request.getTypeInfo()
.then(function(resp) {
// 拿到数据,并执行处理操作
console.log(resp);
})
// 这样,我们就把请求数据,与处理数据分离开来,维护起来就更加方便了,代码结构也足够清晰
})
这就是我所了解的处理ajax的比较好的一个方式,如果你有其他更好的方式也欢迎分享。
第二个应用场景,就是图片加载的问题。
在一些实际应用中,常常会有一些图片需要放置在某一个块中,比如头像,比如某些图片列表。可是源图片的尺寸可能很难保证长宽比例都是一致的,如果我们直接给图片设定宽高,就有可能导致图片变形。变形之后高大上的页面就直接垮掉了。
因此为了解决这个问题,我们需要一个定制的image组件来解决这个问题。我们期望图片能够根据自己的宽高比,合理的缩放,保证在这个块中不变形的情况下尽可能的显示更多的内容。
假如有一堆图片,每一张图片都有一个包裹的div,这些div的宽高,就是我们期望图片能保持的宽高。
当图片宽度值过大时,我们期望图片的高度为100%,并且左右居中。
当图片高度值过大时,我们期望图片的宽度为100%,并且上下居中。
根据这一点,我们来看看具体怎么实现。
首先是样式的定义很重要。
.img-center {
width: 200px;
height: 150px;
margin: 20px;
overflow: hidden;
position: relative;
}
.img-center img {
display: block;
position: absolute;
}
.img-center img.aspectFill-x {
width: 100%;
top: 50%;
transform: translateY(-50%);
}
.img-center img.aspectFill-y {
height: 100%;
left: 50%;
transform: translateX(-50%);
}
我分别定义了aspectFill-x与aspectFill-y,通过判断不同的宽高比,来决定将他们中的其中一个加入到img标签的class中去即可。
获取图片的原始宽高,需要等到图片加载完毕之后才能获取。而当图片已经存在缓存时,则有一个compete属性变成true。那么我们就可以根据这些基础知识,定义一个模块来处理这件事情。
// components/imageCenter.js
define(function(require) {
// 利用Promise封装一个加载函数,这里也是可以单独放在一个功能模块中进一步优化
var imageLoad = function(img) {
return new Promise(function(resolve, reject) {
if (img.complete) {
resolve();
} else {
img.onload = function(event) {
resolve(event);
}
img.onerror = function(err) {
reject(err);
}
}
})
}
var imageCenter = function(domList, mode) {
domList.forEach(function(item) {
var img = item.children[0];
var itemW = item.offsetWidth;
var itemH = item.offsetHeight;
var itemR = itemW / itemH;
imageLoad(img).then(function() {
var imgW = img.naturalWidth;
var imgH = img.naturalHeight;
var imgR = imgW / imgH;
var resultMode = null;
switch (mode) {
// 这样写是因为期待未来可以扩展其他的展示方式
case 'aspectFill':
resultMode = imgR > 1 ? 'aspectFill-x' : 'aspectFill-y';
break;
case 'wspectFill':
resultMode = itemR > imgR ? 'aspectFill-x' : 'aspectFill-y'
break;
default:
}
$(img).addClass(resultMode);
})
})
}
return imageCenter;
})
那么在使用时,直接引入这个模块并调用imageCenter方法即可。
// index.js
var imageCenter = require('imageCenter');
var imageWrapList = document.querySelectorAll('.img-center');
imageCenter(imageWrapList, 'wspectFill');
第三个引用场景,则是自定义弹窗的处理。
因此自己专门定义一个常用的弹窗就变得非常有必要,这对于我们开发效率的提高非常有帮助。当然,我这里只是简单的写了一个简陋的,仅供参考。
我们期望的是利用Promise,当我们点击确认时,状态变成resolved,点击取消时,状态变成rejected。这样也方便将弹窗生成与后续的操作处理区分开来。
先定义一个Dialog模块。使用的是最简单的方式定义,应该不会有什么理解上的困难。主要提供了show和hide2个方法,用于展示和隐藏。
// components/Dialog.js
define(function(require) {
// 利用闭包的特性,判断是否已经存在实例
var instance;
function Dialog(config) {
this.title = config.title ? config.title : '这是标题';
this.content = config.content ? config.content : '这是提示内容';
this.html = '<div class="dialog-dropback">' +
'<div class="container">' +
'<div class="head">'+ this.title +'</div>' +
'<div class="content">'+ this.content +'</div>' +
'<div class="footer">' +
'<button class="cancel">取消</button>' +
'<button class="confirm">确认</button>' +
'</div>' +
'</div>' +
'</div>'
}
Dialog.prototype = {
constructor: Dialog,
show: function() {
var _this = this;
if (instance) {
this.destory();
}
$(this.html).appendTo($(document.body));
instance = this;
return new Promise(function(resolve, reject) {
$('.dialog-dropback .cancel').on('click', function(e) {
_this.hide();
reject(e);
})
$('.dialog-dropback .confirm').on('click', function(e) {
_this.hide();
resolve(e);
})
})
},
destory: function() {
instance = null;
$('.dialog-dropback .cancel').off('click');
$('.dialog-dropback .confirm').off('click');
$('.dialog-dropback').remove();
},
hide: function() {
this.destory();
}
}
return function(config) {
return new Dialog(config);
}
})
那么在另外一个模块中需要使用它时:
define(function(require) {
var Dialog = require('dialog');
$('button.aspect').on('click', function() {
Dialog({
title: '友情提示',
content: '外面空气不太好,你确定你要出门逛逛吗?'
}).show().then(function() {
console.log('你点击了确认按钮.');
}).catch(function() {
console.log('你点击了取消按钮.');
})
})
})
这三种场景就介绍完了,主要是需要大家通过源码来慢慢理解和揣摩。真正掌握之后,相信大家对于Promise在另外的场景中的使用也会变得得心应手。
最后总结一下,这篇文章,涉及到的东西,有点多。大概包括Promise基础知识,ajax基础知识,如何利用Promise封装ajax,如何使用require模块系统,如何在模块中使用Promise,并且对应的三个应用场景又各自有许多需要了解的知识,因此对于基础稍差的朋友来说,理解透彻了肯定会有一个比较大的进步。当然也会花费你更多的时间。
另外在我们的工作中还有一件非常重要的事情是需要我们持续去做的。那就是将常用的场景封装成为可以共用的模块,等到下次使用时,就可以直接拿来使用而节省非常多的开发时间。比如我这里对于img的处理,对于弹窗的处理,都是可以扩展成为一个通用的模块的。慢慢积累多了,你的开发效率就可以得到明显的提高,这些积累,也将会变成你的优势所在。
最后,最近问我怎么学习的人越来越多,我真的有点回答不过来了,我想把我这些文章里的知识都掌握了,应付毕业之后的第一份工作应该不是什么问题的吧?而且为了你们能够掌握Promise的使用,我还专门给读者老爷们创建了一个项目,列举了整整三个实例,还有源代码供你们学习,我学Promise的时候,找好久都没找到一个稍微接近实际应用的案例,学了好久才知道怎么使用,效率之低可想而知。所以静下心来慢慢学习吧,花点时间是值得的 ~ ~ 。
自己实现Promise类
Promise类的主要内容
1、属性status 默认:pending;
2、resolve、reject方法,用于改变status和返回值
3、executer:Promise接收的参数,一个函数:我们需要执行的异步函数
4、then方法,接收两个参数,2个函数。
一个是成功的onFufilled,一个是失败的onReject
//创建一个Promise的类
class Promise{
constructor(executer){//构造函数constructor里面是个执行器
this.status = 'pending';//默认的状态 pending
this.value = undefined//成功的值默认undefined
this.reason = undefined//失败的值默认undefined
//状态只有在pending时候才能改变
let resolve = value =>{
//判断只有等待时才能resolve成功
if(this.status == pending){
this.status = 'resolve';
this.value = value;
}
}
//判断只有等待时才能reject失败
let reject = reason =>{
if(this.status == pending){
this.status = 'reject';
this.reason = reason;
}
}
try{
//把resolve和reject两个函数传给执行器executer
executer(resolve,reject);
}catch(e){
reject(e);//失败的话进catch
}
}
then(onFufilled,onReject){
//如果状态成功调用onFufilled
if(this.status = 'resolve'){
onFufilled(this.value);
}
//如果状态失败调用onReject
if(this.status = 'reject'){
onReject(this.reason);
}
}
}