v-if和v-for哪个优先级更高?
分析:
此题考查常识,文档中曾有详细说明v2|v3;也是一个很好的实践题目,项目中经常会遇到,能够看出面试者应用能力。
思路分析:总分总模式
- 先给出结论
- 为什么是这样的
- 它们能放一起吗
- 如果不能,那应该怎样
- 总结
回答范例:
-
在
Vue 2
中,v-for
优先于v-if
被解析;但在Vue 3
中,则完全相反,v-if
的优先级高于v-for
。 -
我曾经做过实验,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件
-
实践中也不应该把它们放一起,因为哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表。
-
通常有两种情况下导致我们这样做:
-
为了过滤列表中的项目 (比如
v-for="user in users" v-if="user.isActive"
)。此时定义一个计算属性 (比如activeUsers
),让其返回过滤后的列表即可。 -
为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers"
)。此时把v-if
移动至容器元素上 (比如ul
、ol
)即可。
-
-
文档中明确指出永远不要把
v-if
和v-for
同时用在同一个元素上,显然这是一个重要的注意事项。 -
看过源码里面关于代码生成的部分,
知其所以然:
在 Vue 2
中做个测试,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)}
}
在 Vue 3
中做个测试,test-v3.html
两者同级时,渲染函数如下:
(function anonymous(
) {
const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, toDisplayString: _toDisplayString, createCommentVNode: _createCommentVNode } = _Vue
return shouldShowUsers
? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(items, (item) => {
return (_openBlock(), _createElementBlock("div", { key: item.id }, _toDisplayString(item.name), 1 /* TEXT */))
}), 128 /* KEYED_FRAGMENT */))
: _createCommentVNode("v-if", true)
}
}
})
源码中找答案:
Vue 2
:compiler/codegen/index.js
Vue 3
:compiler-core/src/codegen.ts
你知道key的作用吗?
分析:
这是一道特别常见的问题,主要考查大家对虚拟DOM和patch细节的掌握程度,能够反映面试者理解层次。
思路分析:总分总模式
- 给出结论,key的作用是用于优化patch性能
- key的必要性
- 实际使用方式
- 总结:可从源码层面描述一下vue如何判断两个节点是否相同
回答范例:
- key的作用主要是为了更高效的更新虚拟DOM。
- vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。
- 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。
- 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。
测试代码,test.html
上面案例重现的是以下过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OyE6nRPE-1676622244557)(…/assets/v2-6e88cc53a7e427f0ae8340cf930ac30d_hd.jpg)]
不使用key
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wtfq06UC-1676622244558)(…/assets/v2-bf76311258f100b789226ccbb2600071_hd.jpg)]
如果使用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前面
源码中找答案:src\core\vdom\patch.js - sameVnode()
你了解vue中的diff算法吗?
题目分析:vue基于虚拟DOM做更新,diff又是其核心部分,因此常被问道,此题考查面试者深度。
分析
必问题目,涉及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为例:
- 首先判断两个节点是否为相同同类节点,不同则删除重新创建
- 如果双方都是文本则更新文本内容
- 如果双方都是元素节点则递归更新子元素,同时更新元素属性
- 更新子节点时又分了几种情况:
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
- vue3中引入的更新策略:编译期优化patchFlags、block等
知其所以然
patch关键代码
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355
调试
vue中组件之间的通信方式?
题目分析:vue是组件化开发框架,所以对于vue应用来说组件间的数据通信非常重要。此题主要考查大家vue基本功,对于vue基础api运用熟练度。另外一些边界知识如provide/inject/ a t t r s / attrs/ attrs/listeners则体现了面试者的知识面。
思路分析:总分
- 总述知道的所有方式
- 按组件关系阐述使用场景
回答范例:
- 组件通信方式大体有以下8种:
- props
- e m i t / emit/ emit/on
- c h i l d r e n / children/ children/parent
- a t t r s / attrs/ attrs/listeners
- ref
- $root
- eventbus
- vuex
- 根据组件之间关系讨论组件通信最为清晰有效
-
父子组件
props
$emit
/$on
$parent
/$children
ref
$attrs
/$listeners
-
兄弟组件
-
$parent
-
eventbus
-
vuex
-
-
跨层级关系
provide
/inject
$root
eventbus
vuex
简单说一说你对vuex理解?
分析
此题考查实践能力,能说出用法只能60分。更重要的是对vuex设计理念和实现原理的解读。
回答策略:3w1h
- 首先给vuex下一个定义
- vuex解决了哪些问题,解读理念
- 什么时候我们需要vuex
- 你的具体用法
- 简述原理,提升层级
首先是官网定义:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
回答范例:
- vuex是vue专用的状态管理库。它以全局方式集中管理应用的状态,并且可以保证状态变更的可预测性。
- vuex主要解决的问题是多组件之间状态共享的问题,利用各种组件通信方式,我们虽然能够做到状态共享,但是往往需要在多个组件之间保持状态的一致性,这种模式很容易出现问题,也会使程序逻辑变得复杂。vuex通过把组件的共享状态抽取出来,以全局单例模式管理,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向数据流动,我们的代码将变得更结构化且易维护。
- vuex并非必须的,它帮我们管理共享状态,但却带来更多的概念和框架。如果我们不打算开发大型单页应用或者我们的应用并没有大量全局的状态需要维护,完全没有使用vuex的必要。一个简单的store 模式就足够了。反之,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是:Flux 架构就像眼镜:您自会知道什么时候需要它。
- 我在使用vuex过程中有如下理解:首先是对核心概念的理解和运用,将全局状态放入state对象中,它本身一棵状态树,组件中使用store实例的state访问这些状态;然后有配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation;如果应用中有异步操作或者复杂逻辑组合,我们需要编写action,执行结束如果有状态修改仍然需要提交mutation,组件中调用这些action使用dispatch方法派发。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态时注意添加子模块的名称,如果子模块有设置namespace,那么在提交mutation和派发action时还需要额外的命名空间前缀。
- vuex在实现单项数据流时需要做到数据的响应式,通过源码的学习发现是借用了vue的数据响应化特性实现的,它会利用Vue将state作为data对其进行响应化处理,从而使得这些状态发生变化时,能够导致组件重新渲染。
vue-router中如何保护路由?
此题是考查项目实践能力,项目中基本都有路由守卫的需求,保护指定路由考查的就是这个知识点。
答题思路:
- 阐述vue-router中路由保护策略
- 描述具体实现方式
- 简单说一下它们是怎么生效的
回答范例:
- vue-router中保护路由安全通常使用导航守卫来做,通过设置路由导航钩子函数的方式添加守卫函数,在里面判断用户的登录状态和权限,从而达到保护指定路由的目的。
- 具体实现有几个层级:全局前置守卫beforeEach、路由独享守卫beforeEnter或组件内守卫beforeRouteEnter。以全局守卫为例来说,可以使用
router.beforeEach((to,from,next)=>{})
方式设置守卫,每次路由导航时,都会执行该守卫,从而检查当前用户是否可以继续导航,通过给next函数传递多种参数达到不同的目的,比如如果禁止用户继续导航可以传递next(false),正常放行可以不传递参数,传递path字符串可以重定向到一个新的地址等等。 - 这些钩子函数之所以能够生效,也和vue-router工作方式有关,像beforeEach只是注册一个hook,当路由发生变化,router准备导航之前会批量执行这些hooks,并且把目标路由to,当前路由from,以及后续处理函数next传递给我们设置的hook。
可能的追问:
-
能不能说说全局守卫、路由独享守卫和组件内守卫区别?
-
作用范围
-
组件实例的获取
beforeRouteEnter(to,from,next) { next(vm => { }) }
-
名称/数量/顺序
- 导航被触发。
- 在失活的组件里调用离开守卫。
- 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 用创建好的实例调用
beforeRouteEnter
守卫中传给next
的回调函数。
-
-
你项目中的路由守卫是怎么做的?
-
前后端路由一样吗?
-
前端路由是用什么方式实现的?
-
你前面提到的next方法是怎么实现的?
你了解哪些Vue性能优化方法?
分析
这是一道综合实践题目,写过一定数量的代码之后小伙伴们自然会开始关注一些优化方法,答得越多肯定实践经验也越丰富,是很好的题目。
答题思路:
根据题目描述,这里主要探讨Vue代码层面的优化
回答范例
-
我这里主要从Vue代码编写层面说一些优化手段,例如:代码分割、服务端渲染、组件缓存、长列表优化等
-
最常见的路由懒加载:有效拆分App尺寸,访问时才异步加载
const router = createRouter({ routes: [ // 借助webpack的import()实现异步组件 { path: '/foo', component: () => import('./Foo.vue') } ] })
-
keep-alive
缓存页面:避免重复创建组件实例,且能保留缓存组件状态<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>
-
使用
v-show
复用DOM:避免重复创建组件<template> <div class="cell"> <!-- 这种情况用v-show复用DOM,比v-if效果好 --> <div v-show="value" class="on"> <Heavy :n="10000"/> </div> <section v-show="!value" class="off"> <Heavy :n="10000"/> </section> </div> </template>
-
v-for
遍历避免同时使用v-if
:实际上在Vue3中已经是个错误写法<template> <ul> <li v-for="user in activeUsers" <!-- 避免同时使用,vue3中会报错 --> <!-- v-if="user.isActive" --> :key="user.id"> {{ user.name }} </li> </ul> </template> <script> export default { computed: { activeUsers: function () { return this.users.filter(user => user.isActive) } } } </script>
-
v-once和v-memo:不再变化的数据使用
v-once
<!-- single element --> <span v-once>This will never change: {{msg}}</span> <!-- the element have children --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- component --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` directive --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>
按条件跳过更新时使用
v-momo
:下面这个列表只会更新选中状态变化项<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]"> <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p> <p>...more child nodes</p> </div>
https://vuejs.org/api/built-in-directives.html#v-memo
-
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller class="items" :items="items" :item-size="24" > <template v-slot="{ item }"> <FetchItemView :item="item" @vote="voteItem(item)" /> </template> </recycle-scroller>
一些开源库:
-
事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
export default { created() { this.timer = setInterval(this.refresh, 2000) }, beforeUnmount() { clearInterval(this.timer) } }
-
图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
<img v-lazy="/static/img/1.png">
参考项目:vue-lazyload
-
第三方插件按需引入
像
element-plus
这样的第三方组件库可以按需引入避免体积太大。import { createApp } from 'vue'; import { Button, Select } from 'element-plus'; const app = createApp() app.use(Button) app.use(Select)
-
子组件分割策略:较重的状态组件适合拆分
<template> <div> <ChildComp/> </div> </template> <script> export default { components: { ChildComp: { methods: { heavy () { /* 耗时任务 */ } }, render (h) { return h('div', this.heavy()) } } } } </script>
但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:https://vuejs.org/guide/best-practices/performance.html#avoid-unnecessary-component-abstractions
-
服务端渲染/静态网站生成:SSR/SSG
如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考SSR Guide
你知道nextTick吗,它是干什么的,实现原理是什么?
这道题考查大家对vue异步更新队列的理解,有一定深度,如果能够很好回答此题,对面试效果有极大帮助。
答题思路:
- nextTick是啥?下一个定义
- 为什么需要它呢?用异步更新队列实现原理解释
- 我再什么地方用它呢?抓抓头,想想你在平时开发中使用它的地方
- 下面介绍一下如何使用nextTick
- 最后能说出源码实现就会显得你格外优秀
先看看官方定义
Vue.nextTick( [callback, context] )
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
// 修改数据 vm.msg = 'Hello' // DOM 还没有更新 Vue.nextTick(function () { // DOM 更新了 })
回答范例:
- nextTick是Vue提供的一个全局API,由于vue的异步更新策略导致我们对数据的修改不会立刻体现在dom变化上,此时如果想要立即获取更新后的dom状态,就需要使用这个方法
- Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。
- 所以当我们想在修改数据后立即看到dom执行结果就需要用到nextTick方法。
- 比如,我在干什么的时候就会使用nextTick,传一个回调函数进去,在里面执行dom操作即可。
- 我也有简单了解nextTick实现,它会在callbacks里面加入我们传入的函数,然后用timerFunc异步方式调用它们,首选的异步方式会是Promise。这让我明白了为什么可以在nextTick中看到dom操作结果。
说一说你对vue响应式理解?
烂大街的问题,但却不是每个人都能回答到位。因为如果你只是看看别人写的网文,通常没什么底气,也经不住面试官推敲,但像我们这样即看过源码还造过轮子的,回答这个问题就会比较有底气。
答题思路:
- 啥是响应式?
- 为什么vue需要响应式?
- 它能给我们带来什么好处?
- vue的响应式是怎么实现的?有哪些优缺点?
- vue3中的响应式的新变化
回答范例:
- 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。
- mvvm框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
- 以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,可以使我们只需要操作数据,完全不用接触繁琐的dom操作,从而大大提升开发效率,降低开发难度。
- vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖该数组原型的方法,扩展它的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。
- 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy机制代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,我们甚至不需要引入vue都可以体验。
知其所以然
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
你如果想要扩展某个Vue组件时会怎么做?
此题属于实践题,着重考察大家对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方法你用过吗?它能用来做组件扩展吗?
nextTick实现原理
此题属于原理题目,能够体现面试者对vue理解深度,答好了会加分很多。
答题思路:
- 此题实际考查vue异步更新策略
- 说出vue是怎么通过异步、批量的方式更新以提高性能的
- 最后把源码中实现说一下
回答范例:
-
vue有个批量、异步更新策略,数据变化时,vue开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。然后在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
-
源码中,修改一个数据,组件对应的watcher会尝试入队:
queue.push(watcher)
并使用nextTick方法添加一个flushSchedulerQueue回调
nextTick(flushSchedulerQueue)
flushSchedulerQueue被加入callbacks数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx) // cb就是加入的回调
} catch (e) {
handleError(e, ctx, 'nextTick')
}
}
})
然后以异步方式启动
if (!pending) {
pending = true
timerFunc()
}
timerFunc的异步主要利用Promise等微任务方式实现
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
// timerFunc利用p.then向微任务队列添加一个flushCallbacks
// 会异步调用flushCallbacks
timerFunc = () => {
p.then(flushCallbacks)
}
isUsingMicroTask = true
}
flushCallbacks遍历callbacks,执行里面所有回调
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
其中就有前面加入的flushSchedulerQueue,它主要用于执行queue中所有watcher的run方法,从而使组件们更新
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
watcher.run()
}
知其所以然,测试代码:
可以调试一下上述流程,看看是不是这样。
可能的追问
你平时什么时候会用到nextTick?
Vue2和Vue3中的响应式原理对比,分别的具体实现思路
此题非常好,既考察深度又考察广度,面试者要对两个版本的响应式原理都有深入理解才能答好。
答题思路:
- 可以先说vue2响应式原理
- 然后说出它的问题
- 最后说出vue3是怎么解决的
回答范例:
-
vue2数据响应式实现根据对象类型做不同处理,如果是object,则通过
Object.defineProperty(obj,key,descriptor)
拦截对象属性访问function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { return val }, set(v) { val = v notify() } }) }
如果是数组,则覆盖数组的7个变更方法实现变更通知
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) ;['push','pop','shift','unshift','splice','sort','reverse'] .forEach(function (method) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) notify() return result }) })
-
可以看到vue2中有几个问题:
- 初始化时需要遍历对象所有key,如果对象层级较深,性能不好
- 通知更新过程需要维护大量dep实例和watcher实例,额外占用内存较多
- 动态新增、删除对象属性无法拦截,只能用特定set/delete api代替
- 不支持新的Map、Set等数据结构
-
vue3中为了解决以上问题,使用原生的Proxy代替:
function defineReactive(obj) { return new Proxy(obj, { get(target, key) { track(target, key) return Reflect.get(target, key) }, set(target, key, val) { Reflect.set(target, key, val) trigger(target, key) }, deleteProperty(target, key) { Reflect.deleteProperty(target, key) trigger(target, key) } }) }
可以同时支持object和array,动态属性增、删都可以拦截,新增数据结构均支持,对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能取得很大进步。
如果让你从零开始写一个vue路由,说说你的思路
思路分析:
首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
- 借助hash或者history api实现url跳转页面不刷新
- 同时监听hashchange事件或者popstate事件处理跳转
- 根据hash值或者state值从routes表中匹配对应component并渲染之
回答范例:
一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:
- 首先我会定义一个
createRouter
函数,返回路由器实例,实例内部做几件事:- 保存用户传入的配置项
- 监听hash或者popstate事件
- 回调里根据path匹配对应路由
- 将router定义成一个Vue插件,即实现install方法,内部做两件事:
- 实现两个全局组件:router-link和router-view,分别实现页面跳转和内容显示
- 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例
知其所以然:
- createRouter如何创建实例
https://github1s.com/vuejs/router/blob/HEAD/src/router.ts#L355-L356
- 事件监听
https://github1s.com/vuejs/router/blob/HEAD/src/history/html5.ts#L314-L315
RouterView
- 页面跳转RouterLink
https://github1s.com/vuejs/router/blob/HEAD/src/RouterLink.ts#L184-L185
- 内容显示RouterView
https://github1s.com/vuejs/router/blob/HEAD/src/RouterView.ts#L43-L44
watch和computed的区别以及选择?
两个重要API,反应应聘者熟练程度。
思路分析
- 先看两者定义,列举使用上的差异
- 列举使用场景上的差异,如何选择
- 使用细节、注意事项
- vue3变化
回答范例
- 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,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
说一下 Vue 子组件和父组件创建和挂载顺序
这题考查大家对创建过程的理解程度。
思路分析
- 给结论
- 阐述理由
回答范例
- 创建过程自上而下,挂载过程自下而上;即:
- parent created
- child created
- child mounted
- parent mounted
- 之所以会这样是因为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
## Vue组件为什么只能有一个根元素?
这题现在有些落伍,vue3
已经不用一个根了。因此这题目很有说头!
体验一下
vue2直接报错,test-v2.html
new Vue({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).$mount('#app')
vue3中没有问题,test-v3.html
Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
回答思路
- 给一条自己的结论
- 解释为什么会这样
vue3
解决方法原理
范例
vue2
中组件确实只能有一个根,但vue3
中组件已经可以多根节点了。- 之所以需要这样是因为
vdom
是一颗单根树形结构,patch
方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
,自然应该满足这个要求。 vue3
中之所以可以写多个根节点,是因为引入了Fragment
的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。
知其所以然
-
patch方法接收单根vdom:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L354-L355
// 直接获取type等,没有考虑数组的可能性 const { type, ref, shapeFlag } = n2
-
patch方法对Fragment的处理:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1091-L1092
// a fragment can only have array children // since they are either generated by the compiler, or implicitly created // from arrays. mountChildren(n2.children as VNodeArrayChildren, container, ...)
你知道哪些vue3新特性
分析
官网列举的最值得注意的新特性:https://v3-migration.vuejs.org/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Any5XXaa-1676622244560)(https://tva1.sinaimg.cn/large/e6c9d24ely1h0wjzxntraj21a60f2q4z.jpg)]
也就是下面这些:
- Composition API
- SFC Composition API语法糖
- Teleport传送门
- Fragments片段
- Emits选项
- 自定义渲染器
- SFC CSS变量
- Suspense
以上这些是api相关,另外还有很多框架特性也不能落掉。
回答范例
-
api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
-
另外,Vue3.0在框架层面也有很多亮眼的改进:
- 更快
- 虚拟DOM重写
- 编译器优化:静态提升、patchFlags、block等
- 基于Proxy的响应式系统
- 更小:更好的摇树优化
- 更容易维护:TypeScript + 模块化
- 更容易扩展
- 独立的响应化模块
- 自定义渲染器
知其所以然
体验编译器优化
https://sfc.vuejs.org/
reactive实现
https://github1s.com/vuejs/core/blob/HEAD/packages/reactivity/src/reactive.ts#L90-L91
简述 Vue 的生命周期以及每个阶段做的事
必问题目,考查vue基础知识。
思路
- 给出概念
- 列举生命周期各阶段
- 阐述整体流程
- 结合实践
- 扩展:vue3变化
回答范例
1.每个Vue组件实例被创建后都会经过一系列初始化步骤,比如,它需要数据观测,模板编译,挂载实例到dom上,以及数据变化时更新dom。这个过程中会运行叫做生命周期钩子的函数,以便用户在特定阶段有机会添加他们自己的代码。
2.Vue生命周期总共可以分为8个阶段:创建前后, 载入前后, 更新前后, 销毁前后,以及一些特殊场景的生命周期。vue3中新增了三个用于调试和服务端渲染场景。
生命周期v2 | 生命周期v3 | 描述 |
---|---|---|
beforeCreate | beforeCreate | 组件实例被创建之初 |
created | created | 组件实例已经完全创建 |
beforeMount | beforeMount | 组件挂载之前 |
mounted | mounted | 组件挂载到实例上去之后 |
beforeUpdate | beforeUpdate | 组件数据发生变化,更新之前 |
updated | updated | 数据数据更新之后 |
beforeDestroy | beforeUnmount | 组件实例销毁之前 |
destroyed | unmounted | 组件实例销毁之后 |
activated | activated | keep-alive 缓存的组件激活时 |
deactivated | deactivated | keep-alive 缓存的组件停用时调用 |
errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
- | renderTracked | 调试钩子,响应式依赖被收集时调用 |
- | renderTriggered | 调试钩子,响应式依赖被触发时调用 |
- | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用 |
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
说说你对虚拟 DOM 的理解?
分析
现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?围绕这个疑问来解答即可!
思路
- vdom是什么
- 引入vdom的好处
- vdom如何生成,又如何成为dom
- 在后续的diff中的作用
回答范例
-
虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个
JavaScript
对象,只不过它是通过不同的属性去描述一个视图结构。 -
通过引入vdom我们可以获得如下好处:
将真实元素节点抽象成 VNode,有效减少直接操作 dom 次数,从而提高程序性能
- 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
- 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流。
方便实现跨平台
- 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
- Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染。
-
vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。
-
挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。
mount:
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/renderer.ts#L1171-L1172
调试mount过程:mountComponent
file:///Users/yangtao/projects/vue-interview/public/21-vdom/test-render-v3.html