promise的使用

在实际的使用当中,有非常多的应用场景我们不能立即知道应该如何继续往下执行。最重要也是最主要的一个场景就是ajax请求。通俗来说,由于网速的不同,可能你得到返回值的时间也是不同的,这个时候我们就需要等待,结果出来了之后才知道怎么样继续下去。

// 简单的ajax原生实现

var url = 'https://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);

    }

}

在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发并且符合一定条件时,才能拿到我们想要的数据,之后我们才能开始处理数据。

这样做看上去并没有什么麻烦,但是如果这个时候,我们还需要做另外一个ajax请求,这个新的ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候我们就不得不如下这样做:

var url = 'https://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() {

            ...

        }

    }

}

当出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,我们的代码就变成了一场灾难。这场灾难,往往也被称为回调地狱。

因此我们需要一个叫做Promise的东西,来解决这个问题。

当然,除了回调地狱之外,还有一个非常重要的需求:为了我们的代码更加具有可读性和可维护性,我们需要将数据请求与数据处理明确的区分开来。上面的写法,是完全没有区分开,当数据变得复杂时,也许我们自己都无法轻松维护自己的代码了。这也是模块化过程中,必须要掌握的一个重要技能,请一定重视。

从前面几篇文中的知识我们可以知道,当我们想要确保某代码在谁谁之后执行时,我们可以利用函数调用栈,将我们想要执行的代码放入回调函数中。

// 一个简单的封装

function want() {

    console.log('这是你想要执行的代码');

}

function fn(want) {

    console.log('这里表示执行了一大堆各种代码');

    // 其他代码执行完毕,最后执行回调函数

    want && want();

}

fn(want);

利用回调函数封装,是我们在初学JavaScript时常常会使用的技能。

确保我们想要的代码压后执行,除了利用函数调用栈的执行顺序之外,我们还可以利用上一篇文章所述的队列机制。

function want() {

    console.log('这是你想要执行的代码');

}

function fn(want) {

    // 将想要执行的代码放入队列中,根据事件循环的机制,我们就不用非得将它放到最后面了,由你自由选择

    want && setTimeout(want, 0);

    console.log('这里表示执行了一大堆各种代码');

}

fn(want);

如果浏览器已经支持了原生的Promise对象,那么我们就知道,浏览器的js引擎里已经有了Promise队列,这样就可以利用Promise将任务放在它的队列中去。

function want() {

    console.log('这是你想要执行的代码');

}

function fn(want) {

    console.log('这里表示执行了一大堆各种代码');

    // 返回Promise对象

    return new Promise(function(resolve, reject) {

        if (typeof want == 'function') {

            resolve(want);

        } else {

            reject('TypeError: '+ want +'不是一个函数')

        }

    })

}

fn(want).then(function(want) {

    want();

})

fn('1234').catch(function(err) {

    console.log(err);

})

看上去变得更加复杂了。可是代码变得更加健壮,处理了错误输入的情况。

为了更好的往下扩展Promise的应用,这里需要先跟大家介绍一下Promsie的基础知识。

一、 Promise对象有三种状态,他们分别是:

pending: 等待中,或者进行中,表示还没有得到结果

resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行

rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行

这三种状态不受外界影响,而且状态只能从pending改变为resolved或者rejected,并且不可逆。在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。

new Promise(function(resolve, reject) {

    if(true) { resolve() };

    if(false) { reject() };

})

上面的resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。

二、 Promise对象中的then方法,可以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。

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值');

    })

}

fn('hahha');

fn(1234);

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;

})

.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

OK,了解了这些基础知识之后,我们再回过头,利用Promise的知识,对最开始的ajax的例子进行一个简单的封装。看看会是什么样子。

var url = 'https://2017-02-26/2017-06-10';

// 封装一个get请求的方法

function getJSON(url) {

    return new Promise(function(resolve, reject) {

        var XHR = new XMLHttpRequest();

        XHR.open('GET', url, true);

        XHR.send();

        XHR.onreadystatechange = function() {

            if (XHR.readyState == 4) {

                if (XHR.status == 200) {

                    try {

                        var response = JSON.parse(XHR.responseText);

                        resolve(response);

                    } catch (e) {

                        reject(e);

                    }

                } else {

                    reject(new Error(XHR.statusText));

                }

            }

        }

    })

}

getJSON(url).then(resp => console.log(resp));

为了健壮性,处理了很多可能出现的异常,总之,就是正确的返回结果,就resolve一下,错误的返回结果,就reject一下。并且利用上面的参数传递的方式,将正确结果或者错误信息通过他们的参数传递出来。

现在所有的库几乎都将ajax请求利用Promise进行了封装,因此我们在使用jQuery等库中的ajax请求时,都可以利用Promise来让我们的代码更加优雅和简单。这也是Promise最常用的一个场景,因此我们一定要非常非常熟悉它,这样才能在应用的时候更加灵活。

四、Promise.all

当有一个ajax请求,它的参数需要另外2个甚至更多请求都有返回结果之后才能确定,那么这个时候,就需要用到Promise.all来帮助我们应对这个场景。

Promise.all接收一个Promise对象组成的数组作为参数,当这个数组所有的Promise对象状态都变成resolved或者rejected的时候,它才会去调用then方法。

var url = 'https://2017-02-26/2017-06-10';

var url1 = 'https://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);

})

嗯,我所知道的,关于Promise的基础知识就这些了,如果还有别的,欢迎大家补充。

那么接下来,我们要结合三个不同的应用场景来让大家感受一下Promise在模块系统中如何使用。

这里选择requirejs是因为学习成本最低,能够快速上手进行简单的运用。接下来的这些例子,会涉及到很多其他的知识,因此如果想要彻底掌握,一定要动手实践,自己试着完成一遍。

我在github上创建了对应的项目,大家可以直接clone下来进行学习。这样学习效果会更好。

项目地址: https://github.com/yangbo5207/promiseApps

往下阅读例子之前,请一定要对requirejs有一个简单的了解。

requirejs中文文档 http://www.requirejs.cn/

项目的代码结果如上图所示,所有的html文件都放在根目录下。

pages: html直接引入的js

libs: 常用的库

components: 针对项目自定义的模块

首先为了能够让require起作用,我们需要在html中引入require.js,写法如下:

// index.js为入口文件

在入口的index.js中,我们可以对常用的模块进行映射配置,这样在引入时就可以少写一些代码。

// 具体的配置项的含义,请参阅require的中文文档

requirejs.config({

    baseUrl: './',

    paths: {

        jquery: "./libs/jquery-3.2.0",

        API: './libs/API',

        request: './libs/request',

        calendar: './components/calendar',

        imageCenter: './components/imageCenter',

        dialog: './components/Dialog'

    }

})

配置之后,那么我们在其他模块中,引入配置过的模块,就可以简单的这样写:

var $ = require('jquery');

如果不进行配置,也可以这样引入模块:

require('./components/button');

我们可以使用define定义一个模块:

// 其他方式请参阅文档

define(function(require) {

})

使用return可以直接对外提供方法:

// 在其他模块通过require引入时得到的值,就是这里返回的值

define(function(require) {

    return {

        a: 1

    }

})

OK,了解上面这些,应付基础的使用已经没有问题了。我们接下来重点总结第一个常用的应用场景:ajax。

关于ajax的简单使用和简单封装,我们在上面都已经讲过了,这里就不再多说,直接使用jquery封装好的方法即可。而我们需要处理的问题在于,如何有效的将ajax的数据请求和数据处理分别放在不同的模块中进行管理,这样做的主要目的在于降低后期维护成本,便于管理。

来看看怎么样简单操作的。

首先,将所有的url放在一个模块中统一处理。

// libs/API.js

define(function() {

    return {

        dayInfo: 'https://get_day/2017-04-03',

        typeInfo: 'https:/getType/2017/2017-12-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组件来解决这个问题。我们期望图片能够根据自己的宽高比,合理的缩放,保证在这个块中不变形的情况下尽可能的显示更多的内容。

假如有一堆图片 如下:

微信图片_20180110171128.png

每一张图片都有一个包裹的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');
微信图片_20180110171353.png

一堆尺寸乱七八糟的图片就这样被驯服了

第三个应用场景,则是自定义弹窗的处理。


微信图片_20180110171414.png

这种类型的弹窗随处可见,而且十分常用

因此自己专门定义一个常用的弹窗就变得非常有必要,这对于我们开发效率的提高非常有帮助。当然,我这里只是简单的写了一个简陋的,仅供参考。

我们期望的是利用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,并且对应的三个应用场景又各自有许多需要了解的知识,因此对于基础稍差的朋友来说,理解透彻了肯定会有一个比较大的进步。当然也会花费你更多的时间。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348