vue高頻面試知識點彙總【2022寒冬版】
很喜歡‘萬變不離其宗’
這句話,希望在不斷的思考和總結中找到Vue
中的宗
,來解答面試官丟擲的各種Vue
問題,一起加油~
一、MVVM
原理
在Vue2
官方文件中沒有找到Vue
是MVVM
的直接證據,但文件有提到:雖然沒有完全遵循MVVM模型
,但是 Vue 的設計也受到了它的啟發,因此在文件中經常會使用vm
(ViewModel 的縮寫) 這個變數名錶示 Vue 例項。
為了感受MVVM模型
的啟發,我簡單列舉下其概念。
MVVM是Model-View-ViewModel的簡寫,由三部分構成: - Model: 模型持有所有的資料、狀態和程式邏輯 - View: 負責介面的佈局和顯示 - ViewModel:負責模型和介面之間的互動,是Model和View的橋樑
二、SPA
單頁面應用
單頁Web應用(single page web application,SPA),就是隻有一張Web頁面的應用,是載入單個HTML頁面並在使用者與應用程式互動時動態更新該頁面的Web應用程式。我們開發的Vue
專案大多是藉助個官方的CLI
腳手架,快速搭建專案,直接通過new Vue
構建一個例項,並將el:'#app'
掛載引數傳入,最後通過npm run build
的方式打包後生成一個index.html
,稱這種只有一個HTML
的頁面為單頁面應用。
當然,vue
也可以像jq
一樣引入,作為多頁面應用的基礎框架。
三、Vue
的特點
- 清晰的官方文件和好用的
api
,比較容易上手。 - 是一套用於構建使用者介面的漸進式框架,將注意力集中保持在核心庫,而將其他功能如路由和全域性狀態管理交給相關的庫。
- 使用 Virtual DOM。
- 提供了響應式 (Reactive) 和元件化 (Composable) 的檢視元件。
四、Vue
的構建入口
vue使用過程中可以採用以下兩種方式:
- 在vue腳手架中直接使用,參考文件:http://cn.vuejs.org/v2/guide/installation.html
- 或者在html檔案的頭部通過靜態檔案的方式引入:
<script src="http://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script>
那麼問題來了,使用的或者引入的到底是什麼?
答:引入的是已經打包好的vue.js檔案,通過rollup構建打包所得。
構建入口在哪裡?
答:在vue
原始碼的package.json檔案中:
"scripts": {
// ...
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
// ...
},
通過執行npm run build的時候,會進行scripts/build.js檔案的執行,npm run build:ssr和npm run build:weex的時候,將ssr和weex作為引數傳入,按照引數構建出不一樣的vue.js打包檔案。
所以說,vue
中的package.json
檔案就是構建的入口,具體構建流程可以參考vue2入口:構建入口。
五、對import Vue from "vue"
的理解
在使用腳手架開發專案時,會有一行程式碼import Vue from "vue"
,那麼這個Vue
指的是什麼。
答:一個建構函式。
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
我們開發中引入的Vue
其實就是這個建構函式,而且這個建構函式只能通過new Vue
的方式進行使用,否則會在控制檯列印警告資訊。定義完後,還會通過initMixin(Vue)
、initMixin(Vue)
、initMixin(Vue)
、initMixin(Vue)
和initMixin(Vue)
的方式為Vue
原型中混入方法。我們通過import Vue from "Vue"
引入的本質上就是一個原型上掛在了好多方法的建構函式。
六、對new Vue
的理解
``
// main.js檔案
import Vue from "vue";
var app = new Vue({
el: '#app',
data() {
return {
msg: 'hello Vue~'
}
},
template:
console.log(app);
``
new Vue就是對建構函式
Vue`進行例項化,執行結果如下:
可以看出例項化後的例項中包含了很多屬性,用來對當前app
進行描述,當然複雜的Vue
專案這個app
將會是一個樹結構,通過$parent
和$children
維護父子關係。
new Vue
的過程中還會執行this._init
方法進行初始化處理。
七、編譯
虛擬DOM
的生成必須通過render
函式實現,render
函式的產生是在編譯階段完成,核心程式碼如下:
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
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
}
})
主要完成的功能是:
- 通過const ast = parse(template.trim(), options)
將template
轉換成ast
樹
- 通過optimize(ast, options)
對ast
進行優化
- 通過const code = generate(ast, options)
將優化後的ast
轉換成包含render
字串的code
物件,最終render
字串通過new Function
轉換為可執行的render
函式
模板編譯的真實入口
可以參考vue2從template到render:模板編譯入口
parse
可以參考vue2從template到render:AST
optimize
可以參考vue2從template到render:optimize
generate
可以參考vue2從template到render:code
八、虛擬DOM
先看瀏覽器對HTML
的理解:
```
My title
Some text content``` 當瀏覽器讀到這些程式碼時,它會建立一個DOM樹來保持追蹤所有內容,如同你會畫一張家譜樹來追蹤家庭成員的發展一樣。 上述 HTML 對應的 DOM 節點樹如下圖所示:
每個元素都是一個節點。每段文字也是一個節點。甚至註釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。
再看Vue
對HTML template
的理解
Vue 通過建立一個虛擬 DOM 來追蹤自己要如何改變真實 DOM。因為它所包含的資訊會告訴 Vue 頁面上需要渲染什麼樣的節點,包括及其子節點的描述資訊。我們把這樣的節點描述為“虛擬節點 (virtual node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 元件樹建立起來的整個 VNode 樹的稱呼。
簡言之,瀏覽器對HTML的理解是DOM樹,Vue對HTML
的理解是虛擬DOM,最後在patch
階段通過DOM操作的api將其渲染成真實的DOM節點。
九、模板或者元件渲染
Vue
中的編譯會執行到邏輯vm._update(vm._render(), hydrating)
,其中的vm._render
執行會獲取到vNode
,vm._update
就會對vNode
進行patch
的處理,又分為模板渲染和元件渲染。
- 模板渲染,參考vue2從資料到檢視渲染:模板渲染
- 元件渲染,參考vue2從資料到檢視渲染:元件渲染
十、資料響應式處理
Vue
的資料響應式處理的核心是Object.defineProperty
,在遞迴響應式處理物件的過程中,為每一個屬性定義了一個釋出者dep
,當進行_render
函式執行時會訪問到當前值,在get
中通過dep.depend
進行當前Watcher
的收集,當資料發生變化時會在set
中通過dep.notify
進行Watcher
的更新。
資料響應式處理以及釋出訂閱者模式的關係請參考vue2從資料變化到檢視變化:釋出訂閱模式
十一、this.$set
const app = new Vue({
el: "#app",
data() {
return {
obj: {
name: "name-1"
}
};
},
template: `<div @click="change">{{obj.name}}的年齡是{{obj.age}}</div>`,
methods: {
change() {
this.obj.name = 'name-2';
this.obj.age = 30;
}
}
});
以上例子執行的結果是:
name-1的年齡是
當點選後依然是:
name-2的年齡是
可以看出點選後,obj
的name
屬性變化得到了檢視更新,而age
屬性並未進行變化。
name
屬性響應式的過程中鎖定了一個釋出者dep
,在當前檢視渲染時在釋出者dep
的subs
中做了記錄,一旦其發生改變,就會觸發set
方法中的dep.notify
,繼而執行檢視的重新渲染。然而,age
屬性並未進行響應式的處理,當其改變時就不能進行檢視渲染。
此時就需要通過this.$set
的方式對其進行手動響應式的處理。具體細節請參考 手動響應式處理和陣列檢測變化
十二、元件註冊
元件的使用是先註冊後使用,又分為:
- 全域性註冊:可以直接在頁面中使用
- 區域性註冊:使用時需要通過import xxx from xxx
的方式引入,並且在當前元件的選項components
中增加區域性元件的名稱。
全域性註冊和區域性註冊實現原理可以參考 vue2從資料到檢視渲染:元件註冊(全域性元件/區域性元件)
十三、非同步元件
Vue單頁面應用中一個頁面只有一個<div id="app"></div>
承載所有節點,因此複雜專案可能會出現首屏載入白屏等問題,Vue非同步元件就很好的處理了這問題。
非同步元件的分類和實現原理請參考 vue2從資料到檢視渲染:非同步元件
十四、this.$nextTick
因為通過new
例項化建構函式Vue
的時候會執行初始化方法this._init
,其中涉及到的方法大多都是同步執行。nextTick
在vue中是一個很重要的方法,在new Vue
例項化的同步過程中將一些需要非同步處理的函式推到非同步佇列中去,可以等new Vue
所有的同步任務執行完後,再執行非同步佇列中的函式。
nextTick
的實現可以參考 vue2從資料變化到檢視變化:nextTick,
十五、keep-alive
內建元件
vue
中支援元件化,並且也有用於快取的內建元件keep-alive
可直接使用,使用場景為路由元件
和動態元件
。
- activated
表示進入元件的生命週期,deactivated
表示離開元件的生命週期
- include
表示匹配到的才快取,exclude
表示匹配到的都不快取
- max
表示最多可以快取多少元件
keep-alive
的具體實現請參考 vue中的keep-alive(原始碼分析)
十六、生命週期
vue
中的生命週期有哪些?
答案:11
個,分別為beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、activated
、deactivated
、beforeDestroy
、destroyed
和errorCaptured
。
具體實現請參考 vue生命週期
十七、v-show
和v-if
的區別
先看v-if
和v-show
的使用場景:
(1)v-if
更多的使用在需要考慮白屏時間或者切換次數很少的場景
(2)v-show
更多使用在transition
控制的動畫或者需要非常頻繁地切換的場景
再從底層實現思路上分析:
(1)v-if
條件為false
時,會生成空的佔位註釋節點,那麼在考慮首頁白屏時間時,選用v-if
比較合適。條件從false
變化為true
的話會從空的註釋節點變成真實節點,條件再變為false
時真實節點又會變成註釋節點,如果切換次數比較多,那麼開銷會比較大,頻繁切換場景不建議使用v-if
。
(2)v-show
條件為false
時,會生成真實的節點,只是為當前節點增加了display:none
來控制其隱藏,相比v-if
生成空的註釋節點其首次渲染開銷是比較大的,所以不建議用在考慮首屏白屏時間的場景。如果我們頻繁切換v-show
的值,從display:none
到display:block
之間的切換比起空的註釋節點和真實節點的開銷要小很多,這種場景就建議使用v-show
。
可以通過vue中v-if和v-show的區別(原始碼分析)瞭解v-if
和v-show
詳細過程。
十八、v-for
中key
的作用
在v-for
進行迴圈展示過程中,當資料發生變化進行渲染的過程中,會進行新舊節點列表的比對。首先新舊vnode
列表首先通過首首
、尾尾
、首尾
和尾首
的方式進行比對,如果key
相同則採取原地複用的策略進行節點的移動。
如果首尾兩兩比對的方式找不到對應關係,繼續通過key
和vnode
的對應關係進行尋找。
如果key
和vnode
對應關係中找不到,繼續通過sameVnode
的方式在未比對的節點中進行尋找。
如果都找不到,則將其按照新vnode
進行createElm
的方式進行建立,這種方式是比節點移動的方式計算量更大。
最後將舊的vnode
列表中沒有進行匹配的vnode
中的vnode.elm
在父節點中移除。
簡單總結就是,新的vnode
列表在舊的vnode
列表中去尋找具有相同的key
的節點進行原地複用,如果找不到則通過建立的方式createElm
去建立一個,如果舊的vnode
列表中沒有進行匹配則在父節點中移除其vnode.elm
。這就是原地複用邏輯的大體實現。
具體key
和diff
演算法的關係可以參考vue2從資料變化到檢視變化:diff演算法圖解
十九、v-for
和v-if
能同時使用嗎
答案是:用了也能出來預期的效果,但是會有效能浪費。
同時包含v-for
和v-if
的template
模板在編輯階段會執行v-for
比v-if
優先順序更高的編譯流程;在生成vnode
的階段,會包含屬性isComment
為true
的空白佔位vnode
;在patch
階段,會生成真實的佔位節點。雖然一個空的佔位節點無妨,但是如果資料量比較大的話,也是一個性能問題。
當然,可以在獲取到資料(一般是在beforeCreate
或者created
階段)時進行過濾處理,也可以通過計算屬性對其進行處理。
可以通過v-for
和v-if
可以一起使用嗎?瞭解v-for
和v-if
的詳細過程。
二十、vue
中的data
為什麼是函式
答案是:是不是一定是函式,得看場景。並且,也無需擔心什麼時候該將data
寫為函式還是物件,因為vue
內部已經做了處理,並在控制檯輸出錯誤資訊。
場景一:new Vue({data: ...})
這種場景主要為專案入口或者多個html
頁面各例項化一個Vue
時,這裡的data
即可用物件的形式,也可用工廠函式返回物件的形式。因為,這裡的data
只會出現一次,不存在重複引用而引起的資料汙染問題。
場景二:元件場景中的選項
在生成元件vnode
的過程中,元件會在生成建構函式的過程中執行合併策略:
```
// data合併策略
strats.data = function (
parentVal,
childVal,
vm
) {
if (!vm) {
if (childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
``
如果合併過程中發現子元件的資料不是函式,即
typeof childVal !== 'function'成立,進而在開發環境會在控制檯輸出警告並且直接返回
parentVal,說明這裡壓根就沒有把
childVal中的任何
data資訊合併到
options`中去。
可以通過vue
中的data
為什麼是函式?瞭解詳細過程。
二十一、this.$watch
使用場景:用來監聽資料的變化,當資料發生變化的時候,可以做一些業務邏輯的處理。
配置引數:
- deep
:監聽資料的深層變化
- immediate
:立即觸發回撥函式
實現思路: Vue
建構函式定義完成以後,在執行stateMixin(Vue)
時為Vue.prototype
上定義$watch
。該方法通過const watcher = new Watcher(vm, expOrFn, cb, options)
進行Watcher
的例項化,將options
中的user
屬性設定為true
。並且,$watch
邏輯結束的會返回函式function unwatchFn () { watcher.teardown() }
,用來取消偵聽的函式。
可以通過watch
選項和$watch
方法的區別vue中的watch和$watch監聽的事件,執行幾次?來了解詳細過程。
二十二、計算屬性和偵聽屬性的區別
相同點: 兩者都是Watcher
例項化過程中的產物
計算屬性:
- 使用場景:模板內的表示式主要用於簡單運算,對於複雜的計算邏輯可以用計算屬性
- 計算屬性是基於它們的響應式依賴進行快取的,當依賴的資料未發生變化時,多次呼叫無需重複執行函式
- 計算屬性計算結果依賴於data
中的值
- 同步操作,不支援非同步
偵聽屬性:
- 使用場景:當需要在資料變化時執行非同步或開銷較大的操作時,可以用偵聽屬性
- 可配置引數:可以通過配置immediate
和deep
來控制立即執行和深度監聽的行為
- 偵聽屬性偵聽的是data
中定義的
計算屬性請參考vue2從資料變化到檢視變化:計算屬性
偵聽屬性請參考vue2從資料變化到檢視變化:偵聽器
二十三、v-model
// main.js
new Vue({
el: "#app",
data() {
return {
msg: ""
};
},
template: `<div>
<input v-model="msg" placeholder="edit me">
<p>msg is: {{ msg }}</p>
</div>`
});
普通input:input
中的v-model
,最終通過target.addEventListener
處理成在節點上監聽input
事件function($event){msg=$event.target.value}}
的形式,當input
值變化時msg
也跟著改變。
``
// main.js
const inputBox = {
template:
`,
};
new Vue({
el: "#app",
template: <div>
<input-box v-model="msg"></input-box>
<p>{{msg}}</p>
</div>
,
components: {
inputBox
},
data() {
return {
msg: 'hello world!'
};
},
});
``
**元件**:
v-model在元件中則通過給點選事件繫結原生事件,當觸發到
$emit的時候,再進行回撥函式
ƒunction input($$v) {msg=$$v}的執行,進而達到子元件修改父元件中資料
msg`的目的。
二十四、v-slot
v-slot
產生的主要目的是,在元件的使用過程中可以讓父元件有修改子元件內容的能力,就像在子元件裡面放了個插槽,讓父元件往插槽內塞入父元件中的楔子;並且,父元件在子元件中嵌入的楔子也可以訪問子元件中的資料。v-slot
的產生讓元件的應用更加靈活。
1、具名插槽
``
let baseLayout = {
template:
new Vue({
el: "#app",
template: <base-layout>
<template v-slot:header>
<h1>title-txt</h1>
</template>
<p>paragraph-1-txt</p>
<p>paragraph-2-txt</p>
<template v-slot:footer>
<p>foot-txt</p>
</template>
</base-layout>
,
components: {
baseLayout
}
});
``
引入的元件
baseLayout中的
template被添加了屬性
v-slot:header和
v-slot:footer,子元件中定義了對應的插槽被添加了屬性
name="header"和
name="footer",未被進行插槽標識的內容被插入到了匿名的
2、作用域插槽
``
let currentUser = {
template:
new Vue({
el: "#app",
template: <current-user>
<template v-slot:user="slotProps">{{slotProps.userData.lastName}}</template>
</current-user>
,
components: {
currentUser
}
});
``
當前例子中作用域插槽通過
v-bind:userData="childData"的方式,將
childData作為引數,父元件中通過
v-slot:user="slotProps"`的方式進行接收,為父元件使用子元件中的資料提供了可能。
v-slot
的底層實現請參考vue中的v-slot(原始碼分析)
二十五、Vue.filters
filters
類似於管道流可以將上一個過濾函式的結果作為下一個過濾函式的第一個引數,又可以在其中傳遞引數讓過濾器更靈活。
```
// main.js檔案
import Vue from "vue";
Vue.filter("filterEmpty", function(val) { return val || ""; });
Vue.filter("filterA", function(val) { return val + "平時週末的"; });
Vue.filter("filterB", function(val, info, fn) { return val + info + fn; });
new Vue({
el: "#app",
template: <div>{{msg | filterEmpty | filterA | filterB('愛好是', transformHobby('chess'))}}</div>
,
data() {
return {
msg: "張三"
};
},
methods: {
transformHobby(type) {
const map = {
bike: "騎行",
chess: "象棋",
game: "遊戲",
swimming: "游泳"
};
return map[type] || "未知";
}
}
});
``
其中我們對
msg通過
filterEmpty、
filterA和
filterB('愛好是', transformHobby('chess'))}`進行三層過濾。
Vue.filters
的底層實現請檢視vue中的filters(原始碼分析)
二十六、Vue.use
- 作用:
Vue.use
被用來安裝Vue.js外掛,例如vue-router
、vuex
、element-ui
。 install
方法:如果外掛是一個物件,必須提供install
方法。如果外掛是一個函式,它會被作為install
方法。install
方法呼叫時,會將Vue
作為引數傳入。- 呼叫時機:該方法需要在呼叫
new Vue()
之前被呼叫。 - 特點:當 install 方法被同一個外掛多次呼叫,外掛將只會被安裝一次。
二十七、Vue.extend
和選項extends
1、Vue.extend
Vue.extend
使用基礎Vue
構造器建立一個“子類”,引數是一個包含元件選項的物件,例項化的過程中可以修改其中的選項,為實現功能的繼承提供了思路。
new Vue({
el: "#app",
template: `<div><div id="person1"></div><div id="person2"></div></div>`,
mounted() {
// 定義子類建構函式
var Profile = Vue.extend({
template: '<p @click="showInfo">{{name}} 喜歡 {{fruit}}</p>',
data: function () {
return {
name: '張三',
fruit: '蘋果'
}
},
methods: {
showInfo() {
console.log(`${this.name}喜歡${this.fruit}`)
}
}
})
// 例項化1,掛載到`#person1`上
new Profile().$mount('#person1')
// 例項化2,並修改其`data`選項,掛載到`#person2`上
new Profile({
data: function () {
return {
name: '李四',
fruit: '香蕉'
}
},
}).$mount('#person2')
},
});
在當前例子中,通過Vue.extend
構建了子類建構函式Profile
,可以通過new Profile
的方式例項化無數個vm
例項。我們定義初始的template
、data
和methods
供vm
進行使用,如果有變化,在例項的過程中傳入新的選項引數即可,比如例子中例項化第二個vm
的時候就對data
進行了調整。
2、選項extends
extends
允許宣告擴充套件另一個元件 (可以是一個簡單的選項物件或建構函式),而無需使用 Vue.extend
。這主要是為了便於擴充套件單檔案元件,以實現元件繼承的目的。
``
const common = {
template:
const create = { extends: common, data() { return { name: '新增表單' } } }
const edit = { extends: common, data() { return { name: '編輯表單' } } }
new Vue({
el: "#app",
template: <div>
<create></create>
<edit></edit>
</div>
,
components: {
create,
edit,
}
});
``
當前極簡demo中定義了公共的表單
common,然後又在新增表單元件
create和編輯表單元件
edit中擴充套件了
common`。
二十八、Vue.mixin
和選項mixins
全域性混入和區域性混入視情況而定,主要區別在全域性混入是通過Vue.mixin
的方式將選項混入到了Vue.options
中,在所有獲取子元件構建函式的時候都將其進行了合併,是一種影響全部元件的混入策略。
而區域性混入是將選項通過配置mixins
選項的方式合併到當前的子元件中,只有配置了mixins
選項的元件才會受到混入影響,是一種區域性的混入策略。
二十九、Vue.directive
和directives
1、使用場景
主要用於對於DOM的操作,比如:文字框聚焦,節點位置控制、防抖節流、許可權管理、複製操作等功能
2、鉤子函式
bind
:只調用一次,指令第一次繫結到元素時呼叫。在這裡可以進行一次性的初始化設定。inserted
:被繫結元素插入父節點時呼叫 (僅保證父節點存在,但不一定已被插入文件中)。update
:所在元件的 VNode 更新時呼叫,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。但是你可以通過比較更新前後的值來忽略不必要的模板更新。componentUpdated
:指令所在元件的 VNode 及其子 VNode 全部更新後呼叫。unbind
:只調用一次,指令與元素解綁時呼叫。
3、鉤子函式引數
el
:指令所繫結的元素,可以用來直接操作 DOM。binding
:一個物件,包含以下 property:name
:指令名,不包括v-
字首。value
:指令的繫結值,例如:v-my-directive="1 + 1"
中,繫結值為2
。oldValue
:指令繫結的前一個值,僅在update
和componentUpdated
鉤子中可用。無論值是否改變都可用。expression
:字串形式的指令表示式。例如v-my-directive="1 + 1"
中,表示式為"1 + 1"
。arg
:傳給指令的引數,可選。例如v-my-directive:foo
中,引數為"foo"
。modifiers
:一個包含修飾符的物件。例如:v-my-directive.foo.bar
中,修飾符物件為{ foo: true, bar: true }
。
vnode
:Vue 編譯生成的虛擬節點。oldVnode
:上一個虛擬節點,僅在update
和componentUpdated
鉤子中可用。
4、動態指令引數
指令的引數可以是動態的。例如,在 v-mydirective:[argument]="value"
中,argument
引數可以根據元件例項資料進行更新!這使得自定義指令可以在應用中被靈活使用。
三十、vue
中的原生事件
vue
中可以通過@
或者v-on
的方式繫結事件,也可為其新增修飾符。
new Vue({
el: '#app',
template: `<div @click='divClick'><a @clickt='aClick' href=''>點選</a></div>`,
methods: {
divClick() {
console.log('divClick')
},
aClick() {
console.log('aClick')
},
}
})
以上例子如果點選a
會觸發其預設行為,如果href
不為空還會進行跳轉。除此之外,點選還會繼續觸發div
上繫結的點選事件。
如果通過@click.stop.prevent='aClick'
的方式為a
標籤的點選事件新增修飾符stop
和prevent
,那麼就不會觸發其a
的預設行為,即使href
不為空也不會進行跳轉,同時,div
上的點選事件也不會進行觸發。
模板的渲染一般分為編譯生成render
函式、render
函式執行生成vNode
和patch
進行渲染。下面按照這步驟進行簡單分析。
1、render
通過編譯生成的render
函式:
with(this) {
return _c('div', {
on: {
"click": divClick
}
}, [_c('a', {
attrs: {
"href": "http://www.baidu.com"
},
on: {
"click": function ($event) {
$event.stopPropagation();
$event.preventDefault();
return aClick($event)
}
}
}, [_v("點選")])])
}
其中div
的on
作為div
事件描述。a
標籤的attrs
作為屬性描述,on
作為事件描述,在描述中.stop
被編譯成了$event.stopPropagation()
來阻止事件冒泡,.prevent
被編譯成了$event.preventDefault()
用來阻止a
標籤的預設行為。
2、vNode
通過執行Vue.prototype._render
將render
函式轉換成vNode
。
3、patch
patch
的過程中,當完成$el
節點的渲染後會執行invokeCreateHooks(vnode, insertedVnodeQueue)
邏輯,其中,針對attrs
會將其設定為$el
的真實屬性,當前例子中會為a
標籤設定herf
屬性。針對on
會通過target.addEventListener
的方式將其處理過的事件繫結到$el
上,當前例子中會分別對div
和a
中的click
進行處理,再通過addEventListener
的方式進行繫結。
小結
vue
中的事件,從編譯生成render
再通過Vue.prototype._render
函式執行render
到生成vNode
,主要是通過on
作為描述。在patch
渲染階段,將on
描述的事件進行處理再通過addEventListener
的方式繫結到$el
上。
三十一、常用修飾符
1、表單修飾符
(1).lazy
在預設情況下,v-model
在每次 input
事件觸發後將輸入框的值與資料進行同步 ,可以新增 lazy
修飾符,從而轉為在 change
事件之後進行同步:
<input v-model.lazy="msg">
(2).number
如果想自動將使用者的輸入值轉為數值型別,可以給 v-model
新增 number
修飾符:
<input v-model.number="age" type="number">
(3).trim
如果要自動過濾使用者輸入的首尾空白字元,可以給 v-model
新增 trim
修飾符:
<input v-model.trim="msg">
2、事件修飾符
(1).stop
阻止單擊事件繼續傳播。 ```
```
(2).prevent
阻止標籤的預設行為。
<a href="http://www.baidu.com" v-on:click.prevent="aClick">點選</a>
(3).capture
事件先在有.capture
修飾符的節點上觸發,然後在其包裹的內部節點中觸發。
```
```
(4).self
只當在 event.target 是當前元素自身時觸發處理函式,即事件不是從內部元素觸發的。 ```
```
(5).once
不像其它只能對原生的 DOM 事件起作用的修飾符,.once
修飾符還能被用到自定義的元件事件上,表示當前事件只觸發一次。
<a v-on:click.once="aClick">點選</a>
(6).passive
.passive
修飾符尤其能夠提升移動端的效能
```
```
3、其他修飾符
除了表單和事件的修飾符,Vue
還提供了很多其他修飾符,在使用的時候可以查閱文件。
小結
Vue
中提供了很多好用的功能和api
,那麼修飾符的出現就為功能和api
提供了更為豐富的擴充套件屬性和更大的靈活度。
三十二、vue-router
vue
路由是單頁面中檢視切換的方案,有三種mode
:
- hash,#後的僅僅作為引數,不屬於url部分
- history,路徑作為請求url請求資源連結,如果找不到會出現404錯誤
- abstract,服務端渲染場景
hash場景下,會出現url
連結,再修改其view-router中對應的值。
瞭解vue-router
的底層實現請參考vue2檢視切換:vue-router
三十三、vuex
vuex
是狀態管理倉庫,一般使用的場景為:多個檢視依賴於同一狀態,來自不同檢視的行為需要變更同一狀態。其管理的狀態是響應式的,修改也只能顯式提交mutation
的方式修改。vuex
有state
、getter
、mutation
、action
和module
五個核心,並且通過module
實現了vuex
樹的管理。
瞭解vuex
的底層實現請參考vue2狀態管理:vuex
三十四、eventBus
使用場景:兄弟元件傳參 ``` const eventBus = new Vue();
const A = {
template: <div @click="send">component-a</div>
,
methods: {
send() {
eventBus.$emit('sendData', 'data from A')
}
},
}
const B = {
template: <div>component-b</div>
,
created() {
eventBus.$on('sendData', (args) => {
console.log(args)
})
},
}
new Vue({
el: '#app',
components: {
A,
B,
},
template: <div><A></A><B></B></div>
,
})
``
在當前例子中,
A元件和
B元件稱為兄弟元件,
A元件通過事件匯流排
eventBus中的
$emit分發事件,
B元件則通過
$on`來監聽事件。
實現原理:eventsMixin
```
export function eventsMixin (Vue: Class
Vue.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this function on () { vm.$off(event, on) fn.apply(vm, arguments) } on.fn = fn vm.$on(event, on) return vm }
Vue.prototype.$off = function (event?: string | Array
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
Event "${lowerCaseEvent}" is emitted in component
+
${formatComponentName(vm)} but the handler is registered for "${event}".
+
Note that HTML attributes are case-insensitive and you cannot use
+
v-on to listen to camelCase events when using in-DOM templates.
+
You should probably use "${hyphenate(event)}" instead of "${event}".
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = event handler for "${event}"
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
``
在
Vue建構函式定義完執行的
eventsMixin函式中,在
Vue.prototype上分別定義了
$on、
$emit、
$off和
$once的方法易實現對事件的繫結、分發、取消和只執行一次的方法。
eventBus就是利用了當
new Vue例項化後例項上的
$on、
$emit、
$off和
$once`進行資料傳遞。
三十五、ref
使用場景: 父元件獲取子元件資料或者執行子元件方法
``
const A = {
template:
new Vue({
el: '#app',
components: {
A,
},
template: <A ref='childRef' @click.native='changeChildData'></A>
,
methods: {
changeChildData() {
// 執行子元件的方法
this.$refs.childRef.increaseAge()
// 獲取子元件的資料
console.log(this.$refs.childRef.childData);
},
}
})
``
在當前例子中,通過
ref='childRef'的方式在當前元件中定義一個
ref,可以通過
this.$refs.childRef的方式獲取到子元件
A。可以通過
this.$refs.childRef.increaseAge()的方式執行子元件中
age增加的方法,也可以通過
this.$refs.childRef.childData`的方式獲取到子元件中的資料。
三十六、props
使用場景: 父子傳參
``
const A = {
template:
new Vue({
el: '#app',
components: {
A
},
template: <A :childData='parentData' @emitChildData='getChildData'></A>
,
data() {
return {
parentData: 'data from parent'
}
},
methods: {
getChildData(v) {
console.log(v);
}
}
})
``
從當前例子中可以看出,資料父傳子是通過
:childData='parentData'的方式,資料子傳父是通過
this.$emit('emitChildData', 'data from child')的方式,然後,父元件通過
@emitChildData='getChildData'`的方式進行獲取。
1、父元件render
函式
new Vue
中傳入的模板template
經過遍歷生成的render
函式如下:
with(this) {
return _c('A', {
attrs: {
"childData": parentData
},
on: {
"emitChildData": getChildData
}
})
}
其中data
部分有attrs
和on
來描述屬性和方法。
在通過createComponent
建立元件vnode
的過程中,會通過const propsData = extractPropsFromVNodeData(data, Ctor, tag)
的方式獲取props
,通過const listeners = data.on
的方式獲取listeners
,最後將其作為引數通過new VNode(options)
的方式例項化元件vnode
。
2、子元件渲染
在通過const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
建立元件例項的過程中,會執行到元件繼承自Vue
的._init
方法,通過initEvents
將事件處理後儲存到vm._events
中,通過initProps
將childData
賦值到子元件A
的vm
例項上,並進行響應式處理,讓其可以通過vm.childData
的方式訪問,並且資料發生變化時檢視也可以發生改變。
元件模板編譯後對應的render
函式是:
with(this) {
return _c('div', {
on: {
"click": emitData
}
}, [_v(_s(childData))])
}
在createElm
完成節點的建立後,在invokeCreateHooks(vnode, insertedVnodeQueue)
階段,給DOM
原生節點節點繫結emitData
。
3、this.$emit
在點選執行this.$emit
時,會通過var cbs = vm._events[event]
取出_events
中的事件進行執行。
至此,父元件中的傳遞的資料就在子元件中可以通過this.xxx
的方式獲得,也可以通過this.$emit
的方式將子元件中的資料傳遞給父元件。
prop
資料發生改變引起檢視變化的底層邏輯請參考vue2從資料變化到檢視變化:props引起檢視變化詳解
三十七、$attrs
和$listeners
使用場景: 父子元件非props
屬性和非native
方法傳遞
```
// main.js檔案
import Vue from "vue";
const B = {
template: <div @click="emitData">{{ formParentData }}</div>
,
data() {
return {
formParentData: ''
}
},
inheritAttrs: false,
created() { this.formParentData = this.$attrs; console.log(this.$attrs, '--------------a-component-$attrs') console.log(this.$listeners, '--------------b-component-$listeners') }, methods: { emitData() { this.$emit('onFun', 'form B component') } }, }
const A = {
template: <B v-bind='$attrs' v-on='$listeners'></B>
,
components: {
B,
},
props: ['propData'],
inheritAttrs: false,
created() {
console.log(this.$attrs, '--------------b-component-$attrs')
console.log(this.$listeners, '--------------b-component-$listeners')
}
}
new Vue({
el: '#app',
components: {
A,
},
template: <A :attrData='parentData' :propData='parentData' @click.native="nativeFun" @onFun="onFun"></A>
,
data() {
return {
parentData: 'msg'
}
},
methods: {
nativeFun() {
console.log('方法A');
},
onFun(v) {
console.log('方法B', v);
},
}
})
``
當前例子中,
new Vue的
template模板中有
attrData、
propData、
click.native和
onFun在進行傳遞。實際執行後,在
A元件中
this.$attrs為
{attrData: 'msg'},
this.$listeners為
{onFun:f(...)}。在
A元件中通過
v-bind='$attrs'和
v-on='$listeners'的方式繼續進行屬性和方法的傳遞,在
B元件中就可以獲取到
A元件中傳入的
$attrs和
$listeners`。
當前例子中完成了非props
屬性和非native
方法的傳遞,並且通過v-bind='$attrs'
和v-on='$listeners'
的方式實現了屬性和方法的跨層級傳遞。
同時通過this.$emit
的方法觸發了根節點中onFun
事件。
關於例子中的inheritAttrs: false
,預設情況下父作用域的不被認作props
的attribute
繫結將會“回退”且作為普通的HTML
屬性應用在子元件的根元素上。當撰寫包裹一個目標元素或另一個元件的元件時,這可能不會總是符合預期行為。通過設定inheritAttrs
到false
,這些預設行為將會被去掉。
三十八、$parent
和$children
使用場景: 利用父子關係進行資料的獲取或者方法的呼叫
``
const A = {
template:
,
data() {
return {
childRandom: Math.random()
}
},
mounted() {
console.log(this.$parent.parentCount, '--child-created--'); // 獲取父元件中的parentCount
},
methods: {
changeParentData() {
console.log(this.$parent); // 列印當前例項的$parent
this.$parent.changeParentData(); // 呼叫當前父級中的方法
changeParentData},
changeChildData() {
this.childRandom = Math.random();
}
}
}
const B = {
template:
new Vue({
el: '#app',
components: {
A,
B,
},
template: <div><A></A><B></B><p>{{parentCount}}</p><button @click="changeChildrenData">修改子元件資料</button></div>
,
data() {
return {
parentCount: 1
}
},
mounted() {
console.log(this.$children[0].childRandom, '--parent-created--'); // 獲取第一個子元件中的childRandom
},
methods: {
changeParentData() {
this.parentCount++;
},
changeChildrenData() {
console.log(this.$children); // 此時有兩個子元件
this.$children[0].changeChildData(); // 調起第一個子元件中的'changeChildData'方法
}
}
})
``
在當前例子中,父元件可以通過
this.$children獲取所有的子元件,這裡有
A元件和
B元件,可以通過
this.$children[0].childRandom的方式獲取子元件
A中的資料,也可以通過
this.$children[0].changeChildData()的方式調起子元件
A`中的方法。
子元件可以通過this.$parent
的方式獲取父元件,可以通過this.$parent.parentCount
獲取父元件中的資料,也可以通過this.$parent.changeParentData()
的方式修改父元件中的資料。
Vue
中$parent
和$children
父子關係的底層構建請參考雜談:$parent/$children的底層邏輯
三十九、inject
和provide
使用場景:巢狀元件多層級傳參
``
const B = {
template:
const A = {
template: <B></B>
,
components: {
B,
},
}
new Vue({
el: '#app',
components: {
A,
},
template: <A></A>
,
provide: {
parentData1: {
name: 'name-2',
age: 30
},
parentData2: {
name: 'name-2',
age: 29
},
}
})
``
例子中在
new Vue的時候通過
provide提供了兩個資料來源
parentData1和
parentData2,然後跨了一個
A元件在
B元件中通過
inject`注入了這兩個資料。
1、initProvide
在執行元件內部的this._init
初始化方法時,會執行到initProvide
邏輯:
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
如果在當前vm.$options
中存在provide
,會將其執行結果賦值給vm._provided
。
2、initInjections
``
function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
Avoid mutating an injected value directly since the changes will be +
overwritten whenever the provided component re-renders. +
injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
``
如果當前元件中有選項
inject,會以
while迴圈的方式不斷在
source = source.$parent中尋找
_provided`,然後獲取到祖先元件中提供的資料來源,這是實現祖先元件向所有子孫後代注入依賴的核心。
四十、Vue
專案能做的效能優化
1、v-if
和v-show
- 頻繁切換時使用
v-show
,利用其快取特性 - 首屏渲染時使用
v-if
,如果為false
則不進行渲染
2、v-for
的key
- 列表變化時,迴圈時使用唯一不變的
key
,藉助其本地複用策略 - 列表只進行一次渲染時,
key
可以採用迴圈的index
3、偵聽器和計算屬性
- 偵聽器
watch
用於資料變化時引起其他行為 - 多使用
compouter
計算屬性顧名思義就是新計算而來的屬性,如果依賴的資料未發生變化,不會觸發重新計算
4、合理使用生命週期
- 在
destroyed
階段進行繫結事件或者定時器的銷燬 - 使用動態元件的時候通過
keep-alive
包裹進行快取處理,相關的操作可以在actived
階段啟用
5、資料響應式處理
- 不需要響應式處理的資料可以通過
Object.freeze
處理,或者直接通過this.xxx = xxx
的方式進行定義 - 需要響應式處理的屬性可以通過
this.$set
的方式處理,而不是JSON.parse(JSON.stringify(XXX))
的方式
6、路由載入方式
- 頁面元件可以採用非同步載入的方式
7、外掛引入
- 第三方外掛可以採用按需載入的方式,比如
element-ui
。
8、減少程式碼量
- 採用
mixin
的方式抽離公共方法 - 抽離公共元件
- 定義公共方法至公共
js
中 - 抽離公共
css
9、編譯方式
- 如果線上需要
template
的編譯,可以採用完成版vue.esm.js
- 如果線上無需
template
的編譯,可採用執行時版本vue.runtime.esm.js
,相比完整版體積要小大約30%
10、渲染方式
- 服務端渲染,如果是需要
SEO
的網站可以採用服務端渲染的方式 - 前端渲染,一些企業內部使用的後端管理系統可以採用前端渲染的方式
11、字型圖示的使用
- 有些圖片圖示儘可能使用字型圖示
四十一、Vue
專案白屏問題
- 1、開啟
gzip
壓縮減小檔案體積。 - 2、
webpack
設定productionSourceMap:false
,不在線上環境打包.map
檔案。 - 3、路由懶載入
- 4、非同步元件的使用
- 5、靜態資源使用
cdn
連結引入 - 6、採用
ssr
服務端渲染方案 - 7、骨架屏或者
loading
效果填充空白間隙 - 8、首次不渲染的隱藏採用
v-if
- 9、注重程式碼規範:抽取公共元件,公共js,公共css樣式,減小程式碼體積。刪除無用程式碼,減少非必要註釋。防止寫出死迴圈等等
- 10、刪除輔助開發的
console.log
- 11、非
Vue
角度思考:非重要檔案採用非同步載入方式、css樣式採用媒體查詢、採用域名分片技術、http1升級成http2、如果是SSR專案考慮服務端渲染有沒有可優化的點、請求頭是否帶了多餘資訊等思路
內容有些多,大體可以歸類為從服務端拿到資源的速度、資源的體積和渲染是否阻塞的角度去作答。
四十二、從0
到1
構建一個Vue
專案需要注意什麼
- 架子:選用合適的初始化腳手架(
vue-cli2.0
或者vue-cli3.0
) - 請求:資料
axios
請求的配置 - 登入:登入註冊系統
- 路由:路由管理頁面
- 資料:
vuex
全域性資料管理 - 許可權:許可權管理系統
- 埋點:埋點系統
- 外掛:第三方外掛的選取以及引入方式
- 錯誤:錯誤頁面
- 入口:前端資源直接當靜態資源,或者服務端模板拉取
SEO
:如果考慮SEO
建議採用SSR
方案- 元件:基礎元件/業務元件
- 樣式:樣式預處理起,公共樣式抽取
- 方法:公共方法抽離
四十三、SSR
1、什麼是服務端渲染(SSR)?
Vue.js 是構建客戶端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以將同一個元件渲染為伺服器端的 HTML 字串,將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為客戶端上完全可互動的應用程式。
2、為什麼使用服務端渲染(SSR)?
與傳統 SPA (單頁應用程式 (Single-Page Application)) 相比,伺服器端渲染 (SSR) 的優勢主要在於: - 更好的 SEO,由於搜尋引擎爬蟲抓取工具可以直接檢視完全渲染的頁面。 - 更快的內容到達時間 (time-to-content),特別是對於緩慢的網路情況或執行緩慢的裝置。
3、使用伺服器端渲染 (SSR) 時需要考慮的問題?
使用伺服器端渲染 (SSR) 時還需要有一些權衡之處 - 開發條件所限。瀏覽器特定的程式碼,只能在某些生命週期鉤子函式 (lifecycle hook) 中使用;一些外部擴充套件庫 (external library) 可能需要特殊處理,才能在伺服器渲染應用程式中執行。 - 涉及構建設定和部署的更多要求。與可以部署在任何靜態檔案伺服器上的完全靜態單頁面應用程式 (SPA) 不同,伺服器渲染應用程式,需要處於 Node.js server 執行環境。 - 更多的伺服器端負載。在 Node.js 中渲染完整的應用程式,顯然會比僅僅提供靜態檔案的 server 更加大量佔用 CPU 資源 (CPU-intensive - CPU 密集),因此如果你預料在高流量環境 (high traffic) 下使用,請準備相應的伺服器負載,並明智地採用快取策略。
四十四、scoped
在Vue
專案開發的專案中如果樣式中未使用scoped
,元件間的樣式會出現覆蓋的問題。
反例: ``` // app.vue檔案
```
``` // child.vue檔案
```
父元件和子元件的樣式顏色都為green
,子元件中的樣式覆蓋了父元件的樣式。
正例:
```
``
此時,父元件中顏色為
red,子元件中顏色為
green`。
主要原因:
例子中的DOM節點和CSS層疊樣式中都被添加了data-v-xxx
來表示唯一,所以scoped
是給當前元件的節點和樣式唯一標識為data-v-xxx
,避免了樣式覆蓋。
文中內容持續更新中,可以先點贊收藏吆~
在寒冬裡,依然要有一顆火熱的心,一起加油~
- obj.a.b.c獲取值的異常處理
- 設計模式手記(16種)
- vue高頻面試知識點彙總【2022寒冬版】
- 因逾期不發獎(50000QB),@英雄聯盟賽事 微博賬號被判違規
- 【全教程】qt連線mysql——從qt編譯mysql驅動到qt連線mysql資料庫(二、編譯連線)
- 壓測場景下的 TIME_WAIT 處理
- 2021年美國大學生數學建模競賽B題思路分析
- 資料探勘競賽指南:曾經的資料探勘少年,如今的阿里演算法大佬
- 如何申請郵件安全證書(S/MIME)實現郵件加密和數字簽名
- Mysql5.7安裝步驟(安裝版)
- AKS Azure AD整合效果測試
- 如何為 Web 主機做預算
- 淺談凝聚態物理的格林函式方法學習心得
- 阿里雲虛擬主機無法安裝Typecho報錯
- 劍指offer(二):不修改陣列找出重複的數字
- 推薦 :一文帶你熟悉貝葉斯統計
- echarts tree (樹圖) 實現自定義節點圖示 自定義樣式 點選節點後線條變色 自適應高度 搜尋後節點關鍵字標紅 寫的很詳細,建議收藏!
- 百度網盤限速解決方案
- Online Retail-同期群分析:pandas matplotlib seaborn - 知乎
- 加一計時器——每隔1s六位數碼管顯示數字加1,直至999999,之後歸零,重新開始。