相信不少開發者都遇到過回調地獄的問題。因爲微信小程序的API基本都是基於回調函數的異步操做,若是不使用其餘框架或者封裝API,特別是使用較多的wx.request()
,基本很快就會遇到回調地獄的問題,維護起來十分痛苦。javascript
假設此時在正在開發一個社交小程序,其中有一個功能的是,小程序用戶在登陸後,能夠查看附近的人。前端
假設使用如下的實現思路,咱們經過wx.getLocation()
獲取用戶當前位置,而後經過wx.request()
請求後端數據。但在此以前須要登陸,參考以前官方文檔推薦的登陸方式,先調用wx.login()
獲取code,再用wx.request()
請求開發者服務器,成功返回自定義登陸態(通常爲access_token或其餘令牌形式),以後再用自定義登陸態請求業務數據。java
爲了方便看,我把官方文檔裏的登陸流程貼出來⬇️ git
思路肯定後,開始嘗試coding(如下代碼不建議看完)es6
/* 如下爲Page對象的方法 */
getNearby: function() {
// 判斷是否已認證,可採用wx.checkSession()方案
if (isAuth) {
// TODO: 獲取業務數據
return
}
// wx.login獲取code
wx.login({
success(res) {
if (res.code) {
// 獲取自定義登陸態
wx.request({
url,
method,
headers,
data,
success(res) {
// 請求成功
if (res.statuCode === 200) {
// 讀取響應體中的自定義登陸態
let token = res.data.token
// 保存自定義登陸態
wx.setStorageSync("assess_token", token)
// 獲取位置信息
wx.getLocation({
success(res) {
let { latitude, longitude } = res
// 請求業務數據
wx.request({
url,
method,
header,
data: { latitude, longitude },
success(res) {
// 請求成功
if (res.statuCode === 200) {
let data = res.data
// 數據渲染到V層
this.setData({ list: data })
}
// 請求失敗
else if (res.statuCode === 400) {
// TODO
}
// 其餘錯誤狀況狀態碼處理
// TODO
},
fail(err) {
// 調用失敗處理
}
})
},
fail(err) {
// 調用失敗處理
}
})
}
// 請求失敗
else if (res.statuCode == 400) {
// TODO
}
// 其餘錯誤狀況的狀態碼處理
},
fail(err) {
// 調用失敗處理
}
})
}
else {
// TODO
// 登陸失敗
}
},
fail(err) {
// wx.login()調用失敗處理
// TODO: ...
}
})
}
複製代碼
回調地獄出現了。氣功波代碼,別說別人,就連本身看都會以爲噁心。小程序
某天英明的產品經理站了出來,說咱們能夠加點XXXXX,你可能還得找個地方嵌套其餘微信接口或者多加幾個if else
分支,到時候就找個地方哭吧。後端
從某種意義上來講,當今風暴式的前端生態,仰仗於Node以及ES6+的出現。微信小程序
ES6後對於異步有多種解決方案。一種是採用generator/yield
,但generator
函數使用起來其實比較麻煩。另一種是採用Promise
,相對比較簡單。ES7也能夠採用async/await
,但本質上async/await
也是基於Promise
。下面介紹Promise
。api
建立Promise很簡單,Promise
自己是一個構造函數。經過new
建立。構造函數的參數爲一個回調函數,回調函數有兩個參數爲resolve
和reject
(無需手動維護)。resolve
和reject
是用來改變狀態。關於狀態放到後邊講。數組
// Promise實例的建立
let p = new Promise((resolve, reject) => {
// TODO
})
複製代碼
Promise有個缺點,一旦建立便會馬上執行。因此通常會用一個函數進行包裝。
let getPromise = () => {
return new Promise((resolve, reject) => {
// TODO
})
}
複製代碼
Promise實例有三種狀態,pending
、resolved
和rejected
,Promise實例建立後就會處於pending
狀態。回調函數中的resolve
和reject
就是用來改變Promise實例狀態的。當調用resolve
時,Promise實例會從pending
變成resolved
狀態,表示成功。當調用reject
時,Promise實例會從pending
變成rejected
狀態,表示失敗。
let getPromise = () => {
return new Promise((resolve, reject) => {
// TODO
// 處理結果
if (result) {
resolve(successObject)
}
else {
reject(error)
}
})
}
複製代碼
最經常使用的方法爲then()
和catch()
這兩個方法,經過then()
的傳遞效用就能夠解決回調地獄的問題。
其中then()
可接收兩個參數,都是回調函數,第一個回調函數用來處理resolved
狀態,參數爲Promise實例調用resolve
傳遞的成功對象。第二回調函數用來處理rejected
狀態,參數爲調用Promise實例調用reject
傳遞的錯誤對象。
實際中then()
咱們通常只用來處理resolved
的狀況,即只傳遞第一個回調函數。對於rejected
狀況更可能是採用catch()
統一處理。
let getPromise = () => {
return new Promise((resolve, reject) => {
// TODO
// 處理結果
if (result) {
resolve(successObject)
}
else {
reject(error)
}
})
}
getPromise()
.then(res => {
console.log(res)
// TODO
})
.catch(err => {
//TODO
})
複製代碼
使用then()
方法能夠繼續返回一個Promise對象,經過return
一個新的Promise,能夠持續的向下傳遞。
getPromise()
.then(res => { //第一層Promise
console.log(res)
// TODO
return getPromise()
)
.then(res => { // 第二層Promise
console.log(res)
// TODO
})
.catch(err => {
// TODO
})
複製代碼
其餘經常使用方法有諸如Promise.all()
,Promise.race()
。當須要等待多個Promise結果時會採用。兩個方法都是接收一個由Promise組成的對象數組。使用Promise.all()
時,只有當所有的Promise對象所有resolved
Promise.all()
狀態纔是resolved
。而Promise.race()
只需有一個Promise對象爲resolved
時,其狀態就爲resolved
。
更多方法可閱讀相關文檔。
學習了Promise基礎,經過封裝異步操做,使用Promise鏈就能夠解決回調地獄問題。
由於wx.request()
使用頻率比較高,先對wx.request()
封裝。
/* 能夠將公用的方法掛在app.js中 */
request: function(method, url, header, data) {
return new Promise((resolve, reject) => {
wx.request({
method,
url,
header,
data,
success(res) {
resolve(res)
},
fail(err) {
reject(err)
}
})
})
}
複製代碼
基本框架就這樣,咱們能夠進一步修改,好比請求url的基礎路徑,添加一些公用的header,針對狀態碼作一些全局處理等。
request: function(method, url, header = {}, data = {}) {
// 啓動時可將storage中的令牌掛到app.js
let token = app.assess_token
if (token) {
header["Authorization"] = token
}
return new Promise((resolve, reject) => {
wx.request({
method,
url: "https://api.domain.com/v1" + url,
header,
data,
success(res) {
// 請求成功
if (res.statusCode === 200) {
resolve(res)
}
// 請求成功無響應體
else if (res.statusCode === 204) {
/* 可作一些成功提示, 如調用wx.showToast()、wx.showModal()或自定義彈出層等 */
resolve(res)
}
// 未認證
else if (res.statusCode === 401) {
/* 可作一些錯誤提示,或者直接跳轉至登陸頁面等 */
reject(res)
}
else if (res.statusCode == 400) {
/* 可作一些錯誤提示*/
reject(res)
}
else if (res.statuCode === 403) {
/* 無權限錯誤提示*/
reject(res)
}
// ...其餘狀態碼處理
},
fail(err) {
/* 可作一些全局錯誤提示,如網絡錯誤等 */
reject(err)
}
})
})
}
複製代碼
封裝以後,舉個例子,發送請求就能夠修改成
/* 方法體中 */
let app = getApp()
app.request("POST", "/auth", {}, { username, password })
.then(res => { // 第一層請求
// TODO 成功處理
return app.request("GET", "/goods", {}, {})
})
.then(res => { // 第二層請求
// TODO 成功處理
// 渲染視圖
})
.catch(err => {
// TODO 錯誤處理
})
複製代碼
封裝一下其餘的微信接口
/* 能夠將公用的方法掛在app.js中 */
wxLogin: function() {
return new Promise((resovle, reject) => {
wx.login({
success(res) {
if (res.code) {
resovle(res)
}
else {
reject({ message: "登陸失敗" })
}
},
fail(err) {
reject(err)
}
})
})
}
getLocation: function() {
return new Promise((resolve, reject) => {
wx.getLocation({
success(res) {
resolve(res)
},
fail(err) {
reject(err)
}
})
})
}
複製代碼
對於最初的例子,能夠就修改成
/* Page對象的方法 */
getNearby: function() {
// 判斷是否已認證,可採用wx.checkSession()方案
if (isAuth) {
// TODO: 獲取業務數據
return
}
app.wxLogin()
.then(res => {
// 將code發送給開發者服務器,獲取自定義登陸態
return app.request("POST", "/auth", {}, { code, res.code })
})
.then(res => {
// 保存自定義登陸態
setStorage("access_token", res.data.access_token)
// TODO: 其餘登陸成功操做...
return app.getLocation()
})
.then(({ latitude, longitude }) => {
let url = "/nearby?latitude=" + latitude + "&longitude=" + longitude
return app.request("GET", url)
})
.then(res => {
// TODO: 數據處理
let data = res.data
// 渲染視圖層
this.setData({ data })
})
.catch(err => {
// TODO 錯誤處理
})
}
複製代碼
以後如有需添加新的請求或者其餘異步操做,直接在Promise鏈上操做就好了。