Vue 旧版

70 阅读6分钟

Vue3.x

Proxy响应式原理

image.png

image.png

image.png

image.png

image.png

shallowRef与shallowReactive的适用场景

image.png

watch与computed原理

image.png

image.png

image.png

image.png

watchEffect

image.png

image.png

image.png

image.png

vue3.0diff算法

Vue 3 的 Diff 算法(快速 Diff)通过 双端比较 + 最长递增子序列(LIS)优化,将时间复杂度从 O(n²) 降至 O(n log n),核心分为五步:预处理前置节点(头头比对)、预处理后置节点(尾尾比对)、处理仅有新增节点、处理仅有卸载节点、剩余节点存在新增、卸载、移动混合操作(LIS优化) image.png

image.png

image.png

image.png

image.png

setup的上下文和使用

<script setup>里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script> 只在组件被首次引入的时候仅执行一次不同,<script setup>中的代码会在每次组件实例被创建的时候执行,相当于beforeCreate,Created

image.png

image.png

setup,defineProps,defineEmit,defineSlots image.png

defineModel

image.png

defineExpose

image.png

defineOptions

image.png

与普通script一起用

image.png

顶层await支持

image.png

image.png

image.png

ref获取子组件实例的使用useTemplateRef

image.png

image.png

获取父组件实例

image.png

image.png

Vue通用

v-model双向绑定使用和原理defineModel
  • vue中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。默认情况下相当于:value@input
  • 通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。v-model用在自定义组件上时又会有很大不同,vue3中它类似于sync修饰符(:visible.sync="isVisible"),最终展开的结果是:modelValue属性和@update:modelValue事件;vue3中我们甚至可以用参数形式指定多个不同的绑定,例如v-model:foo和v-model:bar,非常强大!

image.png

image.png

nextTick的原理及vue异步更新

image.png

image.png

image.png

vue性能优化技巧

按需加载、动态绑定、内存缓存、异步处理 image.png

image.png

透传 Attributes
  • “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id
  • 当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
  • 如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并
  • v-on事件监听器click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
  • 禁用 Attributes 继承,件选项中设置 inheritAttrs: false。透传进来的 attribute 可以在模板的表达式中直接用 $attrs,这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 classstylev-on 监听器等等;在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute。
组件通信常用方式
  • 父子组件,props/$emit/$parent/ref/$attrs
  • 兄弟组件,$parent/$root/eventbus(emit,emit,on,$off)/vuex
  • 跨层级关系,eventbus/vuex/provide+inject

image.png

image.png

provide和inject依赖注入
// 提供方
<script setup> 
  import { provide, ref } from 'vue'
  const location = ref(0)
  function updateLocation() { 
    location.value = 'South Pole' 
  }
  provide('location', { location, updateLocation }) 
</script>

// 注入方
<script setup> 
  import { inject } from 'vue' 
  const { location, updateLocation } = inject('location')
</script>
  • 一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
  • 供的值是一个 ref,注入进来的会是该 ref 对象,这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
pinia的使用

image.png

image.png

image.png

image.png

image.png

如果子组件改变props里的数据会发生什么
  • 如果修改的是基本类型,则会报错vue warning,prop 是只读的!
  • 当对象或数组作为props被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失,并且父级数据会跟着变。改变引用类型地址也会报错。
自定义指令

image.png

image.png

<script setup> 
const vFocus = {
   // 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用(binding传递给指令的值)
   mounted: (el, binding, vnode, prevVnode) => el.focus() 
   // 在绑定元素的父组件及他自己的所有子节点都更新后调用(binding传递给指令的值)
   updated: (el, binding, vnode, prevVnode) => {}
} 
</script> 

<template> <input v-focus /> </template>

// 将一个自定义指令全局注册到应用层级也是一种常见的做法
const app = createApp({}) 
// 使 v-focus 在所有组件中都可用 
app.directive('focus', { /* ... */ })
  • 在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令,vFocus 即可以在模板中以 v-focus 的形式使用。
  • 在没有使用 <script setup> 的情况下,export default时自定义指令需要通过 directives 选项注册。
  • 将一个自定义指令全局注册,app.directive('focus', { /* ... */ });
  • 当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传attrs(传入当前组件的所有属性和v-on事件)类似,和 attribute 不同,指令不能通过 v-bind="$attrs" 来传递给一个不同的元素。
  • v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
  • 页面权限,配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,通过路由守卫要求用户登录后获取用户角色,根据角色过滤出路由表,通过router.addRoutes(asyncRoutes)方式动态添加路由即可。
  • 按钮权限实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
组合式函数
// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  watchEffect(() => {
    // 在 fetch 之前重置状态
    data.value = null
    error.value = null
    // toValue() 将可能的 ref 或 getter 解包,规范化为值
    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  })

  return { data, error }
}

<script setup> 
    import { ref, watchEffect, toValue } from 'vue'
    import { useFetch } from './fetch.js' 
    const url = ref('/initial-url') 
    const { data, error } = useFetch(url) 
    // 这将会重新触发fetch 
    url.value = '/new-url'
</script> 
  • 这个版本的 useFetch() 现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪 toValue(url) 期间访问的任何依赖项。如果没有跟踪到依赖项(例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。
  • 每一个调用 useFetch() 的组件实例会创建其独有的 dataerror 状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读状态管理这一章。
  • 我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性
  • 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:onMounted()确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,确保在 onUnmounted() 时清理副作用。
  • 组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,将生命周期钩子,1. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
  • <script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
递归组件
<template>
  <li>
    <div> {{ model.name }}</div>
    <ul v-show="isOpen" v-if="isFolder">
      <!-- 注意这里:组件递归渲染了它自己 -->
      <TreeItem
        class="item"
        v-for="model in model.children"
        :model="model">
      </TreeItem>
    </ul>
  </li>
<script>
export default {
  name: 'TreeItem',
  // ...
}
</script>
异步组件
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
  // 加载函数返回Promise
  return new Promise((resolve, reject) => {
    // ...可以从服务器加载组件
    resolve(/* loaded component */)
  })
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent({
    loader: () => import('./components/MyComponent.vue'),
    loadingComponent: LoadingComponent, // 加载异步组件时使用的组件 
    delay: 200, // 展示加载组件前的延迟时间,默认为 200ms 
    errorComponent: ErrorComponent, // 加载失败后展示的组件 
    timeout: 3000
})
  • 不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。(首次加载页面组件不会加载异步组件,分开打包,分割应用为更小的块,并且在需要组件时再加载)
  • defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
Suspense组件

image.png

image.png

image.png

插槽的使用以及原理
  • 具名插槽,v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header> image.png
  • 作用域插槽,插槽的内容可能想要同时使用父组件域内和子组件域内的数据
  • 原理在插槽函数slot调用时传入 props,MyComponent类比成函数调用,接收参数,v-slot="slotProps" 可以类比这里的函数签名,和函数的参数类似。

image.png

Vue的生命周期,讲一讲?

image.png

image.png

image.png

不改变子组件,父组件如何监听子组件的生命周期

image.png

父子组件生命周期顺序

image.png

image.png

说说你对虚拟 DOM 的理解
  • 将真实元素节点抽象成VNode的一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构,保存在内存中

image.png

image.png

image.png

Vue的模板编译原理

image.png

image.png

image.png

KeepAlive组件怎么缓存当前的组件?缓存后怎么更新?

image.png

image.png

image.png

image.png

  • LRU-使用map数据结构(先删除再设置),每当get和set操作map数据时,先判断map.has(key),map.delete(key),map.set(key, temp),当map.size大于固定长度时,删除前面多余的this.map.delete(this.map.keys().next().value)
Teleport组件

image.png

image.png

image.png

MVVM是什么?和MVC有何区别呢?
  • Model(模型):负责从数据库中取数据
  • View(视图):负责展示数据的地方
  • Controller(控制器):用户交互的地方,例如点击事件等等。思想:Controller将Model的数据展示在View上
  • VM:也就是View-Model,【视图】和【模型】之间数据的双向绑定。思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,对应 View 层显示会自动改变。
为什么data是个函数并且返回一个对象呢?
  • data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染
不需要响应式的数据应该怎么处理?
  • Object.freeze()
Vue常用的修饰符有哪些有什么应用场景

表单修饰符:lazy(光标离开标签的时候,才会将值赋予给value) trim number;

事件修饰符:stop prevent self(只当在 event.target 是当前元素自身时触发处理函数) once(绑定了事件以后只能触发一次,第二次就不会触发)capture(使事件触发从包含这个元素的顶层开始往下触发) passive(相当于给onscroll事件整了一个.lazy修饰符) native(让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件)

image.png

Vue中的过滤器了解吗?过滤器的应用场景有哪些?

image.png

v-if和v-show有何区别
  • v-if是通过控制dom元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期
  • v-show是通过控制dom元素的css样式来实现显隐,不会销毁
  • 频繁或者大数量显隐使用v-show,否则使用v-if
为什么v-if和v-for不建议用在同一标签?
  • 永远不要把 v-if 和 v-for 同时用在同一个元素上

image.png

image.png

image.png

为什么不建议用index做key,为什么不建议用随机数做key?
  • index:或用index拼接其他值作为key,改变列表的顺序就无法复用(key相同,type不同)
  • 随机数:改变列表的顺序就无法复用(key不相同)
  • 正确用法 :用唯一值id做key
相同的路由组件如何重新渲染?
  • 改变组件的key即可
Vue组件为什么只能有一个根元素
  • vue2中组件确实只能有一个根,因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历
  • vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。
vue-loader是什么?它有什么作用
  • vue-loader是用于处理单文件组件(SFC,Single-File Component),vue-loader被执行时,它会对SFC中的template、script和style每个语言块用单独的loader链处理。vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,最后将这些单独的块装配成最终的组件模块。
  • 必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块
  • SFC 中的 <style> 标签一般会在开发时注入成原生的 <style> 标签以支持热更新,而生产环境下它们会被抽取、合并成单独的 CSS 文件。
  • Vite 提供 Vue SFC 支持的官方插件@vitejs/plugin-vue
// source.vue被vue-loader处理之后返回的代码// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
​
script.render = render
export default script
  • script lang="ts",webpack会展开成import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
  • style scoped lang="scss",webpack会展开成import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • template lang="pug",webpack会展开成import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'需要用Vue 模板编译器编译template,从而得到render函数
审查元素时发现data-v-xxxxx
  • 这是在标记vue文件中style时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记
  • vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现的。PostCSS 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,然后,给 CSS 选择器额外添加一个对应的属性选择器。
  • scoped内如何实现样式穿透的
    • ::v-deep 操作符( >>> 的别名),.a >>> .b { /* ... */ } 编译成.a[data-v-f3f3eg9] .b { /* ... */ }
    • 定义一个含有 scoped 属性的 style 标签之外,再定义一个不含有 scoped 属性的 style 标签
插件
// plugins/i18n.js
export default {
  install: (app, options) => {
  // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
    // 插件中的 Provide / Inject
    app.provide('i18n', options)
  }
}
//
import { createApp } from 'vue' 
const app = createApp({})
import i18nPlugin from './plugins/i18n' 
app.use(i18nPlugin, {
    // options
    greetings: { hello: 'Bonjour!' } 
})
//
<h1>{{ $translate('greetings.hello') }}</h1>
  • 通过app.component()app.directive()注册一到多个全局组件或自定义指令。
  • 通过 app.provide()使一个资源可被注入进整个应用。
  • app.config.globalProperties中添加一些全局实例属性或方法
高阶组件

image.png

vue中如何扩展一个组件

image.png

vue-router原理
  • router-link默认生成一个a标签,设置to属性定义跳转path。原理阻止a标签默认事件,调用的是navigate方法,avigate内部依然调用router.push()。
  • router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系。原理上是在router-view组件内部判断当前router-view处于嵌套层级的深度,讲这个深度作为匹配组件数组matched的索引,获取对应渲染组件,渲染之。(默认0,加1之后传给后代,同时根据深度获取匹配路由)

image.png

  • vue-router有3个模式,其中history和hash更为常用。hash模式使用和部署简单,但是不会被搜索引擎处理,seo有问题;history模式则建议用在大部分web项目上,但是它要求应用在部署时做特殊配置,服务器需要做回退处理,否则会出现刷新页面404的问题。

  • 路由守卫有三个级别:全局beforeEach,路由独享beforeEnter,组件级beforeRouteEnter。原理用户的任何导航行为都会走navigate方法,内部有个guards队列,runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。

  • 从头开始实现一个简单的路由

<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'
const routes = {
  '/': Home,
  '/about': About
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
  currentPath.value = window.location.hash
})
const currentView = computed(() => {
  return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>
<template>
  <a href="#/">Home</a> |
  <a href="#/about">About</a> |
  <a href="#/non-existent-path">Broken Link</a>
  <component :is="currentView" />
</template>
TypeScript

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png