這篇文章為大家總結分享20 個Vue經典面試題(附源碼級詳解),帶你整理基礎知識,增強Vue知識儲備,值得收藏,快來看看吧!
vue是元件化開發框架,所以對於vue應用程式來說元件間的資料通訊非常重要。 此題主要考查大家vue基本功,對於vue基礎api運用熟練度。 另外一些邊界知識如provide/inject/$attrs則提現了面試者的知識廣度。
元件傳參的各種方式
想法分析:
#總述知道的所有方式
依元件關係闡述使用場景
回答範例:
1、元件通訊常用方式有以下8種:
##ref
$root
eventbus
vuex
https://v3-migration.vuejs.org/breaking-changes/listeners-removed.html
2、根據元件之間關係討論元件通訊最為清晰有效
父子元件
$emit
/ref
/$attrs
/vuex
provide inject
02-v-if和v-for哪個優先權比較高?
分析:此題考查常識,文件中曾有詳細說明v2
|想法分析:#先給結論
哪些場景可能導致我們這樣做,該怎麼處理總結,拔高
#實務上不應該把v- for和v-if放一起
v-for的優先權是高於v-if
,把它們放在一起,輸出的渲染函數中可以看出會先執行循環再判斷條件,哪怕我們只渲染列表中一小部分元素,也得在每次重渲染的時候遍歷整個列表,這會比較浪費;另外需要注意的是在###vue3中則完全相反,v-if的優先權高於v-for###,所以v-if執行時,它調用的變數還不存在,就會導致異常############通常有兩種情況導致我們這樣做:############為了###過濾清單中的項目### (例如###v-for="user in users" v-if="user.isActive"###)。此時定義一個計算屬性 (例如 ###activeUsers###),讓其傳回過濾後的清單即可(例如###users.filter(u=>u.isActive)###)。 ############為了###避免渲染本來應該被隱藏的清單### (例如###v-for="user in users" v-if="shouldShowUsers"## #)。此時把 ###v-if### 移到容器元素上 (例如 ###ul###、###ol###)或外麵包層一層###template###即可。 ##################文件中明確指出###永遠不要把###v-if### 和###v-for### 同時用在同一個元素上###,顯然這是一個重要的注意事項。 ############原始碼裡面關於程式碼產生的部分,能夠清晰的看到是先處理v-if還是v-for,順序上vue2和vue3正好相反,因此產生了一些症狀的不同,但是不管怎樣都是不能把它們寫在一起的。 ###知其所以然:
#做測試,test.html兩者同級時,渲染函數如下:
ƒ anonymous( ) { with(this){return _c('div',{attrs:{"id":"app"}},_l((items),function(item){return (item.isActive)?_c('div',{key:item.id},[_v("\n "+_s(item.name)+"\n ")]):_e()}),0)} }
做個測試,test-v3.html
#源碼中找答案
v2: https://github1s.com/vuejs/vue/blob/HEAD/src/compiler/codegen/index.js#L65-L66
v3:https://github1s.com/vuejs/core/blob /HEAD/packages/compiler-core/src/codegen.ts#L586-L587
必問題目,考查vue基礎知識。
想法
#給出概念
列舉生命週期各階段
闡述整體流程
結合實踐
回答範例
#1、每個Vue元件實例被建立後都會經過一系列初始化步驟,例如,它需要資料觀測,模板編譯,掛載實例到dom上,以及資料變化時更新dom。這個過程中會運行叫做生命週期鉤子的函數,以便使用者在特定階段有機會添加自己的程式碼。 2、Vue生命週期總共可以分為8個階段:建立前後, 載入前後, 更新前後, 銷毀前後,以及一些特殊場景的生命週期。 vue3中新增了三個用於偵錯和服務端渲染場景。
生命週期v3 | 描述 | |
---|---|---|
beforeCreate | 元件實例被建立之初 | |
created | #元件實例已經完全創建 | |
beforeMount | 元件掛載之前 | |
#mounted | 元件掛載到實例上去之後 | |
beforeUpdate | 元件資料發生變化,更新之前 | |
updated | ##beforeDestroy | |
3、Vue
生命周期流程图:
4、结合实践:
beforeCreate:通常用于插件开发中执行一些初始化任务
created:组件初始化完毕,可以访问各种数据,获取接口数据等
mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。
beforeUpdate:此时view
层还未更新,可用于获取更新前各种状态
updated:完成view
层的更新,更新后,所有状态已是最新
beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
可能的追问
setup和created谁先执行?
setup中为什么没有beforeCreate和created?
知其所以然
vue3中生命周期的派发时刻:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L554-L555
vue2中声明周期的派发时刻:
https://github1s.com/vuejs/vue/blob/HEAD/src/core/instance/init.js#L55-L56
题目分析:
双向绑定是vue
的特色之一,开发中必然会用到的知识点,然而此题还问了实现原理,升级为深度考查。
思路分析:
给出双绑定义
双绑带来的好处
在哪使用双绑
使用方式、使用细节、vue3变化
原理实现描述
回答范例:
vue中双向绑定是一个指令v-model
,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。
v-model
是语法糖,默认情况下相当于:value
和@input
。使用v-model
可以减少大量繁琐的事件处理代码,提高开发效率。
通常在表单项上使用v-model
,还可以在自定义组件上使用,表示某个值的输入和输出控制。
通过<input v-model="xxx">
的方式将xxx的值绑定到表单元素value上;对于checkbox,可以使用true-value
和false-value指定特殊的值,对于radio可以使用value指定特殊的值;对于select可以通过options元素的value设置特殊的值;还可以结合.lazy,.number,.trim对v-mode的行为做进一步限定;v-model
用在自定义组件上时又会有很大不同,vue3中它类似于sync
修饰符,最终展开的结果是modelValue属性和update:modelValue事件;vue3中我们甚至可以用参数形式指定多个不同的绑定,例如v-model:foo和v-model:bar,非常强大!
v-model
是一个指令,它的神奇魔法实际上是vue的编译器完成的。我做过测试,包含v-model
的模板,转换为渲染函数之后,实际上还是是value属性的绑定以及input事件监听,事件回调函数中会做相应变量更新操作。编译器根据表单元素的不同会展开不同的DOM属性和事件对,比如text类型的input和textarea会展开为value和input事件;checkbox和radio类型的input会展开为checked和change事件;select用value作为属性,用change作为事件。
可能的追问:
v-model
和sync
修饰符有什么区别
自定义组件使用v-model
如果想要改变事件名或者属性名应该怎么做
知其所以然:
测试代码,test.html
观察输出的渲染函数:
// <input type="text" v-model="foo"> _c('input', { directives: [{ name: "model", rawName: "v-model", value: (foo), expression: "foo" }], attrs: { "type": "text" }, domProps: { "value": (foo) }, on: { "input": function ($event) { if ($event.target.composing) return; foo = $event.target.value } } })
// <input type="checkbox" v-model="bar"> _c('input', { directives: [{ name: "model", rawName: "v-model", value: (bar), expression: "bar" }], attrs: { "type": "checkbox" }, domProps: { "checked": Array.isArray(bar) ? _i(bar, null) > -1 : (bar) }, on: { "change": function ($event) { var $$a = bar, $$el = $event.target, $$c = $$el.checked ? (true) : (false); if (Array.isArray($$a)) { var $$v = null, $$i = _i($$a, $$v); if ($$el.checked) { $$i < 0 && (bar = $$a.concat([$$v])) } else { $$i > -1 && (bar = $$a.slice(0, $$i).concat($$a.slice($$i + 1))) } } else { bar = $$c } } } })
// <select v-model="baz"> // <option value="vue">vue</option> // <option value="react">react</option> // </select> _c('select', { directives: [{ name: "model", rawName: "v-model", value: (baz), expression: "baz" }], on: { "change": function ($event) { var $$selectedVal = Array.prototype.filter.call( $event.target.options, function (o) { return o.selected } ).map( function (o) { var val = "_value" in o ? o._value : o.value; return val } ); baz = $event.target.multiple ? $$selectedVal : $$selectedVal[0] } } }, [ _c('option', { attrs: { "value": "vue" } }, [_v("vue")]), _v(" "), _c('option', { attrs: { "value": "react" } }, [_v("react")]) ])
此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同。
答题思路:
按照逻辑扩展和内容扩展来列举,
逻辑扩展有:mixins、extends、composition api;
内容扩展有slots;
分别说出他们使用方法、场景差异和问题。
作为扩展,还可以说说vue3中新引入的composition api带来的变化
回答范例:
常见的组件扩展方法有:mixins,slots,extends等
混入mixins是分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
// 复用代码:它是一个配置对象,选项和组件里面一样 const mymixin = { methods: { dosomething(){} } } // 全局混入:将混入对象传入 Vue.mixin(mymixin) // 局部混入:做数组项设置到mixins选项,仅作用于当前组件 const Comp = { mixins: [mymixin] }
插槽主要用于vue组件中的内容分发,也可以用于组件扩展。
子组件Child
<div> <slot>这个内容会被父组件传递的内容替换</slot> </div>
父组件Parent
<div> <Child>来自老爹的内容</Child> </div>
如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。
组件选项中还有一个不太常用的选项extends,也可以起到扩展组件的目的
// 扩展对象 const myextends = { methods: { dosomething(){} } } // 组件扩展:做数组项设置到extends选项,仅作用于当前组件 // 跟混入的不同是它只能扩展单个对象 // 另外如果和混入发生冲突,该选项优先级较高,优先起作用 const Comp = { extends: myextends }
混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。例如:
// 复用逻辑1 function useXX() {} // 复用逻辑2 function useYY() {} // 逻辑组合 const Comp = { setup() { const {xx} = useXX() const {yy} = useYY() return {xx, yy} } }
可能的追问
Vue.extend方法你用过吗?它能用来做组件扩展吗?
知其所以然
mixins原理:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L232-L233
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L545
slots原理:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentSlots.ts#L129-L130
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1373-L1374
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/renderSlot.ts#L23-L24
分析
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题。
参考文档:https://staging.vuejs.org/guide/components/props.html#one-way-data-flow
思路
讲讲单项数据流原则,表明为何不能这么做
举几个常见场景的例子说说解决方案
结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
所有的 prop 都使得其父子之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器控制台中发出警告。
const props = defineProps(['foo']) // ❌ 下面行为会被警告, props是只读的! props.foo = 'bar'
实际开发过程中有两个场景会想要修改一个属性:
**这个 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())
实践中如果确实想要改变父组件属性应该emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性。
分析
综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。
思路
权限管理需求分析:页面和按钮权限
权限管理的实现方案:分后端方案和前端方案阐述
说说各自的优缺点
回答范例
权限管理一般需求是页面权限和按钮权限的管理
具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes
数组,需要认证的页面在其路由的meta
中添加一个roles
字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)
方式动态添加路由即可。
后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes
动态添加路由信息
按钮权限的控制通常会实现一个指令,例如v-permission
,将按钮要求角色通过值传给v-permission指令,在指令的moutned
钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
知其所以然
路由守卫
https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/permission.js#L13-L14
路由生成
https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/store/modules/permission.js#L50-L51
动态追加路由
https://github1s.com/PanJiaChen/vue-element-admin/blob/HEAD/src/permission.js#L43-L44
可能的追问
类似Tabs
这类组件能不能使用v-permission
指令实现按钮权限控制?
<el-tabs> <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane> <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane> </el-tabs>
服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表 const map = { //xx: require('@/views/xx.vue').default // 同步的⽅式 xx: () => import('@/views/xx.vue') // 异步的⽅式 } // 服务端返回的asyncRoutes const asyncRoutes = [ { path: '/xx', component: 'xx',... } ] // 遍历asyncRoutes,将component替换为map[component] function mapComponent(asyncRoutes) { asyncRoutes.forEach(route => { route.component = map[route.component]; if(route.children) { route.children.map(child => mapComponent(child)) } }) } mapComponent(asyncRoutes)
分析
这是一道必问题目,但能回答到位的比较少。如果只是看看一些网文,通常没什么底气,经不住面试官推敲,但像我们这样即看过源码还造过轮子的,回答这个问题就会比较有底气啦。
答题思路:
啥是响应式?
为什么vue需要响应式?
它能给我们带来什么好处?
vue的响应式是怎么实现的?有哪些优缺点?
vue3中的响应式的新变化
回答范例:
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
以vue為例說明,透過數據響應式加上虛擬DOM和patch演算法,開發人員只需要操作數據,關心業務,完全不用接觸繁瑣的DOM操作,從而大大提升開發效率,降低開發難度。
vue2中的資料回應式會根據資料型別來做不同處理,如果是物件則採用Object.defineProperty()的方式定義資料攔截,當當資料被存取或變更時,我們感知並作出回應;如果是陣列則透過覆寫陣列物件原型的7個變更方法,使這些方法可以額外的做更新通知,從而作出回應。這種機制很好的解決了資料響應化的問題,但在實際使用上也存在一些缺點:例如初始化時的遞歸遍歷會造成效能損失;新增或刪除屬性時需要使用者使用Vue.set/delete這樣特殊的api才能生效;對於es6中新產生的Map、Set這些資料結構不支援等問題。
為了解決這些問題,vue3重新編寫了這一部分的實作:利用ES6的Proxy代理要回應化的數據,它有很多好處,程式設計體驗是一致的,不需要使用特殊api,初始化效能和記憶體消耗都得到了大幅改善;另外由於響應化的實現程式碼抽取為獨立的reactivity包,使得我們可以更靈活的使用它,第三方的擴展開發起來更加靈活了。
知其所以然
#vue2響應式:
https://github1s.com/vuejs/vue/blob/HEAD/src/core/observer/index.js#L135-L136
vue3響應式:
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L89-L90
# https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/ref.ts#L67-L68
分析
現有框架幾乎都引入了虛擬DOM 來對真實DOM 進行抽象,也就是現在大家所熟知的VNode 和VDOM,那為什麼需要引進虛擬DOM 呢?圍繞這個疑問來解答即可!
想法
vdom是什麼
引入vdom的好處
vdom如何生成,如何成為dom
#在後續的diff中的作用
回答範例
虛擬dom顧名思義就是虛擬的dom對象,它本身就是一個JavaScript
對象,只不過它是透過不同的屬性去描述一個視圖結構。
透過引入vdom我們可以得到以下好處:
#將真實元素節點抽象化為VNode,有效減少直接操作dom 次數,從而提高程式效能
方便實現跨平台
vdom如何產生?在vue中我們常常會為元件寫模板 - template, 這個模板會被編譯器 - compiler編譯為渲染函數,在接下來的掛載(mount)過程中會呼叫render函數,傳回的物件就是虛擬dom。但它們還不是真正的dom,所以會在後續的patch過程中進一步轉化為dom。
掛載程序結束後,vue程式進入更新流程。如果某些響應式資料發生變化,將會引起元件重新render,此時就會產生新的vdom,和上一次的渲染結果diff就能得到變化的地方,從而轉換為最小量的dom操作,高效更新視圖。
知其所以然
#vnode定義:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L127-L128
#觀察渲染函數:21-vdom/test- render-v3.html
建立vnode:
https://github1s.com/vuejs/core/blob/HEAD/ packages/runtime-core/src/vnode.ts#L291-L292
https://github1s.com/vuejs/core/blob/HEAD /packages/runtime-core/src/vnode.ts#L486-L487
https://github1s.com/vuejs/core/ blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L283-L284
mount:
https://github1s.com/vuejs/core/blob/ HEAD/packages/runtime-core/src/renderer.ts#L1171-L1172
調試mount過程:mountComponent
21-vdom/test-render-v3.html
分析
必問題目,涉及vue更新原理,比較考查理解深度。
想法
#diff演算法是做什麼的
它的必要性
它何時執行
具體執行方式
拔高:說一下vue3中的最佳化
回答範例
1.Vue中的diff演算法稱為patching演算法,它是由Snabbdom修改而來,虛擬DOM要轉換成真實DOM就需要透過patch方法轉換。
2、最初Vue1.x視圖中每個依賴都有更新函數對應,可以做到精確更新,因此並不需要虛擬DOM和patching演算法支持,但是這樣粒度過細導致Vue1.x無法承載較大應用;Vue 2.x中為了降低Watcher粒度,每個組件只有一個Watcher與之對應,此時就需要引入patching演算法才能精確找到發生變化的地方並高效更新。
3、vue中diff執行的時刻是元件內部響應式資料變更觸發實例執行其更新函數時,更新函數會再次執行render函數以獲得最新的虛擬DOM,然後執行patch函數,並傳入新舊兩次虛擬DOM,透過比對兩者找到變化的地方,最後將其轉換為對應的DOM操作。
4、patch過程是一個遞歸過程,遵循深度優先、同層比較的策略;以vue3的patch為例:
5、vue3中引入的更新策略:編譯期優化patchFlags、block等
知其所以然
##patch關鍵程式碼##https: //github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355
調試test-v3.html
分析官網列舉的最值得注意的新特性:https ://v3-migration.vuejs.org/
也就是下面這些:
Composition API回答範例1、api層面Vue3新特性主要包括:Composition API、SFC Composition API語法糖、Teleport傳送閘、Fragments 片段、Emits選項、自訂渲染器、SFC CSS變數、Suspense
#2、另外,Vue3.0在框架層級也有很多亮眼的改進:
#更快知其所以然體驗編譯器最佳化
https://sfc.vuejs.org/
#reactive實作
https://github1s .com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L90-L91
分析
API題目,考查基礎能力,不容有失,盡可能說的詳細。
想法
#什麼是動態路由
什麼時候使用動態路由,怎麼定義動態路由
參數如何取得
#細節、注意事項
回答範例
很多時候,我們需要將給定匹配模式的路由對應到同一個元件,這種情況就需要定義動態路由。
例如,我們可能有一個 User
元件,它應該對所有使用者進行渲染,但使用者 ID 不同。在Vue Router 中,我們可以在路徑中使用一個動態欄位來實現,例如:{ path: '/users/:id', component: User }
#,其中:id
就是路徑參數
路徑參數 用冒號:
表示。當一個路由被匹配時,它的 params 的值將在每個元件中以 this.$route.params
的形式暴露出來。
參數還可以有多個,例如/users/:username/posts/:postId
;除了$route.params
之外,$route
物件也公開了其他有用的信息,如$route.query
、$route.hash
等。
如何回應動態路由參數的變化
思路分析:首先思考vue路由要解決的問題:使用者點擊跳轉連結內容切換,頁面不刷新。
借助hash或history api實作url跳轉頁面不刷新回答範例:一個SPA應用的路由需要解決的問題是
頁面跳轉內容改變同時不刷新,同時路由還需要以插件形式存在,所以:
函數,傳迴路由器實例,實例內部做幾件事:
知其所以然:
createRouter如何建立實例RouterView
#頁面跳轉RouterLink分析:這是特別常見的問題,主要考查大家對虛擬DOM和patch細節的掌握程度,能夠反映面試者理解層次。
思路分析:
key的作用主要是为了更高效的更新虚拟DOM。
vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。
知其所以然
测试代码,test-v3.html
上面案例重现的是以下过程
不使用key
如果使用key
// 首次循环patch A A B C D E A B F C D E // 第2次循环patch B B C D E B F C D E // 第3次循环patch E C D E F C D E // 第4次循环patch D C D F C D // 第5次循环patch C C F C // oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面
源码中找答案:
判断是否为相同节点
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L342-L343
更新时的处理
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1752-L1753
不使用key
如果使用key
// 首次循环patch A A B C D E A B F C D E // 第2次循环patch B B C D E B F C D E // 第3次循环patch E C D E F C D E // 第4次循环patch D C D F C D // 第5次循环patch C C F C // oldCh全部处理结束,newCh中剩下的F,创建F并插入到C前面
源码中找答案:
判断是否为相同节点
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/vnode.ts#L342-L343
更新时的处理
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1752-L1753
分析
这道题及考察使用,有考察原理,nextTick在开发过程中应用的也较少,原理上和vue异步更新有密切关系,对于面试者考查很有区分度,如果能够很好回答此题,对面试效果有极大帮助。
答题思路
nextTick是做什么的?
为什么需要它呢?
开发时何时使用它?抓抓头,想想你在平时开发中使用它的地方
下面介绍一下如何使用nextTick
原理解读,结合异步更新和nextTick生效方式,会显得你格外优秀
回答范例:
1、nextTick是等待下一次 DOM 更新刷新的工具方法。
2、Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。
3、开发时,有两个场景我们会用到nextTick:
created中想要获取DOM时;
响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度。
4、nextTick签名如下:function nextTick(callback?: () => void): Promise<void></void>
所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事。
5、在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。
知其所以然:
源码解读:
组件更新函数入队:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1547-L1548
入队函数:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/scheduler.ts#L84-L85
nextTick定义:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/scheduler.ts#L58-L59
测试案例,test-v3.html
两个重要API,反应应聘者熟练程度。
computed特点:具有响应式的返回值
const count = ref(1) const plusOne = computed(() => count.value + 1)
watch特点:侦测变化,执行回调
const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )
回答范例
计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。
计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deep、immediate等选项。
vue3中watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watch、watchEffect可以完全替代目前的watch选项,且功能更加强大。
可能追问
watch会不会立即执行?
watch 和 watchEffect有什么差异
知其所以然
computed的实现
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L79-L80
ComputedRefImpl
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L26-L27
缓存性
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L59-L60
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/computed.ts#L45-L46
watch的实现
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiWatch.ts#L158-L159
这题考查大家对创建过程的理解程度。
思路分析
给结论
阐述理由
回答范例
创建过程自上而下,挂载过程自下而上;即:
之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。
知其所以然
观察beforeCreated和created钩子的处理
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L554-L555
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/componentOptions.ts#L741-L742
观察beforeMount和mounted钩子的处理
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1310-L1311
测试代码,test-v3.html
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多。
思路
缓存用keep-alive,它的作用与用法
使用细节,例如缓存指定/排除、结合router和transition
组件缓存后更新可以利用activated或者beforeRouteEnter
原理阐述
回答范例
开发中缓存组件使用keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
<keep-alive> <component :is="view"></component> </keep-alive>
结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
:
<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>
缓存后如果要获取数据,解决方案可以有以下两种:
beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) },
actived:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子
activated(){ this.getData() // 获取数据 },
keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。
知其所以然
KeepAlive定义
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L73-L74
缓存定义
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L102-L103
缓存组件
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L215-L216
获取缓存组件
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/components/KeepAlive.ts#L241-L242
测试缓存特性,test-v3.html
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可。
思路
构建项目,创建项目基本结构
引入必要的插件:
代码规范:prettier,eslint
提交规范:husky,lint-staged
其他常用:svg-loader,vueuse,nprogress
常见目录结构
回答范例
从0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件
目前vue3项目我会用vite或者create-vue创建项目
接下来引入必要插件:路由插件vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios
其他比较常用的库有vueuse,nprogress,图标可以使用vite-svg-loader
下面是代码规范:结合prettier和eslint即可
最后是提交规范,可以使用husky,lint-staged,commitlint
目录结构我有如下习惯:.vscode
:用来放项目中的 vscode 配置
plugins
:用来放 vite 插件的 plugin 配置
public
:用来放一些诸如 页头icon 之类的公共文件,会被打包到dist根目录下
src
:用来放项目代码文件
api
:用来放http的一些接口配置
assets
:用来放一些 CSS 之类的静态资源
components
:用来放项目通用组件
layout
:用来放项目的布局
router
:用来放项目的路由配置
store
:用来放状态管理Pinia的配置
utils
:用来放项目中的工具方法类
views
:用来放项目的页面文件
看到这样的题目,可以用以下图片来回答:
思路
查看vue官方文档:
风格指南:https://vuejs.org/style-guide/
性能:https://vuejs.org/guide/best-practices/performance.html#overview
安全:https://vuejs.org/guide/best-practices/security.html
访问性:https://vuejs.org/guide/best-practices/accessibility.html
发布:https://vuejs.org/guide/best-practices/production-deployment.html
回答范例
我从编码风格、性能、安全等方面说几条:
编码风格方面:
性能方面:
安全:
template: <div> + userProvidedString + </div>
想法
範例
狀態管理模式庫。它採用集中式存儲,管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式變化。
多個元件共享狀態時,例如:多個視圖依賴相同狀態或來自不同視圖的行為需要變更相同狀態。此時單向資料流的簡潔性很容易被破壞。因此,我們有必要把元件的共享狀態抽取出來,以一個全域單例模式管理。透過定義和隔離狀態管理中的各種概念並透過強制規則維持視圖和狀態間的獨立性,我們的程式碼將會變得更結構化且易於維護。這是vuex存在的必要性,它和react生態中的redux之類是一個概念。
可能的追問
分析
問我們template到render流程,其實是問vue編譯器運作原理。
思路
回答範例
知其所以然
vue3編譯過程窺探:
Vue中編譯器何時執行?
react有沒有編譯器?分析
掛載過程完成了最重要的兩件事:############初始化######## #####建立更新機制############把這兩件事說清楚就好! ############回答範例##########掛載過程指的是app.mount()過程,這個過程中整體上做了兩件事:初始化和建立更新機制
初始化會建立元件實例、初始化元件狀態,建立各種響應式資料
建立更新機制這一步驟會立即執行一次元件更新函數,這會首次執行元件渲染函數並執行patch將前面獲得vnode轉換為dom;同時首次執行渲染函數會創建它內部響應式資料之間和元件更新函數之間的依賴關係,這使得以後資料變化時會執行對應的更新函數。
知其所以然
#測試程式碼,test-v3.html mount函數定義
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiCreateApp.ts#L277-L278
#首次render過程
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L2303-L2304
可能的追問
#響應式資料怎麼建立
依賴關係如何建立
原文網址:https://juejin.cn/post/7097067108663558151
作者:楊村長
#(學習影片分享:vue影片教學)