高频前端面试题汇总之Vue篇(下)

1,117 阅读25分钟

一、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;
}

我们主要说几个注意点:

  1. 通过 pushState/replaceState 改变 URL 不会触发页面刷新,也不会触发 popstate 方法,所以我们可以拦截 pushState/replaceState 的调用来检测 URL 变化,从而触发 router-view 的视图更新。
  2. 通过浏览器前进后退改变 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>
  1. 我们监听 popState 事件。一旦事件触发(例如触发浏览器的前进后端按钮,或者在控制台输入 history,go,back,forward 赋值),就改变 routerView 的内容。
  2. 我们通过 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) => {})
  1. 路由独享守卫
const router = new VueRouter({
  routes: [
    {
      path: '/home',
      beforeEnter: (to, from, next) => {
        //...
      }
    }
  ]
})
  1. 组件内的守卫
  • 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. 讲一下完整的导航守卫流程?

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 重用的组件里调用 beforeRouteUpdate 守卫(2. 2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里面调用 beforeRouterEnter
  8. 调用全局的 beforeResolve 守卫(2. 5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 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 时,相同的组件实例将被重复使用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会被调用,这就会导致路由参数失效。

解决方法:

  1. 通过 watch 监听路由参数再发请求
watch: {
 () => this.$route.params, 
 (toParams, previousParams) => { 
     // 对路由变化做出响应... 
 }
}

2. 使用导航守卫

async beforeRouteUpdate(to, from) { 
    // 对路由变化做出响应... 
    this.userData = await fetchUser(to.params.id) 
}

14. 异步组件和路由懒加载的原理

问题:讲讲异步组件和路由懒加载?异步加载的过程是怎么样的?为什么在router中使用回调的方式引入组件就可以实现异步加载?假如一个页面有特别多的异步组件会带来什么问题?

其实异步组件和异步路由是一样的东西,异步路由引入的也还是对应的组件。

异步加载的过程涉及到JavaScript运行时、事件循环机制以及打包工具的配合工作。过程:

  1. 代码分割:当我们在代码中使用import()函数异步加载组件时,打包工具(如Webpack或Rollup)会将这些组件作为单独的JavaScript文件(通常被称为"chunks")进行打包。
  2. 初始加载:在页面初始加载时,只有主bundle会被下载。主bundle中包含了应用的主要代码,但不包括异步组件的代码。
  3. 异步请求:当用户触发了需要异步组件的操作(如访问一个使用了路由懒加载的路由),JavaScript运行时会发送一个网络请求,请求异步组件对应的JavaScript文件。
  4. 下载和执行:浏览器下载异步组件对应的JavaScript文件,并执行其中的代码。这个过程可能会涉及到一些延迟,因为需要等待网络请求完成,并且JavaScript是单线程的,不能同时执行多段代码。
  5. 渲染组件:异步组件的代码被执行后,组件会被注册到Vue.js应用中,并触发视图的更新。Vue.js会创建一个新的组件实例,并将其渲染到页面上。

在这个过程中,打包工具起到了关键作用。它负责将代码分割成多个小文件,并在构建时生成一份映射表,这样JavaScript运行时就知道每个异步组件对应哪个文件。

同时,JavaScript的事件循环机制也在这个过程中发挥了作用。由于JavaScript是单线程的,它需要使用事件循环来处理异步操作。当我们使用import()函数加载一个组件时,这个操作会被放入微任务队列中。只有当当前的同步代码执行完毕后,才会执行微任务队列中的操作。这也是为什么异步组件不会立即被加载和渲染的原因。

过多的异步组件可能会导致的问题:

  • 网络请求过多导致阻塞、请求失败
  • 加载异步组件导致页面闪烁、抖动,影响用户体验
  • 每个异步组件都可能下载失败,提供错误处理比较复杂

15. 路由之间跳转有哪些方式?

  1. 声明式导航:  通过内置组件 router-link 跳转
<router-link :to="/home"></router-link>
  1. 编程式导航:  通过调用 router 实例的方法跳转
  • 使用 push 方法跳转
this.$router.push({
  path: '/home'
})
  • 使用 repalce 方法跳转
this.$router.replace({
  path: '/home'
})

16. 如何监听路由参数的变化?

有两种方法可以监听路由参数的变化,但是只能用在包含 <router-view/> 的组件内

  1. watch 监听$route 对象
watch: {
  $route(to, from) {
    console.log(to, from)
  }
}
  1. 调用组件内的守卫 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调用一环。

b025e120ca3d0bd2ded3d038d58cacf4.jpg

核心流程中的主要功能:

  • 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 是一个更完整的状态管理库,它提供了更多的功能,比如模块化、插件和严格模式等。具体的区别如下:

  1. pinia 没有 Vuex 中的 mutation
  2. pinia 支持多个 store 实例, 而 Vuex只有一个实例
  3. pinia 语法上比 vuex 更容易理解和使用,灵活
  4. pinia 没有 modules 配置,每一个独立的仓库都是 definStore 生成出来的
  5. 相较于 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的缓存效果,然后通过 发布订阅的思想 将用户定义的 mutationsactions 方法存储起来,当用户触发 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 Domdiff算法,需要遍历所有节点,而且每一个节点都要比较旧的props和新的props有没有变化。在Vue3.0中,只有带PatchFlag的节点会被真正的追踪,在后续更新的过程中,Vue不会追踪静态节点,只追踪带有PatchFlag的节点来达到加快渲染的效果。

(4)新的生命周期钩子

  • 去掉了 Vue2 中的 beforeCreate 和 created 两个阶段,新增了一个setup
  • 每个生命周期函数必须导入才可以使用,并且所有生命周期函数需要统一放在 setup 里使用。

image.png

(5)片段(Fragment):- Vue2中组件必须有一个根标签;Vue3 中组件可以没有根标签, 可以直接写多个根节点,内部会将多个标签包含在一个Fragment虚拟元素中,这样可以减少标签层级, 减小内存占用,提升了渲染性能

02. defineProperty、proxy 的区别

使用 Object.defineProperty有以下问题:

  1. 添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set 来处理。
  2. 无法监控到数组下标和长度的变化。

使用 Proxy 有以下特点:

  1. Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
  2. Proxy 可以监听数组的变化。

03. Vue3.0 为什么要用 proxy?

在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶

  • 可以监听到属性的增加与删除,不需使用 Vue.$setVue.$delete 触发响应式
  • 可以监听数组的变化
  • 支持 Map,Set,WeakMap 和 WeakSet

04. Vue3为什么要使用Composition API,优缺点?

Vue3 使用Composition API是为了解决Vue 2普遍存在的问题:

  • 代码的可读性随着组件变大而变差
  • 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

假设我们要写一个搜索框组件,首先它存在一个基础功能-搜索,则代码结构如下:

过了一段时间,加了个排序的需求,组件代码就变成这样: 目前看着还行,直到经过一个又一个迭代,例如加上了搜索过滤功能,分页功能,各种新增功能的代码块,写在项目里的各个位置(不同组件,props,computed,生命周期函数,data,methods)。

分散在各个地方的逻辑片段,使我们的组件越来越难以阅读,经常为了看一个功能要疯狂上下滑动屏幕,在各个区块里翻找和修改。

而在这个场景下,使用Composition API可以将同一功能的代码写在同一个区域里,无论是维护性还是可读性都是更好的。

image.png

那么使用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 树只能有一个根?

  1. 从查找和遍历的角度来说,如果有多个根,那么我们的查找和遍历的效率会很低。
  2. 如果一个树有多个根,说明可以优化,肯定会有一个节点是可以访问到所有的节点,那这个节点就会成为新的根节点。(维护多个根节点不如用一个新的根节点去管理多个子节点)
  3. 再从 Vue 本身来说,如果说一个组件有多个入口多个根,那不就意味着你的组件还可以进一步拆分成多个组件,进一步组件化,降低代码之间的耦合程度。

(2)Vue3:因为Vue3引入了片段 fragment的概念,Vue3会自动将多个标签用fragment 包裹。这是一个抽象的节点,如果发现组件是多根的会自动创建一个fragment节点,把多根节点视为自己的children。

06. Vue2中 Options API的优缺点?

缺点:反复横跳(新增或者修改一个需求,滚动条反复上下移动)、代码的可读性随着组件变多而变差、每一种代码复用的方式都存在缺点、对TypeScript的支持有限、逻辑过多时会出现this指向不明的问题

优点:条例清晰,相同的放在相同的地方,对于简单的组件功能是其优势所在

07. Vue2 和Vue3 的区别

(1)双向数据绑定原理发生了改变

Vue2Object.definepropert()
Vue3:proxy

(2)根结点的个数

Vue2:根结点只能有一个
Vue3:根结点可以有多个

(3)Composition API

Vue2Options api在代码里分割了不同的属性:data,computed,methods等
Vue3Composition 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)实例化

Vue2new出的实例对象,所有的东西都在这个vue对象上,这样其实⽆论你⽤到还是没⽤到,这样提⾼了性能消耗

Vue3中可以⽤ES module imports按需引⼊,减少了内存消耗、⽤户加载时间,优化⽤户体验。

08. React hooks 和 compose api 有什么区别?

  1. composition api中的 setup 只会调用一次;react hooks 中的函数会被多次调用
  2. react hooks 需要 useMemo  useCallback,  因为 setup 只会被调用一次
  3. composition api 不需要保证顺序, react hooks 要保证 hooks 顺序一致
  4. 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、可以函数式,它只是个轻量视图而已,只做了最核心的东西