vue高頻面試知識點彙總【2022寒冬版】

語言: CN / TW / HK

很喜歡‘萬變不離其宗’這句話,希望在不斷的思考和總結中找到Vue中的,來解答面試官丟擲的各種Vue問題,一起加油~

一、MVVM原理

Vue2官方文件中沒有找到VueMVVM的直接證據,但文件有提到:雖然沒有完全遵循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腳手架中直接使用,參考文件:https://cn.vuejs.org/v2/guide/installation.html - 或者在html檔案的頭部通過靜態檔案的方式引入: <script src="https://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:

{{msg}}
`, })

console.log(app); ``new Vue就是對建構函式Vue`進行例項化,執行結果如下:

image.png

可以看出例項化後的例項中包含了很多屬性,用來對當前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 節點樹如下圖所示:

544ef95bdd7c96a19d700ce613ab425a_dom-tree.png 每個元素都是一個節點。每段文字也是一個節點。甚至註釋也都是節點。一個節點就是頁面的一個部分。就像家譜樹一樣,每個節點都可以有孩子節點 (也就是說每個部分可以包含其它的一些部分)。

再看VueHTML 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執行會獲取到vNodevm._update就會對vNode進行patch的處理,又分為模板渲染和元件渲染。

十、資料響應式處理

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的年齡是
可以看出點選後,objname屬性變化得到了檢視更新,而age屬性並未進行變化。

name屬性響應式的過程中鎖定了一個釋出者dep,在當前檢視渲染時在釋出者depsubs中做了記錄,一旦其發生改變,就會觸發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個,分別為beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedactivateddeactivatedbeforeDestroydestroyederrorCaptured

具體實現請參考 vue生命週期

十七、v-showv-if的區別

先看v-ifv-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:nonedisplay:block之間的切換比起空的註釋節點和真實節點的開銷要小很多,這種場景就建議使用v-show

可以通過vue中v-if和v-show的區別(原始碼分析)瞭解v-ifv-show詳細過程。

十八、v-forkey的作用

v-for進行迴圈展示過程中,當資料發生變化進行渲染的過程中,會進行新舊節點列表的比對。首先新舊vnode列表首先通過首首尾尾首尾尾首的方式進行比對,如果key相同則採取原地複用的策略進行節點的移動。

如果首尾兩兩比對的方式找不到對應關係,繼續通過keyvnode的對應關係進行尋找。

如果keyvnode對應關係中找不到,繼續通過sameVnode的方式在未比對的節點中進行尋找。

如果都找不到,則將其按照新vnode進行createElm的方式進行建立,這種方式是比節點移動的方式計算量更大。

最後將舊的vnode列表中沒有進行匹配的vnode中的vnode.elm在父節點中移除。

簡單總結就是,新的vnode列表在舊的vnode列表中去尋找具有相同的key的節點進行原地複用,如果找不到則通過建立的方式createElm去建立一個,如果舊的vnode列表中沒有進行匹配則在父節點中移除其vnode.elm。這就是原地複用邏輯的大體實現。

具體keydiff演算法的關係可以參考vue2從資料變化到檢視變化:diff演算法圖解

十九、v-forv-if能同時使用嗎

答案是:用了也能出來預期的效果,但是會有效能浪費。

同時包含v-forv-iftemplate模板在編輯階段會執行v-forv-if優先順序更高的編譯流程;在生成vnode的階段,會包含屬性isCommenttrue的空白佔位vnode;在patch階段,會生成真實的佔位節點。雖然一個空的佔位節點無妨,但是如果資料量比較大的話,也是一個性能問題。

當然,可以在獲取到資料(一般是在beforeCreate或者created階段)時進行過濾處理,也可以通過計算屬性對其進行處理。

可以通過v-forv-if可以一起使用嗎?瞭解v-forv-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中的值 - 同步操作,不支援非同步

偵聽屬性: - 使用場景:當需要在資料變化時執行非同步或開銷較大的操作時,可以用偵聽屬性 - 可配置引數:可以通過配置immediatedeep來控制立即執行和深度監聽的行為 - 偵聽屬性偵聽的是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:

`, data() { return { url: "" }; } };

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:headerv-slot:footer,子元件中定義了對應的插槽被添加了屬性name="header"name="footer",未被進行插槽標識的內容被插入到了匿名的`中。

2、作用域插槽

`` let currentUser = { template: {{childData.firstName}} `, data() { return { childData: { firstName: "first", lastName: "last" } }; } };

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通過filterEmptyfilterAfilterB('愛好是', transformHobby('chess'))}`進行三層過濾。

Vue.filters的底層實現請檢視vue中的filters(原始碼分析)

二十六、Vue.use

  • 作用:Vue.use被用來安裝Vue.js外掛,例如vue-routervuexelement-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例項。我們定義初始的templatedatamethodsvm進行使用,如果有變化,在例項的過程中傳入新的選項引數即可,比如例子中例項化第二個vm的時候就對data進行了調整。

2、選項extends

extends允許宣告擴充套件另一個元件 (可以是一個簡單的選項物件或建構函式),而無需使用 Vue.extend。這主要是為了便於擴充套件單檔案元件,以實現元件繼承的目的。 `` const common = { template:

{{name}}
`, data() { return { name: '表單' } } }

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.directivedirectives

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標籤的點選事件新增修飾符stopprevent,那麼就不會觸發其a的預設行為,即使href不為空也不會進行跳轉,同時,div上的點選事件也不會進行觸發。

模板的渲染一般分為編譯生成render函式、render函式執行生成vNodepatch進行渲染。下面按照這步驟進行簡單分析。

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("點選")])]) } 其中divon作為div事件描述。a標籤的attrs作為屬性描述,on作為事件描述,在描述中.stop被編譯成了$event.stopPropagation()來阻止事件冒泡,.prevent被編譯成了$event.preventDefault()用來阻止a標籤的預設行為。

2、vNode

通過執行Vue.prototype._renderrender函式轉換成vNode

3、patch

patch的過程中,當完成$el節點的渲染後會執行invokeCreateHooks(vnode, insertedVnodeQueue)邏輯,其中,針對attrs會將其設定為$el的真實屬性,當前例子中會為a標籤設定herf屬性。針對on會通過target.addEventListener的方式將其處理過的事件繫結到$el上,當前例子中會分別對diva中的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 是當前元素自身時觸發處理函式,即事件不是從內部元素觸發的。 ```

phrase點選

```

(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的方式修改。vuexstategettermutationactionmodule五個核心,並且通過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) { const hookRE = /^hook:/ Vue.prototype.$on = function (event: string | Array, fn: Function): Component { const vm: Component = this if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn) } } else { (vm._events[event] || (vm._events[event] = [])).push(fn) // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true } } return vm }

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, fn?: Function): Component { const vm: Component = this // all if (!arguments.length) { vm._events = Object.create(null) return vm } // array of events if (Array.isArray(event)) { for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // specific event const cbs = vm._events[event] if (!cbs) { return vm } if (!fn) { vm._events[event] = null return vm } // specific handler let cb let i = cbs.length while (i--) { cb = cbs[i] if (cb === fn || cb.fn === fn) { cbs.splice(i, 1) break } } return vm }

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:

{{childData.age}}
`, data() { return { childData: { name: 'qb', age: 30 }, } }, methods: { increaseAge() { this.childData.age++; } } }

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:

{{childData}}
`, props: ['childData'], methods: { emitData() { this.$emit('emitChildData', 'data from child') } }, }

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部分有attrson來描述屬性和方法。

在通過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中,通過initPropschildData賦值到子元件Avm例項上,並進行響應式處理,讓其可以通過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 Vuetemplate模板中有attrDatapropDataclick.nativeonFun在進行傳遞。實際執行後,在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,預設情況下父作用域的不被認作propsattribute繫結將會“回退”且作為普通的HTML屬性應用在子元件的根元素上。當撰寫包裹一個目標元素或另一個元件的元件時,這可能不會總是符合預期行為。通過設定inheritAttrsfalse,這些預設行為將會被去掉。

三十八、$parent$children

使用場景: 利用父子關係進行資料的獲取或者方法的呼叫 `` const A = { template:

{{childRandom}}
, 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:
b-component
`, }

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的底層邏輯

三十九、injectprovide

使用場景:巢狀元件多層級傳參 `` const B = { template:

{{parentData1}}{{parentData2}}
`, inject: ['parentData1', 'parentData2'], }

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提供了兩個資料來源parentData1parentData2,然後跨了一個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-ifv-show

  • 頻繁切換時使用v-show,利用其快取特性
  • 首屏渲染時使用v-if,如果為false則不進行渲染

2、v-forkey

  • 列表變化時,迴圈時使用唯一不變的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專案考慮服務端渲染有沒有可優化的點、請求頭是否帶了多餘資訊等思路

內容有些多,大體可以歸類為從服務端拿到資源的速度、資源的體積和渲染是否阻塞的角度去作答。

四十二、從01構建一個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`。

主要原因:

image.png

image.png 例子中的DOM節點和CSS層疊樣式中都被添加了data-v-xxx來表示唯一,所以scoped是給當前元件的節點和樣式唯一標識為data-v-xxx,避免了樣式覆蓋。

文中內容持續更新中,可以先點贊收藏吆~
在寒冬裡,依然要有一顆火熱的心,一起加油~