前端面试---Vuejs

139 阅读14分钟

Vuejs基础

Vue的基本原理

Vuejs是依靠MVVM框架来实现的,也就是:Model、View、ViewModel

  • View:视图层
  • Model:数据层
  • ViewModel:连接视图与数据的中间件

image.png

其实View和Model之间不能进行直接通信,但是可以通过ViewModel来进行通信。当数据发生改变的时候,ViewModel会监听数据发生了改变,并且通知View来进行页面的修改。当页面触发了事件,ViewModel会监听到事件,并通知model进行响应。

Vuejs数据驱动

vuejs在实例化的过程中,会对遍历传给实例化对象选项中的data 选项,遍历其所有属性并使用 Object.defineProperty 把这些属性全部转为 getter/setter。同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。

双向绑定原理

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile
  3. 同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
  5. 将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

image.png

Object.defineProperty() 来进行数据劫持有什么缺点?

因为Object.defineProperty()对于大部分数组的一些操作,不能拦截,vuejs在内部进行了重写函数。

vue3使用的是Proxy,然后能够对这些进行数据拦截

区别:proxy 的优势还在于监听的目标是整个对象而不是某个属性, Object.defineProperty 监听整个对象需要递归遍历,对性能也有影响,它监听的是属性

Computed和Watch的区别

Computed

  • computed有缓存,优先走缓存,当依赖的数据发生改变的时候,才会重新进行
  • computed是基于响应式来进行缓存的,对于那些ref、reactive,或者父组件传递过来的props中的数据进行计算的。
  • 不支持异步
  • 如果一个属性是由另一个属性计算而来的,这个属性依赖于另一个属性值,那么适合于computed
  • 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。

Watch

  • 没有缓存
  • 可以异步
  • 接收两个参数,新值和旧值
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

运用的场景

  • 当需要进行数据计算的时候,并且依赖其他数据的时候,应该使用computed
  • 当数据的变化需要进行异步操作或者性能消耗很大的时候,就需要使用watch

Computed和Method的区别

  • Computed是根据依赖的属性进行重新计算的
  • Method是调用总是执行

Slot

slot为插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot分为三类:默认插槽,具名插槽和作用域插槽。

  • 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。(这个准备在看看不会这里)

Vue3可以通过useSlot()来获取插槽名字

如何保存页面的当前的状态

分情况:

  • 前组件会被卸载
  • 前组件不会被卸载

前组件会被卸载

存储在localstorage/sessionstorage

子组件即将销毁的时候在localStorage/sessionStorage中把当前组件的state通过Json.stringify()储存下来

优点:简单、兼容性好,不需要额外的库或者工具,还有一些状态管理的库(pinia/vuex)其实都是根据这个来实现的

缺点:Json.stringfy自身的缺点,对于Date对象、Regexp对象来说会得到字符串,而不是原来的值

路由传值(看一下官网)

通过vue-router可以进行路由传值

将 props 传递给路由组件 | Vue Router (vuejs.org))

前组件不会被卸载

单页面渲染

也就是一个页面中含有许多子组件来构成的,由父组件来进行存储页面的状态

优点:代码量少,不需要考虑状态传递过程的错误

缺点:父组件维护成本增高,需要传递prop到B组件

keep-alive

当组件在keep-alive内被切换时,组件的activated、deactivated两个生命钩子会被执行,被包裹在keep-alive中的组建的状态会被保存

常见的事件修饰符

.stop.preventself:只会触发自己范围之内的事件,不包含子组件,.once

v-if和v-show区别

  1. v-if:动态地向DOM树中添加或者删除DOM元素;v-show:只是通过css地display来进行显示和隐藏元素

  2. v-if:切换过程中会销毁dom元素及其内部的事件监听和子组件,v-show:只是简单的css切换

  3. v-if:有更高的切换消耗,v-show:有更高地初始化消耗

  4. v-show适合频繁切换,v-if适合运营条件不太会改变的时候

v-model作用

作用在表单元素

在表单上面绑定了 v-model,相当于input的value绑定了message值,@input动态地监听这个value的变化,并把这个值绑定了message,举个例子

<input v-model="message"></input>
=>等价于
<input :value="message" @input="message"></input>

作用在组件

本质上就是父子组件通信的一个语法糖,子组件通过props和emits来进行实现

例如

//父组件,这个pageTitle为响应式数据
<ChildComponent v-model="pageTitle" />
=>相当于
<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />
//子组件
<template>
    <input :value="modelValue" @input="onChange" /> 
</template>

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  modelValue: {
    type: XXX,
    required: true
  }
})
const emits = defineEmits(['update:modelValue'])

const onChange = () => {
    emits('update:modelValue', 新值)
}
</script>

其实还可以进行修改这个默认的名称(modelValue),v-model:title进行修改

子组件就是这样的

<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  title: {
    type: XXX,
    required: true
  }
})
const emits = defineEmits(['update:title'])
</script>

多个v-model的使用

<Chile v-model:first="first" v-model:last="last" />
<script setup>
import { defineProps, defineEmits } from 'vue'

const props = defineProps({
  first: {
    type: String,
    required: true
  },
  last: {
    type: String,
    required: true
  },
})
const emits = defineEmits(['update:first','update:last'])
</script>

对keep-alive的理解

Vue3的通信

父子通信

  1. props(父传子)Props | Vue.js (vuejs.org))
  2. emits(子传父)子组件通知父组件触发一个事件,并且可以传值给父组件。(简称:子传父)
  3. expose/ref:子组件可以通过defineExpose来进行暴露出数据/函数,父组件通过在子组件上面添加ref来获取自组件的数据/函数
  4. 透传 “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id

例子:

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
<button class="btn large">click me</button>

单根节点

// Parent.vue

<template>
  <Child msg="雷猴 世界!" name="鲨鱼辣椒" />
</template>

<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
</script>

// Child.vue

<template>
  <div>子组件:打开控制台看看</div>
</template>

多根节点:使用$attrs,来进行绑定

// Child.vue

<template>
  <div :message="$attrs.msg">只绑定指定值</div>
  <div v-bind="$attrs">全绑定</div>
</template>

  1. 插槽(默认插槽、具名插槽:子组件中的添加name、作用域插槽)

祖先传值

  1. provide/inject

遇到多层传值时,使用 propsemit 的方式会显得比较笨拙。这时就可以用 provideinject 了。

provide 是在父组件里使用的,可以往下传值。

inject 是在子(后代)组件里使用的,可以网上取值。

无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。

// Parent.vue

<template>
  <Child></Child>
</template>

<script setup>
import { ref, provide, readonly } from 'vue'
import Child from './components/Child.vue'

const name = ref('猛虎下山')
const msg = ref('雷猴')

// 使用readonly可以让子组件无法直接修改,需要调用provide往下传的方法来修改
provide('name', readonly(name))

provide('msg', msg)

provide('changeName', (value) => {
  name.value = value
})
</script>

// Child.vue

<template>
  <div>
    <div>msg: {{ msg }}</div>
    <div>name: {{name}}</div>
    <button @click="handleClick">修改</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const name = inject('name', 'hello') // 看看有没有值,没值的话就适用默认值(这里默认值是hello)
const msg = inject('msg')
const changeName = inject('changeName')

function handleClick() {
  // 这样写不合适,因为vue里推荐使用单向数据流,当父级使用readonly后,这行代码是不会生效的。没使用之前才会生效。
  // name.value = '雷猴'

  // 正确的方式
  changeName('虎躯一震')

  // 因为 msg 没被 readonly 过,所以可以直接修改值
  msg.value = '世界'
}
</script>

状态管理工具

  1. pinia
  2. vuex

pinia和vuex区别

PiniaVuex 相比有以下优点:

  • 调用时代码更简洁了。
  • TS 更友好。
  • 合并了 VuexMutationAction 。天然的支持异步了。
  • 天然分包。

其他的库

  1. mitt.js 官方示例

生命周期

image.png

路由

路由懒加载

  1. 箭头函数 + import
  2. 箭头函数 + require
const router = new Router({
    routes:[
        {
            path:"/list",
            component:resolve=>require([""],reslove)
        }
    ]
})

路由的hash和history

hash模式

vue-router默认的路由模式:example.com/#/about 像这样的带一个#号的,这个#号在浏览器能看到,但是请求接口的时候不会携带。

Hash 模式基于浏览器的 window.location.hash 属性。当 URL 中的哈希部分发生变化时,Vue Router 会监听到这个变化,并相应地切换视图。

优点:

  1. 兼容性好
  2. 不需要服务器进行支持:刷新页面或直接访问某个路由时,服务器需要正确处理这个路由。

劣势:

  1. 不够美观
  2. SEO不友好:搜索引擎不会将哈希部分的内容作为独立的页面来处理。

开启方式

import { createRouter, createWebHashHistory } from 'vue-router'
// 省略...
// 创建路由 const router = createRouter({
    history: createWebHashHistory(),
    // routes: routes 的缩写 
    routes,
 })
 // 省略...

history模式

它是传统的路由分发模式,当输入一个url,服务器就会接受这个请求,并且进行解析这个url,然后做出相应的逻辑处理

原理:History 模式使用浏览器的 History API,通过修改浏览器的历史记录来实现前端路由的切换。在这种模式下,需要确保在任何路径下都返回同一个 HTML 文件,以便 Vue Router 能够正确处理路由。

优势:

  1. 美观
  2. SEO友好

劣势:

  1. 需要服务器支持:刷新页面或直接访问某个路由时,服务器需要正确处理这个路由。
  2. 兼容性差

开启方式

import { createRouter, createWebHistory } from 'vue-router' 
// 省略... 
// 创建路由
const router = createRouter({ 
    history: createWebHistory(),
    // routes: routes 的缩写 
    routes, 
 }) 
 // 省略...

api:分为两大类修改历史状态和切换历史状态

  1. 修改历史状态:history.pushState()history.replaceState(),这两个方法应用于浏览器的历史记录站中,提供了对历史记录的修改,当以需要改变url但是不对页面进行刷新,可以用他们
  2. 切换历史状态:forword,back,go对应浏览器的前进,后退和跳转

监听hash的变化

setup(props, ctx: SetupContext) {
    // 监听hash变化刷新页面,更换头部导航颜色
    const hashChange = () => {
      window.addEventListener(
        'hashchange',
        () => {
          console.log('hash变化');
        },
        false
      );
    };
    hashChange();
}

window.location.hash来读取#值(可读可写)

route和router

useRoute来获取路由的信息对象,path、fullPath、params、hash、query、name

useRouter是路由实例,包含对路由的跳转方法,push,replace

动态定义路由

const User = {
  template: '<div>User</div>',
}

// 这些都会传递给 `createRouter`
const routes = [
  // 动态字段以冒号开始
  { path: '/users/:id', component: User },
]

image.png

可以通过route.param来进行获取

监听路由的变化

const User = {
  template: '...',
  created() {
    this.$watch(
      () => this.$route.params,
      (toParams, previousParams) => {
        // 对路由变化做出响应...
      }
    )
  },
}

捕获404和找不到路由

const routes = [
  // 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
  { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
  // 将匹配以 `/user-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
  { path: '/user-:afterUser(.*)', component: UserGeneric },
]

vue-router路由跳转和location.href区别

  • location.href来跳转会刷新页面
  • vue-router跳转,使用了diff算法,实现了按需加载,减少了dom的消耗

router里的路由守卫

  1. 全局路由守卫:router.beforeEach、router.afterEach(它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。)、router.beforeResolve(解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。)
router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})
  1. 路由独享守卫:beforeEnter
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

beforeEnter 守卫 只在进入路由时触发,不会在 paramsquery 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在 从一个不同的 路由导航时,才会被触发。

  1. 组件内的导航:
  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
const UserDetails = {
  template: `...`,
  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from) {
    // 在导航离开渲染该组件的对应路由时调用
    // 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
  },
}

beforeRouteEnter可以传递一个回调拿到实例

beforeRouteEnter (to, from, next) {
  next(vm => {
    // 通过 `vm` 访问组件实例
  })
}

这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。

beforeRouteLeave (to, from) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (!answer) return false
}

使用的是组合式api的话,你可以通过 onBeforeRouteUpdate 和 onBeforeRouteLeave 分别添加 update 和 leave 守卫。

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

pinia

这一块准备后面写pinia源码,敬请期待

Vue3

相较于vue2的优化

  1. 使用proxy来代替Object.defineProperty
  2. 不仅仅能够监测属性,还能监测对对象,因为proxy本身就是代理的对象本身。
  3. 作用域插槽,vue2导致作用域插槽变了之后,父组件也会相应的重新渲染。vue3把作用域插槽改为函数模式,这样就只会下个子组件的渲染。
  4. 对象式的组件声明方式:
  • vue2是通过声明的方式传入一系列的option,和typescript的结合需要通过一些装饰器来做
  • vue3修改了组件的声明方式,改为了类式的写法,使得typescript的结合更加容易
  1. 基于tree shaking优化,提供了更多的内置功能。

defineProperty和proxy区别

  • defineProperty是对属性的监测,proxy是对整个对象的代理
  • defineProerty:对于添加或删除属性的时候,Vue是无法进行检测到的,因为添加或者删除的对象没有在初始化进行响应式处理,只能通过$set来调用Object.defineProerty()处理
  • 无法监听到数组下标和长度的变化
  • 但这些proxy可以进行解决。

Composition API和 React Hook的区别

React Hook

因为React Hook是根据useState的调用顺序来确定下一次重新渲染的state是来源于哪个useState,出现了下面的限制

  • 不能在循环、条件、嵌套函数中调用Hook
  • 必须确保总是在你的react函数顶层调用Hook
  • useEffect和useMemo需要手动的确定依赖

Composition API

它的设计思想是借鉴的react的

  • 声明在setup函数里面的,一次组件实例化只会调用一次setup,而react hook每次渲染都需要重新调用Hook。
  • composition api的调用不需要考虑顺序,可以在循环、条件、嵌套函数中使用
  • 响应式系统自动实现依赖收集,进而组件的性能优化是由vue内部来进行维护的,react hook必须手动传入依赖,而且保证依赖顺序,否则可能会因为依赖的不正确使得组件性能下降。

虚拟DOM