2023前端vue面試題及答案
Vue3.0 為什麼要用 proxy?
在 Vue2 中, 0bject.defineProperty 會改變原始數據,而 Proxy 是創建對象的虛擬表示,並提供 set 、get 和 deleteProperty 等處理器,這些處理器可在訪問或修改原始對象上的屬性時進行攔截,有以下特點∶
- 不需用使用
Vue.$set
或Vue.$delete
觸發響應式。 - 全方位的數組變化檢測,消除了Vue2 無效的邊界情況。
- 支持 Map,Set,WeakMap 和 WeakSet。
Proxy 實現的響應式原理與 Vue2的實現原理相同,實現方式大同小異∶
- get 收集依賴
- Set、delete 等觸發依賴
- 對於集合類型,就是對集合對象的方法做一層包裝:原方法執行後執行依賴相關的收集或觸發邏輯。
説説你對slot的理解?slot使用場景有哪些
一、slot是什麼
在HTML中 slot
元素 ,作為 Web Components
技術套件的一部分,是Web組件內的一個佔位符
該佔位符可以在後期使用自己的標記語言填充
舉個栗子
html
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
template
不會展示到頁面中,需要用先獲取它的引用,然後添加到DOM
中,
javascript
customElements.define('element-details',
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})
在Vue
中的概念也是如此
Slot
藝名插槽,花名“佔坑”,我們可以理解為solt
在組件模板中佔好了位置,當使用該組件標籤時候,組件標籤裏面的內容就會自動填坑(替換組件模板中slot
位置),作為承載分發內容的出口
二、使用場景
通過插槽可以讓用户可以拓展組件,去更好地複用組件和對其做定製化處理
如果父組件在使用到一個複用組件的時候,獲取這個組件在不同的地方有少量的更改,如果去重寫組件是一件不明智的事情
通過slot
插槽向組件內部指定位置傳遞內容,完成這個複用組件在不同場景的應用
比如佈局組件、表格列、下拉選、彈框顯示內容等
使用vue渲染大量數據時應該怎麼優化?説下你的思路!
分析
企業級項目中渲染大量數據的情況比較常見,因此這是一道非常好的綜合實踐題目。
回答
-
在大型企業級項目中經常需要渲染大量數據,此時很容易出現卡頓的情況。比如大數據量的表格、樹
-
處理時要根據情況做不同處理:
-
可以採取分頁的方式獲取,避免渲染大量數據
-
vue-virtual-scroller (opens new window)等虛擬滾動方案,只渲染視口範圍內的數據
-
如果不需要更新,可以使用v-once方式只渲染一次
-
通過v-memo (opens new window)可以緩存結果,結合
v-for
使用,避免數據變化時不必要的VNode
創建 -
可以採用懶加載方式,在用户需要的時候再加載數據,比如
tree
組件子樹的懶加載 - 還是要看具體需求,首先從設計上避免大數據獲取和渲染;實在需要這樣做可以採用虛表的方式優化渲染;最後優化更新,如果不需要更新可以
v-once
處理,需要更新可以v-memo
進一步優化大數據更新性能。其他可以採用的是交互方式優化,無線滾動、懶加載等方案
scoped樣式穿透
scoped
雖然避免了組件間樣式污染,但是很多時候我們需要修改組件中的某個樣式,但是又不想去除scoped
屬性
- 使用
/deep/
```html
```
- 使用兩個
style
標籤
```html
```
Vue中v-html會導致哪些問題
- 可能會導致
xss
攻擊 v-html
會替換掉標籤內部的子元素
``javascript
let template = require('vue-template-compiler');
let r = template.compile(
// with(this){return _c('div',{domProps: {"innerHTML":_s('hello')}})} console.log(r.render);
// _c 定義在core/instance/render.js // _s 定義在core/instance/render-helpers/index,js if (key === 'textContent' || key === 'innerHTML') { if (vnode.children) vnode.children.length = 0 if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property if (elm.childNodes.length === 1) { elm.removeChild(elm.childNodes[0]) } } ```
如果讓你從零開始寫一個vuex,説説你的思路
思路分析
這個題目很有難度,首先思考vuex
解決的問題:存儲用户全局狀態並提供管理狀態API。
vuex
需求分析- 如何實現這些需求
回答範例
- 官方説
vuex
是一個狀態管理模式和庫,並確保這些狀態以可預期的方式變更。可見要實現一個vuex
- 要實現一個
Store
存儲全局狀態 - 要提供修改狀態所需API:
commit(type, payload), dispatch(type, payload)
- 實現
Store
時,可以定義Store
類,構造函數接收選項options
,設置屬性state
對外暴露狀態,提供commit
和dispatch
修改屬性state
。這裏需要設置state
為響應式對象,同時將Store
定義為一個Vue
插件 commit(type, payload)
方法中可以獲取用户傳入mutations
並執行它,這樣可以按用户提供的方法修改狀態。dispatch(type, payload)
類似,但需要注意它可能是異步的,需要返回一個Promise
給用户以處理異步結果
實踐
Store
的實現:
javascript
class Store {
constructor(options) {
this.state = reactive(options.state)
this.options = options
}
commit(type, payload) {
this.options.mutations[type].call(this, this.state, payload)
}
}
vuex簡易版
```javascript /* * 1 實現插件,掛載$store * 2 實現store /
let Vue;
class Store { constructor(options) { // state響應式處理 // 外部訪問: this.$store.state.*** // 第一種寫法 // this.state = new Vue({ // data: options.state // })
// 第二種寫法:防止外界直接接觸內部vue實例,防止外部強行變更
this._vm = new Vue({
data: {
$$state: options.state
}
})
this._mutations = options.mutations
this._actions = options.actions
this.getters = {}
options.getters && this.handleGetters(options.getters)
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
get state () { return this._vm._data.$$state }
set state (val) { return new Error('Please use replaceState to reset state') }
handleGetters (getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getterskey }) }) }
commit (type, payload) {
let entry = this._mutations[type]
if (!entry) {
return new Error(${type} is not defined
)
}
entry(this.state, payload)
}
dispatch (type, payload) {
let entry = this._actions[type]
if (!entry) {
return new Error(${type} is not defined
)
}
entry(this, payload)
} }
const install = (_Vue) => { Vue = _Vue
Vue.mixin({ beforeCreate () { if (this.$options.store) { Vue.prototype.$store = this.$options.store } }, }) }
export default { Store, install } ```
驗證方式
```javascript import Vue from 'vue' import Vuex from './vuex' // this.$store Vue.use(Vuex)
export default new Vuex.Store({ state: { counter: 0 }, mutations: { // state從哪裏來的 add (state) { state.counter++ } }, getters: { doubleCounter (state) { return state.counter * 2 } }, actions: { add ({ commit }) { setTimeout(() => { commit('add') }, 1000) } }, modules: { } }) ```
參考 前端進階面試題詳細解答
Vue與Angular以及React的區別?
Vue與AngularJS的區別
Angular
採用TypeScript
開發, 而Vue
可以使用javascript
也可以使用TypeScript
AngularJS
依賴對數據做髒檢查,所以Watcher
越多越慢;Vue.js
使用基於依賴追蹤的觀察並且使用異步隊列更新,所有的數據都是獨立觸發的。AngularJS
社區完善,Vue
的學習成本較小
Vue與React的區別
相同點:
Virtual DOM
。其中最大的一個相似之處就是都使用了Virtual DOM
。(當然Vue
是在Vue2.x
才引用的)也就是能讓我們通過操作數據的方式來改變真實的DOM
狀態。因為其實Virtual DOM
的本質就是一個JS
對象,它保存了對真實DOM
的所有描述,是真實DOM
的一個映射,所以當我們在進行頻繁更新元素的時候,改變這個JS
對象的開銷遠比直接改變真實DOM
要小得多。- 組件化的開發思想。第二點來説就是它們都提倡這種組件化的開發思想,也就是建議將應用分拆成一個個功能明確的模塊,再將這些模塊整合在一起以滿足我們的業務需求。
Props
。Vue
和React
中都有props
的概念,允許父組件向子組件傳遞數據。- 構建工具、Chrome插件、配套框架。還有就是它們的構建工具以及Chrome插件、配套框架都很完善。比如構建工具,
React
中可以使用CRA
,Vue
中可以使用對應的腳手架vue-cli
。對於配套框架Vue
中有vuex、vue-router
,React
中有react-router、redux
。
不同點
- 模版的編寫。最大的不同就是模版的編寫,
Vue
鼓勵你去寫近似常規HTML
的模板,React
推薦你使用JSX
去書寫。 - 狀態管理與對象屬性。在
React
中,應用的狀態是比較關鍵的概念,也就是state
對象,它允許你使用setState
去更新狀態。但是在Vue
中,state
對象並不是必須的,數據是由data
屬性在Vue
對象中進行管理。 - 虛擬
DOM
的處理方式不同。Vue
中的虛擬DOM
控制了顆粒度,組件層面走watcher
通知,而組件內部走vdom
做diff
,這樣,既不會有太多watcher
,也不會讓vdom
的規模過大。而React
走了類似於CPU
調度的邏輯,把vdom
這棵樹,微觀上變成了鏈表,然後利用瀏覽器的空閒時間來做diff
Vue項目中你是如何解決跨域的呢
一、跨域是什麼
跨域本質是瀏覽器基於同源策略的一種安全手段
同源策略(Sameoriginpolicy),是一種約定,它是瀏覽器最核心也最基本的安全功能
所謂同源(即指在同一個域)具有以下三個相同點
- 協議相同(protocol)
- 主機相同(host)
- 端口相同(port)
反之非同源請求,也就是協議、端口、主機其中一項不相同的時候,這時候就會產生跨域
一定要注意跨域是瀏覽器的限制,你用抓包工具抓取接口數據,是可以看到接口已經把數據返回回來了,只是瀏覽器的限制,你獲取不到數據。用postman請求接口能夠請求到數據。這些再次印證了跨域是瀏覽器的限制。
Class 與 Style 如何動態綁定
Class
可以通過對象語法和數組語法進行動態綁定
對象語法:
```javascript
data: { isActive: true, hasError: false } ```
數組語法:
```javascript
data: { activeClass: 'active', errorClass: 'text-danger' } ```
Style
也可以通過對象語法和數組語法進行動態綁定
對象語法:
```javascript
data: { activeColor: 'red', fontSize: 30 } ```
數組語法:
```javascript
data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } } ```
瞭解history有哪些方法嗎?説下它們的區別
history
這個對象在html5
的時候新加入兩個api
history.pushState()
和history.repalceState()
這兩個API
可以在不進行刷新的情況下,操作瀏覽器的歷史紀錄。唯一不同的是,前者是新增一個歷史記錄,後者是直接替換當前的歷史記錄。
從參數上來説:
```javascript window.history.pushState(state,title,url) //state:需要保存的數據,這個數據在觸發popstate事件時,可以在event.state裏獲取 //title:標題,基本沒用,一般傳null //url:設定新的歷史紀錄的url。新的url與當前url的origin必須是一樣的,否則會拋出錯誤。url可以時絕對路徑,也可以是相對路徑。 //如 當前url是 https://www.baidu.com/a/,執行history.pushState(null, null, './qq/'),則變成 https://www.baidu.com/a/qq/, //執行history.pushState(null, null, '/qq/'),則變成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url) //與pushState 基本相同,但她是修改當前歷史紀錄,而 pushState 是創建新的歷史紀錄 ```
另外還有:
window.history.back()
後退window.history.forward()
前進window.history.go(1)
前進或者後退幾步
從觸發事件的監聽上來説:
pushState()
和replaceState()
不能被popstate
事件所監聽- 而後面三者可以,且用户點擊瀏覽器前進後退鍵時也可以
在Vue中使用插件的步驟
- 採用
ES6
的import ... from ...
語法或CommonJS
的require()
方法引入插件 - 使用全局方法
Vue.use( plugin )
使用插件,可以傳入一個選項對象Vue.use(MyPlugin, { someOption: true })
$route
和$router
的區別
$route
是“路由信息對象”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息參數。- 而
$router
是“路由實例”對象包括了路由的跳轉方法,鈎子函數等
為什麼要使用異步組件
- 節省打包出的結果,異步組件分開打包,採用
jsonp
的方式進行加載,有效解決文件過大的問題。 - 核心就是包組件定義變成一個函數,依賴
import()
語法,可以實現文件的分割加載。
javascript
components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
javascript
export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默認調用此函數時返回 undefiend
// 第二次渲染時Ctor不為undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染佔位符 空虛擬節點
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染時可以拿到獲取的最新組件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 強制更新視圖重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.將resolve方法和reject方法傳入,用户調用 resolve方法後
sync = false
return factory.resolved
}
函數式組件優勢和原理
函數組件的特點
- 函數式組件需要在聲明組件是指定
functional:true
- 不需要實例化,所以沒有
this
,this
通過render
函數的第二個參數context
來代替 - 沒有生命週期鈎子函數,不能使用計算屬性,
watch
- 不能通過
$emit
對外暴露事件,調用事件只能通過context.listeners.click
的方式調用外部傳入的事件 - 因為函數式組件是沒有實例化的,所以在外部通過
ref
去引用組件時,實際引用的是HTMLElement
- 函數式組件的
props
可以不用顯示聲明,所以沒有在props
裏面聲明的屬性都會被自動隱式解析為prop
,而普通組件所有未聲明的屬性都解析到$attrs
裏面,並自動掛載到組件根元素上面(可以通過inheritAttrs
屬性禁止)
優點
- 由於函數式組件不需要實例化,無狀態,沒有生命週期,所以渲染性能要好於普通組件
- 函數式組件結構比較簡單,代碼結構更清晰
使用場景:
- 一個簡單的展示組件,作為容器組件使用 比如
router-view
就是一個函數式組件 - “高階組件”——用於接收一個組件作為參數,返回一個被包裝過的組件
例子
javascript
Vue.component('functional',{ // 構造函數產生虛擬節點的
functional:true, // 函數式組件 // data={attrs:{}}
render(h){
return h('div','test')
}
})
const vm = new Vue({
el: '#app'
})
源碼相關
```javascript // functional component if (isTrue(Ctor.options.functional)) { // 帶有functional的屬性的就是函數式組件 return createFunctionalComponent(Ctor, propsData, data, context, children) }
// extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // 處理事件 // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn // 處理原生事件
// install component management hooks onto the placeholder node installComponentHooks(data) // 安裝組件相關鈎子 (函數式組件沒有調用此方法,從而性能高於普通組件) ```
Vue.set的實現原理
- 給對應和數組本身都增加了
dep
屬性 - 當給對象新增不存在的屬性則觸發對象依賴的
watcher
去更新 - 當修改數組索引時,我們調用數組本身的
splice
去更新數組(數組的響應式原理就是重新了splice
等方法,調用splice
就會觸發視圖更新)
基本使用
以下方法調用會改變原始數組:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
- 調用方法:
Vue.set(target, key, value )
target
:要更改的數據源(可以是對象或者數組)key
:要更改的具體數據value
:重新賦的值
```html
```
相關源碼
```javascript // src/core/observer/index.js 44 export class Observer { // new Observer(value) value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() // 給所有對象類型增加dep屬性 } } ```
javascript
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是開發環境 target 沒定義或者是基礎類型則報錯
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是數組 Vue.set(array,1,100); 調用我們重寫的splice方法 (這樣可以更新視圖)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用數組的splice變異方法觸發響應式
target.splice(key, 1, val)
return val
}
// 3.如果是對象本身的屬性,則直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改屬性值
return val
}
// 4.如果是Vue實例 或 根數據data時 報錯,(更新_data 無意義)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是響應式的也不需要將其定義成響應式屬性
if (!ob) {
target[key] = val
return val
}
// 6.將屬性定義成響應式的
defineReactive(ob.value, key, val)
// 通知視圖更新
ob.dep.notify()
return val
}
我們閲讀以上源碼可知,vm.$set 的實現原理是:
- 如果目標是數組 ,直接使用數組的
splice
方法觸發相應式; - 如果目標是對象 ,會先判讀屬性是否存在、對象是否是響應式,最終如果要對屬性進行響應式處理,則是通過調用
defineReactive
方法進行響應式處理(defineReactive
方法就是Vue
在初始化對象時,給對象屬性採用Object.defineProperty
動態添加getter
和setter
的功能所調用的方法)
Vue為什麼沒有類似於React中shouldComponentUpdate的生命週期
- 考點:
Vue
的變化偵測原理 - 前置知識: 依賴收集、虛擬
DOM
、響應式系統
根本原因是
Vue
與React
的變化偵測方式有所不同
- 當React知道發生變化後,會使用
Virtual Dom Diff
進行差異檢測,但是很多組件實際上是肯定不會發生變化的,這個時候需要shouldComponentUpdate
進行手動操作來減少diff
,從而提高程序整體的性能 Vue
在一開始就知道那個組件發生了變化,不需要手動控制diff
,而組件內部採用的diff
方式實際上是可以引入類似於shouldComponentUpdate
相關生命週期的,但是通常合理大小的組件不會有過量的diff,手動優化的價值有限,因此目前Vue
並沒有考慮引入shouldComponentUpdate
這種手動優化的生命週期
vue-router中如何保護路由
分析
路由保護在應用開發過程中非常重要,幾乎每個應用都要做各種路由權限管理,因此相當考察使用者基本功。
體驗
全局守衞:
javascript
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消導航
return false
})
路由獨享守衞:
javascript
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
組件內的守衞:
javascript
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染該組件的對應路由被驗證前調用
},
beforeRouteUpdate(to, from) {
// 在當前路由改變,但是該組件被複用時調用
},
beforeRouteLeave(to, from) {
// 在導航離開渲染該組件的對應路由時調用
},
}
回答
vue-router
中保護路由的方法叫做路由守衞,主要用來通過跳轉或取消的方式守衞導航。- 路由守衞有三個級別:
全局
、路由獨享
、組件級
。影響範圍由大到小,例如全局的router.beforeEach()
,可以註冊一個全局前置守衞,每次路由導航都會經過這個守衞,因此在其內部可以加入控制邏輯決定用户是否可以導航到目標路由;在路由註冊的時候可以加入單路由獨享的守衞,例如beforeEnter
,守衞只在進入路由時觸發,因此只會影響這個路由,控制更精確;我們還可以為路由組件添加守衞配置,例如beforeRouteEnter
,會在渲染該組件的對應路由被驗證前調用,控制的範圍更精確了。 - 用户的任何導航行為都會走
navigate
方法,內部有個guards
隊列按順序執行用户註冊的守衞鈎子函數,如果沒有通過驗證邏輯則會取消原有的導航。
原理
runGuardQueue(guards)
鏈式的執行用户在各級別註冊的守衞鈎子函數,通過則繼續下一個級別的守衞,不通過進入catch
流程取消原本導航
```javascript // 源碼 runGuardQueue(guards) .then(() => { // check global guards beforeEach guards = [] for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
}) .then(() => { // check in components beforeRouteUpdate guards = extractComponentsGuards( updatingRecords, 'beforeRouteUpdate', to, from )
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
}) .then(() => { // check the route beforeEnter guards = [] for (const record of to.matched) { // do not trigger beforeEnter on reused views if (record.beforeEnter && !from.matched.includes(record)) { if (isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)) } else { guards.push(guardToPromiseFn(record.beforeEnter, to, from)) } } } guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
}) .then(() => { // check global guards beforeResolve guards = [] for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
}) // catch any navigation canceled .catch(err => isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) ) ```
Vue-router 路由鈎子在生命週期的體現
一、Vue-Router導航守衞
有的時候,需要通過路由來進行一些操作,比如最常見的登錄權限驗證,當用户滿足條件時,才讓其進入導航,否則就取消跳轉,並跳到登錄頁面讓其登錄。 為此有很多種方法可以植入路由的導航過程:全局的,單個路由獨享的,或者組件級的
- 全局路由鈎子
vue-router全局有三個路由鈎子;
- router.beforeEach 全局前置守衞 進入路由之前
- router.beforeResolve 全局解析守衞(2.5.0+)在 beforeRouteEnter 調用之後調用
- router.afterEach 全局後置鈎子 進入路由之後
具體使用∶
- beforeEach(判斷是否登錄了,沒登錄就跳轉到登錄頁)
```javascript
router.beforeEach((to, from, next) => {
let ifInfo = Vue.prototype.$common.getSession('userData'); // 判斷是否登錄的存儲信息
if (!ifInfo) {
// sessionStorage裏沒有儲存user信息
if (to.path == '/') {
//如果是登錄頁面路徑,就直接next()
next();
} else {
//不然就跳轉到登錄
Message.warning("請重新登錄!");
window.location.href = Vue.prototype.$loginUrl;
}
} else {
return next();
}
})
```
- afterEach (跳轉之後滾動條回到頂部)
```javascript
router.afterEach((to, from) => {
// 跳轉之後滾動條回到頂部
window.scrollTo(0,0);
});
```
- 單個路由獨享鈎子
beforeEnter 如果不想全局配置守衞的話,可以為某些路由單獨配置守衞,有三個參數∶ to、from、next
```javascript
export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即將進入登錄頁面')
next()
}
}
]
```
- 組件內鈎子
beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave
這三個鈎子都有三個參數∶to、from、next
- beforeRouteEnter∶ 進入組件前觸發
- beforeRouteUpdate∶ 當前地址改變並且改組件被複用時觸發,舉例來説,帶有動態參數的路徑foo/∶id,在 /foo/1 和 /foo/2 之間跳轉的時候,由於會渲染同樣的foa組件,這個鈎子在這種情況下就會被調用
- beforeRouteLeave∶ 離開組件被調用
注意點,beforeRouteEnter組件內還訪問不到this,因為該守衞執行前組件實例還沒有被創建,需要傳一個回調給 next來訪問,例如:
```javascript
beforeRouteEnter(to, from, next) {
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}
```
二、Vue路由鈎子在生命週期函數的體現
- 完整的路由導航解析流程(不包括其他生命週期)
-
觸發進入其他路由。
-
調用要離開路由的組件守衞beforeRouteLeave
-
調用局前置守衞∶ beforeEach
-
在重用的組件裏調用 beforeRouteUpdate
-
調用路由獨享守衞 beforeEnter。
-
解析異步路由組件。
-
在將要進入的路由組件中調用 beforeRouteEnter
-
調用全局解析守衞 beforeResolve
-
導航被確認。
-
調用全局後置鈎子的 afterEach 鈎子。
-
觸發DOM更新(mounted)。
-
執行beforeRouteEnter 守衞中傳給 next 的回調函數
- 觸發鈎子的完整順序
路由導航、keep-alive、和組件生命週期鈎子結合起來的,觸發順序,假設是從a組件離開,第一次進入b組件∶
- beforeRouteLeave:路由組件的組件離開路由前鈎子,可取消路由離開。
- beforeEach:路由全局前置守衞,可用於登錄驗證、全局路由loading等。
- beforeEnter:路由獨享守衞
- beforeRouteEnter:路由組件的組件進入路由前鈎子。
- beforeResolve:路由全局解析守衞
- afterEach:路由全局後置鈎子
- beforeCreate:組件生命週期,不能訪問tAis。
- created;組件生命週期,可以訪問tAis,不能訪問dom。
- beforeMount:組件生命週期
- deactivated:離開緩存組件a,或者觸發a的beforeDestroy和destroyed組件銷燬鈎子。
- mounted:訪問/操作dom。
- activated:進入緩存組件,進入a的嵌套子組件(如果有的話)。
- 執行beforeRouteEnter回調函數next。
- 導航行為被觸發到導航完成的整個過程
- 導航行為被觸發,此時導航未被確認。
- 在失活的組件裏調用離開守衞 beforeRouteLeave。
- 調用全局的 beforeEach守衞。
- 在重用的組件裏調用 beforeRouteUpdate 守衞(2.2+)。
- 在路由配置裏調用 beforeEnteY。
- 解析異步路由組件(如果有)。
- 在被激活的組件裏調用 beforeRouteEnter。
- 調用全局的 beforeResolve 守衞(2.5+),標示解析階段完成。
- 導航被確認。
- 調用全局的 afterEach 鈎子。
- 非重用組件,開始組件實例的生命週期:beforeCreate&created、beforeMount&mounted
- 觸發 DOM 更新。
- 用創建好的實例調用 beforeRouteEnter守衞中傳給 next 的回調函數。
- 導航完成
Vue-router 導航守衞有哪些
- 全局前置/鈎子:beforeEach、beforeResolve、afterEach
- 路由獨享的守衞:beforeEnter
- 組件內的守衞:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
Vue的diff算法詳細分析
1. 是什麼
diff
算法是一種通過同層的樹節點進行比較的高效算法
其有兩個特點:
- 比較只會在同層級進行, 不會跨層級比較
- 在diff比較的過程中,循環從兩邊向中間比較
diff
算法在很多場景下都有應用,在 vue
中,作用於虛擬 dom
渲染成真實 dom
的新舊 VNode
節點比較
2. 比較方式
diff
整體策略為:深度優先,同層比較
- 比較只會在同層級進行, 不會跨層級比較
- 比較的過程中,循環從兩邊向中間收攏
下面舉個vue
通過diff
算法更新的例子:
新舊VNode
節點如下圖所示:
第一次循環後,發現舊節點D與新節點D相同,直接複用舊節點D作為diff
後的第一個真實節點,同時舊節點endIndex
移動到C,新節點的 startIndex
移動到了 C
第二次循環後,同樣是舊節點的末尾和新節點的開頭(都是 C)相同,同理,diff
後創建了 C 的真實節點插入到第一次創建的 D 節點後面。同時舊節點的 endIndex
移動到了 B,新節點的 startIndex
移動到了 E
第三次循環中,發現E沒有找到,這時候只能直接創建新的真實節點 E,插入到第二次創建的 C 節點之後。同時新節點的 startIndex
移動到了 A。舊節點的 startIndex
和 endIndex
都保持不動
第四次循環中,發現了新舊節點的開頭(都是 A)相同,於是 diff
後創建了 A 的真實節點,插入到前一次創建的 E 節點後面。同時舊節點的 startIndex
移動到了 B,新節點的startIndex
移動到了 B
第五次循環中,情形同第四次循環一樣,因此 diff
後創建了 B 真實節點 插入到前一次創建的 A 節點後面。同時舊節點的 startIndex
移動到了 C,新節點的 startIndex 移動到了 F
新節點的 startIndex
已經大於 endIndex
了,需要創建 newStartIdx
和 newEndIdx
之間的所有節點,也就是節點F,直接創建 F 節點對應的真實節點放到 B 節點後面
3. 原理分析
當數據發生改變時,set
方法會調用Dep.notify
通知所有訂閲者Watcher
,訂閲者就會調用patch
給真實的DOM
打補丁,更新相應的視圖
源碼位置:src/core/vdom/patch.js
```javascript function patch(oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { // 沒有新節點,直接執行destory鈎子函數 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return }
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 沒有舊節點,直接用新節點生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判斷舊節點和新節點自身一樣,一致執行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否則直接銷燬及舊節點,根據新節點生成dom元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
} ```
patch
函數前兩個參數位為oldVnode
和 Vnode
,分別代表新的節點和之前的舊節點,主要做了四個判斷:
- 沒有新節點,直接觸發舊節點的
destory
鈎子 - 沒有舊節點,説明是頁面剛開始初始化的時候,此時,根本不需要比較了,直接全是新建,所以只調用
createElm
- 舊節點和新節點自身一樣,通過
sameVnode
判斷節點是否一樣,一樣時,直接調用patchVnode
去處理這兩個節點 - 舊節點和新節點自身不一樣,當兩個節點不一樣的時候,直接創建新節點,刪除舊節點
下面主要講的是patchVnode
部分
```javascript function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 如果新舊節點一致,什麼都不做 if (oldVnode === vnode) { return }
// 讓vnode.el引用到現在的真實dom,當el修改時,vnode.el會同步變化
const elm = vnode.elm = oldVnode.elm
// 異步佔位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新舊都是靜態節點,並且具有相同的key
// 當vnode是克隆節點或是v-once指令控制的節點時,只需要把oldVnode.elm和oldVnode.child都複製到vnode上
// 也不用再有其他操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本節點或者註釋節點
if (isUndef(vnode.text)) {
// 並且都有子節點
if (isDef(oldCh) && isDef(ch)) {
// 並且子節點不完全一致,則調用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子節點
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已經引用了老的dom節點,在老的dom節點上添加子節點
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode沒有子節點,而vnode有子節點,直接刪除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老節點是文本節點
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本節點或註釋節點
// 但是vnode.text != oldVnode.text時,只需要更新vnode.elm的文本內容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
} ```
patchVnode
主要做了幾個判斷:
- 新節點是否是文本節點,如果是,則直接更新
dom
的文本內容為新節點的文本內容 - 新節點和舊節點如果都有子節點,則處理比較更新子節點
- 只有新節點有子節點,舊節點沒有,那麼不用比較了,所有節點都是全新的,所以直接全部新建就好了,新建是指創建出所有新
DOM
,並且添加進父節點 - 只有舊節點有子節點而新節點沒有,説明更新後的頁面,舊節點全部都不見了,那麼要做的,就是把所有的舊節點刪除,也就是直接把
DOM
刪除
子節點不完全一致,則調用updateChildren
```javascript function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 舊頭索引 let newStartIdx = 0 // 新頭索引 let oldEndIdx = oldCh.length - 1 // 舊尾索引 let newEndIdx = newCh.length - 1 // 新尾索引 let oldStartVnode = oldCh[0] // oldVnode的第一個child let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最後一個child let newStartVnode = newCh[0] // newVnode的第一個child let newEndVnode = newCh[newEndIdx] // newVnode的最後一個child let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,並且新的也都重合了,證明diff完了,循環結束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一個child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最後一個child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一個節點
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,繼續循環
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一個節點
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,繼續循環
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一個節點
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,則將oldStartVnode.eml移動到oldEndVnode.elm之後
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一個節點
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,則將oldEndVnode.elm移動到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 嘗試在oldChildren中尋找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,説明newStartVnode是一個新的節點
if (isUndef(idxInOld)) { // New element
// 創建一個新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比較兩個具有相同的key的新節點是否是同一個節點
//不設key,newCh和oldCh只會進行頭尾兩端的相互比較,設key後,除了頭尾兩端的比較外,還會從用key生成的對象oldKeyToIdx中查找匹配的節點,所以為節點設置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,則將找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移動到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是節點不相同,則創建一個新的節點
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
```
while
循環主要處理了以下五種情景:
- 當新老
VNode
節點的start
相同時,直接patchVnode
,同時新老VNode
節點的開始索引都加 1 - 當新老
VNode
節點的end
相同時,同樣直接patchVnode
,同時新老VNode
節點的結束索引都減 1 - 當老
VNode
節點的start
和新VNode
節點的end
相同時,這時候在patchVnode
後,還需要將當前真實dom
節點移動到oldEndVnode
的後面,同時老VNode
節點開始索引加 1,新VNode
節點的結束索引減 1 - 當老
VNode
節點的end
和新VNode
節點的start
相同時,這時候在patchVnode
後,還需要將當前真實dom
節點移動到oldStartVnode
的前面,同時老VNode
節點結束索引減 1,新VNode
節點的開始索引加 1 - 如果都不滿足以上四種情形,那説明沒有相同的節點可以複用,則會分為以下兩種情況:
- 從舊的
VNode
為key
值,對應index
序列為value
值的哈希表中找到與newStartVnode
一致key
的舊的VNode
節點,再進行patchVnode
,同時將這個真實dom
移動到oldStartVnode
對應的真實dom
的前面 - 調用
createElm
創建一個新的dom
節點放到當前newStartIdx
的位置
小結
- 當數據發生改變時,訂閲者
watcher
就會調用patch
給真實的DOM
打補丁 - 通過
isSameVnode
進行判斷,相同則調用patchVnode
方法 patchVnode
做了以下操作:- 找到對應的真實
dom
,稱為el
- 如果都有都有文本節點且不相等,將
el
文本節點設置為Vnode
的文本節點 - 如果
oldVnode
有子節點而VNode
沒有,則刪除el
子節點 - 如果
oldVnode
沒有子節點而VNode
有,則將VNode
的子節點真實化後添加到el
- 如果兩者都有子節點,則執行
updateChildren
函數比較子節點 updateChildren
主要做了以下操作:- 設置新舊
VNode
的頭尾指針 - 新舊頭尾指針進行比較,循環向中間靠攏,根據情況調用
patchVnode
進行patch
重複流程、調用createElem
創建一個新節點,從哈希表尋找key
一致的VNode
節點再分情況操作