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
節點再分情況操作