2023前端vue面試題及答案

語言: CN / TW / HK

Vue3.0 為什麼要用 proxy?

在 Vue2 中, 0bject.defineProperty 會改變原始數據,而 Proxy 是創建對象的虛擬表示,並提供 set 、get 和 deleteProperty 等處理器,這些處理器可在訪問或修改原始對象上的屬性時進行攔截,有以下特點∶

  • 不需用使用 Vue.$setVue.$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渲染大量數據時應該怎麼優化?説下你的思路!

分析

企業級項目中渲染大量數據的情況比較常見,因此這是一道非常好的綜合實踐題目。

回答

  1. 在大型企業級項目中經常需要渲染大量數據,此時很容易出現卡頓的情況。比如大數據量的表格、樹

  2. 處理時要根據情況做不同處理:

  3. 可以採取分頁的方式獲取,避免渲染大量數據

  4. vue-virtual-scroller (opens new window)等虛擬滾動方案,只渲染視口範圍內的數據

  5. 如果不需要更新,可以使用v-once方式只渲染一次

  6. 通過v-memo (opens new window)可以緩存結果,結合v-for使用,避免數據變化時不必要的VNode創建

  7. 可以採用懶加載方式,在用户需要的時候再加載數據,比如tree組件子樹的懶加載

  8. 還是要看具體需求,首先從設計上避免大數據獲取和渲染;實在需要這樣做可以採用虛表的方式優化渲染;最後優化更新,如果不需要更新可以v-once處理,需要更新可以v-memo進一步優化大數據更新性能。其他可以採用的是交互方式優化,無線滾動、懶加載等方案

scoped樣式穿透

scoped雖然避免了組件間樣式污染,但是很多時候我們需要修改組件中的某個樣式,但是又不想去除scoped屬性

  1. 使用/deep/

```html

```

  1. 使用兩個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需求分析
  • 如何實現這些需求

回答範例

  1. 官方説vuex是一個狀態管理模式和庫,並確保這些狀態以可預期的方式變更。可見要實現一個vuex
  2. 要實現一個Store存儲全局狀態
  3. 要提供修改狀態所需API:commit(type, payload), dispatch(type, payload)
  4. 實現Store時,可以定義Store類,構造函數接收選項options,設置屬性state對外暴露狀態,提供commitdispatch修改屬性state。這裏需要設置state為響應式對象,同時將Store定義為一個Vue插件
  5. 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的區別

相同點:

  1. Virtual DOM。其中最大的一個相似之處就是都使用了Virtual DOM。(當然Vue是在Vue2.x才引用的)也就是能讓我們通過操作數據的方式來改變真實的DOM狀態。因為其實Virtual DOM的本質就是一個JS對象,它保存了對真實DOM的所有描述,是真實DOM的一個映射,所以當我們在進行頻繁更新元素的時候,改變這個JS對象的開銷遠比直接改變真實DOM要小得多。
  2. 組件化的開發思想。第二點來説就是它們都提倡這種組件化的開發思想,也就是建議將應用分拆成一個個功能明確的模塊,再將這些模塊整合在一起以滿足我們的業務需求。
  3. PropsVueReact中都有props的概念,允許父組件向子組件傳遞數據。
  4. 構建工具、Chrome插件、配套框架。還有就是它們的構建工具以及Chrome插件、配套框架都很完善。比如構建工具,React中可以使用CRAVue中可以使用對應的腳手架vue-cli。對於配套框架Vue中有vuex、vue-routerReact中有react-router、redux

不同點

  1. 模版的編寫。最大的不同就是模版的編寫,Vue鼓勵你去寫近似常規HTML的模板,React推薦你使用JSX去書寫。
  2. 狀態管理與對象屬性。在React中,應用的狀態是比較關鍵的概念,也就是state對象,它允許你使用setState去更新狀態。但是在Vue中,state對象並不是必須的,數據是由data屬性在Vue對象中進行管理。
  3. 虛擬DOM的處理方式不同。Vue中的虛擬DOM控制了顆粒度,組件層面走watcher通知,而組件內部走vdomdiff,這樣,既不會有太多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是 http://www.baidu.com/a/,執行history.pushState(null, null, './qq/'),則變成 http://www.baidu.com/a/qq/, //執行history.pushState(null, null, '/qq/'),則變成 http://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中使用插件的步驟

  • 採用ES6import ... from ...語法或CommonJSrequire()方法引入插件
  • 使用全局方法Vue.use( plugin )使用插件,可以傳入一個選項對象Vue.use(MyPlugin, { someOption: true })

$route$router的區別

  • $route是“路由信息對象”,包括pathparamshashqueryfullPathmatchedname等路由信息參數。
  • $router是“路由實例”對象包括了路由的跳轉方法,鈎子函數等

為什麼要使用異步組件

  1. 節省打包出的結果,異步組件分開打包,採用jsonp的方式進行加載,有效解決文件過大的問題。
  2. 核心就是包組件定義變成一個函數,依賴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 }

函數式組件優勢和原理

函數組件的特點

  1. 函數式組件需要在聲明組件是指定 functional:true
  2. 不需要實例化,所以沒有this,this通過render函數的第二個參數context來代替
  3. 沒有生命週期鈎子函數,不能使用計算屬性,watch
  4. 不能通過$emit 對外暴露事件,調用事件只能通過context.listeners.click的方式調用外部傳入的事件
  5. 因為函數式組件是沒有實例化的,所以在外部通過ref去引用組件時,實際引用的是HTMLElement
  6. 函數式組件的props可以不用顯示聲明,所以沒有在props裏面聲明的屬性都會被自動隱式解析為prop,而普通組件所有未聲明的屬性都解析到$attrs裏面,並自動掛載到組件根元素上面(可以通過inheritAttrs屬性禁止)

優點

  1. 由於函數式組件不需要實例化,無狀態,沒有生命週期,所以渲染性能要好於普通組件
  2. 函數式組件結構比較簡單,代碼結構更清晰

使用場景:

  • 一個簡單的展示組件,作為容器組件使用 比如 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

{{user.name}} {{user.age}}

```

相關源碼

```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 動態添加 gettersetter 的功能所調用的方法)

Vue為什麼沒有類似於React中shouldComponentUpdate的生命週期

  • 考點: Vue的變化偵測原理
  • 前置知識: 依賴收集、虛擬DOM、響應式系統

根本原因是VueReact的變化偵測方式有所不同

  • 當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) ) ```

源碼位置(opens new window)

Vue-router 路由鈎子在生命週期的體現

一、Vue-Router導航守衞

有的時候,需要通過路由來進行一些操作,比如最常見的登錄權限驗證,當用户滿足條件時,才讓其進入導航,否則就取消跳轉,並跳到登錄頁面讓其登錄。 為此有很多種方法可以植入路由的導航過程:全局的,單個路由獨享的,或者組件級的

  1. 全局路由鈎子

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); });

```

  1. 單個路由獨享鈎子

beforeEnter 如果不想全局配置守衞的話,可以為某些路由單獨配置守衞,有三個參數∶ to、from、next

```javascript export default [
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即將進入登錄頁面')
next()
}
} ]

```

  1. 組件內鈎子

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路由鈎子在生命週期函數的體現

  1. 完整的路由導航解析流程(不包括其他生命週期)
  2. 觸發進入其他路由。

  3. 調用要離開路由的組件守衞beforeRouteLeave

  4. 調用局前置守衞∶ beforeEach

  5. 在重用的組件裏調用 beforeRouteUpdate

  6. 調用路由獨享守衞 beforeEnter。

  7. 解析異步路由組件。

  8. 在將要進入的路由組件中調用 beforeRouteEnter

  9. 調用全局解析守衞 beforeResolve

  10. 導航被確認。

  11. 調用全局後置鈎子的 afterEach 鈎子。

  12. 觸發DOM更新(mounted)。

  13. 執行beforeRouteEnter 守衞中傳給 next 的回調函數

  14. 觸發鈎子的完整順序

路由導航、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整體策略為:深度優先,同層比較

  1. 比較只會在同層級進行, 不會跨層級比較

  1. 比較的過程中,循環從兩邊向中間收攏

下面舉個vue通過diff算法更新的例子:

新舊VNode節點如下圖所示:

第一次循環後,發現舊節點D與新節點D相同,直接複用舊節點D作為diff後的第一個真實節點,同時舊節點endIndex移動到C,新節點的 startIndex 移動到了 C

第二次循環後,同樣是舊節點的末尾和新節點的開頭(都是 C)相同,同理,diff 後創建了 C 的真實節點插入到第一次創建的 D 節點後面。同時舊節點的 endIndex 移動到了 B,新節點的 startIndex 移動到了 E

第三次循環中,發現E沒有找到,這時候只能直接創建新的真實節點 E,插入到第二次創建的 C 節點之後。同時新節點的 startIndex 移動到了 A。舊節點的 startIndexendIndex 都保持不動

第四次循環中,發現了新舊節點的開頭(都是 A)相同,於是 diff 後創建了 A 的真實節點,插入到前一次創建的 E 節點後面。同時舊節點的 startIndex 移動到了 B,新節點的startIndex 移動到了 B

第五次循環中,情形同第四次循環一樣,因此 diff 後創建了 B 真實節點 插入到前一次創建的 A 節點後面。同時舊節點的 startIndex移動到了 C,新節點的 startIndex 移動到了 F

新節點的 startIndex 已經大於 endIndex 了,需要創建 newStartIdxnewEndIdx 之間的所有節點,也就是節點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函數前兩個參數位為oldVnodeVnode ,分別代表新的節點和之前的舊節點,主要做了四個判斷:

  • 沒有新節點,直接觸發舊節點的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
  • 如果都不滿足以上四種情形,那説明沒有相同的節點可以複用,則會分為以下兩種情況:
  • 從舊的 VNodekey 值,對應 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 節點再分情況操作