vue中async-await的使用誤區

前言

曾經見過爲了讓鉤子函數的異步代碼能夠同步執行而且阻塞主線程直到鉤子所有邏輯處理完,而對鉤子函數使用async/await,就好像下面的代碼:javascript

// exp-01
export default {
  async created() {
    const timeKey = 'cost';
    console.time(timeKey);
    console.log('start created');

    this.list = await this.getList();

    console.log(this.list);
    console.log('end created');
    console.timeEnd(timeKey);
  },

  mounted() {
    const timeKey = 'cost';
    console.time(timeKey);
    console.log('start mounted');
    
    console.log(this.list.rows);

    console.log('end mounted');
    console.timeEnd(timeKey);
  },

  data() {
    return {
      list: []
    };
  },

  methods: {
    getList() {
      return new Promise((resolve) => {
        setTimeout(() => {
          return resolve({
            rows: [
              { name: 'isaac', position: 'coder' }
            ]
          });
        }, 3000);
      });
    }
  }
};
複製代碼

exp-01的代碼最後會輸出:html

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 3171.545166015625ms
複製代碼

很明顯沒有達到預期的效果,爲何?vue

根據exp-01的輸出結果,能夠看出代碼的執行順序,首先是鉤子的執行順序:java

created => mounted
複製代碼

是的,鉤子的執行順序仍是正常的沒有被打亂,證據就是:created鉤子中的同步代碼是在mounted先執行的:ios

start created
start mounted
複製代碼

再看看created鉤子內部的異步代碼:git

this.list = await this.getList();
複製代碼

能夠看見this.list的打印結果github

end mounted
mounted cost: 2.88623046875ms
// 這是created鉤子打印的this.list
{__ob__: Observer}
end created
複製代碼

在mounted鉤子執行完畢以後纔打印,言外之意是使用async/await的鉤子內部的異步代碼並無起到阻塞鉤子主線程的執行。這裏說的鉤子函數的主線程是指:axios

beforeCreate => created => beforeMount => mounted => ...
複製代碼

會寫出以上代碼的緣由我估計有兩個:瀏覽器

  1. 對異步代碼返回的數據有強依賴,所以但願在mounted鉤子內容調用完成前,先執行完created的內容(包括異步代碼的回調函數);
  2. 僅僅是但願鉤子函數的異步代碼能夠按照編寫順序執行(exp-01是確實實現了的),但卻沒想到帶來了反作用;

正文

剖析一下

前言中針對代碼的執行流程分析了一下,很明顯沒有如指望的順序執行,咱們先來回顧一下指望的順序是什麼bash

// step 1
created() {
  // step 1.1
  let endTime;
  const startTime = Date.now();
  console.log(`start created: ${startTime}ms`);
  // step 1.2
  this.list = await this.getList();
  endTime = Date.now();
  console.log(this.list);
  console.log(`end created: ${endTime}ms, cost: ${endTime - startTime}ms`);
},
// step 2
mounted() {
  let endTime;
  const startTime = Date.now();
  console.log(`start mounted: ${startTime}ms`);
  console.log(this.list.rows);
  endTime = Date.now();
  console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
}

// step 1 => step 1.1 => step 1.2 => step 2 
複製代碼

指望的打印結果是:

// step 1(created)
start created
// this.list
{__ob__: Observer}
end created
created cost: 3171.545166015625ms

// step 2(mounted)
start mounted
// this.list.rows
[{…}, __ob__: Observer]
end mounted
mounted cost: 2.88623046875ms

複製代碼

對比實際的打印和指望的打印,就知道問題出在created鉤子內使用了await的異步代碼,並無達到咱們指望的那種的「異步代碼同步執行」的效果,僅僅是必定程度上達到了這個效果。

下面來分析一下爲何會出現這個非預期的結果!

在分析前,讓咱們來回顧一下一些javascript的基礎知識!看看下面這段代碼:

(function __main() {
  console.log('start');
  setTimeout(() => {
    console.log('console in setTimeout');
  }, 0);
  console.log('end');
})()

// output
start
end
console in setTimeout
複製代碼

這個打印順序有沒有讓你想到什麼?!

 

任務隊列!

 

咱們都知道JavaScript的代碼能夠分紅兩類:

同步代碼異步代碼

同步代碼會在主線程按照編寫順序執行;

異步代碼的觸發過程(注意是觸發,好比異步請求的發起,就是在主線程同步觸發的)是同步的,可是異步代碼的實際處理邏輯(回調函數)則會在異步代碼有響應時將處理邏輯代碼推入任務隊列(也叫事件隊列),瀏覽器會在主線程(指當前執行環境的同步代碼)代碼執行完畢後以必定的週期檢測任務隊列,如有須要處理的任務,就會讓隊頭的任務出隊,推入主線程執行。

好比如今咱們發起一個異步請求:

// exp-02
console.log('start');
axios.get('http://xxx.com/getList')
  .then((resp) => {
    console.log('handle response');
  })
  .catch((error) => {
    console.error(error);
  });
console.log('end');
複製代碼

在主線程中,大概首先會發生以下過程:

// exp-03
// step 1
console.log('start');

// step 2
axios.get('http://xxx.com/getList');  // 此時回調函數(即then內部的邏輯)尚未被調用

// step 3
console.log('end');
複製代碼

在看看瀏覽器此時在幹什麼!

此時事件輪詢(Event Loop)登場,其實並不是此時才登場,而是一直都在!

「事件輪詢」這個機制會以必定的週期檢測任務隊列有沒有可執行的任務(所謂任務其實就是callback),有即出隊執行。

step 2的請求有響應了,異步請求的回調函數就會被添加到任務隊列(Task Queue)或者 稱爲 事件隊列(Event Queue),而後等到事件輪詢的下一次檢測任務隊列,隊列裏面任務就會依次出隊,進入主線程執行:即執行下面的代碼:

// 假定沒有出錯的話
((resp) => {
  console.log('handle response');
})()
複製代碼

到此,簡短科普了任務隊列的機制,聯想exp-01的代碼,大概知道出現非預期結果的緣由了吧!

created鉤子中的await函數,雖然是在必定程度上是同步的,可是他仍是被掛起了,實際的處理邏輯(this.list = resp.xxx)則在響應完成後才被添加進任務隊列,而且在主線程的同步代碼執行完畢後執行。 下面是將延時時間設爲0後的打印:

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 9.76611328125ms
複製代碼

這側面說明了await函數確實被被掛起,回調被添加到任務隊列,在主線程代碼執行完畢後等待執行。  

而後是爲何說exp-01的代碼是必定程度的同步呢?!

同步執行的另外一個意思是否是就是:阻塞當前線程的繼續執行直到當前邏輯執行完畢~

看看exp-01的打印:

{__ob__: Observer}
end created
created cost: 3171.545166015625ms
複製代碼

end created這句打印,是主線程的代碼,若是是通常的異步請求的話,這句打印應該是在{__ob__: Observer}這句打印以前的yo,至於爲何會這樣,這裏就很少解析,自行google!

另外,這裏來個小插曲,你應該注意到,我一直強調,回調函數被添加進任務隊列的時機是在響應完成以後,沒錯確實如此的!

但在不清除這個機制前,你大概會有兩種猜測:

  1. 在觸發異步代碼的時,處理邏輯就會被添加進任務隊列;
  2. 上面說到的,在異步代碼響應完成後,處理邏輯纔會被添加進任務隊列;

其實大可推斷一下

隊列的數據結構特徵是:先進先出(First in First out)

此時假如主線程中有兩個異步請求以下:

// exp-04
syncRequest01(callback01);
syncRequest02(callback02);
複製代碼

假設處理機制是第一點描述那樣,那麼callback01就會先被添加進任務隊列,而後是callback02。

而後,咱們再假設syncRequest01的響應時間是10s,syncRequest02的響應時間是5s。

到這裏,有沒有察覺到違和感!

異步請求的實際表現是什麼?是誰快誰的回調先被執行,對吧!那麼實際表現就是callback02會先於callback01執行!

那麼基於這個事實,再看看上面的假設(callback01會執行)~

ok!插曲完畢!

解法

首先讓我回顧一下目的,路由組件對異步請求返回的數據有強依賴,所以但願阻塞組件的渲染流程,待到異步請求響應完畢以後再執行。

這就是咱們須要作的事情,須要強調的一點是:咱們對數據有強依賴,言外之意就是數據沒有按預期返回,就會致使以後的邏輯出現不可避免的異常。

接下來,咱們就須要探討一下解決方案!

組件內路由守衛瞭解一下!?

beforeRouteEnter

beforeRouteUpdate (2.2 新增)

beforeRouteLeave

這裏須要用到的路由守衛是:beforeRouterEnter, 先看代碼:

// exp-05
const storage = {};
export default {
  beforeRouteEnter(to, from, next) {
    showLoading();
    getList()
      .then((resp) => {
        hideLoading();
        storage.list = resp.data;
        next();
      })
      .catch((error) => {
        hideLoading();
        // handle error
      });
  },

  mounted() {
    let endTime;
    const startTime = Date.now();
    console.log(`start mounted: ${startTime}ms`);
    console.log(storage.list.rows);
    endTime = Date.now();
    console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
  },
};
複製代碼

路由守衛beforeRouterEnter,觸發這個鉤子後,主線程都會阻塞,頁面會一直保持假死狀態,直到在調用beforeRouterEnter的回調函數next,纔會跳轉路由進行新路由組件的渲染。

看起這個解決方案至關適合上面咱們提出的需求,在調用next前,就能夠去拉取數據!

可是如剛剛說到的,頁面在一直假死,加入數據獲取花費時間過長就不免變得很難看,用戶體驗未免太差

爲此,在exp-05中我在請完成先後分別調用了showLoading()hideLoading()以便頁面keep-alive

這個處理假死的loading有沒有讓你想到寫什麼,沒錯就是下面這個github跳轉頁面是頂部的小藍條

想一想就有點cool,固然還有不少的實現方式提高用戶體驗,好比做爲body子元素的全屏loading,或者button-loading等等……

固然,咱們知道阻塞主線程怎麼都是阻塞了,loading只是一種自欺欺人式的優化(此時這個成語可不是什麼貶義的詞語)!

所以,不是對數據有很是強的依賴,都應在路由的鉤子進行數據抓取,這樣就可讓用戶「更快」地跳轉到目的頁。爲避免頁面對數據依賴拋出的異常(大概就是 undefined of xxx),咱們能夠對初始數據進行一些預設,好比exp-01中對this.list.rows的依賴,咱們能夠預設this.list

list: {
  rows: []
}
複製代碼

這樣就不會拋出異常,待到異步請求完成,基於vue的update機制二次渲染咱們的預期數據~

修改後的聲明

在修改前,原有的解決方式是:

// exp-05
export default {
  beforeRouteEnter(to, from, next) {
    this.showLoading();
    this.getList()
      .then((resp) => {
        this.hideLoading();
        this.list = resp.data;
        next();
      })
      .catch((error) => {
        this.hideLoading();
        // handle error
      });
  },

  mounted() {
    let endTime;
    const startTime = Date.now();
    console.log(`start mounted: ${startTime}ms`);
    console.log(this.list.rows);
    endTime = Date.now();
    console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
  },
};
複製代碼

通過的評論中的大佬們教作人後,針對如下兩點作了修改:

  1. beforeRouteEnter的函數體中是訪問不到當前組件的上下文的,須要在回調參參數next(這是個函數引用)中,使用next這個函數的回調參數vm(next(vm => {}))中的vm才能訪問到當前組件的上下文;
  2. 雖然vm能夠訪問到組件的上下文,可是有個問題,就是你經過vm來作get/set,這個get/set動做是在beforeCreatecreatedbeforeMountmounted這些鉤子執行以後~這樣的話,beforeRouterEnter的阻塞做用基本就廢掉了!

小結

對於exp-01的寫法,也不能說他是錯誤或很差的寫法,凡事都要看咱們是出於什麼目的,若是僅僅是爲了保證多個異步函數的執行順序,exp-01的寫法沒有任何錯誤,所以async/await不能用在路由鉤子上什麼的並不存在!

it just a tool!

歡迎提出不一樣得看法,提bug和評論,咱們issue相見

相關文章
相關標籤/搜索