一、vue-router
01. vue-router的原理
vue-router的作用就是通过改变URL,在不重新请求页面的情况下,更新页面视图。通过 hash 与 History 两种方式实现前端路由,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改。
02. 路由有哪两种模式?
(1)hash模式:hash 值会出现在 URL 中,但不会包含在 http 请求中,所以改变 hash 值时不会刷新页面,也不会向服务器发送请求,hash 值的改变会触发 hashchange 事件,通过监听 hashchange 事件来完成操作实现前端路由。
(2)history模式:利用 HTML5 中新增的 pushState()和 replaceState()方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能;url 的改变属于 http 请求,借助 history.pushState 实现页面的无刷新跳转,因此会重新请求服务器,所以需要服务器的配置,否则会 404。
两种模式其实都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。
程序会根据你选择的模式类型创建不同的 history 对象:
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
03. 哈希模式
hash 模式会创建 hashHistory 对象,这个对象有两个方法:HashHistory.push()和HasHistory.replace()。在访问不同的路由的时候,会发生两件事:HashHistory.push()将新的路由添加到浏览器访问历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由。
因为hash发生变化的url都会被浏览器记录(历史访问栈)下来,从而你会发现浏览器的前进后退都可以用了。这样一来,尽管浏览器没有请求服务器,但是页面状态和url一一关联起来。
04. 历史模式
因为HTML5标准发布,多了两个 修改历史状态的API:pushState、replaceState。通过这两个 API (1)可以改变 url 地址且不会发送请求,(2)不仅可以读取历史记录栈,还可以对浏览器历史记录栈进行修改。
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前URL改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 模式下,不怕前进,不怕后退,就怕刷新,(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的。前端的 URL 必须和实际向后端发起请求的 URL 一致,如 www.abc.com/book/id 如果后端缺少对 /book/id 的路由处理,将返回 404 错误。
// 例如 Nginx 配置
location / {
try_files $uri $uri/ /index.html;
}
我们主要说几个注意点:
- 通过 pushState/replaceState 改变 URL 不会触发页面刷新,也不会触发 popstate 方法,所以我们可以拦截 pushState/replaceState 的调用来检测 URL 变化,从而触发 router-view 的视图更新。
- 通过浏览器前进后退改变 URL ,或者通过 js 调用 history 的 back,go,forward 方法,都会触发 popstate 事件,所以我们可以监听 popstate 来触发 router-view 的视图更新。
所以,我们其实是需要监听 popstate 以及拦截 pushState/placeState 以及 a 的点击去实现监听 URL 的变化。
DEMO:
<!DOCTYPE html>
<html lang="en">
<body>
<ul>
<ul>
<li><a href="/home">home</a></li>
<li><a href="/about">about</a></li>
<div id="routeView"></div>
</ul>
</ul>
</body>
<script>
let routerView = document.getElementById('routeView');
window.addEventListener('DOMContentLoaded', () => {
routerView.innerHTML = location.pathname;
var linkList = document.querySelectorAll('a[href]');
linkList.forEach(el =>
el.addEventListener('click', function(e) {
e.preventDefault();
history.pushState(null, '', el.getAttribute('href'));
routerView.innerHTML = location.pathname;
}),
);
});
window.addEventListener('popstate', () => {
routerView.innerHTML = location.pathname;
});
</script>
</html>
- 我们监听 popState 事件。一旦事件触发(例如触发浏览器的前进后端按钮,或者在控制台输入 history,go,back,forward 赋值),就改变 routerView 的内容。
- 我们通过 a 标签的 href 属性来改变 URL 的 path 值。这里需要注意的就是,当改变 path 值时,默认会触发页面的跳转,所以需要拦截标签点击事件的默认行为,这样就阻止了 a 标签自动跳转的行为,点击时使用 pushState 修改 URL 并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
05. route 和 router 的区别
this.$route是当前路由信息对象,包括 path、params、hash、query、fullPath、matched、name 等路由信息参数this.$router是路由实例对象,包括了路由的跳转方式 push()、go(),钩子函数等
06. 路由钩子有哪些?
1. 全局守卫
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 必须调用next
})
// 全局解析守卫
router.beforeResolve((to, from, next) => {
// 必须调用next
})
// 全局后置钩子
router.afterEach((to, from) => {})
- 路由独享守卫
const router = new VueRouter({
routes: [
{
path: '/home',
beforeEnter: (to, from, next) => {
//...
}
}
]
})
- 组件内的守卫
- beforeRouteEnter
- beforeRouteUpdate
- beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意:只有 beforeRouteEnter 支持给 next 传递回调
07. Vue路由钩子在生命周期函数的体现
路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入b组件∶
- beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开
- beforeEach:路由全局前置守卫,可用于登录验证、全局路由loading等
- beforeEnter:路由独享守卫
- beforeRouteEnter:路由组件的组件进入路由前钩子
- beforeResolve:路由全局解析守卫
- afterEach:路由全局后置钩子
- beforeCreate:组件生命周期,不能访问this
- created;组件生命周期,可以访问this,不能访问dom
- beforeMount:组件生命周期
- deactivated:离开缓存组件a,或者触发a的beforeDestroy和destroyed组件销毁钩子
- mounted:访问/操作dom
- activated:进入缓存组件,进入a的嵌套子组件(如果有的话)
- 执行beforeRouteEnter回调函数next
08. 导航守卫三个参数的含义?
-
to:即将要进入的目标路由对象
-
from:当前导航正要离开的路由对象
-
next:一定要调用该方法来 resolve 这个钩子,不然路由跳转不过去
- next():进入下一个路由
- next(false):中断当前的导航
- next('/') 或 next({path: '/'}):当前导航被中断,进行新的一个导航
09. router跳转和location.href有什么区别
history.pushState():无刷新页面,静态跳转;location.href:简单方便,但是刷新了页面
10. params 和 query 的区别
跳转方式不同:query(name、path),params(name)
//query传参,使用name跳转
this.$router.push({
name:'second',
query: {
queryId:'20180822'
}
})
//query传参,使用path跳转
this.$router.push({
path:'second',
query: {
queryId:'20180822'
}
})
//params传参 使用name
this.$router.push({
name:'second',
params: {
name: 'query'
}
})
- query 刷新页面参数不会消失,params 传参页面参数会消失,可以考虑本地存储解决
- query 传参会显示在 url 地址上,params 传参不会显示地址上
11. 讲一下完整的导航守卫流程?
- 导航被触发。
- 在失活的组件里调用离开守卫。
- 调用全局的
beforeEach守卫。 - 在
重用的组件里调用beforeRouteUpdate守卫(2. 2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里面调用
beforeRouterEnter。 - 调用全局的
beforeResolve守卫(2. 5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发
DOM更新。 - 用创建好的实例调用
beforeRouteEnter守卫中传给next的回调函数。
12. 对前端路由的理解
在前端技术早期,切换页面必然伴随着页面的刷新,这个体验并不好。后来 Ajax 出现了,它允许人们在不刷新页面的情况下发起请求,同时有“不刷新页面即可更新页面内容”的需求出现。在这样的背景下,出现了 SPA。
SPA极大地提升了用户体验,但是在 SPA 诞生之初,人们并没有考虑到定位这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:
- SPA 其实并不知道当前的页面进展到了哪一步
- 由于只有一个 URL 给页面做映射,搜索引擎无法收集全面的信息
为了解决这个问题,前端路由出现了。前端路由可以帮助我们在仅有一个页面的情况下,记住用户当前走到了哪一步。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。
13. 动态路由是什么,有什么问题
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么我们可以使用动态路由来解决这个问题。同时可以通过$route.params.id 获取参数。
const User = {
template: "<div>User</div>",
};
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: "/user/:id", component: User },
],
});
问题:组件复用导致路由参数失效怎么办?
分析:我们一般是在生命周期钩子中调用接口获取数据,当用户从 /users/johnny 导航到 /users/jolyne 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用,这就会导致路由参数失效。
解决方法:
- 通过 watch 监听路由参数再发请求
watch: {
() => this.$route.params,
(toParams, previousParams) => {
// 对路由变化做出响应...
}
}
2. 使用导航守卫
async beforeRouteUpdate(to, from) {
// 对路由变化做出响应...
this.userData = await fetchUser(to.params.id)
}
14. 异步组件和路由懒加载的原理
问题:讲讲异步组件和路由懒加载?异步加载的过程是怎么样的?为什么在router中使用回调的方式引入组件就可以实现异步加载?假如一个页面有特别多的异步组件会带来什么问题?
其实异步组件和异步路由是一样的东西,异步路由引入的也还是对应的组件。
异步加载的过程涉及到JavaScript运行时、事件循环机制以及打包工具的配合工作。过程:
- 代码分割:当我们在代码中使用
import()函数异步加载组件时,打包工具(如Webpack或Rollup)会将这些组件作为单独的JavaScript文件(通常被称为"chunks")进行打包。 - 初始加载:在页面初始加载时,只有主bundle会被下载。主bundle中包含了应用的主要代码,但不包括异步组件的代码。
- 异步请求:当用户触发了需要异步组件的操作(如访问一个使用了路由懒加载的路由),JavaScript运行时会发送一个网络请求,请求异步组件对应的JavaScript文件。
- 下载和执行:浏览器下载异步组件对应的JavaScript文件,并执行其中的代码。这个过程可能会涉及到一些延迟,因为需要等待网络请求完成,并且JavaScript是单线程的,不能同时执行多段代码。
- 渲染组件:异步组件的代码被执行后,组件会被注册到Vue.js应用中,并触发视图的更新。Vue.js会创建一个新的组件实例,并将其渲染到页面上。
在这个过程中,打包工具起到了关键作用。它负责将代码分割成多个小文件,并在构建时生成一份映射表,这样JavaScript运行时就知道每个异步组件对应哪个文件。
同时,JavaScript的事件循环机制也在这个过程中发挥了作用。由于JavaScript是单线程的,它需要使用事件循环来处理异步操作。当我们使用import()函数加载一个组件时,这个操作会被放入微任务队列中。只有当当前的同步代码执行完毕后,才会执行微任务队列中的操作。这也是为什么异步组件不会立即被加载和渲染的原因。
过多的异步组件可能会导致的问题:
- 网络请求过多导致阻塞、请求失败
- 加载异步组件导致页面闪烁、抖动,影响用户体验
- 每个异步组件都可能下载失败,提供错误处理比较复杂
15. 路由之间跳转有哪些方式?
- 声明式导航: 通过内置组件
router-link跳转
<router-link :to="/home"></router-link>
- 编程式导航: 通过调用
router实例的方法跳转
- 使用 push 方法跳转
this.$router.push({
path: '/home'
})
- 使用 repalce 方法跳转
this.$router.replace({
path: '/home'
})
16. 如何监听路由参数的变化?
有两种方法可以监听路由参数的变化,但是只能用在包含 <router-view/> 的组件内
- watch 监听$route 对象
watch: {
$route(to, from) {
console.log(to, from)
}
}
- 调用组件内的守卫 beforeRouteUpdate
beforeRouteUpdate(to, from, next) {
console.log(to, from)
next()
}
17. vue-router怎么配置 404 页面?
由于路由是从上到下执行的,只要在路由配置中最后面放个*号就可以了
const router = new Router({
routes: [
{
path: '*',
redirect: '/404'
}
]
})
二、Vuex
01. 谈谈对Vuex的理解/Vuex 的原理
Vuex 是一个专为 Vue.js 开发的状态管理模式,每一个 Vuex 应用的核心就是 store(仓库),里面包含着应用中的状态 ( state )。Vuex 的状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到更新。Vuex 为Vue Components建立起了一个完整的生态圈,包括开发中的API调用一环。
核心流程中的主要功能:
- Vue 组件会触发(dispatch)一些事件或动作(Actions)
- 在组件中发出的动作,都是想获取或者改变数据的,但是在 Vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中
- 然后 Mutations 就去改变(Mutate)State 中的数据
- 当 State 中的数据改变之后,就会重新渲染(Render),进行更新
02. Vuex中action和mutation的区别
- Mutation专注于修改State,理论上是修改State的唯一途径;Action业务代码、异步请求。
- Mutation:必须同步执行;Action:可以异步,但不能直接操作State。
- 在视图更新时,先触发 Action,Action 再触发 Mutation
- Mutation的参数是State,它包含store中的数据;store的参数是context,它是 State 的父级,包含 state、getters
03. Vuex 和 localStorage 的区别
区别一:
1.Vuex存储在内存中
2.localstorage以文件的方式存储在本地,只能存储字符串类型的数据
区别二:
Vuex能做到数据的响应式,localstorage不能
区别三:
刷新页面时vuex存储的值会丢失,localstorage不会
应用场景:
1.Vuex用于组件之间的传值
2.localstorage是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用
04. 为什么要用 Vuex
由于传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。所以需要把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,组件树构成了一个巨大的"视图",不管在树的哪个位置,任何组件都能获取状态或者触发行为。同时也会使代码变得更结构化且易维护。
05. Vuex有哪几种属性?
state => 基本数据(数据源存放地)
getters => 从基本数据派生出来的数据
mutations => 提交更改数据的方法,同步
actions => 像一个装饰器,包裹mutations,使之可以异步。
modules => 模块化Vuex
06. Vuex和单纯的全局对象有什么区别?
1. Vuex 的状态存储是响应式的
2. 不能直接改变store中的状态
07. 为什么 Vuex 的 mutation 中不能做异步操作?
1.因为同步操作方便跟踪每一个状态的变化
2.如果支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪
08. Vuex的严格模式有什么作用,如何开启?
在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。在Vuex.Store 构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
09. 如何在组件中批量使用Vuex的getter属性
使用mapGetters辅助函数,在 computed 中配置:
import { mapGetters } from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
10. 如何在组件中重复使用Vuex的mutation
使用mapMutations辅助函数,在 methods中配置,然后调用this.setNumber(10)相当于调用this.$store.commit('SET_NUMBER',10)
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
11. 你有使用过 Vuex 的 module吗?
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
- 项目规模变大之后,单独一个 store 对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
- 使用时要注意访问子模块状态时需要加上注册时模块名:
store.state.a.xxx - 模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
12. Vuex 页面刷新数据丢失怎么解决
需要做 Vuex 数据持久化,一般使用本地存储(localStorege)的方案来保存数据,可以自己设计存储方案,也可以使用第三方插件。
13. Vuex 为什么要分模块并且加命名空间
模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。
命名空间:默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
14. 简述Vuex的数据传输流程
当组件进行数据修改的时候我们需要调用dispatch来触发actions里面的方法。actions里面的每个方法中都会有一个commit方法,当方法执行的时候会通过commit来触发mutations里面的方法进行数据的修改。mutations里面的每个函数都会有一个state参数,这样就可以在mutations里面进行state的数据修改,当数据修改完毕后,会传导给页面,页面的数据也会发生改变。
15. Vuex的getter的作用
getter 有点类似 Vue.js 的计算属性,当我们需要从 store 的 state 中派生出一些状态,那么我们就需要使用 getter,getter 会接收 state 作为第 一个参数,而且 getter 的返回值会根据它的依赖被缓存起来,只有 getter 中的依赖值(state 中的某个需要派生状态的值)发生改变的时候才会被重新计算。
16. Vuex的优缺点
优点:1.支持调试功能,如时间旅行和编辑; 2. 适用于大型、高复杂度的Vue.js项目; 3. Vuex 的社区支持很大
缺点:1. 从 Vue 3 开始,getter 的结果不会像计算属性那样缓存; 2. Vuex 4有一些与类型安全相关的问题; 3. 页面刷新,数据丢失(使用持久化插件解决 vuex-persistedstate)
17. pinia的优缺点
优点: 1. 更加轻量级,因为它不需要使用 Vuex 的一些复杂的概念,如模块和 getter; 2. API 设计更加简单易用,因为它使用了 Vue3 的新特性; 3. 更加灵活,因为它支持多个 store 实例; 4. 完整的 TypeScript 支持
缺点:1. 不支持时间旅行和编辑等调试功能; 2. 页面刷新,数据丢失(使用持久化插件解决 pinia-plugin-persistedstate); 3. 相对较新,可能存在一些未知的问题和限制; 4. 生态系统不够完善
18. pinia和Vuex的区别
Pinia 和 Vuex 都是 Vue.js 状态管理库,但它们在一些方面有所不同。Pinia 是一个轻量级的状态管理库,它专注于提供一个简单的 API 来管理应用程序的状态。相比之下,Vuex 是一个更完整的状态管理库,它提供了更多的功能,比如模块化、插件和严格模式等。具体的区别如下:
- pinia 没有 Vuex 中的 mutation
- pinia 支持多个 store 实例, 而 Vuex只有一个实例
- pinia 语法上比 vuex 更容易理解和使用,灵活
- pinia 没有 modules 配置,每一个独立的仓库都是 definStore 生成出来的
- 相较于 Vuex,pinia 更加的轻量级,API 更加简单易用
Vuex 仍然是构建大型 SPA 的理想解决方案,如果你正在构建一个不太复杂的应用程序,可以使用 Pinia。pinia 和 vuex 在 vue2 和 vue3 都可以使用,一般来说vue2使用vuex,vue3使用pinia。
19. 双向绑定和 vuex 是否冲突
当在严格模式中使用 Vuex 时,如果用 v-model 绑定 Vuex 中 state 的数据,在用户输入时 v-model 会试图直接修改 obj.message,这样会报错。因为在严格模式中,如果修改不是在 mutation 函数中执行的, 那么会抛出一个错误。
<input v-model="obj.message">
解决方法:
computed: {
message: {
set (value) {
this.$store.dispatch('updateMessage', value);
},
get () {
return this.$store.state.obj.message
}
}
}
20. Vuex的底层原理
使用方式:
vuex 是个状态管理模式,其基本工作流程是:当用户修改状态的时候,如果是 同步 修改状态,会先提交(commit)触发mutations方法,改变 Store实例中的状态,这也是 唯一 修改状态的途径,如果是 异步 的修改状态的方法,比如发起数据请求等,会提交 dispatch触发 actions方法,然后在 actions方法中通过 commit 触发 mutations方法进而更新Store实例的状态,然后再将状态更新到Vue组件中。如果我们的项目是大型的单页面应用,通过这个状态管理模式我们可以很轻松的实现组件之间的通信,当然vuex也有自己的缺陷,就是状态不能够持久化。
实现原理:
通过在 install方法内通过 vue的 mixin方法将 Store 实例注入到每个组件内,内部主要是借助 vue本身的响应式原理来实现状态的响应式,借助computed来实现 getters的缓存效果,然后通过 发布订阅的思想 将用户定义的 mutations 和 actions 方法存储起来,当用户触发 commit、dispatch 的时候就去订阅mutations 和 actions 找出对应的方法。如果存在模块嵌套的情况下,首先,vuex内部会通过一个 moduleCollection类 将所有模块格式化为一个树形结构,其次,通过 installModule 方法将格式化好的树形结构安装到 Store 实例上,最后,通过 resetStoreVm方法 借助 vue内部响应式的原理将所有模块的状态都置为响应式。Store内部有 subscribe方法 和 replaceState方法,通过这两个方法我们实现 vuex的状态的持久化,以上说明了 vuex是高度依赖 vue响应式 和 插件系统的。
21. 滥用Vuex会引起哪些问题
简单应用引入Vuex,代码冗余,不利于维护
Vuex 的状态管理是响应式的,频繁的状态变化可能导致性能开销增加
如果 Vuex 的模块划分不合理,可能导致在维护时难以理解和追踪状态的流动
22. Vuex如何实现持久化数据
方法一:使用 localStorage
方法二:安装 Vuex 的插件 vuex-persistedstate
import Vuex from "vuex";
// 引入插件
import createPersistedState from "vuex-persistedstate";
Vue.use(Vuex);
const state = {};
const mutations = {};
const actions = {};
const store = new Vuex.Store({
state,
mutations,
actions,
/* vuex数据持久化配置 */
plugins: [
createPersistedState({
// 存储方式:localStorage、sessionStorage、cookies
storage: window.sessionStorage,
// 存储的 key 的key值
key: "store",
render(state) {
// 要存储的数据:本项目采用es6扩展运算符的方式存储了state中所有的数据
return { ...state };
}
})
]
});
export default store;
三、Vue 3.0
01. Vue3.0有什么更新
(1)监测机制的改变:Object.defineProperty -> Proxy
(2)Composition API:使用传统OptionsAPI时,新增或者修改一个需求,就需要分别在data,methods,computed里修改。当业务逻辑和功能越来越多的时候理解和维护复杂组件变得困难。
而 Vue3 的组合式 API 将每个功能点抽成一个 function 使我们可以更加优雅的组织我们的代码,让相关功能的代码更加有序的组织在一起。
(2)只能监测属性,不能监测对象
- 检测属性的添加和删除
- 检测数组索引和长度的变更
- 支持 Map、Set、WeakMap 和 WeakSet
(3)重写 VDOM:优化前Virtual Dom的diff算法,需要遍历所有节点,而且每一个节点都要比较旧的props和新的props有没有变化。在Vue3.0中,只有带PatchFlag的节点会被真正的追踪,在后续更新的过程中,Vue不会追踪静态节点,只追踪带有PatchFlag的节点来达到加快渲染的效果。
(4)新的生命周期钩子:
- 去掉了 Vue2 中的 beforeCreate 和 created 两个阶段,新增了一个
setup - 每个生命周期函数必须导入才可以使用,并且所有生命周期函数需要统一放在
setup里使用。
(5)片段(Fragment):- Vue2中组件必须有一个根标签;Vue3 中组件可以没有根标签, 可以直接写多个根节点,内部会将多个标签包含在一个Fragment虚拟元素中,这样可以减少标签层级, 减小内存占用,提升了渲染性能
02. defineProperty、proxy 的区别
使用 Object.defineProperty有以下问题:
- 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过
$set来处理。 - 无法监控到数组下标和长度的变化。
使用 Proxy 有以下特点:
- Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
- Proxy 可以监听数组的变化。
03. Vue3.0 为什么要用 proxy?
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
- 可以监听到属性的增加与删除,不需使用
Vue.$set或Vue.$delete触发响应式 - 可以监听数组的变化
- 支持 Map,Set,WeakMap 和 WeakSet
04. Vue3为什么要使用Composition API,优缺点?
Vue3 使用Composition API是为了解决Vue 2普遍存在的问题:
- 代码的可读性随着组件变大而变差
- 每一种代码复用的方式,都存在缺点
- TypeScript支持有限
假设我们要写一个搜索框组件,首先它存在一个基础功能-搜索,则代码结构如下:
过了一段时间,加了个排序的需求,组件代码就变成这样: 目前看着还行,直到经过一个又一个迭代,例如加上了搜索过滤功能,分页功能,各种新增功能的代码块,写在项目里的各个位置(不同组件,props,computed,生命周期函数,data,methods)。
分散在各个地方的逻辑片段,使我们的组件越来越难以阅读,经常为了看一个功能要疯狂上下滑动屏幕,在各个区块里翻找和修改。
而在这个场景下,使用Composition API可以将同一功能的代码写在同一个区域里,无论是维护性还是可读性都是更好的。
那么使用Composition API的优点呢?
(1)更好的逻辑复用: 使我们能够通过组合函数来实现更加简洁高效的逻辑复用,解决了了选项式 API 中 逻辑复用 mixins 的缺陷。
(2)更灵活的代码组织: 将与同一个逻辑关注点相关的代码被归为了一组:我们无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外,我们现在可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,大大降低了重构成本,这在长期维护的大型项目中非常关键。
(3)更好的类型推导
(4)更小的生产包体积: 对 tree-shaking 友好,代码也更容易压缩
(5)没有使用this,减少了this指向不明的情况
(6)如果是小型组件,可以继续使用Options API,也是十分友好的
从目前的使用情况来看,Composition API还未出现缺点。
05. 为什么Vue3可以有多个根结点,而Vue2只能有一个?
(1)Vue2:因为需要构建VNode,而VNode只能有一个根节点,当前构建和diff virutalDOM 的算法还未支撑这样的结构,也很难在保证性能的情况下支撑。
为什么抽象出来的 DOM 树只能有一个根?
- 从查找和遍历的角度来说,如果有多个根,那么我们的查找和遍历的效率会很低。
- 如果一个树有多个根,说明可以优化,肯定会有一个节点是可以访问到所有的节点,那这个节点就会成为新的根节点。(维护多个根节点不如用一个新的根节点去管理多个子节点)
- 再从 Vue 本身来说,如果说一个组件有多个入口多个根,那不就意味着你的组件还可以进一步拆分成多个组件,进一步组件化,降低代码之间的耦合程度。
(2)Vue3:因为Vue3引入了片段 fragment的概念,Vue3会自动将多个标签用fragment 包裹。这是一个抽象的节点,如果发现组件是多根的会自动创建一个fragment节点,把多根节点视为自己的children。
06. Vue2中 Options API的优缺点?
缺点:反复横跳(新增或者修改一个需求,滚动条反复上下移动)、代码的可读性随着组件变多而变差、每一种代码复用的方式都存在缺点、对TypeScript的支持有限、逻辑过多时会出现this指向不明的问题
优点:条例清晰,相同的放在相同的地方,对于简单的组件功能是其优势所在
07. Vue2 和Vue3 的区别
(1)双向数据绑定原理发生了改变
Vue2:Object.definepropert()
Vue3:proxy
(2)根结点的个数
Vue2:根结点只能有一个
Vue3:根结点可以有多个
(3)Composition API
Vue2:Options api在代码里分割了不同的属性:data,computed,methods等
Vue3:Composition api能让我们使用方法来分割,这样代码会更加简便和整洁
(4)生命周期
vue2 ---------------------------- vue3
beforeCreate setup()
Created setup()
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroyed onBeforeUnmount
destroyed onUnmounted
activated onActivated
deactivated onDeactivated
(5)实例化
Vue2中new出的实例对象,所有的东西都在这个vue对象上,这样其实⽆论你⽤到还是没⽤到,这样提⾼了性能消耗
Vue3中可以⽤ES module imports按需引⼊,减少了内存消耗、⽤户加载时间,优化⽤户体验。
08. React hooks 和 compose api 有什么区别?
- composition api中的 setup 只会调用一次;react hooks 中的函数会被多次调用
- react hooks 需要 useMemo useCallback, 因为 setup 只会被调用一次
- composition api 不需要保证顺序, react hooks 要保证 hooks 顺序一致
- ref toRef toRefs reactive 比起 useState 太繁琐了
09. Vue2 和 Vue3 响应式性能的区别
Vue2 深度监听,性能差;Vue3 性能提升了一大截。Vue3 对于数据的深层监听只有当获取值的时候(get)才会响应式处理(也就是调用observe()进行观测),它不会深度遍历;而 Vue2 响应化过程需要递归遍历消耗较大,在 get 和 set 之前调用observe()进行观测。
10. Vue2 和 Vue3 Diff算法的区别
- Vue2 是全量 Diff(当数据发生变化,它就会新生成一个DOM树,并和之前的DOM树进行比较,找到不同的节点然后更新。);Vue3 是静态标记 + 非全量 Diff(Vue 3在创建虚拟DOM树的时候,会根据DOM中的内容会不会发生变化,添加一个静态标记,之后在与上次虚拟节点进行对比的时候,就只会对比这些带有静态标记的节点。)
- Vue3 使用最长递增子序列优化对比流程,可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作
11. Vue2 和 vue3 代码如何在同一个项目中共存?
其实现在 Vue2.7+ 版本已经内置支持组合式 API,Vue2.6 及之前的版本也可以使用 @vue/composition-api 插件来支持,所以完全可以只写一套代码同时支持 Vue2 和 3。虽然如此,但是实际开发中,同一个 API 在不同的版本中可能导入的来源不一样,比如 ref 方法,在 Vue2.7+ 中直接从 vue 中导入,但是在 Vue2.6 中只能从 @vue/composition-api 中导入,那么必然会涉及到版本判断,Vue Demi 就是用来解决这个问题。
使用很简单,只要从 Vue Demi 中导出你需要的内容即可:
import { ref, reactive, defineComponent } from 'vue-demi'
Vue-demi 会根据你的项目判断到底使用哪个版本的 Vue,具体来说,它的策略如下:
<=2.6: 从 Vue 和 @vue/composition-api 中导出2.7: 从 Vue 中导出(组合式 API 内置于 Vue 2.7 中)>=3.0: 从 Vue 中导出,并且还 polyfill 了两个 Vue 2 版本的 set 和 del API
四、虚拟DOM
01. 对虚拟DOM的理解?
Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构,使跨平台渲染成为可能。通过将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少回流重绘,提高渲染性能。
虚拟DOM是对DOM的抽象,它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM。在代码渲染到页面之前,Vue会把代码转换成一个对象,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,拿现在的虚拟DOM会与缓存的虚拟DOM进行比较。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
02. 虚拟DOM的解析过程
- 首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- 当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
- 最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
03. 为什么要用虚拟DOM
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能
下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新
(2)跨平台
04. 虚拟DOM真的比真实DOM性能好吗
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢
- 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的
05. diff 算法的原理
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
06. 虚拟DOM的优劣如何?
优点:
- 保证性能下限,虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多
- 无需手动操作DOM,极大提高开发效率
- 实现跨平台
缺点:
- 无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化
07. Vue 中key的作用
对于用 v-for 渲染的列表数据来说,数据量一般很庞大,而且还要对这个数据进行一些增删改操作。假设我们给列表增加一条数据,整个列表都要重新渲染一遍,那就很费事。 key 的出现就是尽可能的回避这个问题,提高效率,如果我们给列表增加了一条数据,页面只渲染这数据即可。 v-for 默认使用就地复用策略,列表数据修改的时候,会根据 key 值去判断某个值是否修改,如果修改,则重新渲染这一项,否则复用之前的元素。
08. 为什么不推荐index作为key
举个例子:
<div v-for="(item, index) in list" :key="index">{{item.num}}</div>
list = [
{
id: 1,
num: 1
},
{
id: 2,
num: 2
},
{
id: 3,
num: 3
},
];
(1)在数组后面追加一条数据
list = [
{
id: 1,
num: '1'
},
{
id: 2,
num: 2
},
{
id: 3,
num: 3
},
{
id: 4,
num: '新增加的数据4'
}
];
此时前三条数据页面不会重新渲染,直接复用之前的,只会新渲染最后一条数据,此时用 index 作为 key ,没有任何问题。
(2)在数组中间插入一条数据
list = [
{
id: 3,
num: 1
},
{
id: 4,
num: '新增加的数据4'
},
{
id: 2,
num: '2'
},
{
id: 3,
num: '3'
}
];
如果使用 index 作为 key ,页面在渲染数据的时候会有:
之前的数据 之后的数据
key: 0 index: 0 num: 1 key: 0 index: 0 num: 1
key: 1 index: 1 num: 2 key: 1 index: 1 num: '新增加的数据4'
key: 2 index: 2 num: 3 key: 2 index: 2 num: 2
key: 3 index: 3 num: 3
通过对比,发现除了第一个数据可以复用之前的之外,另外三条数据都需要重新渲染。明明只是插入了一条数据,怎么三条数据都要重新渲染?而我想要的只是新增的那一条数据新渲染出来即可。
最好的办法是使用数组中不会变化的那一项作为 key 值,每条数据都有一个唯一的 id ,来标识这条数据的唯一性。如果使用 id 作为 key ,页面在渲染数据的时候会有:
之前的数据 之后的数据
key: 1 id: 1 index: 0 num: 1 key: 1 id: 1 index: 0 num: 1
key: 2 id: 2 index: 1 num: 2 key: 4 id: 4 index: 1 num: '新增加的数据4'
key: 3 id: 3 index: 2 num: 3 key: 2 id: 2 index: 2 num: 2
key: 3 id: 3 index: 3 num: 3
现在对比发现只有一条数据变化了,就是id为4的那条数据,因此只要新渲染这一条数据就可以了,其他都是就复用之前的,极大提高了性能。
五、其他
01. 最新的前端技术趋势
反 TypeScript:比如:Svelte、Turbo,后面可能会有越来越多的开发者加入这个阵营
Turbopack: webpack 作者使用 Rust 开发的新的打包工具,其目的就是为了对抗 vite
Rust: 尤雨溪宣布 Vite 的底层即将用 Rust 重写,即开发一个基于 Rust 的打包工具 Rolldown,以此替换掉原有的 Esbuild 和 Rollup
大前端: 小程序、App、桌面应用
微前端: wujie、qiankun
低代码: 通过可拖拽、可配置的方式,实现不需要手写代码就可以搭建一个应用
原子化CSS: unocss
WebRTC: 可通过简单的 API 为浏览器和移动应用程序提供实时通信(RTC)功能
02. Element-UI和Element-Plus区别
Element-UI:对应Vue2、基本不支持手机版、定制方面更加灵活
Element-Plus:对应Vue3、组件布局考虑了手机版展示、性能更好、去掉一些不常用的组件
一些组件的用法不同
03. 如何理解vue的渐进式开发
渐进式:主张最少,没有多做职责之外的事;每个框架都不可避免会有自己的一些特点,从而会对使用者有一定的要求,这些要求就是主张,主张有强有弱,它的强势程度会影响在业务开发中的使用方式。
我们可以通俗的理解为:用什么拿什么(你可以有很多选择,并不是非常强制你一定要用那种方式,vue只是为我们提供了视图层,至于底层的实现,还是有非常多的选择的。)
使用Angular,必须接受以下东西:
1、必须使用它的模块机制
2、必须使用它的依赖注入
3、必须使用它的特殊形式定义组件(这一点每个视图框架都有,这是难以避免的)
所以Angular是带有比较强的排它性的,如果你的应用不是从头开始,而是要不断考虑是否跟其他东西集成,这些主张会带来一些困扰。
使用React,你必须理解:
1、函数式编程的理念
2、需要知道它的副作用
3、什么是纯函数
4、如何隔离、避免副作用
5、它的侵入性看似没有Angular那么强,主要因为它是属于软性侵入的
Vue与React、Angular的不同是,它是渐进的:
1、可以在原有的大系统的上面,把一两个组件改用它实现,就是当成jQuery来使用
2、可以整个用它全家桶开发,当Angular来使用
3、可以用它的视图,搭配你自己设计的整个下层使用
4、可以在底层数据逻辑的地方用OO和设计模式的那套理念
5、可以函数式,它只是个轻量视图而已,只做了最核心的东西