文末有福利
面試時,很經常會說對某某項目進行了性能優化,使性能有很大的提高之類的話。如果面試官問,來講講做了那些優化,這時候你就要很清晰地把你做過的優化一一說出來。
本文謹以自己的Vue項目經驗來教你怎么在面試中說優化,如果有錯誤,有不足的,請指教,不吝指教,一起進步,找到更好的工作。
一、開場白
我個人認為性能優化可以從三個方面來進行,一是代碼層面的優化,二是項目打包的優化,三是項目部署的優化。
開場白切記要簡短,概況一下你接下來說的內容,不要長篇大論,面試官沒那么多時間聽你瞎扯。
下面教大家從常見優化一一說起。另外所有優化都是有原理,希望大家能接得住面試官的深層次的考察。
二、代碼層面的優化
1、利用v-if和v-show減少初始化渲染和切換渲染的性能開銷
在頁面加載時,利用v-if來控制組件僅在首次使用時渲染減少初始化渲染,隨后用v-show來控制組件顯示隱藏減少切換渲染的性能開銷。
也許上面描述不夠清楚,下面用一個實例來加深印象。
iview彈窗組件大家很經常用吧,彈窗組件是用v-show來控制其顯示和隱藏,那么在頁面加載時,彈窗組件(包括里面的內容)是會被初始化渲染的,如果一個頁面中只有一個彈窗組件,不會對性能造成影響,但是假如一個頁面中有幾十個彈窗組件,會不會影響到性能,大家可以做一個右鍵菜單去試一下。
ps: 偷偷告訴大家,element彈窗組件初次渲染時,彈窗body里面的內容不會被渲染的。
下面用代碼來實現一下。
<template>
<div>
<Button type="primary" @click.native="add">添加</Button>
<add v-model="add.show" v-bind="add"></add>
</div>
</template>
<script>
export default{
data(){
return{
add:{
show:false,
init:false
}
}
},
components:{
add:() =>import('./add.vue')
},
methods:{
add(){
this.add.show=true;
this.add.init=true
}
}
}
</script>
復制代碼
<template>
<div v-if="init">
<Modal v-model="show" title="添加" @on-cancel="handleClose"></Modal>
</div>
</template>
<script>
export default{
props:{
value:{
type:Boolean,
default:false
},
init:{
type:Boolean,
default:false
}
},
data(){
return{
show:false,
}
},
watch:{
value(val){
if(val){
this.show = val;
}
}
},
methods:{
handleClose(val) {
this.$emit('input', val);
},
}
}
</script>
復制代碼
原理:
v-if
綁定值為false時,初始渲染時,** 不會** 渲染其條件塊。
v-if
綁定值,在true和false之間切換時,** 會 **銷毀和重新渲染其條件塊。
v-show
綁定值不管為true還是為false,初始渲染時,總是 **會 **渲染其條件塊。
v-show
綁定值,在true和false之間切換時,** 不會 **銷毀和重新渲染其條件塊,只是用 display:none
樣式來控制其顯示隱藏。
2、computed、watch、methods區分使用場景
對于有些需求,computed、watch、methods都可以實現,但是還是要區分一下使用場景。用錯場景雖然功能實現了但是影響了性能。
computed
:
computed
watch
:
一個數據影響多個數據的。
當數據變化時,需要執行異步或開銷較大的操作時。如果數據變化時請求一個接口。
methods
:
希望數據是實時更新,不需要緩存。
3、提前處理好數據解決v-if和v-for必須同級的問題
因為當Vue處理指令時,v-for
比v-if
具有更高的優先級,意味著 v-if
將分別重復運行于每個v-for
循環中。
//userList.vue
<template>
<div>
<div v-for="item in userList" :key="item.id" v-if="item.age > 18">{{ item.name }}</div>
</div>
</template>
復制代碼
//userList.vue
<template>
<div>
<div v-for="item in userComputedList" :key="item.id">{{ item.name }}</div>
</div>
</template>
export default {
computed:{
userComputedList:function(){
return this.userList.filter(function (item) {
return item.age > 18
})
}
}
}
復制代碼
也許面試官還會為什么v-for
比 v-if
具有更高的優先級?這個問題已經涉及到原理層次,如果這個也會回答,會給面試加分不少。
上面說到 “v-if
將分別重復運行于每個v-for
循環中”,這個過程只有在渲染頁面時才有,而Vue最終是通過render函數來渲染頁面的,先把組件編譯生成的render打印出來。
//home.vue
<script>
import userList from './userList'
console.log(userList.render)
</script>
復制代碼
打印出來的內容如下所示
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h
return _c(
"div",
_vm._l(_vm.userList, function(item) {
return item.age > 18
? _c("div", { key: item.id }, [_vm._v(_vm._s(item.name))])
: _vm._e()
}),
0
)
}
var staticRenderFns = []
render._withStripped = true
export { render, staticRenderFns }
復制代碼
其中 _l
方法是v-for
指令通過 genFor
函數生成的renderList方法, item.age > 18?
是 v-if
指令通過genIf
函數生成的三元運算符的代碼,_v
方法是createTextVNode方法用來創建文本節點,_e
方式是createEmptyVNode方法用來創建空節點。到這里是不是已經很清楚,v-if
運行在每個v-for
中。
歸根到底還是在生成render函數中,導致v-for
比v-if
具有更高的優先級,我們去render函數的生成過程中看一下。
Vue提供了2個版本,一個是Runtime + Compiler 的,一個是Runtime only 的,前者是包含編譯代碼的,可以把編譯過程放在運行時做,后者是不包含編譯代碼的,需要借助 webpack 的 vue-loader 事先把模板編譯成 render函數。
這里不研究vue-loader,所以用Runtime + Compiler來研究,也是用CDN引入Vue.js,此時Vue的入口在 src/platforms/web/entry-runtime-with-compiler.js 中。
const vm = new Vue({
render: h => h(App)
}).$mount('#app')
復制代碼
Vue實例是通過$mount
掛載到DOM上。在入口文件中尋找$mount
方法,在其方法中再找 render
字段,發現以下代碼
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
復制代碼
說明render函數是通過compileToFunctions方法生成,再去尋找compileToFunctions方法在哪里。
compileToFunctions方法在 src/platforms/web/compiler/index.js 中定義。
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
復制代碼
compileToFunctions方法又是createCompiler方法生成的,繼續尋找createCompiler方法。
createCompiler方法在 src/compiler/index.js 中定義。
export const createCompiler = createCompilerCreator(
function baseCompile(template,options) {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
復制代碼
在上面代碼中可以看到,render是code中的render,而code是generate方法生成的。
在這里額外提一下ast是什么,就是由tempalte生成的語法書,在執行generate方法前執行以下幾個邏輯
const ast = parse(template.trim(), options)
optimize(ast, options)
const code = generate(ast, options)
繼續尋找generate方法,其在 src/compiler/codegen/index.js 中定義。
export function generate (ast,options){
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
復制代碼
發現code是genElement方法生成的,繼續尋找genElement方法,其實這里已經解決根本原因了,給出幾行關鍵代碼。
export function genElement(el,state){
if(){
//...
}else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
}
}
復制代碼
由上述代碼可以看出,el.for就是v-for
,el.if就是 v-if
,el.for先于el.if判斷執行,所以 v-for
比v-if
具有更高的優先級。
另外在genFor方法最后面會繼續調用genElement方法,形成一級一級往下執行。
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
復制代碼
把尋找原因的整個思路都寫出來,就是讓小伙伴在看面試題時不要死記硬背,要去理解,可以按照上面的思路,自己去閱讀源碼找一下原因。畢竟閱讀源碼能力也是可以為面試加分的。
復制代碼
4、給v-for循環項加上key提高diff計算速度
一旦你給面試官講了此項優化,你要做好被面試官深入提問的準備如以下兩個問題。
為什么加key會提高diff計算速度。
經過舊頭新頭、舊尾新尾、舊頭新尾、舊尾新頭四次交叉比對后,都沒有匹配到值得比對的節點,這時如果新節點有key的話。可以通過map直接獲得值得對比的舊節點的下標,如果沒有key的話,就要通過循環舊節點數組用sameVnode方法判斷新節點和該舊節點是否值得比較,值得就返回該舊節點的下標。顯然通過map比通過循環數組的計算速度來的快。
什么是diff計算。
對于渲染watcher觸發時會執行 vm._update(vm._render(), hydrating)
,在 vm._undata
方法中會調用 vm.__patch__
,而 vm.__patch__
指向patch方法,diff計算是指在調用patch方法開始,用sameVnode方法判斷節點是否值得比較,若不值得直接新節點替換舊節點后結束。值得對比進入patchVnode方法,分別處理一下幾種情況,若新舊節點都有文本節點,新節點下的文本節點直接替換舊節點下的文本節點,如果新節點有子節點,舊節點沒有子節點,那么直接把新節點查到舊節點的父級中,如果新節點沒有子節點,舊節點有子節點,那么舊節點的父級下的子節點都刪了。如果新舊節點都有子節點,進入updateChildren方法,通過舊頭新頭、舊尾新尾、舊頭新尾、舊尾新頭四次交叉比對,如果值得對比再進入patchVnode方法,如果都不值得對比,有key用map獲得值得對比的舊節點,沒有key通過循環舊節點獲得值得對比的舊節點。當新節點都對比完,舊節點還沒對比完,將還沒對比完的舊節點刪掉。當舊節點都對比完,新節點還沒對比完,將新節點添加到最后一個對比過的新節點后面,完成diff計算。
這兩個問題是可以連續提問,一旦你答出第一個問題,可能會被繼續深入提問第二個問題。
以下是詳細過程,面試中也不可能表述那么詳細。主要是提供給大家理解用。
首先介紹一下什么diff計算,diff計算就是對比新舊虛擬DOM(virtual DOM),virtual DOM是將真實的DOM的數據抽取出來,以對象的形式模擬樹形結構,再說簡白一點,diff計算就是對兩個對象進行對比。
在采取diff算法比較新舊節點的時候,比較只會在同層級進行, 不會跨層級比較。
先上步驟圖,可以先看圖,再看文字介紹
每次對比的邏輯大概如下所示
1、在patch方法內,用sameVnode判斷新舊節點是否值得比較。
2、如果不值得比較,直接在舊節點的父級中添加新節點,然后刪除舊節點,退出對比。
3、如果值得比較,調用patchVnode方法。
4、如果新舊節點是否完全相等,如果是,退出對比。
5、如果不是,找到對應的真實DOM,記為el。
6、如果新舊節點都有文本節點并且不相等,那么將el的文本節點設置為新節點的文本節點,退出對比。
7、如果新節點有子節點,舊節點沒有子節點,則將新節點的子節點生成真實DOM后添加到el中,退出對比。
8、如果新節點沒有子節點,舊節點有子節點,則刪除el的子節點,退出對比。
9、如果新節點和舊節點都有子節點,則開始對比它們的子節點,用的是updateChildren方法。
10、將舊節點的子節點記為oldCh
是個數組,其頭部用 oldCh[oldStartIdx]
獲取記為 oldStartVnode
,oldStartIdx
初始為0。其尾部用oldCh[oldEndIdx]
獲取記為oldEndVnode
,oldEndIdx
初始為 oldCh.length - 1
。
11、將舊節點的子節點記為 newCh
是個數組,其頭部用 newCh[newStartIdx]
獲取記為 newStartVnode
,newStartIdx
初始為0。其尾部用newCh[newEndIdx]
獲取記為 newEndVnode
, newEndIdx
初始為newCh.length - 1
。
12、將舊子節點的頭部和新子節點的頭部,簡稱舊頭和新頭用sameVnode判斷是否值得比較。
13、如果值得比較,調用patchVnode方法,重新執行第3步。同時用 oldCh[++oldStartIdx]
重新獲取舊子節點頭部,用 newCh[++newStartIdx]
重新獲取新子節點頭部。
14、如果不值得比較,將舊子節點的尾部和新子節點的尾部,簡稱舊尾和新尾用sameVnode判斷是否值得比較。
15、如果值得比較,調用patchVnode方法,重新執行第3步。同時用oldCh[--oldEndIdx]
重新獲取舊子節點尾部,重新用 newCh[--newEndIdx]
獲取新子節點尾部。
16、如果不值得比較,將舊子節點的頭部和新子節點的尾部,簡稱舊頭和新尾用sameVnode判斷是否值得比較。
17、如果值得比較,調用patchVnode方法,重新執行第3步。同時將舊子節點的頭部oldStartVnode
對應的真實DOM移動到舊子節點的尾部oldEndVnode
對應的真實DOM后面。同時用 oldCh[++oldStartIdx]
重新獲取舊子節點頭部,用 newCh[--newEndIdx]
重新獲取新子節點尾部。
18、如果不值得比較,將舊子節點的尾部和新子節點的頭部,簡稱舊尾和新頭用sameVnode判斷是否值得比較。
19、如果值得比較,調用patchVnode方法,重新執行第3步。同時將舊子節點的尾部oldEndVnode
對應的真實DOM移動到舊子節點的頭部 oldStartVnode
對應的真實DOM后面。同時用 oldCh[--oldEndIdx]
重新獲取舊子節點尾部,用 newCh[++newStartIdx]
重新獲取新子節點頭部。
20、如果不值得比較,如果舊子節點有key,可以用createKeyToOldIdx方法獲得以舊子節點的key為健,其下標為值的map結構,記為oldKeyToIdx
。
21、如果新子節點的頭部 newStartVnode
有key屬性,直接通過 oldKeyToIdx[newStartVnode.key]
獲取對應的下標idxInOld
。
22、如果新子節點的頭部 newStartVnode
沒有key屬性,要用過findIdxInOld方法,找到值得對比的舊子節點對應的下標idxInOld
。
23、經過查找。如果 idxInOld
不存在。則調用createElm方法直接生成 newStartVnode
對應的真實DOM插入 oldStartVnode
對應真實DOM前面。
24、如果idxInOld
存在,則把用通過oldCh[idxInOld]
獲取到Vnode記為 vnodeToMove
和 newStartVnode
用sameVnode判斷是否值得比較。
25、如果值得比較,調用patchVnode方法,重新執行第3步。同時執行 oldCh[idxInOld] = undefined
,免得被重復比較。同時將 vnodeToMove
對應的真實DOM移動到舊子節點的頭部oldStartVnode
對應的真實DOM前面。
26、如果不值得比較,則調用createElm方法直接生成 newStartVnode
對應的真實DOM插入 oldStartVnode
對應真實DOM前面。
27、用 newCh[++newStartIdx]
重新獲取新子節點頭部
28、如果滿足 oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx
繼續執行步驟9。
29、如果oldStartIdx > oldEndIdx
,說明所有舊子節點已經都比對完了,還剩下未比對的新子節點都調用createElm方法生成對應的真實DOM,插到newCh[newEndIdx + 1]
對應的真實DOM后面。
30、如果 newStartIdx > newEndIdx
,說明所有新子節點都比較完,那么還剩下的舊子節點都刪除掉。
5、利用v-once處理只會渲染一次的元素或組件
只渲染元素和組件一次。隨后的重新渲染,元素/組件及其所有的子節點將被視為靜態內容并跳過。這可以用于優化更新性能。
例如某個頁面是合同范文,里面大部分內容從服務端獲取且是固定不變,只有姓名、產品、金額等內容會變動。這時就可以把 v-once
添加到那些包裹固定內容的元素上,當生成新的合同可以跳過那些固定內容,只重新渲染姓名、產品、金額等內容即可。
和v-if
一起使用時,v-once
不生效。在v-for
循環內的元素或組件上使用,必須加上key。
講到此優化,要防止面試官問你 v-once
怎么實現只渲染一次元素或組件?
說到渲染應該和render
函數有關,那要去生成 render
函數的地方去尋找答案。
在 src/compiler/codegen/index.js 中,找到genElement方法
else if (el.once && !el.onceProcessed) {若設置v-once,則調用genOnce()函數
return genOnce(el, state)
}
復制代碼
再看genOnce方法
function genOnce(el, state){
el.onceProcessed = true
if (el.if && !el.ifProcessed) {//如果有定義了v-if指令
//...
} else if (el.staticInFor) {//如果是在v-for下面的元素或組件上
//...
return `_o(${genElement(el, state)},${state.onceId++},${key})`
} else {
return genStatic(el, state)
}
}
復制代碼
如果有定義v-if
指令,如果v-if
指令的值不存在,最后還是會調用genStatic方法。再看genStatic方法
function genStatic(el, state) {
//...
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
return `_m(${state.staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
}
復制代碼
其中_m方法就是 src\core\instance\render-helpers\render-static.js 中的renderStatic方法,這個方法就是v-once
實現只渲染一次元素或組件的關鍵所在。
function renderStatic(index,isInFor){
const cached = this._staticTrees || (this._staticTrees = [])
let tree = cached[index]
if (tree && !isInFor) {
return tree
}
tree = cached[index] = this.$options.staticRenderFns[index].call(this._renderProxy,null,this)
return tree
}
復制代碼
其中cached
是帶 v-once
的元素或組件渲染生成的虛擬DOM節點的緩存,如果某個虛擬DOM節點的緩存存在,且虛擬DOM節點不是在v-for
中直接返回該虛擬DOM節點緩存,如果該虛擬DOM節點沒有緩存,則調用genStatic方法中存在staticRenderFns
數組中的渲染函數,渲染出虛擬DOM節點且存在 cached
,以便下次不用重新渲染直接返回該虛擬DOM節點,并同時調用markOnce方法在該虛擬DOM節點上加上isOnce標志,值為true。
如果有定義 v-for
,最終會調用 _o(${genElement(el, state)},${state.onceId++},${key})
,其中_o
方法就是 src\core\instance\render-helpers\render-static.js 中的markOnce方法,其作用是在生成的虛擬DOM節點上加上isOnce標志,為true代表該虛擬DOM節點是靜態節點,當patch時,會判斷 vnode.isOnce
是否為true,為true時,直接返回舊節點,不進行比對,相當實現渲染一次。
6、利用Object.freeze()凍結不需要響應式變化的數據
Vue初始化過程中,會把data傳入observe函數中進行數據劫持,把data中的數據都轉換成響應式的。
復制代碼
在observe函數內部調用defineReactive函數處理數據,配置getter/setter屬性,轉成響應式,如果使用 Object.freeze()
將data中某些數據凍結了,也就是將其configurable屬性(可配置)設置為false。
defineReactive函數中有段代碼,檢測數據上某個key對應的值的configurable屬性是否為false,若是就直接返回,若不是繼續配置getter/setter屬性。
export function defineReactive(obj,key,val,customSetter,shallow){
//...
const property = Object.getOwnPropertyDescriptor(obj, key)//獲取obj[key]的屬性
if (property && property.configurable === false) {
return
}
//...
}
復制代碼
在項目中如果遇到不需要響應式變化的數據,可以用 Object.freeze()
把該數據凍結了,可以跳過初始化時數據劫持的步驟,大大提高初次渲染速度。
7、提前過濾掉非必須數據,優化data選項中的數據結構
Vue初始化時,會將選項data傳入observe函數中進行數據劫持,
initData(vm){
let data = vm.$options.data
//...
observe(data, true)
}
復制代碼
在observe函數會調用
observe(value,asRootData){
//...
ob = new Observer(value);
}
復制代碼
在Observer原型中defineReactive函數處理數據,配置getter/setter屬性,轉成響應式
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
復制代碼
defineReactive函數中,會將數據的值再次傳入observe函數中
export function defineReactive(obj,key,val,customSetter,shallow){
//...
if (arguments.length === 2) {
val = obj[key]
}
let childOb = observe(val);
//...
}
復制代碼
observe函數中有段代碼,將數據傳入,Observer類中。
export function observe(value,asRootData){
//...
ob = new Observer(value)
//...
return ob
}
復制代碼
以上構成了一個遞歸調用。
接收服務端傳來的數據,都會有一些渲染頁面時用不到的數據。服務端的慣例,寧可多傳也不會少傳。
所以要先把服務端傳來的數據中那些渲染頁面用不到的數據先過濾掉。然后再賦值到data選項中。可以避免去劫持那些非渲染頁面需要的數據,減少循環和遞歸調用,從而提高渲染速度。
8、避免在v-for循環中讀取data中數組類型的數據
export function defineReactive(obj,key,val,customSetter,shallow){
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
})
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
export function observe (value, asRootData){
if (!isObject(value) || value instanceof VNode) {
return
}
//...
}
復制代碼
為什么要避免在v-for循環中讀取data中數組類型的數據,因為在數據劫持中會調用defineReactive函數中。由于 getter是函數,并且引用了 dep
、childOb
,形成了閉包,所以dep
、childOb
一直存在于內存(每個數據的getter函數)中,dep
是每個數據的依賴收集容器,childOb
是經過響應式處理后的數據。
在渲染視圖、使用watch監聽、使用計算屬性過程中,讀取數據,都會對Dep.target
進行賦值,其值為Watcher(依賴),例如在渲染視圖過程中讀取數據時,Dep.target
為renderWatcher。
接著先調用dep.depend()
給自身收集依賴,如果val(自身的值)不是對象,則childOb
為false。如果val(自身的值)是對象,用 childOb.dep.depend()
收集依賴,若value(自身的值)是數組用 dependArray(value)
遞歸每一項來收集依賴。
為什么要避免在v-for循環中讀取data中數組類型的數據,其原因就是 若value(自身的值)是數組用 dependArray(value)
遞歸每一項來收集依賴
舉個簡單的例子,表格中每行有兩個輸入框,分別可以輸入駕駛員和電話,代碼這么實現。
<template>
<div class="g-table-content">
<el-table :data="tableData">
<el-table-column prop="carno" label="車牌號"></el-table-column>
<el-table-column prop="cartype" label="車型"></el-table-column>
<el-table-column label="駕駛員">
<template slot-scope="{row,column,$index}">
<el-input v-model="driverList[$index].name"></el-input>
</template>
</el-table-column>
<el-table-column label="電話">
<template slot-scope="{row,column,$index}">
<el-input v-model="driverList[$index].phone"></el-input>
</template>
</el-table-column>
</el-table>
</div>
</template>
復制代碼
假設表格有500條數據,那么讀取driverList共500次,每次都讀取driverList都會進入 dependArray(value)
中,總共要循環500*500=25萬次,若有分頁,每次切換頁碼,都會至少循環25萬次。
如果我們在從服務獲取到數據后,做了如下預處理,在賦值給 this.tableData
,會是怎么樣?
res.data.forEach(item =>{
item.name='';
item.phone='';
})
復制代碼
模板這樣實現
<template>
<div class="g-table-content">
<el-table :data="tableData">
<el-table-column prop="carno" label="車牌號"></el-table-column>
<el-table-column prop="cartype" label="車型"></el-table-column>
<el-table-column label="駕駛員">
<template slot-scope="{row}">
<el-input v-model="row.name"></el-input>
</template>
</el-table-column>
<el-table-column label="電話">
<template slot-scope="{row,column,$index}">
<el-input v-model="row.phone"></el-input>
</template>
</el-table-column>
</el-table>
</div>
</template>
復制代碼
也可以實現需求,渲染過程中求值時也不會進入dependArray(value)
中,也不會造成25萬次的不必要的循環。大大提高了性能。
9、防抖和節流
防抖和節流是針對用戶操作的優化。首先來了解一下防抖和節流的概念。
防抖:觸發事件后規定時間內事件只會執行一次。簡單來說就是防止手抖,短時間操作了好多次。
節流:事件在規定時間內只執行一次。
應用場景: 節流不管事件有沒有觸發還是頻繁觸發,在規定時間內一定會只執行一次事件,而防抖是在規定時間內事件被觸發,且是最后一次被觸發才執行一次事件。假如事件需要定時執行,但是其他操作也會讓事件執行,這種場景可以用節流。假如事件不需要定時執行,需被觸發才執行,且短時間內不能執行多次,這種場景可以用防抖。
在用Vue Cli腳手架搭建的Vue項目中,可以通過引用Lodash工具庫里面的debounce防抖函數和throttle節流函數。
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
export default{
methods:{
a: debounce(function (){
//...
},200,{
'leading': false,
'trailing': true
}),
b: throttle(function (){
//...
},200,{
'leading': false,
'trailing': true
})
}
}
復制代碼
debounce(func, [wait=0], [options={}])
創建一個防抖函數,該函數會從上一次被調用后,延遲 wait 毫秒后調用 func 方法。 返回一個防抖函數debounceFn, debounce.cancel
取消防抖,debounce.flush
立即調用該func。
options.leading
為true時,func在節流開始前調用。
options.trailing
為true時,func在節流結束后調用。
leading
和 trailing
都為true,func在wait期間多次調用。
10、圖片大小優化和懶加載
關于圖片大小的優化,可以用image-webpack-loader進行壓縮圖片,在webpack插件中配置,具體可以看本文中這點。
關于圖片懶加載,可以用vue-lazyload插件實現。
執行命令 npm install vue-lazyload --save
安裝vue-lazyload插件。在main.js中引入配置
import VueLazyload from 'vue-lazyload';
Vue.use(VueLazyload, {
preLoad: 1.3,//預載高度比例
error: 'dist/error.png',//加載失敗顯示圖片
loading: 'dist/loading.gif',//加載過程中顯示圖片
attempt: 1,//嘗試次數
})
復制代碼
在項目中使用
<img v-lazy="/static/img/1.png">
復制代碼
11、利用掛載節點會被替換的特性優化白屏問題
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
復制代碼
Vue 選項中的 render 函數若存在,則 Vue 構造函數不會從 template 選項或通過 el 選項指定的掛載元素中提取出的 HTML 模板
編譯渲染函數。
復制代碼
也就是說渲染時,會直接用render渲染出來的內容替換 <div id="app"></div>
。
Vue項目有個缺點,首次渲染會有一段時間的白屏原因是首次渲染時需要加載一堆資源,如js、css、圖片。很多優化策略,最終目的是提高這些資源的加載速度。但是如果遇上網絡慢的情況,無論優化到極致還是需要一定加載時間,這時就會出現白屏現象。
首先加載是index.html頁面,其是沒有內容,就會出現白屏。如果 <div id="app"></div>
里面有內容,就不會出現白屏。所以我們可以在 <div id="app"></div>
里添加首屏的靜態頁面。等真正的首屏加載出來后就會把<div id="app"></div>
這塊結構都替換掉,給人一種視覺上的誤差,就不會產生白屏。
11、組件庫的按需引入
組件庫按需引入的方法,一般文檔都會介紹。
如element UI庫,用babel-plugin-component插件實現按需引入。
執行命令npm install babel-plugin-component --save-dev
,安裝插件。
在根目錄下.babelrc.js文件中按如下配置
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
復制代碼
其中libraryName
為組件庫的名稱, styleLibraryName
為組件庫打包后樣式存放的文件夾名稱。 在main.js中就可以按需引入。
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
復制代碼
其中libraryName
為組件庫的名稱,styleLibraryName
為組件庫打包后樣式存放的文件夾名稱。 在main.js中就可以按需引入。
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
復制代碼
其實babel-plugin-component插件是element用babel-plugin-import插件改造后特定給element UI使用。一般的組件庫還是babel-plugin-import插件實現按需引入。
執行命令npm install babel-plugin-import --save-dev
,安裝插件。
在根目錄下.babelrc.js文件中按如下配置
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}]
]
}
復制代碼
其中libraryName
為組件庫的名稱, libraryDirectory
表示從庫的package.json的main入口文件或者module入口文件所在文件夾名稱,否則默認為lib。
在介紹style
選項配置之前。先看一下Vant 組件庫打包后生成文件的結構和內容。
index.js文件內容如下所示
less.js文件內容如下所示
style
為true時,會按需在項目中引入對應style文件中的index.js。
style
為css時,會按需在項目中引入對應style文件中的less.js。
style
為Function,babel-plugin-import將自動導入文件路徑等于函數返回值的文件。這對于組件庫開發人員很有用。
三、項目打包的優化
在說這個之前,先要明確什么是打包。通俗來說,就是把一個項目打包成一個個js文件、css文件等資源,最后在index.html文件中引入,大家可以看一下項目中dist文件夾中的index.html。
如下圖,紅框中就是一個項目通過打包出來的資源。其實說優化,就是優化這些資源。那么要怎么優化這些資源呢?
在早期沒有Webpack時,這些資源都是開發者按照團隊規范來處理和引入。并通過優化來實現最快的、最合理從服務器下載這些資源。這時期的優化主要體現在:
js、css代碼按需引入。
js、css代碼公用代碼提取。
js、css代碼的最小化壓縮。
圖片資源的壓縮。
現在項目是用Webpack打包的,可以通過配置Webpack來優化。
如果你的Vue項目是用Vue Cli3搭建起來,可以在根目錄新建一個 vue.config.js 文件,在這個文件中配置Webpack來優化這些資源。
優化還是提現在上面四點。下面總結了5個優化手段,其中兩個手段雖然在生產環境已經是默認優化的,但是還是要了解一下。
優化自然要前后對比,先安裝插件webpack-bundle-analyzer,可以幫助你可視化的分析打包后的各個資源的大小。
npm install webpack-bundle-analyzer --save-dev
復制代碼
在 vue.config.js 中引入這插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports={
configureWebpack:config =>{
return {
plugins:[
new BundleAnalyzerPlugin()
]
}
}
}
復制代碼
執行命令npm run build
,會在瀏覽器打開一份打包分析圖,如下圖所示。
1、利用import()異步引入組件實現按需引入
說起
import()
,我們自然會想到路由懶加載,所謂的懶加載就是用 import()
異步引入組件。
在網上隨便一搜,懶加載還可以通過resolve =>require(['需要加載組件的地址'],resolve)
來實現。
component: () =>import('views/home.vue'),
component: resolve =>require(['views/home.vue'],resolve)
復制代碼
但是用 resolve =>require(['需要加載組件的地址'],resolve)
來異步引入組件,通過Webpack4打包后,發現所有組件的代碼被打包成一個js文件這和預期的不符,預期應該是每個組件的代碼都被打包成對應的js文件,加載組件時會對應加載js文件,這才是懶加載。
用
import()
來異步引入組件后,執行命令 npm run build
后,看一下打包分析圖。
對比后發現,原來一個1.42MB的js文件不見了,被拆分成許多如32.55KB、31.69KB的js小文件。這些js小文件只有在對應的組件加載時才會加載,這才是懶加載。
或許你看這些js文件名會感到混亂,不能和項目中的組件一一對上,現在教你一個小技巧。 webpackChunkName :chunk文件的名稱。[request]表示實際解析的文件名。
function load(component) {
return () => import(/* webpackChunkName: "[request]" */ `views/${component}`)
}
復制代碼
執行命令npm run build
,看一下打包分析圖。
如圖中紅框的js文件是views/flow_card_manage/flow_card_list/index.vue這個組件打包出來的。
在瀏覽器上打開項目,用F12抓包看一下,搜一下 flow_card_manage-flow_card_list.67de1ef8.js
這個文件。在首頁時,還沒加載到這個路由組件時。這個js文件是有被加載,只是預取(prefetch)一下,沒有返回內容的。目的是告訴瀏覽器,空閑的時候給我加載這個js文件。
直到真正加載這個路由組件時,這個js文件再次被加載,如果瀏覽器已經加載好了直接返回內容,如果瀏覽器還沒加載好,就去服務器請求這個js文件,再返回內容。這樣就是懶加載,按需加載。
原理: 可以看我的下一篇文章
2、利用externals提取第三方依賴并用CDN引入
從打包分析圖中可以發現chunk-vendors.js和chunk-80f6b79a.js這兩個文件還是很大。這兩個文件內有element-ui、jquery、xlsx等第三方依賴的資源。
在Webpack中的
externals
配置選項,可避免將第三方依賴打包,而是在項目運行時從外部獲取第三方依賴。執行命令
npm run build
,看一下打包分析圖。chunk-vendors.js和chunk-80f6b79a.js的文件大小和之前相比,有大幅度的減小。
用externals提取第三方依賴時,需切記中庸之道。雖然我們的最終目的是減少http請求資源大小,但是過猶不及,提取的過細將會增加http請求數量。
復制代碼
3、利用SplitChunks插件提取公共js代碼和分割js代碼
用Webpack打包后,還是有很多資源被重復打包到各個js文件,可以用SplitChunks插件進一步優化,減少打包生成文件的總體大小。
另外CDN是第三方,具有不穩定性,萬一CDN突然掛了,系統也就崩了,有一定的風險。也可以用SplitChunks插件實現externals
配置的效果,第三方依賴還是在自己服務器上,減少風險。
4、利用MiniCssExtractPlugin插件提取css樣式
在用Vue Cli3搭建的Vue項目中是用css.extract
來控制MiniCssExtractPlugin插件是否啟用,雖然在生產環境中,css.extract
默認是為true,也就是說MiniCssExtractPlugin插件是啟用的。但是還是要熟悉一下MiniCssExtractPlugin插件的用法,以防面試官細問。
5、利用OptimizeCssnanoPlugin插件壓縮和去重css樣式文件
在用Vue Cli3搭建的Vue項目中默認是使用OptimizeCssnanoPlugin插件來壓縮和去重css樣式文件,
這里來講一下怎么使用這款插件。 先安裝OptimizeCssnanoPlugin插件
cnpm install --save-dev @intervolga/optimize-cssnano-plugin
復制代碼
在 vue.config.js 中這么配置
const OptimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
module.exports={
configureWebpack:config =>{
return {
plugins:[
new OptimizeCssnanoPlugin({
sourceMap: false,
cssnanoOptions: {
preset: [
'default',
{
mergeLonghand: false,
cssDeclarationSorter: false
}
]
},
}),
]
}
}
}
復制代碼
其中cssnanoOptions的配置可以看 這里 。
mergeLonghand:false
,表示關閉如margin,padding和border類似css樣式屬性合并。
.box {
margin-top: 10px;
margin-right: 20px;
margin-bottom: 10px;
margin-left: 20px;
}
//壓縮后
.box {
margin: 10px 20px;
}
復制代碼
cssDeclarationSorter:false
,表示關閉根據CSS的屬性名稱對CSS進行排序。
body {
animation: none;
color: #C55;
border: 0;
}
//壓縮后
body {
animation: none;
border: 0;
color: #C55;
}
復制代碼
6、開啟optimization.minimize來壓縮js代碼
optimization.minimize
選項有兩個值true
和false
,為 true
開啟壓縮js代碼,為false
關閉壓縮js代碼。
在生產環境中默認為 true
,在開發環境中默認為false
。
如果你在開發環境不需要用debug調試代碼,可以也設置為true
來壓縮js代碼,提高頁面加載速度。
在 vue.config.js 中這么配置
module.exports={
configureWebpack:config =>{
return {
optimization:{
minimize: true
}
}
}
}
復制代碼
在Vue Cli3中默認用TerserPlugin插件來壓縮js代碼,其中配置已經是最優了。
如果想用其它插件來壓縮js代碼,可以在 optimization.minimizer
選項中添加,其值為數組。
用chainWebpack來添加,其中WebpackPlugin為插件名稱,args為插件參數。
const WebpackPlugin = require(插件名稱)
module.exports = {
chainWebpack: config =>{
config.optimization
.minimizer(name)
.use(WebpackPlugin, args)
},
}
復制代碼
7、利用image-webpack-loader進行壓縮圖片
用Vue Cli3搭建的Vue項目中,圖片是沒進行壓縮就直接用url-loader和file-loader處理。
優化一下用image-webpack-loader進行壓縮圖片后再給url-loader和file-loader處理。
在Vue Cli3已經配置了對圖片處理的loader,要對其進行修改,具體方法可以看我另一篇文章 Webpack之loader配置詳解 。
先安裝image-webpack-loader
cnpm install image-webpack-loader --save-dev
復制代碼
然后在 vue.config.js 中這么配置
module.exports = {
chainWebpack: config =>{
config.module
.rule('images')
.use('imageWebpackLoader')
.loader('image-webpack-loader')
},
}
復制代碼
添加image-webpack-loader前,打包后 homeBg.png 圖片 如下所示
添加image-webpack-loader后,打包后 homeBg.png 圖片 如下所示
可以看到圖片大小從251KB減少到110KB,優化效果明顯。
支持壓縮PNG,JPEG,GIF,SVG和WEBP圖片,下面介紹一下其常用的參數。* bypassOnDebug
ture/false
,默認為 false
,為 true
時禁用壓縮圖片,在 webpack@1.x 中使用。
- disable
ture/false
,默認為false
,為true
時禁用壓縮圖片,在 webpack@2.x 及更高版本中使用。 可以在開發環境中禁用壓縮圖片,使其編譯速度更快。
module.exports = {
chainWebpack: config =>{
config.module
.rule('images')
.use('imageWebpackLoader')
.loader('image-webpack-loader')
.options({
disable: process.env.NODE_ENV === 'development',
})
},
}
復制代碼
mozjpeg: 控制壓縮JPEG圖像的配置,默認啟用。參數值為對象,常用的子參數有:
quality 壓縮質量,范圍0(最差)至100(最完美)。
optipng:控制壓縮PNG圖像的配置,默認啟用。參數值為對象,常用的子參數有:
OptimizationLevel 優化級別,在0和7之間選擇一個優化級別,數值越高,壓縮質量越好,但是速度越慢,默認為3。
pngquant:控制壓縮PNG圖像的配置,默認啟用。參數值為對象,常用的子參數有:
[0 , 1]
gifsicle:控制壓縮GIF圖像的配置,默認啟用。參數值為對象,常用的子參數有: -OptimizationLevel 優化級別,在1和3之間選擇一個優化級別,優化級別確定完成多少優化;較高的水平需要更長的時間,但可能會有更好的效果。
webp: 將JPG和PNG圖像壓縮為WEBP,默認不啟用,需要配置后才啟用。啟用后,可以將JPG和PNG圖像壓縮輸出大小更小的圖片,但比用mozjpeg、optipng、pngquant壓縮更耗時,會影響編譯打包速度,需自己取舍。 參數值為對象,常用的子參數有
quality 品質因數,在0和之間100設置,默認為75,值越高品質越好。
lossless 是否無損壓縮,默認為false,為true時開啟無損壓縮。
nearLossless 使用額外的有損預處理步驟進行無損編碼,其品質因數介于0(最大預處理)和100(等于lossless)之間。
module.exports = {
chainWebpack: config =>{
config.module
.rule('images')
.use('imageWebpackLoader')
.loader('image-webpack-loader')
.options({
disable: process.env.NODE_ENV === 'development',
mozjpeg:{
quality:75
},
optipng:{
OptimizationLevel:3
},
pngquant:{
speed:4,
quality:[0.2,0.5]
},
gifsicle:{
OptimizationLevel:1
},
webp:{
quality:75,
lossless:true,
nearLossless:75
}
})
},
}
復制代碼
四、項目部署的優化
這里只講一個比較常見和簡單的優化手段,gzip壓縮。其實還有其他優化手段,涉及到服務端,如果面試官深究會其反作用。
1、識別gzip壓縮是否開啟
這個很簡單,只要看響應頭部(Response headers)中 有沒有 Content-Encoding: gzip 這個屬性即可,有代表有開啟gzip壓縮。
2、在Nginx上開啟gzip壓縮
在nginx/conf/nginx.conf中配置
http {
gzip on;
gzip_min_length 1k;
gzip_comp_level 5;
gzip_types application/javascript image/png image/gif image/jpeg text/css text/plain;
gzip_buffers 4 4k;
gzip_http_version 1.1;
gzip_vary on;
}
復制代碼
gzip
gzip_min_length
gzip_comp_level
gzip_types
gzip_buffers
gzip_http_version
gzip_vary
開啟gzip壓縮前
開啟gzip壓縮后
對比一下,優化效果非常明顯。自己也可以在本地嘗試一下,怎么用Nginx部署Vue項目可以看我這篇文章。
3、在Webpack上開啟gzip壓縮
利用CompressionWebpack插件來實現gzip壓縮。
首先安裝CompressionWebpack插件
npm install compression-webpack-plugin --save-dev
復制代碼
然后在 vue.config.js 中這么配置
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
configureWebpack: config =>{
return {
plugins: [
new CompressionPlugin()
],
}
}
}
復制代碼
執行 npm run build
命令后,打開 dist 文件,會發現多出很多名字相同的文件,只是其中一個文件后綴為 .gz
,這就是用gzip壓縮后的文件。
4、Nginx和Webpack壓縮的區別
不管Nginx還是Webpack壓縮,在Nginx中都要開啟gzip壓縮,不然瀏覽器加載還是未壓縮的資源。
還可以在Nginx加上 gzip_static on
; 的配置。gzip_static
啟用后, 瀏覽器請求資源時,Nginx會先檢查是否存該資源名稱且后綴為.gz
的文件,如果有則直接返回該gz文件內容,可以避免Nginx對該資源再進行gzip壓縮,浪費服務器的CPU。
用Nginx壓縮會占用服務器的CPU,瀏覽器每次請求資源,Nginx會對該資源實時壓縮,壓縮完畢后才會返回該資源,如果資源很大的話,還是壓縮級別設置很高,都會導致返回資源的時間過長,造成不好的用戶體驗。
用Webpack會使打包時間變長。但是用CompressionPlugin插件壓縮,會有緩存,可以相對減少打包時間。
建議Nginx和Webpack壓縮都開啟壓縮,且在Nginx加上gzip_static on
; 的配置,減少服務器的CPU的使用,當然還是要根據項目的情況實際選擇。
5、CompressionPlugin插件的參數詳細詳解
test
:String|RegExp|Array<String|RegExp>,資源的名稱 符合條件的才會被壓縮,默認為undefined,即全部符合,例如只要壓縮js文件
plugins: [
new CompressionPlugin({
test: /\.js(\?.*)?$/i,
})
],
復制代碼
include
:String|RegExp|Array<String|RegExp>, 資源的名稱 符合條件的才會被壓縮,默認為undefined,是在 test
參數的范圍內在進行篩選,滿足test
參數的條件,且滿足include
參數的條件的資源才會被壓縮。
exclude
:String|RegExp|Array<String|RegExp>,壓縮時排除 資源的名稱 符合條件的資源,默認為undefined,是在 test
參數的范圍內在進行排除,滿足 test
參數的條件,不滿足 exclude
參數的條件的資源才會被壓縮。
algorithm
:壓縮算法/功能,默認gzip,一般不做更改。
compressionOptions
,對 algorithm
參數所選用的壓縮功能的參數設置,一般用來設置壓縮級別,1-9,數字越大,壓縮后的大小越小,也越占用CPU,花費時間也越長。
plugins: [
new CompressionPlugin({
compressionOptions: { level: 1 },
})
],
復制代碼
threshold
:Number,設置被壓縮資源的最小大小,單位為字節。默認為0。
minRatio
:Number,設置壓縮比率,壓縮比率 = 壓縮后的資源的大小/壓縮后的資源,小于壓縮比率的資源才會被壓縮。和threshold
參數是‘與’的關系。
``filename``` :類型:String|Function,設置壓縮資源后的名稱,默認值:[path].gz[query], [file]被替換為原始資產文件名。 [path]替換為原始資產的路徑。 [dir]替換為原始資產的目錄。 [name]被替換為原始資產的文件名。 [ext]替換為原始資產的擴展名。 [query]被查詢替換。 下面用函數把各類的值都打印出來。
new CompressionPlugin({
filename(info) {
console.log(info)
return `${info.path}.gz${info.query}`;
},
})
復制代碼
deleteOriginalAssets
:Boolean,默認為 false
,為 true
時刪除原始資源文件。不建議設置。cache :Boolean|String,默認為
true
,為true
時,啟用文件緩存。緩存目錄的默認路徑: node_modules/.cache/compression-webpack-plugin 。值為String時。啟用文件緩存并設置緩存目錄的路徑。
new CompressionPlugin({
cache: 'path/to/cache',
}),
復制代碼
如果你現在也想學習前端開發技術,在學習前端的過程當中有遇見任何關于學習方法,學習路線,學習效率等方面的問題,你都可以申請加入我的Q群:前114中6649后671,里面有許多前端學習資料 大廠面試真題免費獲取,希望能夠對你們有所幫助。