前端一面常見vue面試題彙總

語言: CN / TW / HK
ead>

說說你對 proxy 的理解,Proxy 相比於 defineProperty 的優勢

Object.defineProperty() 的問題主要有三個:

  • 不能監聽陣列的變化 :無法監控到陣列下標的變化,導致通過陣列下標新增元素,不能實時響應
  • 必須遍歷物件的每個屬性 :只能劫持物件的屬性,從而需要對每個物件,每個屬性進行遍歷,如果屬性值是物件,還需要深度遍歷。Proxy 可以劫持整個物件,並返回一個新的物件
  • 必須深層遍歷巢狀的物件

Proxy的優勢如下:

  • 針對物件: 針對整個物件,而不是物件的某個屬性 ,所以也就不需要對 keys 進行遍歷
  • 支援陣列:Proxy 不需要對陣列的方法進行過載,省去了眾多 hack,減少程式碼量等於減少了維護成本,而且標準的就是最好的
  • Proxy的第二個引數可以有 13 種攔截方:不限於applyownKeysdeletePropertyhas等等是Object.defineProperty不具備的
  • Proxy返回的是一個新物件,我們可以只操作新的物件達到目的,而Object.defineProperty只能遍歷物件屬性直接修改
  • Proxy作為新標準將受到瀏覽器廠商重點持續的效能優化,也就是傳說中的新標準的效能紅利

proxy詳細使用點選檢視(opens new window)

Object.defineProperty的優勢如下:

相容性好,支援 IE9,而 Proxy 的存在瀏覽器相容性問題,而且無法用 polyfill 磨平

defineProperty的屬性值有哪些

```javascript Object.defineProperty(obj, prop, descriptor)

// obj 要定義屬性的物件 // prop 要定義或修改的屬性的名稱 // descriptor 要定義或修改的屬性描述符

Object.defineProperty(obj,"name",{ value:"poetry", // 初始值 writable:true, // 該屬性是否可寫入 enumerable:true, // 該屬性是否可被遍歷得到(for...in, Object.keys等) configurable:true, // 定該屬性是否可被刪除,且除writable外的其他描述符是否可被修改 get: function() {}, set: function(newVal) {} }) ```

相關程式碼如下

```javascript import { mutableHandlers } from "./baseHandlers"; // 代理相關邏輯 import { isObject } from "./util"; // 工具方法

export function reactive(target) { // 根據不同引數建立不同響應式物件 return createReactiveObject(target, mutableHandlers); } function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed; }

const get = createGetter(); const set = createSetter();

function createGetter() { return function get(target, key, receiver) { // 對獲取的值進行放射 const res = Reflect.get(target, key, receiver); console.log("屬性獲取", key); if (isObject(res)) { // 如果獲取的值是物件型別,則返回當前物件的代理物件 return reactive(res); } return res; }; } function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("屬性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("屬性值被修改", key, value); } return result; }; } export const mutableHandlers = { get, // 當獲取屬性時呼叫此方法 set, // 當修改屬性時呼叫此方法 }; ```

Proxy只會代理物件的第一層,那麼Vue3又是怎樣處理這個問題的呢?

判斷當前Reflect.get的返回值是否為Object,如果是則再通過reactive方法做代理, 這樣就實現了深度觀測。

監測陣列的時候可能觸發多次get/set,那麼如何防止觸發多次呢?

我們可以判斷key是否為當前被代理物件target自身屬性,也可以判斷舊值與新值是否相等,只有滿足以上兩個條件之一時,才有可能執行trigger

元件通訊

元件通訊的方式如下:

(1) props / $emit

父元件通過props向子元件傳遞資料,子元件通過$emit和父元件通訊

1. 父元件向子元件傳值
  • props只能是父元件向子元件進行傳值,props使得父子元件之間形成了一個單向下行繫結。子元件的資料會隨著父元件不斷更新。
  • props 可以顯示定義一個或一個以上的資料,對於接收的資料,可以是各種資料型別,同樣也可以傳遞一個函式。
  • props屬性名規則:若在props中使用駝峰形式,模板中需要使用短橫線的形式

```javascript // 父元件

```

```javascript // 子元件

```

2. 子元件向父元件傳值
  • $emit繫結一個自定義事件,當這個事件被執行的時就會將引數傳遞給父元件,而父元件通過v-on監聽並接收引數。

```javascript // 父元件

```

```javascript //子元件

```

(2)eventBus事件匯流排($emit / $on

eventBus事件匯流排適用於父子元件非父子元件等之間的通訊,使用步驟如下: (1)建立事件中心管理元件之間的通訊

```javascript // event-bus.js

import Vue from 'vue' export const EventBus = new Vue()

```

(2)傳送事件 假設有兩個兄弟元件firstComsecondCom

```javascript

```

firstCom元件中傳送事件:

```javascript

```

(3)接收事件secondCom元件中傳送事件:

```javascript

```

在上述程式碼中,這就相當於將num值存貯在了事件匯流排中,在其他元件中可以直接訪問。事件匯流排就相當於一個橋樑,不用元件通過它來通訊。

雖然看起來比較簡單,但是這種方法也有不變之處,如果專案過大,使用這種方式進行通訊,後期維護起來會很困難。

(3)依賴注入(provide / inject)

這種方式就是Vue中的依賴注入,該方法用於父子元件之間的通訊。當然這裡所說的父子不一定是真正的父子,也可以是祖孫元件,在層數很深的情況下,可以使用這種方法來進行傳值。就不用一層一層的傳遞了。

provide / inject是Vue提供的兩個鉤子,和datamethods是同級的。並且provide的書寫形式和data一樣。

  • provide 鉤子用來發送資料或方法
  • inject鉤子用來接收資料或方法

在父元件中:

```javascript provide() { return {
num: this.num
}; }

```

在子元件中:

```javascript inject: ['num']

```

還可以這樣寫,這樣寫就可以訪問父元件中的所有屬性:

```javascript provide() { return { app: this }; } data() { return { num: 1 }; }

inject: ['app'] console.log(this.app.num)

```

注意: 依賴注入所提供的屬性是非響應式的。

(3)ref / $refs

這種方式也是實現父子元件之間的通訊。

ref: 這個屬性用在子元件上,它的引用就指向了子元件的例項。可以通過例項來訪問元件的資料和方法。

在子元件中:

```javascript export default { data () { return { name: 'JavaScript' } }, methods: { sayHello () { console.log('hello') } } }

```

在父元件中:

```javascript

```

(4)$parent / $children

  • 使用$parent可以讓元件訪問父元件的例項(訪問的是上一級父元件的屬性和方法)
  • 使用$children可以讓元件訪問子元件的例項,但是,$children並不能保證順序,並且訪問的資料也不是響應式的。

在子元件中:

```javascript

```

在父元件中:

```javascript // 父元件中

```

在上面的程式碼中,子元件獲取到了父元件的parentVal值,父元件改變了子元件中message的值。 需要注意:

  • 通過$parent訪問到的是上一級父元件的例項,可以使用$root來訪問根元件的例項
  • 在元件中使用$children拿到的是所有的子元件的例項,它是一個數組,並且是無序的
  • 在根元件#app上拿$parent得到的是new Vue()的例項,在這例項上再拿$parent得到的是undefined,而在最底層的子元件拿$children是個空陣列
  • $children 的值是陣列,而$parent是個物件

(5)$attrs / $listeners

考慮一種場景,如果A是B元件的父元件,B是C元件的父元件。如果想要元件A給元件C傳遞資料,這種隔代的資料,該使用哪種方式呢?

如果是用props/$emit來一級一級的傳遞,確實可以完成,但是比較複雜;如果使用事件匯流排,在多人開發或者專案較大的時候,維護起來很麻煩;如果使用Vuex,的確也可以,但是如果僅僅是傳遞資料,那可能就有點浪費了。

針對上述情況,Vue引入了$attrs / $listeners,實現元件之間的跨代通訊。

先來看一下inheritAttrs,它的預設值true,繼承所有的父元件屬性除props之外的所有屬性;inheritAttrs:false 只繼承class屬性 。

  • $attrs:繼承所有的父元件屬性(除了prop傳遞的屬性、class 和 style ),一般用在子元件的子元素上
  • $listeners:該屬性是一個物件,裡面包含了作用在這個元件上的所有監聽器,可以配合 v-on="$listeners" 將所有的事件監聽器指向這個元件的某個特定的子元素。(相當於子元件繼承父元件的事件)

A元件(APP.vue):

```javascript

```

B元件(Child1.vue):

```javascript

```

C 元件 (Child2.vue):

```javascript

```

在上述程式碼中:

  • C元件中能直接觸發test的原因在於 B元件呼叫C元件時 使用 v-on 綁定了$listeners 屬性
  • 在B元件中通過v-bind 繫結$attrs屬性,C元件可以直接獲取到A元件中傳遞下來的props(除了B元件中props宣告的)

(6)總結

(1)父子元件間通訊

  • 子元件通過 props 屬性來接受父元件的資料,然後父元件在子元件上註冊監聽事件,子元件通過 emit 觸發事件來向父元件傳送資料。
  • 通過 ref 屬性給子元件設定一個名字。父元件通過 $refs 元件名來獲得子元件,子元件通過 $parent 獲得父元件,這樣也可以實現通訊。
  • 使用 provide/inject,在父元件中通過 provide提供變數,在子元件中通過 inject 來將變數注入到元件中。不論子元件有多深,只要呼叫了 inject 那麼就可以注入 provide中的資料。

(2)兄弟元件間通訊

  • 使用 eventBus 的方法,它的本質是通過建立一個空的 Vue 例項來作為訊息傳遞的物件,通訊的元件引入這個例項,通訊的元件通過在這個例項上監聽和觸發事件,來實現訊息的傳遞。
  • 通過 $parent/$refs 來獲取到兄弟元件,也可以進行通訊。

(3)任意元件之間

  • 使用 eventBus ,其實就是建立一個事件中心,相當於中轉站,可以用它來傳遞事件和接收事件。

如果業務邏輯複雜,很多元件之間需要同時處理一些公共的資料,這個時候採用上面這一些方法可能不利於專案的維護。這個時候可以使用 vuex ,vuex 的思想就是將這一些公共的資料抽離出來,將它作為一個全域性的變數來管理,然後其他元件就可以對這個公共資料進行讀寫操作,這樣達到了解耦的目的。

mixin 和 mixins 區別

mixin 用於全域性混入,會影響到每個元件例項,通常外掛都是這樣做初始化的。

```javascript Vue.mixin({ beforeCreate() { // ...邏輯 // 這種方式會影響到每個元件的 beforeCreate 鉤子函式 }, });

```

雖然文件不建議在應用中直接使用 mixin,但是如果不濫用的話也是很有幫助的,比如可以全域性混入封裝好的 ajax 或者一些工具函式等等。

mixins 應該是最常使用的擴充套件元件的方式了。如果多個元件中有相同的業務邏輯,就可以將這些邏輯剝離出來,通過 mixins 混入程式碼,比如上拉下拉載入資料這種邏輯等等。 另外需要注意的是 mixins 混入的鉤子函式會先於元件內的鉤子函式執行,並且在遇到同名選項的時候也會有選擇性的進行合併。

Vue 的父子元件生命週期鉤子函式執行順序

  • 渲染順序 :先父後子,完成順序:先子後父
  • 更新順序 :父更新導致子更新,子更新完成後父
  • 銷燬順序 :先父後子,完成順序:先子後父

載入渲染過程

beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted子元件先掛載,然後到父元件

子元件更新過程

beforeUpdate->子 beforeUpdate->子 updated->父 updated

父元件更新過程

beforeUpdate->父 updated

銷燬過程

beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

之所以會這樣是因為Vue建立過程是一個遞迴過程,先建立父元件,有子元件就會建立子元件,因此建立時先有父元件再有子元件;子元件首次建立時會新增mounted鉤子到佇列,等到patch結束再執行它們,可見子元件的mounted鉤子是先進入到佇列中的,因此等到patch結束執行這些鉤子時也先執行。

```javascript function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 定義收集所有元件的insert hook方法的陣列 // somthing ... createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )// somthing... // 最終會依次呼叫收集的insert hook invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }

function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // createChildren 會遞迴建立兒子元件 createChildren(vnode, children, insertedVnodeQueue) // something... } // 將元件的vnode插入到陣列中 function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.createi } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } } // insert方法中會依次呼叫mounted方法 insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } } function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); // 呼叫insert方法 } } }

Vue.prototype.$destroy = function () { callHook(vm, 'beforeDestroy') // invoke destroy hooks on current rendered tree vm.patch(vm._vnode, null) // 先銷燬兒子 // fire destroyed hook callHook(vm, 'destroyed') } ```

在Vue中使用外掛的步驟

  • 採用ES6import ... from ...語法或CommonJSrequire()方法引入外掛
  • 使用全域性方法Vue.use( plugin )使用外掛,可以傳入一個選項物件Vue.use(MyPlugin, { someOption: true })

Vue中diff演算法原理

DOM操作是非常昂貴的,因此我們需要儘量地減少DOM操作。這就需要找出本次DOM必須更新的節點來更新,其他的不更新,這個找出的過程,就需要應用diff演算法

vuediff演算法是平級比較,不考慮跨級比較的情況。內部採用深度遞迴的方式+雙指標(頭尾都加指標)的方式進行比較。

簡單來說,Diff演算法有以下過程

  • 同級比較,再比較子節點(根據keytag標籤名判斷)
  • 先判斷一方有子節點和一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
  • 比較都有子節點的情況(核心diff)
  • 遞迴比較子節點
  • 正常Diff兩個樹的時間複雜度是O(n^3),但實際情況下我們很少會進行跨層級的移動DOM,所以VueDiff進行了優化,從O(n^3) -> O(n),只有當新舊children都為多個子節點時才需要用核心的Diff演算法進行同層級比較。
  • Vue2的核心Diff演算法採用了雙端比較的演算法,同時從新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操作。相比ReactDiff演算法,同樣情況下可以減少移動節點次數,減少不必要的效能損耗,更加的優雅
  • 在建立VNode時就確定其型別,以及在mount/patch的過程中採用位運算來判斷一個VNode的型別,在這個基礎之上再配合核心的Diff演算法,使得效能上較Vue2.x有了提升

vue3中採用最長遞增子序列來實現diff優化

回答範例

思路

  • diff演算法是幹什麼的
  • 它的必要性
  • 它何時執行
  • 具體執行方式
  • 拔高:說一下vue3中的優化

回答範例

  1. Vue中的diff演算法稱為patching演算法,它由Snabbdom修改而來,虛擬DOM要想轉化為真實DOM就需要通過patch方法轉換
  2. 最初Vue1.x檢視中每個依賴均有更新函式對應,可以做到精準更新,因此並不需要虛擬DOMpatching演算法支援,但是這樣粒度過細導致Vue1.x無法承載較大應用;Vue 2.x中為了降低Watcher粒度,每個元件只有一個Watcher與之對應,此時就需要引入patching演算法才能精確找到發生變化的地方並高效更新
  3. vuediff執行的時刻是元件內響應式資料變更觸發例項執行其更新函式時,更新函式會再次執行render函式獲得最新的虛擬DOM,然後執行patch函式,並傳入新舊兩次虛擬DOM,通過比對兩者找到變化的地方,最後將其轉化為對應的DOM操作
  4. patch過程是一個遞迴過程,遵循深度優先、同層比較的策略;以vue3patch為例
  5. 首先判斷兩個節點是否為相同同類節點,不同則刪除重新建立
  6. 如果雙方都是文字則更新文字內容
  7. 如果雙方都是元素節點則遞迴更新子元素,同時更新元素屬性
  8. 更新子節點時又分了幾種情況
  9. 新的子節點是文字,老的子節點是陣列則清空,並設定文字;
  10. 新的子節點是文字,老的子節點是文字則直接更新文字;
  11. 新的子節點是陣列,老的子節點是文字則清空文字,並建立新子節點陣列中的子元素;
  12. 新的子節點是陣列,老的子節點也是陣列,那麼比較兩組子節點,更新細節blabla
  13. vue3中引入的更新策略:靜態節點標記等

vdom中diff演算法的簡易實現

以下程式碼只是幫助大家理解diff演算法的原理和流程

  1. vdom轉化為真實dom

```javascript const createElement = (vnode) => { let tag = vnode.tag; let attrs = vnode.attrs || {}; let children = vnode.children || []; if(!tag) { return null; } //建立元素 let elem = document.createElement(tag); //屬性 let attrName; for (attrName in attrs) { if(attrs.hasOwnProperty(attrName)) { elem.setAttribute(attrName, attrs[attrName]); } } //子元素 children.forEach(childVnode => { //給elem新增子元素 elem.appendChild(createElement(childVnode)); })

//返回真實的dom元素 return elem; } ```

  1. 用簡易diff演算法做更新操作

```javascript function updateChildren(vnode, newVnode) { let children = vnode.children || []; let newChildren = newVnode.children || [];

children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深層次對比, 遞迴過程 updateChildren(childVnode, newChildVNode); } else { //替換 replaceNode(childVnode, newChildVNode); } }) } ```

參考 前端進階面試題詳細解答

v-model實現原理

我們在 vue 專案中主要使用 v-model 指令在表單 inputtextareaselect 等元素上建立雙向資料繫結,我們知道 v-model 本質上不過是語法糖(可以看成是value + input方法的語法糖),v-model 在內部為不同的輸入元素使用不同的屬性並丟擲不同的事件:

  • texttextarea 元素使用 value 屬性和 input 事件
  • checkboxradio 使用 checked 屬性和 change 事件
  • select 欄位將 value 作為 prop 並將 change 作為事件

所以我們可以v-model進行如下改寫:

```html

```

當在input元素中使用v-model實現雙資料繫結,其實就是在輸入的時候觸發元素的input事件,通過這個語法糖,實現了資料的雙向繫結

  • 這個語法糖必須是固定的,也就是說屬性必須為value,方法名必須為:input
  • 知道了v-model的原理,我們可以在自定義元件上實現v-model

```javascript //Parent export default { data(){ return { num: 0 } } }

//Child export default { props: ['value'], // 屬性必須為value methods:{ add(){ // 方法名為input this.$emit('input', this.value + 1) } } } ```

原理

會將元件的 v-model 預設轉化成value+input

```javascript const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('');

// 觀察輸出的渲染函式: // with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // } ```

```javascript // 原始碼位置 core/vdom/create-component.js line:155

function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } } ```

原生的 v-model,會根據標籤的不同生成不同的事件和屬性

```javascript const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('');

// with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) }, // on: {"input": function ($event) { // if ($event.target.composing) return; // value = $event.target.value // } // } // }) // } ```

編譯時:不同的標籤解析出的內容不一樣 platforms/web/compiler/directives/model.js

javascript if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } else if (tag === 'select') { genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false }

執行時:會對元素處理一些關於輸入法的問題 platforms/web/runtime/directives/model.js

javascript inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) } el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true } } } }

請說明Vue中key的作用和原理,談談你對它的理解

  • key是為Vue中的VNode標記的唯一id,在patch過程中通過key可以判斷兩個虛擬節點是否是相同節點,通過這個key,我們的diff操作可以更準確、更快速
  • diff演算法的過程中,先會進行新舊節點的首尾交叉對比,當無法匹配的時候會用新節點的key與舊節點進行比對,然後檢出差異
  • 儘量不要採用索引作為key
  • 如果不加key,那麼vue會選擇複用節點(Vue的就地更新策略),導致之前節點的狀態被保留下來,會產生一系列的bug
  • 更準確 :因為帶 key 就不是就地複用了,在 sameNode 函式 a.key === b.key 對比中可以避免就地複用的情況。所以會更加準確。
  • 更快速key的唯一性可以被Map資料結構充分利用,相比於遍歷查詢的時間複雜度O(n)Map的時間複雜度僅僅為O(1),比遍歷方式更快。

原始碼如下:

javascript function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }

回答範例

分析

這是一道特別常見的問題,主要考查大家對虛擬DOMpatch細節的掌握程度,能夠反映面試者理解層次

思路分析:

  • 給出結論,key的作用是用於優化patch效能
  • key的必要性
  • 實際使用方式
  • 總結:可從原始碼層面描述一下vue如何判斷兩個節點是否相同

回答範例:

  1. key的作用主要是為了更高效的更新虛擬DOM
  2. vuepatch過程中 判斷兩個節點是否是相同節點是key是一個必要條件 ,渲染一組列表時,key往往是唯一標識,所以如果不定義key的話,vue只能認為比較的兩個節點是同一個,哪怕它們實際上不是,這導致了頻繁更新元素,使得整個patch過程比較低效,影響效能
  3. 實際使用中在渲染一組列表時key必須設定,而且必須是唯一標識,應該避免使用陣列索引作為key,這可能導致一些隱蔽的bugvue中在使用相同標籤元素過渡切換時,也會使用key屬性,其目的也是為了讓vue可以區分它們,否則vue只會替換其內部屬性而不會觸發過渡效果
  4. 從原始碼中可以知道,vue判斷兩個節點是否相同時主要判斷兩者的key標籤型別(如div)等,因此如果不設定key,它的值就是undefined,則可能永遠認為這是兩個相同節點,只能去做更新操作,這造成了大量的dom更新操作,明顯是不可取的

如果不使用 keyVue 會使用一種最大限度減少動態元素並且儘可能的嘗試就地修改/複用相同型別元素的演算法。key 是為 Vuevnode 的唯一標記,通過這個 key,我們的 diff 操作可以更準確、更快速

diff程可以概括為:oldChnewCh各有兩個頭尾的變數StartIdxEndIdx,它們的2個變數相互比較,一共有4種比較方式。如果4種比較都沒匹配,如果設定了key,就會用key進行比較,在比較的過程中,變數會往中間靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一個已經遍歷完了,就會結束比較,這四種比較方式就是舊尾新頭舊頭新尾

相關程式碼如下

```javascript // 判斷兩個vnode的標籤和key是否相同 如果相同 就可以認為是同一節點就地複用 function isSameVnode(oldVnode, newVnode) { return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key; }

// 根據key來建立老的兒子的index對映表 類似 {'a':0,'b':1} 代表key為'a'的節點在第一個位置 key為'b'的節點在第二個位置 function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map; } // 生成的對映表 let map = makeIndexByKey(oldCh); ```

Vuex中actions和mutations有什麼區別

題目分析

  • mutationsactionsvuex帶來的兩個獨特的概念。新手程式設計師容易混淆,所以面試官喜歡問。
  • 我們只需記住修改狀態只能是mutationsactions只能通過提交mutation修改狀態即可

回答範例

  1. 更改 Vuexstore 中的狀態的唯一方法是提交 mutationmutation 非常類似於事件:每個 mutation 都有一個字串的型別 (type)和一個 回撥函式 (handler) 。Action 類似於 mutation,不同在於:Action可以包含任意非同步操作,但它不能修改狀態, 需要提交mutation才能變更狀態
  2. 開發時,包含非同步操作或者複雜業務組合時使用action;需要直接修改狀態則提交mutation。但由於dispatchcommit是兩個API,容易引起混淆,實踐中也會採用統一使用dispatch action的方式。呼叫dispatchcommit兩個API時幾乎完全一樣,但是定義兩者時卻不甚相同,mutation的回撥函式接收引數是state物件。action則是與Store例項具有相同方法和屬性的上下文context物件,因此一般會解構它為{commit, dispatch, state},從而方便編碼。另外dispatch會返回Promise例項便於處理內部非同步結果
  3. 實現上commit(type)方法相當於呼叫options.mutations[type](state)dispatch(type)方法相當於呼叫options.actions[type](store),這樣就很容易理解兩者使用上的不同了

實現

我們可以像下面這樣簡單實現commitdispatch,從而辨別兩者不同

javascript class Store { constructor(options) { this.state = reactive(options.state) this.options = options } commit(type, payload) { // 傳入上下文和引數1都是state物件 this.options.mutations[type].call(this.state, this.state, payload) } dispatch(type, payload) { // 傳入上下文和引數1都是store本身 this.options.actions[type].call(this, this, payload) } }

對Vue SSR的理解

Vue.js 是構建客戶端應用程式的框架。預設情況下,可以在瀏覽器中輸出 Vue 元件,進行生成 DOM 和操作 DOM。然而,也可以將同一個元件渲染為服務端的 HTML 字串,將它們直接傳送到瀏覽器,最後將這些靜態標記"啟用"為客戶端上完全可互動的應用程式。

SSR也就是服務端渲染,也就是將 Vue 在客戶端把標籤渲染成 HTML 的工作放在服務端完成,然後再把 html 直接返回給客戶端

  • 優點SSR 有著更好的 SEO、並且首屏載入速度更快
  • 因為 SPA 頁面的內容是通過 Ajax 獲取,而搜尋引擎爬取工具並不會等待 Ajax 非同步完成後再抓取頁面內容,所以在 SPA 中是抓取不到頁面通過 Ajax獲取到的內容;而 SSR 是直接由服務端返回已經渲染好的頁面(資料已經包含在頁面中),所以搜尋引擎爬取工具可以抓取渲染好的頁面
  • 更快的內容到達時間(首屏載入更快): SPA 會等待所有 Vue 編譯後的 js 檔案都下載完成後,才開始進行頁面的渲染,檔案下載等需要一定的時間等,所以首屏渲染需要一定的時間;SSR 直接由服務端渲染好頁面直接返回顯示,無需等待下載 js 檔案及再去渲染等,所以 SSR 有更快的內容到達時間
  • 缺點 : 開發條件會受到限制,伺服器端渲染只支援 beforeCreatecreated 兩個鉤子,當我們需要一些外部擴充套件庫時需要特殊處理,服務端渲染應用程式也需要處於 Node.js 的執行環境。伺服器會有更大的負載需求
  • 在 Node.js 中渲染完整的應用程式,顯然會比僅僅提供靜態檔案的 server 更加大量佔用CPU資源 (CPU-intensive - CPU 密集),因此如果你預料在高流量環境 ( high traffic ) 下使用,請準備相應的伺服器負載,並明智地採用快取策略

其基本實現原理

  • app.js 作為客戶端與服務端的公用入口,匯出 Vue 根例項,供客戶端 entry 與服務端 entry 使用。客戶端 entry 主要作用掛載到 DOM 上,服務端 entry 除了建立和返回例項,還進行路由匹配與資料預獲取。
  • webpack 為客服端打包一個 Client Bundle ,為服務端打包一個 Server Bundle
  • 伺服器接收請求時,會根據 url,載入相應元件,獲取和解析非同步資料,建立一個讀取 Server BundleBundleRenderer,然後生成 html 傳送給客戶端。
  • 客戶端混合,客戶端收到從服務端傳來的 DOM 與自己的生成的 DOM 進行對比,把不相同的 DOM 啟用,使其可以能夠響應後續變化,這個過程稱為客戶端啟用 。為確保混合成功,客戶端與伺服器端需要共享同一套資料。在服務端,可以在渲染之前獲取資料,填充到 stroe 裡,這樣,在客戶端掛載到 DOM 之前,可以直接從 store裡取資料。首屏的動態資料通過 window.__INITIAL_STATE__傳送到客戶端

Vue SSR 的實現,主要就是把 Vue 的元件輸出成一個完整 HTML, vue-server-renderer 就是幹這事的

Vue SSR需要做的事多點(輸出完整 HTML),除了complier -> vnode,還需如資料獲取填充至 HTML、客戶端混合(hydration)、快取等等。相比於其他模板引擎(ejs, jade 等),最終要實現的目的是一樣的,效能上可能要差點

怎麼實現路由懶載入呢

這是一道應用題。當打包應用時,JavaScript 包會變得非常大,影響頁面載入。如果我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被訪問時才載入對應元件,這樣就會更加高效

javascript // 將 // import UserDetails from './views/UserDetails' // 替換為 const UserDetails = () => import('./views/UserDetails') ​ const router = createRouter({ // ... routes: [{ path: '/users/:id', component: UserDetails }], })

回答範例

  1. 當打包構建應用時,JavaScript 包會變得非常大,影響頁面載入。利用路由懶載入我們能把不同路由對應的元件分割成不同的程式碼塊,然後當路由被訪問的時候才載入對應元件,這樣會更加高效,是一種優化手段
  2. 一般來說,對所有的路由都使用動態匯入是個好主意
  3. component選項配置一個返回 Promise 元件的函式就可以定義懶載入路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 結合註釋 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack程式碼分塊

你覺得vuex有什麼缺點

分析

相較於reduxvuex已經相當簡便好用了。但模組的使用比較繁瑣,對ts支援也不好。

體驗

使用模組:用起來比較繁瑣,使用模式也不統一,基本上得不到型別系統的任何支援

javascript const store = createStore({ modules: { a: moduleA } }) store.state.a // -> 要帶上 moduleA 的key,內嵌模組的話會很長,不得不配合mapState使用 store.getters.c // -> moduleA裡的getters,沒有namespaced時又變成了全域性的 store.getters['a/c'] // -> 有namespaced時要加path,使用模式又和state不一樣 store.commit('d') // -> 沒有namespaced時變成了全域性的,能同時觸發多個子模組中同名mutation store.commit('a/d') // -> 有namespaced時要加path,配合mapMutations使用感覺也沒簡化

回答範例

  1. vuex利用響應式,使用起來已經相當方便快捷了。但是在使用過程中感覺模組化這一塊做的過於複雜,用的時候容易出錯,還要經常檢視文件
  2. 比如:訪問state時要帶上模組key,內嵌模組的話會很長,不得不配合mapState使用,加不加namespaced區別也很大,gettersmutationsactions這些預設是全域性,加上之後必須用字串型別的path來匹配,使用模式不統一,容易出錯;對ts的支援也不友好,在使用模組時沒有程式碼提示。
  3. 之前Vue2專案中用過vuex-module-decorators的解決方案,雖然型別支援上有所改善,但又要學一套新東西,增加了學習成本。pinia出現之後使用體驗好了很多,Vue3 + pinia會是更好的組合

原理

下面我們來看看vuexstore.state.x.y這種巢狀的路徑是怎麼搞出來的

首先是子模組安裝過程:父模組狀態parentState上面設定了子模組名稱moduleName,值為當前模組state物件。放在上面的例子中相當於:store.state['x'] = moduleX.state。此過程是遞迴的,那麼store.state.x.y安裝時就是:store.state['x']['y'] = moduleY.state

javascript //原始碼位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115 if (!isRoot && !hot) { // 獲取父模組state const parentState = getNestedState(rootState, path.slice(0, -1)) // 獲取子模組名稱 const moduleName = path[path.length - 1] store._withCommit(() => { // 把子模組state設定到父模組上 parentState[moduleName] = module.state }) }

v-if和v-show區別

  • v-show隱藏則是為該元素新增css--display:nonedom元素依舊還在。v-if顯示隱藏是將dom元素整個新增或刪除
  • 編譯過程:v-if切換有一個區域性編譯/解除安裝的過程,切換過程中合適地銷燬和重建內部的事件監聽和子元件;v-show只是簡單的基於css切換
  • 編譯條件:v-if是真正的條件渲染,它會確保在切換過程中條件塊內的事件監聽器和子元件適當地被銷燬和重建。只有渲染條件為假時,並不做操作,直到為真才渲染
  • v-showfalse變為true的時候不會觸發元件的生命週期
  • v-iffalse變為true的時候,觸發元件的beforeCreatecreatebeforeMountmounted鉤子,由true變為false的時候觸發元件的beforeDestorydestoryed方法
  • 效能消耗:v-if有更高的切換消耗;v-show有更高的初始渲染消耗

v-show與v-if的使用場景

  • v-ifv-show 都能控制dom元素在頁面的顯示
  • v-if 相比 v-show 開銷更大的(直接操作dom節點增加與刪除)
  • 如果需要非常頻繁地切換,則使用 v-show 較好
  • 如果在執行時條件很少改變,則使用 v-if 較好

v-show與v-if原理分析

  1. v-show原理

不管初始條件是什麼,元素總是會被渲染

我們看一下在vue中是如何實現的

程式碼很好理解,有transition就執行transition,沒有就直接設定display屬性

javascript // https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts export const vShow: ObjectDirective<VShowElement> = { beforeMount(el, { value }, { transition }) { el._vod = el.style.display === 'none' ? '' : el.style.display if (transition && value) { transition.beforeEnter(el) } else { setDisplay(el, value) } }, mounted(el, { value }, { transition }) { if (transition && value) { transition.enter(el) } }, updated(el, { value, oldValue }, { transition }) { // ... }, beforeUnmount(el, { value }) { setDisplay(el, value) } }

  1. v-if原理

v-if在實現上比v-show要複雜的多,因為還有else else-if 等條件需要處理,這裡我們也只摘抄原始碼中處理 v-if 的一小部分

返回一個node節點,render函式通過表示式的值來決定是否生成DOM

javascript // https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { // ... return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch( branch, key, context ) as IfConditionalExpression } else { // attach this branch's codegen node to the v-if root. const parentCondition = getParentCondition(ifNode.codegenNode!) parentCondition.alternate = createCodegenNodeForBranch( branch, key + ifNode.branches.length - 1, context ) } } }) } )

Vue路由的鉤子函式

首頁可以控制導航跳轉,beforeEachafterEach等,一般用於頁面title的修改。一些需要登入才能調整頁面的重定向功能。

  • beforeEach主要有3個引數tofromnext
  • toroute即將進入的目標路由物件。
  • fromroute當前導航正要離開的路由。
  • nextfunction一定要呼叫該方法resolve這個鉤子。執行效果依賴next方法的呼叫引數。可以控制網頁的跳轉

vue-router 路由鉤子函式是什麼 執行順序是什麼

路由鉤子的執行流程, 鉤子函式種類有:全域性守衛路由守衛元件守衛

  1. 導航被觸發。
  2. 在失活的元件裡呼叫 beforeRouteLeave 守衛。
  3. 呼叫全域性的 beforeEach 守衛。
  4. 在重用的元件裡呼叫 beforeRouteUpdate 守衛 (2.2+)。
  5. 在路由配置裡呼叫 beforeEnter
  6. 解析非同步路由元件。
  7. 在被啟用的元件裡呼叫 beforeRouteEnter
  8. 呼叫全域性的 beforeResolve 守衛 (2.5+)。
  9. 導航被確認。
  10. 呼叫全域性的 afterEach 鉤子。
  11. 觸發 DOM 更新。
  12. 呼叫 beforeRouteEnter 守衛中傳給 next 的回撥函式,建立好的元件例項會作為回撥函式的引數傳入

Composition API 與 Options API 有什麼不同

分析

Vue3最重要更新之一就是Composition API,它具有一些列優點,其中不少是針對Options API暴露的一些問題量身打造。是Vue3推薦的寫法,因此掌握好Composition API應用對掌握好Vue3至關重要

What is Composition API?(opens new window)

  • Composition API出現就是為了解決Options API導致相同功能程式碼分散的現象

體驗

Composition API能更好的組織程式碼,下面用composition api可以提取為useCount(),用於組合、複用

compositon api提供了以下幾個函式:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命週期的hooks

回答範例

  1. Composition API是一組API,包括:Reactivity API生命週期鉤子依賴注入,使使用者可以通過匯入函式方式編寫vue元件。而Options API則通過宣告元件選項的物件形式編寫元件
  2. Composition API最主要作用是能夠簡潔、高效複用邏輯。解決了過去Options APImixins的各種缺點;另外Composition API具有更加敏捷的程式碼組織能力,很多使用者喜歡Options API,認為所有東西都有固定位置的選項放置程式碼,但是單個元件增長過大之後這反而成為限制,一個邏輯關注點分散在元件各處,形成程式碼碎片,維護時需要反覆橫跳,Composition API則可以將它們有效組織在一起。最後Composition API擁有更好的型別推斷,對ts支援更友好,Options API在設計之初並未考慮型別推斷因素,雖然官方為此做了很多複雜的型別體操,確保使用者可以在使用Options API時獲得型別推斷,然而還是沒辦法用在mixinsprovide/inject
  3. Vue3首推Composition API,但是這會讓我們在程式碼組織上多花點心思,因此在選擇上,如果我們專案屬於中低複雜度的場景,Options API仍是一個好選擇。對於那些大型,高擴充套件,強維護的專案上,Composition API會獲得更大收益

可能的追問

  1. Composition API能否和Options API一起使用?

可以在同一個元件中使用兩個script標籤,一個使用vue3,一個使用vue2寫法,一起使用沒有問題

```html

```

子元件可以直接改變父元件的資料麼,說明原因

這是一個實踐知識點,元件化開發過程中有個單項資料流原則,不在子元件中修改父元件是個常識問題

思路

  • 講講單項資料流原則,表明為何不能這麼做
  • 舉幾個常見場景的例子說說解決方案
  • 結合實踐講講如果需要修改父元件狀態應該如何做

回答範例

  1. 所有的 prop 都使得其父子之間形成了一個單向下行繫結:父級 prop 的更新會向下流動到子元件中,但是反過來則不行。這樣會防止從子元件意外變更父級元件的狀態,從而導致你的應用的資料流向難以理解。另外,每次父級元件發生變更時,子元件中所有的 prop 都將會重新整理為最新的值。這意味著你不應該在一個子元件內部改變 prop。如果你這樣做了,Vue 會在瀏覽器控制檯中發出警告

javascript const props = defineProps(['foo']) // ❌ 下面行為會被警告, props是隻讀的! props.foo = 'bar'

  1. 實際開發過程中有兩個場景會想要修改一個屬性:

這個 prop 用來傳遞一個初始值;這個子元件接下來希望將其作為一個本地的 prop 資料來使用。 在這種情況下,最好定義一個本地的 data,並將這個 prop 用作其初始值:

javascript const props = defineProps(['initialCounter']) const counter = ref(props.initialCounter)

這個 prop 以一種原始的值傳入且需要進行轉換。 在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:

javascript const props = defineProps(['size']) // prop變化,計算屬性自動更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())

  1. 實踐中如果確實想要改變父元件屬性應該emit一個事件讓父元件去做這個變更。注意雖然我們不能直接修改一個傳入的物件或者陣列型別的prop,但是我們還是能夠直接改內嵌的物件或屬性

既然Vue通過資料劫持可以精準探測資料變化,為什麼還需要虛擬DOM進行diff檢測差異

  • 響應式資料變化,Vue確實可以在資料變化時,響應式系統可以立刻得知。但是如果給每個屬性都新增watcher用於更新的話,會產生大量的watcher從而降低效能
  • 而且粒度過細也得導致更新不準確的問題,所以vue採用了元件級的watcher配合diff來檢測差異

Vue為什麼需要虛擬DOM?優缺點有哪些

由於在瀏覽器中操作 DOM是很昂貴的。頻繁的操作 DOM,會產生一定的效能問題。這就是虛擬 Dom 的產生原因。Vue2Virtual DOM 借鑑了開源庫 snabbdom 的實現。Virtual DOM 本質就是用一個原生的 JS 物件去描述一個 DOM 節點,是對真實 DOM 的一層抽象

優點:

  • 保證效能下限 : 框架的虛擬 DOM 需要適配任何上層 API 可能產生的操作,它的一些 DOM 操作的實現必須是普適的,所以它的效能並不是最優的;但是比起粗暴的 DOM 操作效能要好很多,因此框架的虛擬 DOM 至少可以保證在你不需要手動優化的情況下,依然可以提供還不錯的效能,即保證效能的下限;
  • 無需手動操作 DOM : 我們不再需要手動去操作 DOM,只需要寫好 View-Model 的程式碼邏輯,框架會根據虛擬 DOM 和 資料雙向繫結,幫我們以可預期的方式更新檢視,極大提高我們的開發效率;
  • 跨平臺 : 虛擬 DOM 本質上是 JavaScript 物件,而 DOM 與平臺強相關,相比之下虛擬 DOM 可以進行更方便地跨平臺操作,例如伺服器渲染、weex 開發等等。

缺點:

  • 無法進行極致優化:雖然虛擬 DOM + 合理的優化,足以應對絕大部分應用的效能需求,但在一些效能要求極高的應用中虛擬 DOM 無法進行鍼對性的極致優化。
  • 首次渲染大量DOM時,由於多了一層虛擬 DOM 的計算,會比 innerHTML 插入慢。

虛擬 DOM 實現原理?

虛擬 DOM 的實現原理主要包括以下 3 部分:

  • JavaScript 物件模擬真實 DOM 樹,對真實 DOM 進行抽象;
  • diff 演算法 — 比較兩棵虛擬 DOM 樹的差異;
  • pach 演算法 — 將兩個虛擬 DOM 物件的差異應用到真正的 DOM 樹。

說說你對虛擬 DOM 的理解?回答範例

思路

  • vdom是什麼
  • 引入vdom的好處
  • vdom如何生成,又如何成為dom
  • 在後續的diff中的作用

回答範例

  1. 虛擬dom顧名思義就是虛擬的dom物件,它本身就是一個 JavaScript 物件,只不過它是通過不同的屬性去描述一個檢視結構

  2. 通過引入vdom我們可以獲得如下好處:

  3. 將真實元素節點抽象成 VNode,有效減少直接操作 dom 次數,從而提高程式效能

  4. 直接操作 dom 是有限制的,比如:diffclone 等操作,一個真實元素上有許多的內容,如果直接對其進行 diff 操作,會去額外 diff 一些沒有必要的內容;同樣的,如果需要進行 clone 那麼需要將其全部內容進行復制,這也是沒必要的。但是,如果將這些操作轉移到 JavaScript 物件上,那麼就會變得簡單了

  5. 操作 dom 是比較昂貴的操作,頻繁的dom操作容易引起頁面的重繪和迴流,但是通過抽象 VNode 進行中間處理,可以有效減少直接操作dom的次數,從而減少頁面重繪和迴流

  6. 方便實現跨平臺

  7. 同一 VNode 節點可以渲染成不同平臺上的對應的內容,比如:渲染在瀏覽器是 dom 元素節點,渲染在 Native( iOS、Android)變為對應的控制元件、可以實現 SSR 、渲染到 WebGL 中等等

  8. Vue3 中允許開發者基於 VNode 實現自定義渲染器(renderer),以便於針對不同平臺進行渲染
  9. vdom如何生成?在vue中我們常常會為元件編寫模板 - template, 這個模板會被編譯器 - compiler編譯為渲染函式,在接下來的掛載(mount)過程中會呼叫render函式,返回的物件就是虛擬dom。但它們還不是真正的dom,所以會在後續的patch過程中進一步轉化為dom

  1. 掛載過程結束後,vue程式進入更新流程。如果某些響應式資料發生變化,將會引起元件重新render,此時就會生成新的vdom,和上一次的渲染結果diff就能得到變化的地方,從而轉換為最小量的dom操作,高效更新檢視

為什麼要用vdom?案例解析

現在有一個場景,實現以下需求:

javascript [ { name: "張三", age: "20", address: "北京"}, { name: "李四", age: "21", address: "武漢"}, { name: "王五", age: "22", address: "杭州"}, ]

將該資料展示成一個表格,並且隨便修改一個資訊,表格也跟著修改。 用jQuery實現如下:

```html

Document

```

  • 這樣點選按鈕,會有相應的檢視變化,但是你審查以下元素,每次改動之後,table標籤都得重新建立,也就是說table下面的每一個欄目,不管是資料是否和原來一樣,都得重新渲染,這並不是理想中的情況,當其中的一欄資料和原來一樣,我們希望這一欄不要重新渲染,因為DOM重繪相當消耗瀏覽器效能。
  • 因此我們採用JS物件模擬的方法,將DOM的比對操作放在JS層,減少瀏覽器不必要的重繪,提高效率。
  • 當然有人說虛擬DOM並不比真實的DOM快,其實也是有道理的。當上述table中的每一條資料都改變時,顯然真實的DOM操作更快,因為虛擬DOM還存在jsdiff演算法的比對過程。所以,上述效能優勢僅僅適用於大量資料的渲染並且改變的資料只是一小部分的情況。

如下DOM結構:

```html

  • Item1
  • Item2

```

對映成虛擬DOM就是這樣:

javascript { tag: "ul", attrs: { id: "list" }, children: [ { tag: "li", attrs: { className: "item" }, children: ["Item1"] }, { tag: "li", attrs: { className: "item" }, children: ["Item2"] } ] }

使用snabbdom實現vdom

這是一個簡易的實現vdom功能的庫,相比vuereact,對於vdom這塊更加簡易,適合我們學習vdomvdom裡面有兩個核心的api,一個是h函式,一個是patch函式,前者用來生成vdom物件,後者的功能在於做虛擬dom的比對和將vdom掛載到真實DOM

簡單介紹一下這兩個函式的用法:

javascript h('標籤名', {屬性}, [子元素]) h('標籤名', {屬性}, [文字]) patch(container, vnode) // container為容器DOM元素 patch(vnode, newVnode)

現在我們就來用snabbdom重寫一下剛才的例子:

```html

Document

```

你會發現, 只有改變的欄目才閃爍,也就是進行重繪 ,資料沒有改變的欄目還是保持原樣,這樣就大大節省了瀏覽器重新渲染的開銷

vue中使用h函式生成虛擬DOM返回

javascript const vm = new Vue({ el: '#app', data: { user: {name:'poetry'} }, render(h){ // h() // h(App) // h('div',[]) let vnode = h('div',{},'hello world'); return vnode } });

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)