在了解了一些實用的開發技巧和編碼理念后,我們在項目的開發過程中難免也會遇到因為不熟悉 Vue API 而導致的技術問題,而往往就是這樣的一些問題消耗了我們大量的開發時間,造成代碼可讀性下降、功能紊亂甚至 `bug` 量的增加,其根本原因還是自己對 Vue API 的 “**無知**”。
本文將介紹 Vue 項目開發中比較難以理解并可能被你忽視的 API,唯有知己知彼,才能百戰不殆。
## API 解析
### 使用 performance 開啟性能追蹤
`performance API` 是 Vue 全局配置 API 中的一個,我們可以使用它來進行網頁性能的追蹤,我們可以在入口文件中添加:
```
if (process.env.NODE_ENV !== 'production') {
Vue.config.performance = true;
}
```
來開啟這一功能,該 API(2.2.0 新增)功能只適用于開發模式和支持 `performance.mark` API 的瀏覽器上,開啟后我們可以下載 [Vue Performance Devtool](https://chrome.google.com/webstore/search/vue%20performance%20devtool) 這一 chrome 插件來看查看各個組件的加載情況,如圖:

從中我們可以清晰的看到頁面組件在每個階段的耗時情況,而針對耗時比較久的組件,我們便可以對其進行相應優化。
而其在 Vue 源碼中主要使用了 [window.performance](https://developer.mozilla.org/zh-CN/docs/Web/API/Performance) 來獲取網頁性能數據,其中包含了 `performance.mark` 和 `performance.measure`。
* performance.mark 主要用于創建標記
* performance.measure 主要用于記錄兩個標記的時間間隔
例如:
```
performance.mark('start'); // 創建 start 標記
performance.mark('end'); // 創建 end 標記
performance.measure('output', 'start', 'end'); // 計算兩者時間間隔
performance.getEntriesByName('output'); // 獲取標記,返回值是一個數組,包含了間隔時間數據
```
熟練的使用 performance 我們可以查看并分析網頁的很多數據,為我們項目優化提供保障。除了上述介紹的兩個方法,我們還可以使用 `performance.timing` 來計算頁面各個階段的加載情況,關于 performance.timing 的介紹可以查看我之前寫的一篇文章:[利用 Navigation Timing 測量頁面加載時間](https://www.cnblogs.com/luozhihao/p/4681564.html)
### 使用 errorHandler 來捕獲異常
在瀏覽器異常捕獲的方法上,我們熟知的一般有:`try ... catch` 和 `window.onerror`,這也是原生 JavaScript 提供給我們處理異常的方式。但是在 Vue 2.x 中如果你一如既往的想使用 window.onerror 來捕獲異常,那么其實你是捕獲不到的,因為異常信息被框架自身的異常機制捕獲了,你可以使用 `errorHandler` 來進行異常信息的獲取:
```
Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 異常信息
name, // 異常名稱
stack // 異常堆棧信息
} = err;
// vm 為拋出異常的 Vue 實例
// info 為 Vue 特定的錯誤信息,比如錯誤所在的生命周期鉤子
}
```
在入口文件中加入上述代碼后,我們便可以捕獲到 Vue 項目中的一些異常信息了,但是需要注意的是 Vue 2.4.0 起的版本才支持捕獲 Vue 自定義事件處理函數內部的錯誤,比如:
```
<template>
<my-component @eventFn="doSomething"></my-component>
</template>
<script>
export default {
methods: {
doSomething() {
console.log(a); // a is not defined
}
}
}
</script>
```
使用 Vue 中的異常捕獲機制,我們可以針對捕獲到的數據進行分析和上報,為實現前端異常監控奠定基礎。關于對異常捕獲的詳細介紹,感興趣的同學可以查看我的這篇文章:[談談前端異常捕獲與上報](https://www.cnblogs.com/luozhihao/p/8635507.html)
### 使用 nextTick 將回調延遲到下次 DOM 更新循環之后執行
在某些情況下,我們改變頁面中綁定的數據后需要對新視圖進行一些操作,而這時候新視圖其實還未生成,需要等待 DOM 的更新后才能獲取的到,在這種場景下我們便可以使用 nextTick 來延遲回調的執行。比如未使用 `nextTick` 時的代碼:
```
<template>
<ul ref="box">
<li v-for="(item, index) in arr" :key="index"></li>
</ul>
</template>
<script>
export default {
data() {
return {
arr: []
}
},
mounted() {
this.getData();
},
methods: {
getData() {
this.arr = [1, 2, 3];
this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
}
}
}
</script>
```
上方代碼我們在實際運行的時候肯定會報錯,因為我們獲取 DOM 元素 li 的時候其還未被渲染,我們將方法放入 nextTick 回調中即可解決該問題:
```
this.$nextTick(() => {
this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
})
```
當然你也可以使用 ES6 的 `async/await` 語法來改寫上述方法:
```
methods: {
async getData() {
this.arr = [1, 2, 3];
await this.$nextTick();
this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
}
}
```
那么接下來我們來分析下 Vue 是如何做到的,其源碼中使用了 3 種方式:
* promise.then 延遲調用
* setTimeout(func, 0) 延遲功能
* MutationObserver 監聽變化
前兩種方式相信大家都比較熟悉,其都具備延遲執行的功能,我們也可以直接替換 nextTick 為這兩種方式中的一種,同樣可以解決問題。這里主要介紹下 [MutationObserver](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver) 這一 HTML5 新特性,那么什么是 `MutationObserver` 呢?用一句話介紹就是:我們可以使用它創建一個觀察者對象,其會監聽某個 DOM 元素,并在它的 DOM 樹發生變化時執行我們提供的回調函數。實例化代碼及配置如下:
```
// 傳入回調函數進行實例化
var observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
console.log(mutation.type);
})
});
// 選擇目標節點
var target = document.querySelector('#box');
// 配置觀察選項
var config = {
attributes: true, // 是否觀察屬性的變動
childList: true, // 是否觀察子節點的變動(指新增,刪除或者更改)
characterData: true // 是否觀察節點內容或節點文本的變動
};
// 傳入目標節點和觀察選項
observer.observe(target, config);
// 停止觀察
observer.disconnect();
```
這樣我們便可以觀察 id 為 box 下的 DOM 樹變化,一旦發生變化就會觸發相應的回調方法,實現延遲調用的功能。
### 使用 watch 的深度遍歷和立即調用功能
相信很多同學使用 `watch` 來監聽數據變化的時候通常只使用過其中的 `handler` 回調,其實其還有兩個參數,便是:
* deep 設置為 true 用于監聽對象內部值的變化
* immediate 設置為 true 將立即以表達式的當前值觸發回調
我們來看下代碼中的配置:
```
<template>
<button @click="obj.a = 2">修改</button>
</template>
<script>
export default {
data() {
return {
obj: {
a: 1,
}
}
},
watch: {
obj: {
handler: function(newVal, oldVal) {
console.log(newVal);
},
deep: true,
immediate: true
}
}
}
</script>
```
以上代碼我們修改了 obj 對象中 a 屬性的值,我們可以觸發其 watch 中的 handler 回調輸出新的對象,而如果不加 `deep: true`,我們只能監聽 obj 的改變,并不會觸發回調。同時我們也添加了 `immediate: true` 配置,其會立即以 obj 的當前值觸發回調。
在 Vue 源碼中,主要使用了 [Object.defineProperty (obj, key, option)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) 方法來實現數據的監聽,同時其也是 Vue 數據雙向綁定的關鍵方法之一。示例代碼如下:
```
function Observer() {
var result = null;
Object.defineProperty(this, 'result', {
get: function() {
console.log('你訪問了 result');
return result;
},
set: function(value) {
result = value;
console.log('你設置了 result = ' + value);
}
});
}
var app = new Observer(); // 實例化
app.result; // 你訪問了 result
app.result = 11; // 你設置了 result = 11
```
我們通過實例化了 `Observer` 方法來實現了一個簡單的監聽數據訪問與變化的功能。`Object.defineProperty` 是 ES5 的語法,這也就是為什么 Vue 不支持 IE8 以及更低版本瀏覽器的主要原因。
### 對低開銷的靜態組件使用 v-once
Vue 提供了 `v-once` 指令用于只渲染元素和組件一次,一般可以用于存在大量靜態數據組件的更新性能優化,注意是大量靜態數據,因為少數情況下我們的頁面渲染會因為一些靜態數據而變慢。如果你需要對一個組件使用 v-once,可以直接在組件上綁定:
```
<my-component v-once :data="msg"></my-component>
```
這時候因為組件綁定了 v-once,所以無論 msg 的值如何變化,組件內渲染的永遠是其第一次獲取到的初始值。因此我們在使用 v-once 的時候需要考慮該組件今后的更新情況,避免不必要的問題產生。
### 使用 $isServer 判斷當前實例是否運行于服務器
當我們的 Vue 項目中存在服務端渲染(SSR)的時候,有些項目文件可能會同時在客戶端和服務端加載,這時候代碼中的一些客戶端瀏覽器才支持的屬性或變量在服務端便會加載出錯,比如 window、 document 等,這時候我們需要進行環境的判斷來區分客戶端和服務端,如果你不知道 `$isServer`,那么你可能會使用 `try ... catch` 或者 `process.env.VUE_ENV` 來判斷:
```
try {
document.title = 'test';
} catch(e) {}
// process.env.VUE_ENV 需要在 webpack 中進行配置
if (process.env.VUE_ENV === 'client') {
document.title = 'test';
}
```
而使用 $isServer 則無需進行配置,在組件中直接使用該 API 即可:
```
if (this.$isServer) {
document.title = 'test';
}
```
其源碼中使用了 `Object.defineProperty` 來進行數據監測:
```
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
});
var _isServer;
var isServerRendering = function () {
if (_isServer === undefined) {
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
_isServer = global['process'].env.VUE_ENV === 'server';
} else {
_isServer = false;
}
}
return _isServer
};
```
當我們訪問 $isServer 屬性時,其會調用 `isServerRendering` 方法,該方法會首先判斷當前環境,如果在瀏覽器或者 Weex 下則返回 false,否則繼續判斷當前全局環境下的 `process.env.VUE_ENV` 是否為 server 來返回最終結果。
## 結語
每一門語言、一個框架都有其 API 文檔,在 Vue 的項目開發過程中,很多時候當你一籌莫展之際,你可以嘗試瀏覽一下 Vue 的 API 列表,或許你就會柳暗花明。
## 思考 & 作業
* 使用 watch 監聽某一值時,同時修改該值兩次會觸發幾次 watch 回調?
* 使用 `errorHandler` 捕獲異常堆棧后如何解析 `source-map` 信息?
* 除了本文介紹的 Vue 盲點外,還有哪些需要注意并容易忽略的 API?
- 開篇:Vue CLI 3 項目構建基礎
- 構建基礎篇 1:你需要了解的包管理工具與配置項
- 構建基礎篇 2:webpack 在 CLI 3 中的應用
- 構建基礎篇 3:env 文件與環境設置
- 構建實戰篇 1:單頁應用的基本配置
- 構建實戰篇 2:使用 pages 構建多頁應用
- 構建實戰篇 3:多頁路由與模板解析
- 構建實戰篇 4:項目整合與優化
- 開發指南篇 1:從編碼技巧與規范開始
- 開發指南篇 2:學會編寫可復用性模塊
- 開發指南篇 3:合理劃分容器組件與展示組件
- 開發指南篇 4:數據驅動與拼圖游戲
- 開發指南篇 5:Vue API 盲點解析
- 開發拓展篇 1:擴充你的開發工具
- 開發拓展篇 2:將 UI 界面交給第三方庫
- 開發拓展篇 3:嘗試使用外部數據
- 總結篇:寫在最后