前言

要学习一个知识点,如果能对相关程序进行随意改动,那我们可以认为已经学会了。最近因为有相关需求,学习了ES6语法中的Promise对象,决定更一期文章,描述一下Promise是啥。然后对比之前的回调函数方式,再改一个具体的Promise需求。

本文Promise原理部分参考阮一峰老师的ES6教程,和廖雪峰老师的ES6教程

Promise是啥

Promise是编程语言中一种解决异步问题的机制,在ES语法中被实现为Promise对象,用来更合理高效地解决异步程序的问题。

早在es6之前的语法中,程序都是使用回调的方式来实现异步问题,例如最常见的ajax。Promise比回调函数更加合理且高效。它最初由社区提出,ES6将它纳入标准,并统一了用法,并且在ES6语法中原生提供Promise对象。

Promise是许诺在未来才结束的程序,它有三种状态,pending(进行中)resolved(已成功)rejected(已失败)

Promise一旦创建则无法取消,一旦状态改变则不会再变。Promise只能从pending变为resolved或从pending变为rejected

你可以将下面这一串代码打入浏览器的控制台,来检查你的浏览器是否支持Promise

1
2
3
'use strict';

new Promise(function () {});

Promise对象是一个构造函数,用来创建一个Promise对象。下面的代码使用Promise创建了一个实例。

1
2
3
4
5
6
7
const promise = new Promise(function(resolve, reject) {
if (true){
resolve(value);
} else {
reject(error);
}
});

Promise接受一个函数作为参数,这个函数的两个参数分别是resovereject。当Promisepending变为resolved时执行resolve,当Promisepending变为rejected时执行rejected

Promise生成后,可以使用then函数指定resolvedrejected的具体实现。

1
2
3
4
5
promise.then(function(value) {
// success
}, function(error) {
// failure
});

then函数接受两个参数,分别指定resolvedrejected状态的回调函数。

Promise内部发生错误时,可以使用catch进行捕获。

如果你想让程序“如论如何也要执行”一些事情,可以使用finally来指定要做的事情。

1
2
3
4
5
6
7
promise.then(() => {

}).catch(() => {

}).finally(() => {

})

简单介绍了Promise,如果用Promise的方式来写一个ajax方法,大概是这个样子。

1
2
3
4
5
6
7
8
9
10
function ajax(url) {
return new Promise((resove, reject) => {
// 在这进行ajax网络请求
if (success) {
resove()
} else {
reject()
}
})
}

调用方式是

1
2
3
4
5
ajax(url).then((res) => {
console.log(res)
}).catch(() => {
console.log('网络请求出现了问题')
})

之前的JavaScript回调

如果用之前的方式来写一个ajax,函数原型大概是

1
2
3
4
function ajax(url, cb) {
//发送ajax请求
cb && cb(res)
}

调用方式是

1
2
3
ajax('url', function() {
console.log(res)
})

如果需求复杂了的话,回调是特别恐怖的。相关领域的开发人员应该听说过回调地狱。随便从网上搜两端代码,给各位体验一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
var sayhello = function (name, callback) {
setTimeout(function () {
console.log(name);
callback();
}, 1000);
}
sayhello("first", function () {
sayhello("second", function () {
sayhello("third", function () {
console.log("end");
});
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const verifyUser = function(username, password, callback){
dataBase.verifyUser(username, password, (error, userInfo) => {
if (error) {
callback(error)
}else{
dataBase.getRoles(username, (error, roles) => {
if (error){
callback(error)
}else {
dataBase.logAccess(username, (error) => {
if (error){
callback(error);
}else{
callback(null, userInfo, roles);
}
})
}
})
}
})
};

如果回调逻辑再复杂些,代码的可读性几乎为0。在Promise出现之前,大家对回调地狱也只能忍让。Promise出现之后,人们让Promisereject继续返回一个Promise对象,这样就可以一直用.then()进行链式处理(例如上边的.then().catch().finally()形式)。这样的处理方式更加便捷也更加合理。

Promise的帮助下,上边回调地狱的例子可以改造为

1
2
3
4
5
6
7
8
9
10
11
const verifyUser = function(username, password) {
database.verifyUser(username, password)
.then(userInfo => dataBase.getRoles(userInfo))
.then(rolesInfo => dataBase.logAccess(rolesInfo))
.then(finalResult => {
//do whatever the 'callback' would do
})
.catch((err) => {
//do whatever the error handler needs
});
};

显然比回调地狱的方式简洁多了。

用之前的方式改一个回调逻辑

落在具体的业务上,我们思考这样一个问题。有如下代码:

1
2
3
4
5
6
7
8
9
function ajax(url, data, cb) {
var xhr = new .....
...
formdata...
xhr.onload = function() {
cb && cb()
}
xhr.send(url)
}

某框架提供的ajax方法,回调中的结果是一个http请求对象,而我们希望在回调中直接处理http返回的数据包中的数据。例如正常的处理逻辑是

1
2
3
4
5
6
7
8
ajax(url, {}, function(res) {
var needData = res.result.data; //真正需要的是这个数据
if (needData.code == 200) {
// 数据包返回成功
} else {
// 数据包返回失败
}
})

我们想让这个函数这样运作

1
2
3
4
5
6
7
8
ajax(url, {}, function(res) {
var needData = res; //真正需要的是这个数据
if (needData.code == 200) {
// 数据包返回成功
} else {
// 数据包返回失败
}
})

这样的话,我们就需要对ajax方法进行改装。

1
2
3
4
5
function myajax(url, data, cb) {
ajax(url, data, function(res) {
cb && cb(res.result.data)
})
}

这样改装完,就可以使用这个“套了壳”的ajax函数了。

1
2
3
4
5
6
7
myajax(url, {}, function(res) {
if (res.code == 200) {
// 数据包返回成功
} else {
// 数据包返回失败
}
})

用Promise改一个回调逻辑

在实际项目中,我使用axios发送ajax请求。axios官网中自述是

Promise based HTTP client for the browser and node.js

基于Promise的HTTP客户端,用于浏览器和node.js。

它的一般用法是

1
2
3
4
5
6
7
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
1
2
3
4
5
6
7
8
9
10
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

在实际的业务中,我们需要基于原始的axios进行相对的改造。

  1. 对于传入的url,我们希望可以有统一的前缀,每次使用的时候只提供url中不同的部分。例如下面是几个不同的接口

    1
    2
    3
    http://localhost:1123/account/login
    http://localhost:1123/account/signin
    http://localhost:1123/moment/list

我们希望上面的接口只需要对axios提供

1
2
3
account/login
account/signin
moment/list

即可。

  1. 对于返回的数据,我们希望先对其进行预处理,就是上边提到过的问题。axios.get().then((res) => {})res实际上是axios提供的返回对象,其中的res.data才是服务器真正返回的数据包。在开发中我们实际需要的数据都是res.data。为了开发便捷,我们希望可以在then中直接使用res代表原本的res.data
  2. 我们希望可以在vue项目中使用this.$get这样的方式来进行ajax请求。

基于这两个问题,我们需要封装两个方法,分别是getpost。进行12的处理之后,将两个函数挂载到Vue的根节点,这样就能在Vue的任何地方都能用this访问到了。

在改进axios之前,我们尝试使用原始的axios来发送一次ajax请求,以产生对比。用原始axios来发送请求的代码是

1
2
3
4
5
axios.post('http://hostname:port/index.php/account/sendSms', {
phone: '18300000000'
}).then((res) => {
console.log(res)
})

在控制台输出的结果是


在图片上可以看到,config是本次请求的配置信心、headers是请求头信息,request是请求信息,status是请求状态,statusText是状态信息,只有data下是我们接口返回的数据信息,这部分的信息是我们真正需要的,也是我们接下来要编写的程序想要获取到的数据。

接下来编写我们自己的ajax函数。

下面的叙述是在vue项目下,使用vue-cli创建,如果对我的叙述一头雾水,建议先了解一下vue的目录结构。

/src下新建ajax.js,编写getpost方法的具体实现。

文件/src/ajax.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import axios from 'axios'

const apiPre = 'http://localhost:1187/index.php/'

export function get(url, params) {
return new Promise((resolve, reject) => {
let _url = []
for(const i in params) {
_url.push(i + '=' + params[i])
}
const sendUrl = apiPre + url + '?' + _url.join('&')

return axios.get(sendUrl)
.then((res) => {
resolve(res.data)
}, (res) => {
reject(res)
})
})
}
export function post(url, data) {
return new Promise((resolve, reject) => {
return axios.post(apiPre + url, data)
.then((res) => {
resolve(res.data)
}, (res) => {
reject(res)
})
})
}

为了可以在Vue的任何地方都能使用this来访问postget方法,我们需要修改/src/main.js

import代码下增加一行

1
import { post, get } from '@/ajax.js'

const app ...上边增加

1
2
Vue.prototype.$get = get
Vue.prototype.$post = post

这样我们就改好了代码。接下来在一个页面上调用刚刚写好的post方法。

1
2
3
4
5
this.$post('account/sendSms', {
phone: '18300000000'
}).then((res) => {
console.log(res)
})

在控制台我们可以看到调用结果

可以看到,我们的改装已经完成了。

技术总结

  1. Promise是一个会在未来执行完毕的事件,它只有三种状态,并且只能从进行中变成成功或从进行中变成失败。
  2. then用来进行状态变化之后的处理,接受两个参数,分别是resolve的回调和reject的回调。
  3. 无论是改造回调方式的函数还是Promise方式的函数,都是写一个对应方式的函数套在原有函数上,让旧的回调来调用新的回调。
  4. 没事不要瞎改别人的函数,不够用还是水平不够高,飘了是咋地。

对于上一篇文章的补充与改正。

上一篇文章求求你了学一学markdown吧中提到markdown是github唯一的说明文档格式。友人Sliver指出,.rst在github上也可作为帮助文档被显示在首页。