基礎篇
1. 對 SPA 單頁⾯的理解,優缺點是什麼?
- SPA( single-page application )僅在 Web 頁⾯初始化時加載相應的 HTML、JavaScript 和 CSS。 ⼀旦頁⾯加載完成,SPA 不會因為⽤戶的操作⽽進⾏⻚⾯的重新加載或跳轉;取⽽代之的是利⽤路由機制實現 HTML 內容的變換,UI 與⽤戶的交互,避免頁⾯的重新加載。
優點:
- ⽤戶體驗好、快,內容的改變不需要重新加載整個頁⾯,避免了不必要的跳轉和重複渲染
- SPA 相對對服務器壓⼒⼩
- 前後端職責分離,架構清晰,前端進⾏交互邏輯,後端負責數據處理
缺點:
- ⾸屏(初次)加載慢:為實現單⻚ Web 應⽤功能及顯示效果,需要在加載⻚⾯的時候將JavaScript、CSS 統⼀加載,部分⻚⾯按需加載
- 不利於 SEO:由於所有的內容都在⼀個⻚⾯中動態替換顯示,所以在 SEO 上其有著天然的弱勢
2. new Vue() 發生了什麼?
- 結論:new Vue()是創建Vue實例,它內部執行了根實例的初始化過程
- 具體包括以下操作:
- 選項合併,children, refs, slot, createElement等实例属性的方法初始化
- 自定義事件處理
- 數據響應式處理
- 生命週期鉤子調用 (beforecreate created)
- 可能的掛載
- 總結:new Vue()創建了根實例並準備好數據和方法,未來執行掛載時,此過程還會遞歸的應用於它的子組件上,最終形成一個有緊密關係的組件實例樹
3. Vue.use是幹什麼的?原理是什麼?
- vue.use 是用來使用插件的,我們可以在插件中擴展全局組件、指令、原型方法等。
- 檢查插件是否註冊,若已註冊,則直接跳出
- 處理入參,將第一個參數之後的參數歸集,並在首部塞入 this 上下文
- 執行註冊方法,調用定義好的 install 方法,傳入處理的參數,若沒有 install 方法並且插件本身為 function 則直接進行註冊
- 插件不能重複的加載,install 方法的第一個參數是vue的構造函數,其他參數是Vue.set中除了第一個參數的其他參數; 代碼:args.unshift(this)
- 調用插件的install 方法 代碼:typeof plugin.install === “function”
- 插件本身是一個函數,直接讓函數執行。代碼:plugin.apply(null, args)
- 緩存插件。代碼:installedPlugins.push(plugin)
4. 請說一下響應式數據的理解?
- 根據數據類型來做不同處理,數組和對像類型當值變化時如何劫持。
- 對象內部通過defineReactive方法,使用 Object.defineProperty() 監聽數據屬性的 get 來進行數據依賴收集,再通過 set 來完成數據更新的派發
- 數組則通過重寫數組方法來實現的。擴展它的 7 個變更⽅法,通過監聽這些方法可以做到依賴收集和派發更新
內部依賴收集是怎麼做到的?每個屬性都擁有自己的dep屬性,存放他所依賴的 watcher,當屬性變化後會通知自己對應的 watcher去更新
響應式流程:
- defineReactive 把數據定義成響應式的
- 給屬性增加一個 dep,用來收集對應的那些watcher
- 等數據變化進行更新
dep.depend() // get 取值:進行依賴收集
dep.notify() // set 設置時:通知視圖更新
- 對象層級過深,性能就會差
- 不需要響應數據的內容不要放在data中
- object.freeze() 可以凍結數據
5. Vue如何檢測數組變化?
- 數組考慮性能原因沒有用defineProperty對數組的每一項進行攔截,而是選擇重寫數組 方法以進行重寫。當數組調用到這 7 個方法的時候,執行 ob.dep.notify() 進行派發通知 Watcher 更新
- 在Vue中修改數組的索引和長度是無法監控到的。需要通過以下7種變異方法修改數組才會觸發數組對應的wacther進行更新。數組中如果是對像數據類型也會進行遞歸劫持
那如果想要改索引更新數據怎麼辦?
- 可以通過Vue.set()來進行處理 ➞ 核心內部用的是 splice 方法
|
|
6. Vue.set 方法是如何實現的?
- 為什麼$set可以觸發更新,我們給對象和數組本身都增加了dep屬性,當給對像新增不存在的屬性則觸發對象依賴的watcher去更新,當修改數組索引時我們調用數組本身的splice方法去更新數組
官方定義 Vue.set(object, key, value)
7. Vue中模板編譯原理?
- 如何將template轉換成render函數(這裡要注意的是我們在開發時盡量不要使用template,因為將template轉化成render方法需要在運行時進行編譯操作會有性能損耗,同時引用帶有complier包的vue體積也會變大) 默認.vue文件中的 template處理是通過vue-loader 來進行處理的並不是通過運行時的編譯
- 將 template 模板轉換成 ast 語法樹 - parserHTML
- 對靜態語法做靜態標記 - markUp
- 重新生成代碼 - codeGen
模板引擎的實現原理就是new Function + with來進行實現的
vue-loader中处理template属性主要靠的是 vue-template-compiler
|
|
8. Proxy 與 Object.defineProperty 優劣對比
- Proxy 的優勢如下
- Proxy 可以直接監聽對象而非屬性
- Proxy 可以直接監聽數組的變化
- Proxy 有多達 13 種攔截方法,不限於 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具備的
- Proxy 返回的是一個新對象,我們可以只操作新的對象達到目的,而 Object.defineProperty 只能遍歷對象屬性直接修改
- Proxy 作為新標準將受到瀏覽器廠商重點持續的性能優化,也就是傳說中的新標準的性能紅利
- Object.defineProperty 的優勢如下
- 兼容性好,支持 IE9,而 Proxy 的存在瀏覽器兼容性問題,而且無法用 polyfill 磨平,因此 Vue 的作者才聲明需要等到下個大版本( 3.0 )才能用 Proxy 重寫
9. Vue3.x響應式數據原理
- Vue3.x改用Proxy替代Object.defineProperty。因為Proxy可以直接監聽對象和數組的變化,並且有多達13種攔截方法。並且作為新標準將受到瀏覽器廠商重點持續的性能優化
Proxy只會代理對象的第一層,那麼Vue3又是怎樣處理這個問題的呢?
- 判斷當前Reflect.get的返回值是否為Object,如果是則再通過reactive方法做代理, 這樣就實現了深度觀測
監測數組的時候可能觸發多次get/set,那麼如何防止觸發多次呢?
- 我們可以判斷key是否為當前被代理對象target自身屬性,也可以判斷舊值與新值是否相等,只有滿足以上兩個條件之一時,才有可能執行trigger
生命周期篇
Vue的生命週期方法有哪些?一般在哪一步發起請求及原因
- 總共分為8個階段:創建前/後,載入前/後,更新前/後,銷毀前/後
創建前/後:
beforeCreate階段:vue實例的掛載元素el和數據對象data都為undefined,還未初始化。
說明:在當前階段data、methods、computed以及watch上的數據和方法都不能被訪問。created階段:vue實例的數據對象data有了,el還沒有。
說明:可以做一些初始數據的獲取,在當前階段無法與Dom進行交互,如果非要想,可以通過vm.$nextTick來訪問Dom。
載入前/後:
beforeMount階段:vue實例的$el和data都初始化了,但還是掛載之前為虛擬的dom節點。
說明:當前階段虛擬Dom已經創建完成,即將開始渲染。在此時也可以對數據進行更改,不會觸發updated。mounted階段:vue實例掛載完成,data.message成功渲染。
說明:在當前階段,真實的Dom掛載完畢,數據完成雙向綁定,可以訪問到Dom節點,使用$refs屬性對Dom進行操作。
更新前/後:
beforeUpdate階段:響應式數據更新時調用,發生在虛擬DOM打補丁之前,適合在更新之前訪問現有的DOM,比如手動移除已添加的事件監聽器。
說明:可以在當前階段進行更改數據,不會造成重渲染。updated階段:虛擬DOM重新渲染和打補丁之後調用,組成新的DOM已經更新,避免在這個鉤子函數中操作數據,防止死循環。
說明:當前階段組件Dom已完成更新。要注意的是避免在此期間更改數據,因為這可能會導致無限循環的更新。
銷毀前/後:
beforeDestroy階段:實例銷毀前調用,實例還可以用,this能獲取到實例,常用於銷毀定時器,解綁事件。
說明:在當前階段實例完全可以被使用,我們可以在這時進行善後收尾工作,比如清除計時器。destroyed階段:實例銷毀後調用,調用後所有事件監聽器會被移除,所有的子實例都會被銷毀。
說明:當前階段組件已被拆解,數據綁定被卸除,監聽被移出,子實例也統統被銷毀。
補充:
第一次頁面加載時會觸發:beforeCreate, created, beforeMount, mounted。
- created 實例已經創建完成,因為它是最早觸發的原因可以進行一些數據,資源的請求。 (服務器渲染支持created方法)
- mounted 實例已經掛載完成,可以進行一些DOM操作。 (接口請求)
生命週期鉤子是如何實現的?
- Vue的生命週期鉤子就是回調函數而已,當創建組件實例的過程中會調用對應的鉤子方法。
補充:
內部主要是使用callHook方法來調用對應的方法。核心是一個發布訂閱模式,將鉤子訂閱好(內部採用數組的方式存儲),在對應的階段進行發布。
Vue 的父組件和子組件生命週期鉤子執行順序
- 第一次頁面加載時會觸發 beforeCreate, created, beforeMount, mounted 這幾個鉤子。
渲染過程:
父組件掛載完成一定是等子組件都掛載完成後,才算是父組件掛載完,所以父組件的mounted在子組件mouted之後
父beforeCreate ➡ 父created ➡ 父beforeMount ➡ 子beforeCreate ➡ 子created ➡ 子beforeMount ➡ 子mounted ➡ 父mounted
子組件更新過程:
影響到父組件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
不影響父組件:子beforeUpdate -> 子updated
父組件更新過程:
影響到子組件:父beforeUpdate -> 子beforeUpdate->子updated -> 父updted
不影響子組件:父beforeUpdate -> 父updated
銷毀過程:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
重要:父組件等待子組件完成後,才會執行自己對應完成的鉤子。
組件通信篇
Vue中的組件的data 為什麼是一個函數?
- 每次使用組件時都會對組件進行實例化操作,並且調用data函數返回一個對像作為組件的數據源。這樣可以保證多個組件間數據互不影響。
- 如果data是對象的話,對象屬於引用類型,會影響到所有的實例。所以為了保證組件不同的實例之間data不衝突,data必須是一個函數。
Vue 組件間通信有哪幾種方式?
- Vue 組件間通信只要指以下 3 類通信:父子組件通信、隔代組件通信、兄弟組件通信,下面我們分別介紹每種通信方式且會說明此種方法可適用於哪類組件間通信。
props / $emit 適用父子組件通信
- 這種方法是 Vue 組件的基礎,相信大部分同學耳聞能詳,所以此處就不舉例展開介紹。
ref 與 $parent / children适用父子组件通信
- ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子組件上,引用就指向組件實例
- parent / $children:訪問父 / 子實例
EventBus ($emit / $on) 適用於 父子、隔代、兄弟組件通信
- 這種方法通過一個空的 Vue 實例作為中央事件總線(事件中心),用它來觸發事件和監聽事件,從而實現任何組件間的通信,包括父子、隔代、兄弟組件。
attrs / listeners 適用於隔代組件通信
- attrs:包含了父作用域中不被prop所識別(且獲取)的特性綁定(class和style除外)。當一個組件沒有聲明任何prop時,這裡會包含所有父作用域的綁定(class和style除外),並且可以通過v−bind=“attrs” 傳入內部組件。通常配合 inheritAttrs 選項一起使用。
- listeners:包含了父作用域中的(不含.native修飾器的) v-on事件監聽器。它可以通過v−on=“listeners” 傳入內部組件
provide / inject 適用於隔代組件通信
- 祖先組件中通過 provider 來提供變量,然後在子孫組件中通過 inject 來注入變量。 provide / inject API 主要解決了跨級組件間的通信問題,不過它的使用場景,主要是子組件獲取上級組件的狀態,跨級組件間建立了一種主動提供與依賴注入的關係。
Vuex 適用於 父子、隔代、兄弟組件通信
- Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。每一個 Vuex 應用的核心就是 store(倉庫)。
- “store” 基本上就是一個容器,它包含著你的應用中大部分的狀態 (state)。
組件中寫 name選項有哪些好處及作用?
- 可以通過名字找到對應的組件(遞歸組件)
- 可以通過name屬性實現緩存功能 (keep-alive)
- 可以通過name來識別組件(跨級組件通信時非常重要)
|
|
keep-alive平時在哪裡使用?原理是?
- keep-alive 主要是組件緩存,採用的是LRU算法。最近最久未使用法。
- 常用的兩個屬性include/exclude,允許組件有條件的進行緩存。
- 兩個生命週期activated/deactivated,用來得知當前組件是否處於活躍狀態。
|
|
Vue.minxin的使用場景和原理?
- Vue.mixin的作用就是抽離公共的業務邏輯,原理類似"對象的繼承",當組件初始化時會調用 mergeOptions方法進行合併,採用策略模式針對不同的屬性進行合併,如果混入的數據和本身組件中的數據衝突,會採用"就近原則"以組件的數據為準。
補充:
mixin中有很多缺陷"命名衝突問題"、“依賴問題”、“數據來源問題”,這裡強調一下mixin的數據是不會被共享的。
路由篇
Vue-router有幾種鉤子函數?具體是什麼及執行流程是怎樣的?
- 路由鉤子的執行流程,鉤子函數種類有:全局守衛、路由守衛、組件守衛。
完整的導航解析流程
- 導航被觸發
- 在失活的組件裡調用beforeRouteLeave守衛
- 調用全局beforeEach守衛
- 在復用組件裡調用beforeRouteUpdate守衛
- 調用路由配置裡的beforeEnter守衛
- 解析異步路由組件
- 在被激活的組件裡調用beforeRouteEnter守衛
- 調用全局beforeResolve守衛
- 導航被確認
- 調用全局的afterEach鉤子
- DOM更新
- 用創建好的實例調用beforeRouteEnter守衛中傳給next的回調函數
vue-router 兩種模式的區別?
- vue-router 有 3 種路由模式:
hash
、history
、abstract
。
hash模式:hash + hashChange
特點:hash雖然在URL中,但不被包括在HTTP請求中;用來指導瀏覽器動作,對服務端安全無用,hash不會重加載頁面。通過監聽 hash(#)的變化來執行js代碼 從而實現 頁面的改變。
核心代码:
|
|
history模式:historyApi + popState
HTML5推出的history API,由pushState()記錄操作歷史,監聽popstate事件來監聽到狀態變更。
因為只要刷新這個url(
www.ff.ff/jjkj/fdfd/fdf/fd
)就會請求服務器,然而服務器上根本沒有這個資源,所以就會報404,解決方案就配置一下服務器端。說明:
1. hash: 使用 URL hash 值來作路由。支持所有瀏覽器,包括不支持 HTML5 History Api 的瀏覽器
2. history : 依賴 HTML5 History API 和服務器配置。具體可以查看 HTML5 History 模式
3. abstract : 支持所有 JavaScript 運行環境,如 Node.js 服務器端。如果發現沒有瀏覽器的 API,路由會自動強制進入這個模式
屬性作用與對比篇
nextTick在哪裡使用?原理是?
- nextTick的回調是在下次DOM更新循環結束之後執行的延遲回調。在修改數據之後立即使用這個方法,獲取更新後的DOM。 nextTick主要使用了宏任務和微任務。原理就是異步方法(promise, mutationObserver, setImmediate, setTimeout)經常與事件循環一起來問。
補充:
vue多次更新數據,最終會進行批處理更新。內部調用的就是nextTick實現了延遲更新,用戶自定義的nextTick中的回調會被延遲到更新完成後調用,從而可以獲取更新後的DOM。
Vue 為什麼需要虛擬DOM?虛擬DOM的優劣如何?
- Virtual DOM 就是用js對象來描述真實DOM,是對真實DOM的抽象,由於直接操作DOM性能低但是js層的操作效率高,可以將DOM操作轉化成對像操作,最終通過diff算法比對差異進行更新DOM(減少了對真實DOM的操作)。虛擬DOM不依賴真實平台環境從而也可以實現跨平台。
補充:
虛擬DOM的實現就是普通對象包含tag、data、children等屬性對真實節點的描述。(本質上就是在JS和DOM之間的一個緩存)Vue2的 Virtual DOM 借鑒了開源庫snabbdom的實現。
VirtualDOM映射到真實DOM要經歷VNode的create、diff、patch等階段。
Vue中key的作用和工作原理,說說你對它的理解
例如:
|
|
- key的作用主要是為了高效的更新虛擬DOM,其原理是vue在patch過程中通過key可以精準判斷兩個節點是否是同一個,從而避免頻繁更新不同元素,使得整個patch過程更加高效,減少DOM操作量,提高性能。
補充:
- 若不設置key還可能在列表更新時引發一些隱蔽的bug
- vue中在使用相同標籤名元素的過渡切換時,也會使用到key屬性,其目的也是為了讓vue可以區分它們,否則vue只會替換其內部屬性而不會觸發過渡效果
Vue 中的diff原理
- vue的diff算法是平級比較,不考慮跨級比較的情況。內部採用深度遞歸的方式 + 雙指針的方式進行比較。
補充:
- 先比較是否是相同節點
- 相同節點比較屬性,並複用老節點
- 比較兒子節點,考慮老節點和新節點兒子的情況
- 優化比較:頭頭、尾尾、頭尾、尾頭
- 比對查找進行複用
Vue2 與 Vue3.x 的diff算法:
- Vue2的核心Diff算法採用了雙端比較的算法,同時從新舊children的兩端開始進行比較,借助key值找到可複用的節點,再進行相關操作。
- Vue3.x借鑒了ivi算法和 inferno算法,該算法中還運用了動態規劃的思想求解最長遞歸子序列。 (實際的實現可以結合Vue3.x源碼看。)
v-if 與 v-for的優先級
- v-for優先於v-if被解析
- 如果同時出現,每次渲染都會先執行循環再判斷條件,無論如何循環都不可避免,浪費了性能
- 要避免出現這種情況,則在外層嵌套template,在這一層進行v-if判斷,然後在內部進行v-for循環
- 如果條件出現在循環內部,可通過計算屬性提前過濾掉那些不需要顯示的項
v-if與v-show的區別
- v-if 是真正的條件渲染,直到條件第一次變為真時,才會開始渲染。
- v-show 不管初始條件是什麼會渲染,並且只是簡單地基於 CSS 的 “display” 屬性進行切換。
注意:v-if 適用於不需要頻繁切換條件的場景;v-show 則適用於需要非常頻繁切換條件的場景。
computed 和 watch 的區別和運用的場景?
- computed: 計算屬性。依賴其它屬性值,並且 computed 的值有緩存,只有它依賴的屬性值發生改變,下一次獲取 computed 的值時才會重新計算 computed 的值
- watch: 監聽數據的變化。更多的是「觀察」的作用,類似於某些數據的監聽回調 ,每當監聽的數據變化時都會執行回調進行後續操作
運用場景:
- 當我們需要進行數值計算,並且依賴於其它數據時,應該使用 computed,因為可以利用 computed 的緩存特性,避免每次獲取值時,都要重新計算。
- 當我們需要在數據變化時執行異步或開銷較大的操作時,應該使用 watch,使用 watch 選項允許我們執行異步操作 ( 訪問一個 API ),限制我們執行該操作的頻率,並在我們得到最終結果前,設置中間狀態。這些都是計算屬性無法做到的。
如何理解自定義指令?
- 指令的實現原理,可以從編譯原理 ➡ 代碼生成 ➡ 指令鉤子實現進行概述
- 在生成 ast 語法樹時,遇到指令會給當前元素添加directives屬性
- 通過 genDirectives 生成指令代碼
- 在patch前將指令的鉤子提取到 cbs中,在patch過程中調用對應的鉤子
- 當執行指令對應鉤子函數時,調用對應指令定義的方法
v-model的原理是什麼?
- v-model本質就是一個語法糖,可以看成是value + input方法的語法糖。可以通過model屬性的prop和event屬性來進行自定義。原生的v-model,會根據標籤的不同生成不同的事件和屬性。
- v-model 在內部為不同的輸入元素使用不同的屬性並拋出不同的事件:
- text 和 textarea 元素使用 value 屬性和 input 事件。
- checkbox 和 radio 使用 checked 屬性和 change 事件。
- select 字段將 value 作為 prop 並將 change 作為事件。
性能優化篇
Vue性能優化 - 編碼階段
- 盡量減少data中的數據,data中的數據都會增加getter和setter,會收集對應的watcher
- 如果需要使用v-for給每項元素綁定事件時使用事件代理
- SPA 頁面採用keep-alive緩存組件
- 在更多的情況下,使用v-if替代v-show
- key保證唯一
- 使用路由懶加載、異步組件
- 防抖、節流
- 第三方模塊按需導入
- 長列表滾動到可視區域動態加載
- 圖片懶加載
Vue性能優化 - 用戶體驗
- 骨架屏
- PWA
- 使用緩存(客戶端緩存、服務端緩存)優化、服務端開啟gzip壓縮等
Vue性能優化 - SEO優化
- 預渲染
- 服務端渲染SSR
Vue性能優化 - 打包優化
- 壓縮代碼
- Tree Shaking/Scope Hoisting
- 使用cdn加載第三方模塊
- 多線程打包happypack
- splitChunks抽離公共文件
- sourceMap優化