16. watch和computed的区别以及选择?
思路分析
computed特点:具有响应式的返回值, 有缓存
//vue2
computed:{
fullName(){
return this.firstName +'-'+ this.lastName
}
},
//可传参
<h3>{{fullName('我喜欢画画')}}</h3>
computed:{
fullName(){
return(str) =>{
return this.firstName +'-'+ this.lastName + str
}
}
},
//vue3
const count = ref(1)
const plusOne = computed(() => count.value + 1)
//可传参
<template>
<div>
<h3>count:{{getCountByParams(10,20)}}</h3>
</div>
</template>
<script>
import { computed, reactive } from 'vue'
export default {
setup() {
const getCountByParams = computed(() => (a,b) => {
return a*b
})
return { getCountByParams }
},
}
</script>
watch特点:侦测变化,执行回调
//vue2
//简单写法
num(newValue, oldValue){
console.log(newValue, oldValue)
}
//复杂写法
watch:{
obj:{
handler(newValue, oldValue){
console.log(newValue, oldValue)
},
immediate: true,
deep:true
}
},
//vue3
//监听一个ref类型的值
watch(count, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
//监听多个ref类型的值
watch([count, sum], (newValue, oldValue) => {
console.log(newValue, oldValue) //[12, 2000] (2) [11, 200]
/*
newValue时一个数组:包含count和sum的变化后的值
oldValue时一个数组:包含count和sum的变化前的值
*/
} ,{immediate:true})
//监听reactive类型的值
const person = reactive({
name: '张三',
age: 20,
job:{
salery:20
}
})
watch(person, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
回答范例
- 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。
- 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何种方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性。
- 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deep、immediate等选项。
vue3中watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watch、watchEffect可以完全替代目前的watch选项,且功能更加强大。
可能追问
watch会不会立即执行?
- 在vue2中watch不会立即执行, 因为immediate为false, 如果要立即执行需要把immediate置为true。在vue3中,watch监听ref类型的响应式数据, 默认immediate也是false, 但是如果监听reactive类型的响应式数据,immediate默认为true.
watch 和 watchEffect有什么差异
- watch的套路:既要指明监视的属性, 也要指明监视的回调。
- watchEffect的套路是:不用指明监视哪个属性, 监视的回调中用到哪个属性, 就监视哪个属性。
- 默认开启了immediate: true,首次就会执行一次。
知其所以然
computed的实现
github1s.com/vuejs/core/…
ComputedRefImpl
github1s.com/vuejs/core/…
缓存性
github1s.com/vuejs/core/…
github1s.com/vuejs/core/…
watch的实现
github1s.com/vuejs/core/…17. 说一下 Vue 子组件和父组件创建和挂载顺序
这题考查大家对创建过程的理解程度。思路分析
给结论
-
回答范例
创建过程自上而下,挂载过程自下而上;即:
- parent created
- child created
- child mounted
- parent mounted
之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。
知其所以然
观察beforeCreated和created钩子的处理
github1s.com/vuejs/core/…
github1s.com/vuejs/core/…
观察beforeMount和mounted钩子的处理
github1s.com/vuejs/core/…
测试代码,test-v3.html18. 怎么缓存当前的组件?缓存后怎么更新?
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多。
思路
缓存用keep-alive,它的作用与用法
- 使用细节,例如缓存指定/排除、结合router和transition
- 组件缓存后更新可以利用activated或者beforeRouteEnter
-
回答范例
keep-alive作为一种vue的内置组件,主要作用是缓存组件状态。当需要组件的切换时,不用重新渲染组件,避免多次渲染,就可以使用keep-alive包裹组件。 ```javascript
<a name="xbhOx"></a>
### props
- include 字符串或者正则表达式,只有名称匹配的组件会被缓存
- exclude 字符串或者正则表达式,任何名臣匹配的组件都不会被缓存
- max 数字,最多可以缓多少组件实例
当组件在keep-alive内被切换,它的activated和deactivated这两个生命周期钩子函数将会被对应执行
```javascript
export default[
{
path:'/',
name:'home',
components:Home,
meta:{
keepAlive:true //需要被缓存的组件
},
{
path:'/book',
name:'book',
components:Book,
meta:{
keepAlive:false //不需要被缓存的组件
}
]
<keep-alive>
<router-view v-if="this.$route.meat.keepAlive"></router-view>
<!--这里是会被缓存的组件-->
</keep-alive>
<keep-alive v-if="!this.$router.meta.keepAlive"></keep-alive>
<!--这里是不会被缓存的组件-->
实现前进刷新,后退不刷新
//需求
//默认显示 A
//B 跳到 A,A 不刷新
//C 跳到 A,A 刷新
//在 A 路由里面设置 meta 属性
{
path: '/',
name: 'A',
component: A,
meta: {
keepAlive: true // 需要被缓存
}
}
//B 在 B 组件里面设置 beforeRouteLeave:
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = true; // 让 A 缓存,即不刷新
next();
}
};
//C 在 C 组件里面设置 beforeRouteLeave:
export default {
data() {
return {};
},
methods: {},
beforeRouteLeave(to, from, next) {
// 设置下一个路由的 meta
to.meta.keepAlive = false; // 让 A 不缓存,即刷新
next();
}
};
缓存后如果要获取数据,解决方案可以有以下两种:
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函数就会重新执行。
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>
知其所以然
KeepAlive定义
github1s.com/vuejs/core/…
缓存定义
github1s.com/vuejs/core/…
缓存组件
github1s.com/vuejs/core/…
获取缓存组件
github1s.com/vuejs/core/…
测试缓存特性,test-v3.html19. 从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可。
思路
构建项目,创建项目基本结构
- 引入必要的插件:
- 代码规范: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:用来放项目的页面文件
20. 实际工作中,你总结的vue最佳实践有哪些?
思路
查看vue官方文档:
风格指南:vuejs.org/style-guide…
性能:vuejs.org/guide/best-…
安全:vuejs.org/guide/best-…
访问性:vuejs.org/guide/best-…
发布:vuejs.org/guide/best-…
回答范例
我从编码风格、性能、安全等方面说几条:
- 编码风格方面:
- 命名组件时使用“多词”风格避免和HTML元素冲突
- 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”
- 使用v-for时务必加上key,且不要跟v-if写在一起
- 性能方面:
- 路由懒加载减少应用尺寸
- 利用SSR减少首屏加载时间
- 利用v-once渲染那些不需要更新的内容
- 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用shallowRef或shallowReactive降低开销
- 避免不必要的组件抽象
- 安全:
- 不使用不可信模板,例如使用用户输入拼接模板:template: + userProvidedString +
- 小心使用v-html,:url,:style等,避免html、url、样式等注入
- 不使用不可信模板,例如使用用户输入拼接模板:template:
-
21. 简单说一说你对vuex理解?
思路
给定义
- 必要性阐述
- 何时使用
- 拓展:一些个人思考、实践经验等
范例
- Vuex 是一个专为 Vue.js 应用开发的状态管理模式 + 库。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念。
- Vuex 解决状态管理的同时引入了不少概念:例如state、mutation、action等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
-
可能的追问
vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
- 刷新浏览器,vuex中的state会重新变为初始状态 ;
- 解决方案-插件 vuex-persistedstate
在开发过程中遇到的问题:
最近在做Vue项目中的登录模块,登陆成功后获取到token,将token存储在vuex中,然而我发现切换路由后vuex中的数据都恢复默认了,原来页面刷新之后vuex的数据都会恢复默认。而后面进行鉴权处理需要token,于是我们要将vuex中的数据进行本地存储。
这里就用到了vuex持久化插件vuex-persistedstate.
参考文档:https://juejin.cn/post/7054184638142644238
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
export default createStore({
state: { },
mutations: { },
actions: { },
modules: { },
plugins: [
createPersistedState(),
],
})
import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
export default createStore({
state: { },
mutations: { },
actions: { },
modules: { },
plugins: [
// 把vuex的数据存储到sessionStorage
createPersistedState({
storage: window.sessionStorage,
}),
],
})
- action和mutation的区别是什么?为什么要区分它们?
第一点:流程顺序不同
- “相应视图—>修改State”拆分成两部分,视图触发Action,Action再触发Mutation。
第二点:基于流程顺序,二者扮演不同的角色。
- Mutation:专注于修改State,理论上是修改State的唯一途径。
- Action:业务代码、异步请求。
第三点:基于角色不同,二者也有不同的限制。
22. 说说从 template 到 render 处理过程
分析
问我们template到render过程,其实是问vue编译器工作原理。
思路
- 引入vue编译器概念
- 说明编译器的必要性
-
回答范例
Vue中有个独特的编译器模块,称为“compiler”,它的主要作用是将用户编写的template编译为js中可执行的render函数。
- 之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。
在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。
知其所以然
vue3编译过程窥探:
github1s.com/vuejs/core/…
测试,test-v3.html可能的追问
Vue中编译器何时执行?
-
23. Vue实例挂载的过程中发生了什么?
分析
挂载过程完成了最重要的两件事:
初始化
- 建立更新机制
回答范例
- 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化和建立更新机制
- 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。
知其所以然
测试代码,test-v3.html mount函数定义
github1s.com/vuejs/core/…
首次render过程
github1s.com/vuejs/core/…可能的追问
响应式数据怎么创建
- 在 Vue2 中,我们只需要把数据放入 data 函数,Vue2 会遍历 data 中的所有属性,使用 Object.defineProperty 把每个 property 全部转为 getter/setter,getter 用来收集依赖,setter 用来执行 notify,发布更新事件。Vue2 对每个属性创建一个 Dep 对象,作为订阅发布模式的中间机构来收集依赖。Vue 追踪这些依赖,在其被访问和修改时通知变更。
- Vue3 会对需要转化为响应式的普通 JavaScript 对象转换为 Proxy。proxy 对象对于用户来说是不可见的,但是在内部,它们使 Vue3 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。并且在 Vue 3 中,响应性数据可以在『独立的包』中使用。
- 依赖关系如何建立
24-Vue 3.0的设计目标是什么?做了哪些优化?
分析
还是问新特性,陈述典型新特性,分析其给你带来的变化即可。
思路
从以下几方面分门别类阐述:易用性、性能、扩展性、可维护性、开发体验等
范例
- Vue3的最大设计目标是替代Vue2(皮一下),为了实现这一点,Vue3在以下几个方面做了很大改进,如:易用性、框架性能、扩展性、可维护性、开发体验等
- 易用性方面主要是API简化,比如v-model在Vue3中变成了Vue2中v-model和sync修饰符的结合体,用户不用区分两者不同,也不用选择困难。类似的简化还有用于渲染函数内部生成VNode的h(type, props, children),其中props不用考虑区分属性、特性、事件等,框架替我们判断,易用性大增。
- 开发体验方面,新组件Teleport传送门、Fragments 、Suspense等都会简化特定场景的代码编写,SFC Composition API语法糖更是极大提升我们开发体验。
- 扩展性方面提升如独立的reactivity模块,custom renderer API等
- 可维护性方面主要是Composition API,更容易编写高复用性的业务逻辑。还有对TypeScript支持的提升。
- 性能方面的改进也很显著,例如编译期优化、基于Proxy的响应式系统
- 。。。
可能的追问
Vue3做了哪些编译优化?
- https://juejin.cn/post/6850418112878575629#heading-6
1. 源码体积优化
- 首先,移除一些冷门的 feature(比如 filter、inline-template 等);
- 其次,引入 tree-shaking 的技术,减少打包体积。
- tree-shaking,它的原理很简单,tree-shaking 依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记。那么在打包的时候它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。
- 数据劫持优化
- tree-shaking,它的原理很简单,tree-shaking 依赖 ES2015 模块语法的静态结构(即 import 和 export),通过编译阶段的静态分析,找到没有引入的模块并打上标记。那么在打包的时候它们对应的代码就不会打包,这样也就间接达到了减少项目引入的 Vue.js 包体积的目的。
- Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,具体是这样的:Object.defineProperty(data, ‘a’,{ })
- 这个API存在一些缺陷, 它必须要预先知道你要拦截的key是什么, 所以它并不能检测对象属性的添加和删除。vue提供了Vue.set()和Vue.delete()方法, 但是用户在体验方面还是不友好。
- 如果遇到嵌套层次比较深的对象, 如果要劫持它内部的生层次的对象变化, 就需要递归遍历这个对象, 执行Object.defineProperty把每一层对象数据都变成响应式的。 但是这样有相当大的性能问题。
- 如果是数组, vue重写的数组的原型。
- vue3x采用 Proxy API 做数据劫持const observer = new Proxy(obj, {get(){}, set(){}})
- 由于Proxy劫持的是整个对象, 那么自然对于对象的属性的增加和删除都能检测到。
- Proxy API并不能检测到内部生层次的对象变化, 因此 Vue3.0的处理方式是在getter中去递归响应式, 这样的好处是真正访问到内部对象才变成响应式的, 而不是无脑递归, 这样提高了很大的性能。
3. 编译优化 vue3.0在编译阶段对静态模板的分析, 编译生成了Block tree。Block tree是一个将模板基于动态节点指令切割的嵌套区块, 每个区块内部的节点结构式固定的, 而且每个区块只需要以一个Array来追踪自身包含的动态节点,借助Block tree,Vue.js将Vnode更新性能由模板整体大小提升为与动态内容的数量相关。
- https://juejin.cn/post/6850418112878575629#heading-6
-
25-你了解哪些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-memo:下面这个列表只会更新选中状态变化项
v-memo:可以做性能优化,v-memo中值若不发生变化,整个子树的更新会被跳过。 ```javascript
```javascript
<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>
- vuejs.org/api/built-i…
长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容<
<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">
第三方插件按需引入像element-plus这样的第三方组件库可以按需引入避免体积太大。
import { createApp } from 'vue'; import { Button, Select } from 'element-plus'; const app = createApp() app.use(Button) app.use(Select)
子组件分割策略:较重的状态组件适合拆分 ```javascript
- 但同时也不宜过度拆分组件,尤其是为了所谓组件抽象将一些不需要渲染的组件特意抽出来,组件实例消耗远大于纯dom节点。参考:[vuejs.org/guide/best-…](https://link.juejin.cn?target=https%3A%2F%2Fvuejs.org%2Fguide%2Fbest-practices%2Fperformance.html%23avoid-unnecessary-component-abstractions)
- 服务端渲染/静态网站生成:SSR/SSG如果SPA应用有首屏渲染慢的问题,可以考虑SSR、SSG方案优化。参考[SSR Guide](https://link.juejin.cn?target=https%3A%2F%2Fvuejs.org%2Fguide%2Fscaling-up%2Fssr.html)
<a name="AxnWJ"></a>
## 26-Vue组件为什么只能有一个根元素?
这题现在有些落伍,vue3已经不用一个根了。因此这题目很有说头!
<a name="KcWA8"></a>
### 体验一下
vue2直接报错,test-v2.html<br />new Vue({ components: { comp: { template: ` <div>root1</div> <div>root2</div> ` } } }).$mount('#app') 复制代码<br /><br />vue3中没有问题,test-v3.html<br />Vue.createApp({ components: { comp: { template: ` <div>root1</div> <div>root2</div> ` } } }).mount('#app') 复制代码<br />
<a name="SGOU4"></a>
### 回答思路
- 给一条自己的结论
- 解释为什么会这样
- vue3解决方法原理
<a name="AY3OR"></a>
### 范例
- vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。
- 之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom,自然应该满足这个要求。
- vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新。
<a name="Qbvel"></a>
### 知其所以然
- patch方法接收单根vdom:[github1s.com/vuejs/core/…](https://link.juejin.cn?target=https%3A%2F%2Fgithub1s.com%2Fvuejs%2Fcore%2Fblob%2FHEAD%2Fpackages%2Fruntime-core%2Fsrc%2Frenderer.ts%23L354-L355)// 直接获取type等,没有考虑数组的可能性 const { type, ref, shapeFlag } = n2 复制代码
- patch方法对Fragment的处理:[github1s.com/vuejs/core/…](https://link.juejin.cn?target=https%3A%2F%2Fgithub1s.com%2Fvuejs%2Fcore%2Fblob%2FHEAD%2Fpackages%2Fruntime-core%2Fsrc%2Frenderer.ts%23L1091-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, ...) 复制代码
<a name="kZyHh"></a>
## 27-你有使用过vuex的module吗?
这是基本应用能力考察,稍微上点规模的项目都要拆分vuex模块便于维护。
<a name="zKnOP"></a>
### 体验
[vuex.vuejs.org/zh/guide/mo…](https://link.juejin.cn?target=https%3A%2F%2Fvuex.vuejs.org%2Fzh%2Fguide%2Fmodules.html)
```javascript
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.getters.c // -> moduleA里的getters
store.commit('d') // -> 能同时触发子模块中同名mutation
store.dispatch('e') // -> 能同时触发子模块中同名action
思路
- 概念和必要性
- 怎么拆
- 使用细节
-
范例
用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
- 可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:createStore({modules:{…}})
- 不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子模块加上namespace选项,此时再访问它们就要加上命名空间前缀。
很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了。
可能的追问
-
28-怎么实现路由懒加载呢?
分析
这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效。 ```javascript // 将 // import UserDetails from ‘./views/UserDetails’ // 替换为 const UserDetails = () => import(‘./views/UserDetails’)
const router = createRouter({ // … routes: [{ path: ‘/users/:id’, component: UserDetails }], })
参考[router.vuejs.org/zh/guide/ad…](https://link.juejin.cn?target=https%3A%2F%2Frouter.vuejs.org%2Fzh%2Fguide%2Fadvanced%2Flazy-loading.html)
<a name="VAMRJ"></a>
### 思路
1. 必要性
2. 何时用
3. 怎么用
4. 使用细节
<a name="HEYBv"></a>
### 回答范例
1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。**利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。**
2. 一般来说,对所有的路由**都使用动态导入**是个好主意。
3. 给component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
```javascript
{ path: '/users/:id', component: () => import('./views/UserDetails') }
- 结合注释() => import(/ webpackChunkName: “group-user” / ‘./UserDetails.vue’)可以做webpack代码分块vite中结合rollupOptions定义分块
-
知其所以然
component (和 components) 配置如果接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。
github1s.com/vuejs/route…29-ref和reactive异同
这是Vue3数据响应式中非常重要的两个概念,自然的,跟我们写代码关系也很大。
体验
ref:vuejs.org/api/reactiv…
const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1 复制代码
reactive:vuejs.org/api/reactiv…
const obj = reactive({ count: 0 }) obj.count++ 复制代码回答思路
两者概念
- 两者使用场景
- 两者异同
- 使用细节
-
回答范例
ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
- 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
- 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
- ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(…)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
reactive内部使用Proxy代理传入对象并拦截该对象各种操作(trap),从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式。
知其所以然
reactive实现响应式:
github1s.com/vuejs/core/…
ref实现响应式:
github1s.com/vuejs/core/…30-watch和watchEffect异同
我们经常性需要侦测响应式数据的变化,vue3中除了watch之外又出现了watchEffect,不少同学会混淆这两个api。
体验
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。
const count = ref(0) watchEffect(() => console.log(count.value)) // -> logs 0 count.value++ // -> logs 1
watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )
思路
给出两者定义
- 给出场景上的不同
- 给出使用方式和细节
-
范例
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
- watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。
- watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。
- 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})
知其所以然
watchEffect定义:github1s.com/vuejs/core/…
watch定义如下:github1s.com/vuejs/core/…
很明显watchEffect就是一种特殊的watch实现。