2023前端二面高频vue面试题集锦
vuex是什么?怎么使用?哪種功能場景使用它?
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。vuex 就是一個倉庫,倉庫里放了很多對象。其中 state 就是數據源存放地,對應于一般 vue 對象里面的 data 里面存放的數據是響應式的,vue 組件從 store 讀取數據,若是 store 中的數據發生改變,依賴這相數據的組件也會發生更新它通過 mapState 把全局的 state 和 getters 映射到當前組件的 computed 計算屬性
- vuex 一般用于中大型 web 單頁應用中對應用的狀態進行管理,對于一些組件間關系較為簡單的小型應用,使用 vuex 的必要性不是很大,因為完全可以用組件 prop 屬性或者事件來完成父子組件之間的通信,vuex 更多地用于解決跨組件通信以及作為數據中心集中式存儲數據。
- 使用Vuex解決非父子組件之間通信問題 vuex 是通過將 state 作為數據中心、各個組件共享 state 實現跨組件通信的,此時的數據完全獨立于組件,因此將組件間共享的數據置于 State 中能有效解決多層級組件嵌套的跨組件通信問題
vuex 的 State 在單頁應用的開發中本身具有一個“數據庫”的作用,可以將組件中用到的數據存儲在 State 中,并在 Action 中封裝數據讀寫的邏輯。這時候存在一個問題,一般什么樣的數據會放在 State 中呢? 目前主要有兩種數據會使用 vuex 進行管理:
- 組件之間全局共享的數據
- 通過后端異步請求的數據
包括以下幾個模塊
- state:Vuex 使用單一狀態樹,即每個應用將僅僅包含一個store 實例。里面存放的數據是響應式的,vue 組件從 store 讀取數據,若是 store 中的數據發生改變,依賴這相數據的組件也會發生更新。它通過 mapState 把全局的 state 和 getters 映射到當前組件的 computed 計算屬性
- mutations:更改Vuex的store中的狀態的唯一方法是提交mutation
- getters:getter 可以對 state 進行計算操作,它就是 store 的計算屬性雖然在組件內也可以做計算屬性,但是 getters 可以在多給件之間復用如果一個狀態只在一個組件內使用,是可以不用 getters
- action:action 類似于 muation, 不同在于:action 提交的是 mutation,而不是直接變更狀態action 可以包含任意異步操作
- modules:面對復雜的應用程序,當管理的狀態比較多時;我們需要將vuex的store對象分割成模塊(modules)
modules:項目特別復雜的時候,可以讓每一個模塊擁有自己的state、mutation、action、getters,使得結構非常清晰,方便管理
回答范例
思路
- 給定義
- 必要性闡述
- 何時使用
- 拓展:一些個人思考、實踐經驗等
回答范例
可能的追問
- 刷新瀏覽器,vuex中的state會重新變為初始狀態。解決方案-插件 vuex-persistedstate
- action中處理異步,mutation不可以
- mutation做原子操作
- action可以整合多個mutation的集合
- mutation 是同步更新數據(內部會進行是否為異步方式更新數據的檢測) $watch 嚴格模式下會報錯
- action 異步操作,可以獲取數據后調傭 mutation 提交最終數據
- 流程順序:“相應視圖—>修改State”拆分成兩部分,視圖觸發Action,Action再觸發Mutation`。
- 基于流程順序,二者扮演不同的角色:Mutation:專注于修改State,理論上是修改State的唯一途徑。Action:業務代碼、異步請求
- 角色不同,二者有不同的限制:Mutation:必須同步執行。Action:可以異步,但不能直接操作State
Watch中的deep:true是如何實現的
當用戶指定了 watch 中的deep屬性為 true 時,如果當前監控的值是數組類型。會對對象中的每一項進行求值,此時會將當前 watcher存入到對應屬性的依賴中,這樣數組中對象發生變化時也會通知數據更新
源碼相關
get () { pushTarget(this) // 先將當前依賴放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果需要深度監控 traverse(value) // 會對對象中的每一項取值,取值時會執行對應的get方法 }popTarget() }Vue3速度快的原因
Vue3.0 性能提升體現在哪些方面
- 代碼層面性能優化主要體現在全新響應式API,基于Proxy實現,初始化時間和內存占用均大幅改進;
- 編譯層面做了更多編譯優化處理,比如靜態標記pachFlag(diff算法增加了一個靜態標記,只對比有標記的dom元素)、事件增加緩存、靜態提升(對不參與更新的元素,會做靜態提升,只會被創建一次,之后會在每次渲染時候被不停的復用)等,可以有效跳過大量diff過程;
- 打包時更好的支持tree-shaking,因此整體體積更小,加載更快
- ssr渲染以字符串方式渲染
一、編譯階段
試想一下,一個組件結構如下圖
<template><div id="content"><p class="text">靜態文本</p><p class="text">靜態文本</p><p class="text">{ message }</p><p class="text">靜態文本</p>...<p class="text">靜態文本</p></div> </template>可以看到,組件內部只有一個動態節點,剩余一堆都是靜態節點,所以這里很多 diff 和遍歷其實都是不需要的,造成性能浪費
因此,Vue3在編譯階段,做了進一步優化。主要有如下:
- diff算法優化
- 靜態提升
- 事件監聽緩存
- SSR優化
1. diff 算法優化
- Vue 2x 中的虛擬 dom 是進行全量的對比。
- Vue 3x 中新增了靜態標記(PatchFlag):在與上次虛擬結點進行對比的時候,值對比 帶有 patch flag 的節點,并且可以通過 flag 的信息得知當前節點要對比的具體內容化
Vue2.x的diff算法
vue2.x的diff算法叫做全量比較,顧名思義,就是當數據改變的時候,會從頭到尾的進行vDom對比,即使有些內容是永恒固定不變的
Vue3.0的diff算法
vue3.0的diff算法有個叫靜態標記(PatchFlag)的小玩意,啥是靜態標記呢?簡單點說,就是如果你的內容會變,我會給你一個flag,下次數據更新的時候我直接來對比你,我就不對比那些沒有標記的了
已經標記靜態節點的p標簽在diff過程中則不會比較,把性能進一步提高
export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, [_createVNode("p", null, "'HelloWorld'"),_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)//上面這個1就是靜態標記])) }關于靜態類型枚舉如下
TEXT = 1 // 動態文本節點 CLASS=1<<1,1 // 2//動態class STYLE=1<<2,// 4 //動態style PROPS=1<<3,// 8 //動態屬性,但不包含類名和樣式 FULLPR0PS=1<<4,// 16 //具有動態key屬性,當key改變時,需要進行完整的diff比較。 HYDRATE_ EVENTS = 1 << 5,// 32 //帶有監聽事件的節點 STABLE FRAGMENT = 1 << 6, // 64 //一個不會改變子節點順序的fragment KEYED_ FRAGMENT = 1 << 7, // 128 //帶有key屬性的fragment 或部分子字節有key UNKEYED FRAGMENT = 1<< 8, // 256 //子節點沒有key 的fragment NEED PATCH = 1 << 9, // 512 //一個節點只會進行非props比較 DYNAMIC_SLOTS = 1 << 10 // 1024 // 動態slot HOISTED = -1 // 靜態節點 // 指示在diff算法中退出優化模式 BALL = -22. hoistStatic 靜態提升
- Vue 2x : 無論元素是否參與更新,每次都會重新創建。
- Vue 3x : 對不參與更新的元素,會做靜態提升,只會被創建一次,之后會在每次渲染時候被不停的復用。這樣就免去了重復的創建節點,大型應用會受益于這個改動,免去了重復的創建操作,優化了運行時候的內存占用
開啟靜態提升前
export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, [_createVNode("p", null, "'HelloWorld'"),_createVNode("p", null, "'HelloWorld'"),_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)])) }開啟靜態提升后編譯結果
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, [_hoisted_1,_hoisted_2,_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)])) }可以看到開啟了靜態提升后,直接將那兩個內容為helloworld的p標簽聲明在外面了,直接就拿來用了。同時 _hoisted_1和_hoisted_2 被打上了 PatchFlag ,靜態標記值為 -1 ,特殊標志是負整數表示永遠不會用于 Diff
3. cacheHandlers 事件監聽緩存
- 默認情況下 綁定事件會被視為動態綁定 ,所以每次都會去追蹤它的變化
- 但是因為是同一個函數,所以沒有追蹤變化,直接緩存起來復用即可
開啟事件偵聽器緩存之前:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, [_createVNode("button", { onClick: _ctx.onClick }, "點我", 8 /* PROPS */, ["onClick"])// PROPS=1<<3,// 8 //動態屬性,但不包含類名和樣式])) })這里有一個8,表示著這個節點有了靜態標記,有靜態標記就會進行diff算法對比差異,所以會浪費時間
開啟事件偵聽器緩存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {return (_openBlock(), _createBlock("div", null, [_createVNode("button", {onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))}, "點我")])) }上述發現開啟了緩存后,沒有了靜態標記。也就是說下次diff算法的時候直接使用
4. SSR優化
當靜態內容大到一定量級時候,會用createStaticVNode方法在客戶端去生成一個static node,這些靜態node,會被直接innerHtml,就不需要創建對象,然后根據對象渲染
<div><div><span>你好</span></div>... // 很多個靜態屬性<div><span>{{ message }}</span></div> </div>編譯后
import { mergeProps as _mergeProps } from "vue" import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {const _cssVars = { style: { color: _ctx.color }}_push(`<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}><div><span>你好</span>...<div><span>你好</span><div><span>${_ssrInterpolate(_ctx.message)}</span></div></div>`) }二、源碼體積
相比Vue2,Vue3整體體積變小了,除了移出一些不常用的API,再重要的是Tree shanking
任何一個函數,如ref、reactive、computed等,僅僅在用到的時候才打包,沒用到的模塊都被搖掉,打包的整體體積變小
import { computed, defineComponent, ref } from 'vue'; export default defineComponent({setup(props, context) {const age = ref(18)let state = reactive({name: 'test'})const readOnlyAge = computed(() => age.value++) // 19return {age,state,readOnlyAge}} });三、響應式系統
vue2中采用 defineProperty來劫持整個對象,然后進行深度遍歷所有屬性,給每個屬性添加getter和setter,實現響應式
vue3采用proxy重寫了響應式系統,因為proxy可以對整個對象進行監聽,所以不需要深度遍歷
- 可以監聽動態屬性的添加
- 可以監聽到數組的索引和數組length屬性
- 可以監聽刪除屬性
什么是遞歸組件?舉個例子說明下?
分析
遞歸組件我們用的比較少,但是在Tree、Menu這類組件中會被用到。
體驗
組件通過組件名稱引用它自己,這種情況就是遞歸組件
<template><li><div> {{ model.name }}</div><ul v-show="isOpen" v-if="isFolder"><!-- 注意這里:組件遞歸渲染了它自己 --><TreeItemclass="item"v-for="model in model.children":model="model"></TreeItem></ul></li> <script> export default {name: 'TreeItem',// ... } </script>回答范例
原理
遞歸組件編譯結果中,獲取組件時會傳遞一個標識符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)就是在傳遞maybeSelfReference
export function resolveComponent(name: string,maybeSelfReference?: boolean ): ConcreteComponent | string {return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name }resolveAsset中最終返回的是組件自身:
if (!res && maybeSelfReference) {// fallback to implicit self-referencereturn Component }怎樣理解 Vue 的單向數據流
數據總是從父組件傳到子組件,子組件沒有權利修改父組件傳過來的數據,只能請求父組件對原始數據進行修改。這樣會 防止從子組件意外改變父級組件的狀態 ,從而導致你的應用的數據流向難以理解
注意 :在子組件直接用 v-model 綁定父組件傳過來的 prop 這樣是不規范的寫法 開發環境會報警告
如果實在要改變父組件的 prop 值,可以在 data 里面定義一個變量 并用 prop 的值初始化它 之后用$emit 通知父組件去修改
有兩種常見的試圖改變一個 prop 的情形 :
為什么不建議用index作為key?
使用index 作為 key和沒寫基本上沒區別,因為不管數組的順序怎么顛倒,index 都是 0, 1, 2…這樣排列,導致 Vue 會復用錯誤的舊子節點,做很多額外的工作。
參考 前端進階面試題詳細解答
怎么緩存當前的組件?緩存后怎么更新
緩存組件使用keep-alive組件,這是一個非常常見且有用的優化手段,vue3中keep-alive有比較大的更新,能說的點比較多
思路
- 緩存用keep-alive,它的作用與用法
- 使用細節,例如緩存指定/排除、結合router和transition
- 組件緩存后更新可以利用activated或者beforeRouteEnter
- 原理闡述
回答范例
- beforeRouteEnter:在有vue-router的項目,每次進入路由的時候,都會執行beforeRouteEnter
- actived:在keep-alive緩存的組件被激活的時候,都會執行actived鉤子
Vue中組件和插件有什么區別
1. 組件是什么
組件就是把圖形、非圖形的各種邏輯均抽象為一個統一的概念(組件)來實現開發的模式,在Vue中每一個.vue文件都可以視為一個組件
組件的優勢
- 降低整個系統的耦合度,在保持接口不變的情況下,我們可以替換不同的組件快速完成需求,例如輸入框,可以替換為日歷、時間、范圍等組件作具體的實現
- 調試方便,由于整個系統是通過組件組合起來的,在出現問題的時候,可以用排除法直接移除組件,或者根據報錯的組件快速定位問題,之所以能夠快速定位,是因為每個組件之間低耦合,職責單一,所以邏輯會比分析整個系統要簡單
- 提高可維護性,由于每個組件的職責單一,并且組件在系統中是被復用的,所以對代碼進行優化可獲得系統的整體升級
2. 插件是什么
插件通常用來為 Vue 添加全局功能。插件的功能范圍沒有嚴格的限制——一般有下面幾種:
- 添加全局方法或者屬性。如: vue-custom-element
- 添加全局資源:指令/過濾器/過渡等。如 vue-touch
- 通過全局混入來添加一些組件選項。如vue-router
- 添加 Vue 實例方法,通過把它們添加到 Vue.prototype 上實現。
- 一個庫,提供自己的 API,同時提供上面提到的一個或多個功能。如vue-router
3. 兩者的區別
兩者的區別主要表現在以下幾個方面:
- 編寫形式
- 注冊形式
- 使用場景
3.1 編寫形式
編寫組件
編寫一個組件,可以有很多方式,我們最常見的就是vue單文件的這種格式,每一個.vue文件我們都可以看成是一個組件
vue文件標準格式
<template> </template> <script> export default{ ... } </script> <style> </style>我們還可以通過template屬性來編寫一個組件,如果組件內容多,我們可以在外部定義template組件內容,如果組件內容并不多,我們可直接寫在template屬性上
<template id="testComponent"> // 組件顯示的內容<div>component!</div> </template>Vue.component('componentA',{ template: '#testComponent' template: `<div>component</div>` // 組件內容少可以通過這種形式 })編寫插件
vue插件的實現應該暴露一個 install 方法。這個方法的第一個參數是 Vue 構造器,第二個參數是一個可選的選項對象
MyPlugin.install = function (Vue, options) {// 1. 添加全局方法或 propertyVue.myGlobalMethod = function () {// 邏輯...}// 2. 添加全局資源Vue.directive('my-directive', {bind (el, binding, vnode, oldVnode) {// 邏輯...}...})// 3. 注入組件選項Vue.mixin({created: function () {// 邏輯...}...})// 4. 添加實例方法Vue.prototype.$myMethod = function (methodOptions) {// 邏輯...} }3.2 注冊形式
組件注冊
vue組件注冊主要分為全局注冊與局部注冊
全局注冊通過Vue.component方法,第一個參數為組件的名稱,第二個參數為傳入的配置項
Vue.component('my-component-name', { /* ... */ })局部注冊只需在用到的地方通過components屬性注冊一個組件
const component1 = {...} // 定義一個組件export default {components:{component1 // 局部注冊} }插件注冊
插件的注冊通過Vue.use()的方式進行注冊(安裝),第一個參數為插件的名字,第二個參數是可選擇的配置項
Vue.use(插件名字,{ /* ... */} )注意的是:
注冊插件的時候,需要在調用 new Vue() 啟動應用之前完成
Vue.use會自動阻止多次注冊相同插件,只會注冊一次
4. 使用場景
- 組件 (Component) 是用來構成你的 App 的業務模塊,它的目標是 App.vue
- 插件 (Plugin) 是用來增強你的技術棧的功能模塊,它的目標是 Vue 本身
簡單來說,插件就是指對Vue的功能的增強或補充
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
回答范例
可能的追問
可以在同一個組件中使用兩個script標簽,一個使用vue3,一個使用vue2寫法,一起使用沒有問題
<!-- vue3 --> <script setup>// vue3寫法 </script><!-- 降級vue2 --> <script>export default {data() {},methods: {}} </script>為什么要使用異步組件
原理
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 }子組件可以直接改變父組件的數據么,說明原因
這是一個實踐知識點,組件化開發過程中有個單項數據流原則,不在子組件中修改父組件是個常識問題
思路
- 講講單項數據流原則,表明為何不能這么做
- 舉幾個常見場景的例子說說解決方案
- 結合實踐講講如果需要修改父組件狀態應該如何做
回答范例
這個 prop 用來傳遞一個初始值;這個子組件接下來希望將其作為一個本地的 prop 數據來使用。 在這種情況下,最好定義一個本地的 data,并將這個 prop 用作其初始值:
const props = defineProps(['initialCounter']) const counter = ref(props.initialCounter)這個 prop 以一種原始的值傳入且需要進行轉換。 在這種情況下,最好使用這個 prop 的值來定義一個計算屬性:
const props = defineProps(['size']) // prop變化,計算屬性自動更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())Vue組件之間通信方式有哪些
Vue 組件間通信是面試常考的知識點之一,這題有點類似于開放題,你回答出越多方法當然越加分,表明你對 Vue 掌握的越熟練。 Vue 組件間通信只要指以下 3 類通信 :父子組件通信、隔代組件通信、兄弟組件通信,下面我們分別介紹每種通信方式且會說明此種方法可適用于哪類組件間通信
組件傳參的各種方式
組件通信常用方式有以下幾種
- props / $emit 適用 父子組件通信
- 父組件向子組件傳遞數據是通過 prop 傳遞的,子組件傳遞數據給父組件是通過$emit 觸發事件來做到的
- ref 與 $parent / $children(vue3廢棄) 適用 父子組件通信
- ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件實例
- $parent / $children:訪問訪問父組件的屬性或方法 / 訪問子組件的屬性或方法
- EventBus ($emit / $on) 適用于 父子、隔代、兄弟組件通信
- 這種方法通過一個空的 Vue 實例作為中央事件總線(事件中心),用它來觸發事件和監聽事件,從而實現任何組件間的通信,包括父子、隔代、兄弟組件
- $attrs / $listeners(vue3廢棄) 適用于 隔代組件通信
- $attrs:包含了父作用域中不被 prop 所識別 (且獲取) 的特性綁定 ( class 和 style 除外 )。當一個組件沒有聲明任何 prop時,這里會包含所有父作用域的綁定 ( class 和 style 除外 ),并且可以通過 v-bind="$attrs" 傳入內部組件。通常配合 inheritAttrs 選項一起使用
- $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部組件
- provide / inject 適用于 隔代組件通信
- 祖先組件中通過 provider 來提供變量,然后在子孫組件中通過 inject 來注入變量。 provide / inject API 主要解決了跨級組件間的通信問題, 不過它的使用場景,主要是子組件獲取上級組件的狀態 ,跨級組件間建立了一種主動提供與依賴注入的關系
- $root 適用于 隔代組件通信 訪問根組件中的屬性或方法,是根組件,不是父組件。$root只對根組件有用
- Vuex 適用于 父子、隔代、兄弟組件通信
- Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。每一個 Vuex 應用的核心就是 store(倉庫)。“store” 基本上就是一個容器,它包含著你的應用中大部分的狀態 ( state )
- Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
- 改變 store 中的狀態的唯一途徑就是顯式地提交 (commit) mutation。這樣使得我們可以方便地跟蹤每一個狀態的變化。
根據組件之間關系討論組件通信最為清晰有效
- 父子組件:props/$emit/$parent/ref
- 兄弟組件:$parent/eventbus/vuex
- 跨層級關系:eventbus/vuex/provide+inject/$attrs + $listeners/$root
下面演示組件之間通訊三種情況: 父傳子、子傳父、兄弟組件之間的通訊
1. 父子組件通信
使用props,父組件可以使用props向子組件傳遞數據。
父組件vue模板father.vue:
<template><child :msg="message"></child> </template><script> import child from './child.vue'; export default {components: {child},data () {return {message: 'father message';}} } </script>子組件vue模板child.vue:
<template><div>{{msg}}</div> </template><script> export default {props: {msg: {type: String,required: true}} } </script>回調函數(callBack)
父傳子:將父組件里定義的method作為props傳入子組件
// 父組件Parent.vue: <Child :changeMsgFn="changeMessage"> methods: {changeMessage(){this.message = 'test'} } // 子組件Child.vue: <button @click="changeMsgFn"> props:['changeMsgFn']子組件向父組件通信
父組件向子組件傳遞事件方法,子組件通過$emit觸發事件,回調給父組件
父組件vue模板father.vue:
<template><child @msgFunc="func"></child> </template><script> import child from './child.vue'; export default {components: {child},methods: {func (msg) {console.log(msg);}} } </script>子組件vue模板child.vue:
<template><button @click="handleClick">點我</button> </template><script> export default {props: {msg: {type: String,required: true}},methods () {handleClick () {//........this.$emit('msgFunc');}} } </script>2. provide / inject 跨級訪問祖先組件的數據
父組件通過使用provide(){return{}}提供需要傳遞的數據
export default {data() {return {title: '我是父組件',name: 'poetry'}},methods: {say() {alert(1)}},// provide屬性 能夠為后面的后代組件/嵌套的組件提供所需要的變量和方法provide() {return {message: '我是祖先組件提供的數據',name: this.name, // 傳遞屬性say: this.say}} }子組件通過使用inject:[“參數1”,”參數2”,…]接收父組件傳遞的參數
<template><p>曾孫組件</p><p>{{message}}</p> </template> <script> export default {// inject 注入/接收祖先組件傳遞的所需要的數據即可 //接收到的數據 變量 跟data里面的變量一樣 可以直接綁定到頁面 {{}}inject: [ "message","say"],mounted() {this.say();}, }; </script>3. $parent + $children 獲取父組件實例和子組件實例的集合
- this.$parent 可以直接訪問該組件的父實例或組件
- 父組件也可以通過 this.$children 訪問它所有的子組件;需要注意 $children 并不保證順序,也不是響應式的
4. $attrs + $listeners多級組件通信
$attrs 包含了從父組件傳過來的所有props屬性
// 父組件Parent.vue: <Child :name="name" :age="age"/>// 子組件Child.vue: <GrandChild v-bind="$attrs" />// 孫子組件GrandChild <p>姓名:{{$attrs.name}}</p> <p>年齡:{{$attrs.age}}</p>$listeners包含了父組件監聽的所有事件
// 父組件Parent.vue: <Child :name="name" :age="age" @changeNameFn="changeName"/>// 子組件Child.vue: <button @click="$listeners.changeNameFn"></button>5. ref 父子組件通信
// 父組件Parent.vue: <Child ref="childComp"/> <button @click="changeName"></button> changeName(){console.log(this.$refs.childComp.age);this.$refs.childComp.changeAge() }// 子組件Child.vue: data(){return{age:20} }, methods(){changeAge(){this.age=15} }6. 非父子, 兄弟組件之間通信
vue2中廢棄了broadcast廣播和分發事件的方法。父子組件中可以用props和$emit()。如何實現非父子組件間的通信,可以通過實例一個vue實例Bus作為媒介,要相互通信的兄弟組件之中,都引入Bus,然后通過分別調用Bus事件觸發和監聽來實現通信和參數傳遞。Bus.js可以是這樣:
// Bus.js// 創建一個中央時間總線類 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } } // main.js Vue.prototype.$bus = new Bus() // 將$bus掛載到vue實例的原型上 // 另一種方式 Vue.prototype.$bus = new Vue() // Vue已經實現了Bus的功能 <template><button @click="toBus">子組件傳給兄弟組件</button> </template><script> export default{methods: {toBus () {this.$bus.$emit('foo', '來自兄弟組件')}} } </script>另一個組件也在鉤子函數中監聽on事件
export default {data() {return {message: ''}},mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})} }7. $root 訪問根組件中的屬性或方法
- 作用:訪問根組件中的屬性或方法
- 注意:是根組件,不是父組件。$root只對根組件有用
8. vuex
- 適用場景: 復雜關系的組件數據傳遞
- Vuex作用相當于一個用來存儲共享變量的容器
- state用來存放共享變量的地方
- getter,可以增加一個getter派生狀態,(相當于store中的計算屬性),用來獲得共享變量的值
- mutations用來存放修改state的方法。
- actions也是用來存放修改state的方法,不過action是在mutations的基礎上進行。常用來做一些異步操作
小結
- 父子關系的組件數據傳遞選擇 props 與 $emit進行傳遞,也可選擇ref
- 兄弟關系的組件數據傳遞可選擇$bus,其次可以選擇$parent進行傳遞
- 祖先與后代組件數據傳遞可選擇attrs與listeners或者 Provide與 Inject
- 復雜關系的組件數據傳遞可以通過vuex存放共享的變量
怎么監聽vuex數據的變化
分析
- vuex數據狀態是響應式的,所以狀態變視圖跟著變,但是有時還是需要知道數據狀態變了從而做一些事情。
- 既然狀態都是響應式的,那自然可以watch,另外vuex也提供了訂閱的API:store.subscribe()
回答范例
- 可以通過watch選項或者watch方法監聽狀態
- 可以使用vuex提供的API:store.subscribe()
實踐
watch方式
const app = createApp({watch: {'$store.state.counter'() {console.log('counter change!');}} })subscribe方式:
store.subscribe((mutation, state) => {if (mutation.type === 'add') {console.log('counter change in subscribe()!');} })vue3.2 自定義全局指令、局部指令
// 在src目錄下新建一個directive文件,在此文件夾下新建一個index.js文件夾,接著輸入如下內容 const directives = (app) => {//這里是給元素取得名字,雖然是focus,但是實際引用的時候必須以v開頭app.directive('focus',{//這里的el就是獲取的元素mounted(el) {el.focus() }}) }//默認導出 directives export default directives // 在全局注冊directive import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' import directives from './directives'const app = createApp(App) directives(app)app.use(store).use(router).mount('#app') <!-- 在你需要的頁面進行自定義指令的使用 --> <template><div class="container"><div class="content"><input type="text" v-focus>內容</div></div> </template><script setup> import { reactive, ref } from 'vue' // const vMove:Directive = () =>{// } </script>在vue3.2 setup語法糖模式下,自定義指令變得及其簡單
<input type="text" v-model="value" v-focus><script setup> //直接寫,但是必須是v開頭 const vFocus = {mounted(el) {// 獲取input,并調用其focus()方法el.focus()} } </script> <!-- demo 進去頁面自動獲取焦點,然后讓盒子的顏色根據你input框輸入的內容變色,并且作防抖處理 --><template><div class="container"><div class="content" v-move="{ background: value }">內容<input type="text" v-model="value" v-focus @keyup="see"></div></div> </template><script setup> import { reactive, ref } from 'vue' const value = ref('')const vFocus = {mounted(el) {// 獲取input,并調用其focus()方法el.focus()} }let timer = nullconst vMove = (el, binding) => {if (timer !== null) {clearTimeout(timer)}timer = setTimeout(() => {el.style.background = binding.value.backgroundconsole.log(el);}, 1000); }</script><style lang="scss" scoped> .container {width: 100%;height: 100%;display: flex;justify-content: center;align-items: center;.content {border-top: 5px solid black;width: 200px;height: 200px;cursor: pointer;border-left: 1px solid #ccc;border-right: 1px solid #ccc;border-bottom: 1px solid #ccc;} } </style>Vue computed 實現
- 建立與其他屬性(如:data、 Store)的聯系;
- 屬性改變后,通知計算屬性重新計算
實現時,主要如下
- 初始化 data, 使用 Object.defineProperty 把這些屬性全部轉為 getter/setter。
- 初始化 computed, 遍歷 computed 里的每個屬性,每個 computed 屬性都是一個 watch 實例。每個屬性提供的函數作為屬性的 getter,使用 Object.defineProperty 轉化。
- Object.defineProperty getter 依賴收集。用于依賴發生變化時,觸發屬性重新計算。
- 若出現當前 computed 計算屬性嵌套其他 computed 計算屬性時,先進行其他的依賴收集
Vue中diff算法原理
DOM操作是非常昂貴的,因此我們需要盡量地減少DOM操作。這就需要找出本次DOM必須更新的節點來更新,其他的不更新,這個找出的過程,就需要應用diff算法
vue的diff算法是平級比較,不考慮跨級比較的情況。內部采用深度遞歸的方式+雙指針(頭尾都加指針)的方式進行比較。
簡單來說,Diff算法有以下過程
- 同級比較,再比較子節點(根據key和tag標簽名判斷)
- 先判斷一方有子節點和一方沒有子節點的情況(如果新的children沒有子節點,將舊的子節點移除)
- 比較都有子節點的情況(核心diff)
- 遞歸比較子節點
- 正常Diff兩個樹的時間復雜度是O(n^3),但實際情況下我們很少會進行跨層級的移動DOM,所以Vue將Diff進行了優化,從O(n^3) -> O(n),只有當新舊children都為多個子節點時才需要用核心的Diff算法進行同層級比較。
- Vue2的核心Diff算法采用了雙端比較的算法,同時從新舊children的兩端開始進行比較,借助key值找到可復用的節點,再進行相關操作。相比React的Diff算法,同樣情況下可以減少移動節點次數,減少不必要的性能損耗,更加的優雅
- 在創建VNode時就確定其類型,以及在mount/patch的過程中采用位運算來判斷一個VNode的類型,在這個基礎之上再配合核心的Diff算法,使得性能上較Vue2.x有了提升
vue3中采用最長遞增子序列來實現diff優化
回答范例
思路
- diff算法是干什么的
- 它的必要性
- 它何時執行
- 具體執行方式
- 拔高:說一下vue3中的優化
回答范例
- 首先判斷兩個節點是否為相同同類節點,不同則刪除重新創建
- 如果雙方都是文本則更新文本內容
- 如果雙方都是元素節點則遞歸更新子元素,同時更新元素屬性
- 更新子節點時又分了幾種情況
- 新的子節點是文本,老的子節點是數組則清空,并設置文本;
- 新的子節點是文本,老的子節點是文本則直接更新文本;
- 新的子節點是數組,老的子節點是文本則清空文本,并創建新子節點數組中的子元素;
- 新的子節點是數組,老的子節點也是數組,那么比較兩組子節點,更新細節blabla
- vue3中引入的更新策略:靜態節點標記等
vdom中diff算法的簡易實現
以下代碼只是幫助大家理解diff算法的原理和流程
動態給vue的data添加一個新的屬性時會發生什么?怎樣解決?
Vue 不允許在已經創建的實例上動態添加新的響應式屬性
若想實現數據與視圖同步更新,可采取下面三種解決方案:
- Vue.set()
- Object.assign()
- $forcecUpdated()
Vue.set()
Vue.set( target, propertyName/index, value )參數
- {Object | Array} target
- {string | number} propertyName/index
- {any} value
返回值:設置的值
通過Vue.set向響應式對象中添加一個property,并確保這個新 property同樣是響應式的,且觸發視圖更新
關于Vue.set源碼(省略了很多與本節不相關的代碼)
源碼位置:src\core\observer\index.js
function set (target: Array<any> | Object, key: any, val: any): any {...defineReactive(ob.value, key, val)ob.dep.notify()return val }這里無非再次調用defineReactive方法,實現新增屬性的響應式
關于defineReactive方法,內部還是通過Object.defineProperty實現屬性攔截
大致代碼如下:
function defineReactive(obj, key, val) {Object.defineProperty(obj, key, {get() {console.log(`get ${key}:${val}`);return val},set(newVal) {if (newVal !== val) {console.log(`set ${key}:${newVal}`);val = newVal}}}) }Object.assign()
直接使用Object.assign()添加到對象的新屬性不會觸發更新
應創建一個新的對象,合并原對象和混入對象的屬性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})$forceUpdate
如果你發現你自己需要在 Vue中做一次強制更新,99.9% 的情況,是你在某個地方做錯了事
$forceUpdate迫使Vue 實例重新渲染
PS:僅僅影響實例本身和插入插槽內容的子組件,而不是所有子組件。
小結
- 如果為對象添加少量的新屬性,可以直接采用Vue.set()
- 如果需要為新對象添加大量的新屬性,則通過Object.assign()創建新對象
- 如果你實在不知道怎么操作時,可采取$forceUpdate()進行強制刷新 (不建議)
PS:vue3是用過proxy實現數據響應式的,直接動態添加新屬性仍可以實現數據響應式
v-if和v-show區別
- v-show隱藏則是為該元素添加css--display:none,dom元素依舊還在。v-if顯示隱藏是將dom元素整個添加或刪除
- 編譯過程:v-if切換有一個局部編譯/卸載的過程,切換過程中合適地銷毀和重建內部的事件監聽和子組件;v-show只是簡單的基于css切換
- 編譯條件:v-if是真正的條件渲染,它會確保在切換過程中條件塊內的事件監聽器和子組件適當地被銷毀和重建。只有渲染條件為假時,并不做操作,直到為真才渲染
- v-show 由false變為true的時候不會觸發組件的生命周期
- v-if由false變為true的時候,觸發組件的beforeCreate、create、beforeMount、mounted鉤子,由true變為false的時候觸發組件的beforeDestory、destoryed方法
- 性能消耗:v-if有更高的切換消耗;v-show有更高的初始渲染消耗
v-show與v-if的使用場景
- v-if 與 v-show 都能控制dom元素在頁面的顯示
- v-if 相比 v-show 開銷更大的(直接操作dom節點增加與刪除)
- 如果需要非常頻繁地切換,則使用 v-show 較好
- 如果在運行時條件很少改變,則使用 v-if 較好
v-show與v-if原理分析
不管初始條件是什么,元素總是會被渲染
我們看一下在vue中是如何實現的
代碼很好理解,有transition就執行transition,沒有就直接設置display屬性
// 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.displayif (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)} }v-if在實現上比v-show要復雜的多,因為還有else else-if 等條件需要處理,這里我們也只摘抄源碼中處理 v-if 的一小部分
返回一個node節點,render函數通過表達式的值來決定是否生成DOM
// 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)}}})} )了解history有哪些方法嗎?說下它們的區別
history 這個對象在html5的時候新加入兩個api history.pushState() 和 history.repalceState() 這兩個API可以在不進行刷新的情況下,操作瀏覽器的歷史紀錄。唯一不同的是,前者是新增一個歷史記錄,后者是直接替換當前的歷史記錄。
從參數上來說:
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事件所監聽
- 而后面三者可以,且用戶點擊瀏覽器前進后退鍵時也可以
從0到1自己構架一個vue項目,說說有哪些步驟、哪些重要插件、目錄結構你會怎么組織
綜合實踐類題目,考查實戰能力。沒有什么絕對的正確答案,把平時工作的重點有條理的描述一下即可
思路
- 構建項目,創建項目基本結構
- 引入必要的插件:
- 代碼規范:prettier,eslint
- 提交規范:husky,lint-staged`
- 其他常用:svg-loader,vueuse,nprogress
- 常見目錄結構
回答范例
- plugins:用來放 vite 插件的 plugin 配置
- public:用來放一些諸如 頁頭icon 之類的公共文件,會被打包到dist根目錄下
- src:用來放項目代碼文件
- api:用來放http的一些接口配置
- assets:用來放一些 CSS 之類的靜態資源
- components:用來放項目通用組件
- layout:用來放項目的布局
- router:用來放項目的路由配置
- store:用來放狀態管理Pinia的配置
- utils:用來放項目中的工具方法類
- views:用來放項目的頁面文件
總結
以上是生活随笔為你收集整理的2023前端二面高频vue面试题集锦的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: meshlab比较模型误差
- 下一篇: Android毕业项目计算机毕业论文及毕