Promise的重要性我认为我没有必要多讲,归纳起来讲就是必须得掌握,并且还要掌握透彻。这篇文章的开头,主要跟你们分析一下,为何会有Promise出现。html
在实际的使用当中,有很是多的应用场景咱们不能当即知道应该如何继续往下执行。最重要也是最主要的一个场景就是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); } }
在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发而且符合必定条件时,才能拿到咱们想要的数据,以后咱们才能开始处理数据。react
这样作看上去并无什么麻烦,可是若是这个时候,咱们还须要作另一个ajax请求,这个新的ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候咱们就不得不以下这样作:jquery
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() { ... } } }
当出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,咱们的代码就变成了一场灾难。这场灾难,每每也被称为回调地狱。git
所以咱们须要一个叫作Promise的东西,来解决这个问题。es6
固然,除了回调地狱以外,还有一个很是重要的需求:为了咱们的代码更加具备可读性和可维护性,咱们须要将数据请求与数据处理明确的区分开来。上面的写法,是彻底没有区分开,当数据变得复杂时,也许咱们本身都没法轻松维护本身的代码了。这也是模块化过程当中,必需要掌握的一个重要技能,请必定重视。github
从前面几篇文中的知识咱们能够知道,当咱们想要确保某代码在谁谁以后执行时,咱们能够利用函数调用栈,将咱们想要执行的代码放入回调函数中。ajax
// 一个简单的封装 function want() { console.log('这是你想要执行的代码'); } function fn(want) { console.log('这里表示执行了一大堆各类代码'); // 其余代码执行完毕,最后执行回调函数 want && want(); } fn(want);
利用回调函数封装,是咱们在初学JavaScript时经常会使用的技能。segmentfault
确保咱们想要的代码压后执行,除了利用函数调用栈的执行顺序以外,咱们还能够利用上一篇文章所述的队列机制。数组
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的基础知识。
1、 Promise对象有三种状态,他们分别是:
这三种状态不受外界影响,并且状态只能从pending改变为resolved或者rejected,而且不可逆。在Promise对象的构造函数中,将一个函数做为第一个参数。而这个函数,就是用来处理Promise的状态变化。
new Promise(function(resolve, reject) { if(true) { resolve() }; if(false) { reject() }; })
上面的resolve和reject都为一个函数,他们的做用分别是将状态修改成resolved和rejected。
2、 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() {})
3、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://hq.tigerbrokers.com/fundamental/finance_calendar/getType/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最经常使用的一个场景,所以咱们必定要很是很是熟悉它,这样才能在应用的时候更加灵活。
4、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); })
5、 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下来进行学习。这样学习效果会更好。
往下阅读例子以前,请必定要对requirejs有一个简单的了解。requirejs中文文档 http://www.requirejs.cn/
项目的代码结果如上图所示,全部的html文件都放在根目录下。
首先为了可以让require起做用,咱们须要在html中引入require.js,写法以下:
// index.js为入口文件 <script data-main="./pages/index.js" src="./libs/require.js"></script>
在入口的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://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组件来解决这个问题。咱们指望图片可以根据本身的宽高比,合理的缩放,保证在这个块中不变形的状况下尽量的显示更多的内容。
假若有一堆图片,以下:
<section class="img-wrap"> <div class="img-center">  </div> <div class="img-center">  </div> <div class="img-center">  </div> <div class="img-center">  </div> </section>
每一张图片都有一个包裹的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的处理,对于弹窗的处理,都是能够扩展成为一个通用的模块的。慢慢积累多了,你的开发效率就能够获得明显的提升,这些积累,也将会变成你的优点所在。
后续的文章我会分享如何利用react与es6模块系统封装的共用组件,你们也能够学习了以后,根据本身的需求,封装最适合你本身的一套组件。
最后,最近问我怎么学习的人愈来愈多,我真的有点回答不过来了,我想把我这些文章里的知识都掌握了,应付毕业以后的第一份工做应该不是什么问题的吧?并且为了大家可以掌握Promise的使用,我还专门给读者老爷们建立了一个项目,列举了整整三个实例,还有源代码供大家学习,我学Promise的时候,找很久都没找到一个稍微接近实际应用的案例,学了很久才知道怎么使用,效率之低可想而知。因此静下心来慢慢学习吧,花点时间是值得的 ~ ~ 。