Vue

watch 和 computed 和 method 的区别

思路:先翻译单词,再阐述作用,最后强行找不同。

  • computed 和 methods 相比,最大区别是 computed 有缓存:如果 computed 属性依赖的属性没有变化,那么 computed 属性就不会重新计算。methods 则是看到一次计算一次。
  • watch 和 computed 相比,computed是计算属性,watch是侦听器;computed 是计算出一个属性,它会根据你所依赖的数据动态显示新的计算结果。计算结果会被缓存,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算,而 watch 是当侦听的 data 的数据变化,执行回调,即可能是做别的事情(如上报数据)
    • 如果一个数据依赖于其他数据,那么把这个数据设计为computed的
    • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化,在某些时机执行回调

      computed以及里面的getter和setter

vue的data为什么是一个方法

因为组件是用来复用的,而对象是引用类型,如果组件中的data是对象的话,组件被多次复用就会创建多个实例,那么多个实例中的数据会互相影响且冲突;
如果data是一个方法的话,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的data属性就不会互相影响。

Vue生命周期函数

每个组件都有生命周期,一旦一个组件被创建出来,就会在对应时机回调对应钩子函数

vue2中:11个
image.png
请求放在mounted中比较合适,放在updated中容易造成死循环,因为请求了会去更新,更新了则会再次请求
放在created中的话,ssr会在后端服务器上请求一次created,然后ssr还会把组件放在前端,前端再执行一次created

有哪些、什么时候、在哪里请求数据、有什么作用
组件在初始化或者数据更新时会触发的钩子函数
可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子
beforecreate:数据绑定之前,el 和 data 并未初始化均为undefined
created:完成了 data 数据的初始化且可获取其数据,el没有初始化(常用获取数据)
beforemount:完成了el初始化,但data并没有挂载完成,数据已经渲染好了,但还没被挂载到页面上
mounted:完成了挂载(常用对数据的操作、DOM访问)
beforeupdate
updated
beforedestroy
destoryed
Vue.nextTick( [callback, context] ):在数据改变之后,会在模板渲染之后尽快执行,可放在created钩子中
Vue技术栈面试题集 - 图2
ps:其实每个钩子都可以请求数据,只是看需求

Vue 如何实现组件间通信

参考:https://juejin.cn/post/6887709516616433677#heading-20
父子组件:

  • 使用props/$emit通过监听和触发事件来通信
    • 子组件使用 props 用于接收来自父组件通过v-bind传递的数据
    • 子组件使用 $emit 来触发父组件v-on在子组件上绑定相应事件的监听
  • v-slot
    • 父向子传值:父组件通过将父组件的message值传递给子组件
    • 子组件通过接收到相应内容,实现了父向子传值。

爷孙组件:

  • 使用两次父子组件通信实现
  • 使用 provide + inject

任意组件:

  • 使用 eventBus = new Vue() 来通信,eventBus.$on 和 eventBus.$emit 是主要API
    • 一般将eventBus声明到单独文件中,通过注册一个新的Vue实例,通过调用这个实例的$emit和$on等来监听和触发这个实例的事件,通过传入参数从而实现组件的全局通信。
    • 它是一个不具备 DOM 的组件,有的仅仅只是它实例方法而已,因此非常的轻便。
    • 缺点是事件多了容易乱,难以维护
  • 使用 Vuex 通信(vue3可以使用pinia嗲提vuex)

    EventBus原理

    使用订阅-发布模式,实现$emit和$on两个方法即可

Vue2和Vue3的响应式数据原理

(Proxy使用过吗?具体如何实现的?)
双向绑定 即问 v-model / .sync
v-model要点:

  • 它是v-bind:value 和 v-on:input 的语法糖,即绑定value和监听input事件
    • v-bind:value 实现了data => UI 的单向绑定
    • v-on:input 实现了 UI => data 的单向绑定

那这两个单向绑定是如何实现的?

  • data到UI的方向是 通过Object.defineProperty 给data的每一个属性 递归地创建getter 和setter,用于监听data地改变,data一变化就会安排改变UI
  • UI 到 data的方向 是 通过 template compiler 给DOM元素添加事件监听,vue的监听都是事件委托,监听器放在根元素上,只要DOM元素发生变化就会去修改data

问 双向绑定原理 即问 数据响应式原理
以下讲解数据响应式原理
要点是 数据劫持+ 发布订阅模式(即通知)
Object.defineProperty相对数据已经流通了,但缺少一个通知的机制

vue2(Object.defineProperty)

要点:
使用 Object.defineProperty 把这些属性全部转为 getter/setter
Vue 不能检测到对象属性的添加或删除,解决方法是手动调用 Vue.set 或者 this.$set(Vue.set是怎么用的?)

vue3(Proxy 和 Reflect)

Proxy 对象用于创建一个对象的代理,从而对象拦截操作
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法,这些方法和Proxy handlers相同,即它需要的参数和handler需要的参数是一致的
image.png

其中:
return Reflect.get(…arguments) 等价于 return target[props]
return Reflect.set(…arguments) 等价于 return target[key] = value

区别

proxy对对象的代理是全权的代理,任何属性的新增和修改都能接管
而Object.defineProperty只能接管单个对象和其指定属性,如果想要接管对象的所有属性则需要遍历

vue3为什么适用Proxy?
Object. defineProperty缺点:
动态创建的data属性 需要用Vue.set 来赋值,Vue3用Proxy 直接代理了这个对象
无法监控到数组下标及数组长度的变化,当直接通过数组的下标给数组设置值或者改变数组长度时,不能实时响应,Vue2篡改了数组的7个API,而Vue3 用Proxy 可以完全对数组的访问做控制;
性能问题,当data中数据比较多且层级很深的时候,defineProperty需要提前(组件创建的时候)递归地遍历data做到数据响应式,而Proxy可以真正用到深层数据的时候再做响应式(惰性)

对比区别:
Object.defineProperty只能劫持对象的属性,对新增属性需要手动进行 Observe,而 Proxy 是直接代理对象
为什么 Proxy 可以解决以上的痛点呢? 本质的原因在于 Proxy 是一个内置了拦截器的对象,所有的外部访问都得先经过这一层拦截。不管是先前就定义好的,还是新添加属性,访问时都会被拦截(proxy具体学习请看阮一峰老师的ES6教程 Proxy)

Vue3为什么使用组合式API

vue2中:

  • 代码的可读性问题:少量逻辑代码时使用vue2的选项API,因为非常便于阅读和理解,但当组件功能复杂,代码过多时,可读性变差。
  • 逻辑复用的问题: 相同的代码逻辑分散在组件各个地方,逻辑关注点分散
  • TS 相关问题: 对 TypeScript 的支持并不友好

Vue3对比Vue2做了哪些改动

createdApp() 代替了new Vue()
v-model 代替了以前的v-model 和 .sync
根元素可以不止一个元素
新增 Teleport传送门、动态组件和组件缓存
生命周期中 destroyed 被改名为 unmounted,beforeDestory 被改为beforeUnmounted
ref 属性支持函数了

teleport组件:可以将组件中的内容传送到另一个DOM元素上,
应用场景:组件模板的技术逻辑一部分属于该组件,但在视觉逻辑上将其移动到其他位置更合适,比如模态框、对话框、下拉框等需要通过z-index设置层级关系,
<Teleport to="body">...(要被传送的内容)</Teleport>
image.png

vue3里的ref和reactive

ref 可以理解为 针对 基本数据类型,但会封装为 响应式对象
reactive 可以理解为 针对 对象,一定程度上可以替代ref

toRef 单数 ;将普通
toRefs 复数

Reflect 和 reactive 区别

Reflect 动词 反射
reactive 形容词 响应式的

Reflect 用于:在创建代理proxy的时候内置的一个对象,上面有set和get方法
reactive 用于: 针对 对象 ,返回对象的响应式副本

v-for 列表渲染

v-for为什么需要key

v-for 默认使用“就地更新”策略,若数据项的顺序被改变,Vue不会移动DOM元素来匹配数据项的顺序,而是就地重新更新每个元素,即数据项顺序的改变,DOM仍然会全部渲染一次
为能够跟踪每个DOM节点得到身份,重用和重新正确排序现有元素,提升性能,需要使用key值作为唯一标识

v-for 和 v-if

在vue2中v-for比v-if优先级高
本质:v-for优先执行,会创建对应的dom节点,如果v-if为false,会删除这个dom节点;这样创建后再删除,引起布局回流,造成页面卡顿。

在vue3中v-if比v-for优先级高,意味着 v-if 将没有权限访问 v-for 里的变量
总之不推荐同时在同一标签上使用,可以改为 嵌套使用

解决方法:
针对:“全部子内容遍历展示与否”:
vue2中:
外面用v-if包裹
vue3中:

  1. <ul>
  2. <template v-for="user in users" :key="user.id">
  3. <li v-if="user.isActive">
  4. {{ user.name }}
  5. </li>
  6. </template>
  7. </ul>

注意:使用template标签则不会在页面创建DOM,使用div标签则会创建DOM
特殊:若要筛选子元素,则用计算属性,来返回过滤或排序后的数组或对象
image.png
在计算属性不适用的情况下 (例如,在嵌套的 v-for 循环中) 可以使用方法:
image.png

Vuex

当项目庞大以后,在多人维护同一个项目时,如果使用事件总线进行全局通信,容易让全局的变量的变化难以预测。于是有了Vuex的诞生
Vuex 是一个专为 Vue.js 应用程序开发的状态管理工具。
五个核心概念,分别是什么,怎么用

State/Getter/Mutation/Action/Module
Vuex是Vue项目的全局状态管理工具,它有几个核心概念分别是:store:存储状态的容器;state:状态;mutation:同步改变状态的方法;getter:store的计算属性;action:异步改变状态的方法;Module:将 store 进行模块化管理。
image.png
复杂——更容易维护
简单——更容易理解

Vue-router

是vue.js官方的路由管理器,适用于构建单页应用。

重定向和别名

重定向可用于设置默认路径,当用户访问根目录 / 的时候重定向到 home首页 可使用 path:'/',redirect :'/home'
将 / 别名为 /home,意味着当用户访问 /home 时,URL 仍然是 /home,但会被匹配为用户正在访问 /

  1. const routes = [{ path: '/', component: Homepage, alias: '/home' }]

注意:嵌套路由中设置默认路径则path为空即 path:''

hash模式和history模式

区别

  • 一个用的hash,一个用的html5的history API
  • 一个不需要后端nginx配合(把所有的html请求都重定向到index),一个需要

vue-router 默认 hash 模式,若要修改为history模式:
vue2中:

  1. const router = new VueRouter({
  2. mode: 'history',
  3. routes: [...]
  4. })

vue3中:

  1. createRouter({
  2. history: createWebHistory(),
  3. // 其他配置...
  4. })
  5. //大多数 web 应用程序都应该使用 createWebHistory,但它要求正确配置服务器。
  6. 你还可以使用 createWebHashHistory 的基于 hash 的历史记录,
  7. 它不需要在服务器上进行任何配置,但是搜索引擎根本不会处理它,在 SEO 上表现很差

实际上是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面

嵌套路由

image.png
要点:

  1. 路由配置信息中添加对应子路由,路由中可以添加children属性,是个数组;
  2. home组件中添加占位,即子路由渲染再router-view中,而标签可根据需求在哪点击则在哪个组件添加

    导航守卫

作用:监听路由跳转的过程,在跳转的过程(进入路由、离开路由)做一些操作

全局守卫

全局前置守卫

应用场景:实现登录控制

  1. router.beforeEach((to,from,next) => {
  2. if(to.path === '/login') return next() //前往登录页面直接放行
  3. if(to.path === '受控页面' && 没有登录) next('/login') //需要登录信息并且没有登录
  4. next() //不需要登录的
  5. })

假如:目前页面只有一个title定义在index.html中,我们的需求是页面跳转到对应组件,则title渲染对应组件的title,如何做?

解决方法:
方法一:(页面较多的情况下不易维护)
每个组件都有生命周期,在组件内部声明钩子,在对应时机则会自动回调对应钩子函数,传入document.title = ‘xxx’

方法二:
导航守卫
image.png
1、在对应组件route中配置meta元信息
注意:嵌套路由中只需要在最外层配置meta,并且在matched[0]拿到
image.png
2、利用导航守卫router.beforeEach
获取路由的值:
console.log(this.$route.metched)可以拿到当前路由对象中一系列匹配路径的所有参数对象
image.png
image.png
注意:next()必须要在前置守卫函数内部调用!
执行效果依赖next方法的调用参数

  • next()默认进行管道中的下一个钩子
  • next(false)终端当前导航
  • next('/') 或者 next({path:'/'})跳转到指定地址,常用于用户未登录进行跳转的应用场景

全局后置钩子

路由独享守卫

组件内的守卫

补充:路由元信息

将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。定义路由的时候你可以这样配置 meta 字段:

  1. const routes = [
  2. {
  3. path: '/posts',
  4. component: PostsLayout,
  5. children: [
  6. {
  7. path: 'new',
  8. component: PostsNew,
  9. // 只有经过身份验证的用户才能创建帖子
  10. meta: { requiresAuth: true }
  11. },
  12. {
  13. path: ':id',
  14. component: PostsDetail
  15. // 任何人都可以阅读文章
  16. meta: { requiresAuth: false }
  17. }
  18. ]
  19. }
  20. ]

一个路由匹配到的所有路由记录会暴露为 $route 对象(还有在导航守卫中的路由对象)的$route.matched 数组。

  1. router.beforeEach((to, from) => {
  2. // 而不是去检查每条路由记录
  3. // to.matched.some(record => record.meta.requiresAuth)
  4. if (to.meta.requiresAuth && !auth.isLoggedIn()) {
  5. // 此路由需要授权,请检查是否已登录
  6. // 如果没有,则重定向到登录页面
  7. return {
  8. path: '/login',
  9. // 保存我们所在的位置,以便以后再来
  10. query: { redirect: to.fullPath },
  11. }
  12. }
  13. })

keep-alive

保留 路由跳转间组件的状态
因为每次路由跳转,即切换不同组件实际上 是销毁和创建新的组件的过程
keep-alive是vue内置的组件
包裹动态组件时,会缓存不活动的组件实例,保留其状态,而不是销毁它们然后重新创建渲染。
它有两个非常重要的属性:

  • include 字符串或正则表达式,包含
  • exclude 字符串或正则表达式,不包含
  • image.png

是vue router内置的组件,若直接包在keep-alive里面,则其路径匹配到的视图组件都会被缓存

以下两个函数,只有当该组件被保持了状态使用了keep-alive时候,才是有效的,即
<keep-alive><router-view/></keep-alive>

image.png

路由懒加载

vue router 打包文件

当打包构建应用时,JavaScript包会变得非常大,影响页面加载
如果能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就更高效了
因为包过大的话,从服务器获取包的过程就变长了,客户端会出现短暂空白页面

路由懒加载即用到对应组件的时候再从服务器获取对应组件
主要作用是将路由对应的组件打包成一个个的js代码块
只有在这个路由被访问到的时候,才加载对应的组件
image.png
image.png

说出常用API:
router-link/router-view/this.$router.push/this.$router.replace/this.$route.params

$router 和 $route 的区别

$router是一个全局的实例对象,包含项目中所有的路由,想要导航到不同url则适用$router.push()方法
$route 是一个局部的路由对象,用于当前活跃的路由,表示当前的路由信息,包含url解析到的一些数据,有当前路径的hash query name等
image.png

hash和history模式实现原理

路由配置一般有两种模式,一是hash模式,一是html5的history模式
image.png

image.png
image.png
image.png

路由跳转之间传递参数

image.png
需要传递大量数据则用query对象
image.png
image.png
image.png
编程式路由也是传递query对象
image.png

query 和params区别

image.png

路由组件传参

布尔模式:在index路由文件中配置对应routes路由的属性,在需要组件传参的routes上添加props:true
image.png
image.png

虚拟DOM和DOM diff

前置:DOM操作慢?这是相对而言的

  • vue是基于DOM的库,它是利用DOM操作DOM的,如果直接操作DOM,那DOM比Vue更快
  • JS用时是很快的,只不过浏览器在渲染页面的时候让页面不可交互
  • 规模小的时候虚拟DOM是快的,但规模大的时候虚拟DOM开销也很大

    虚拟DOM

    概念:
    一个能代表DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性

React和Vue中的虚拟DOM:
image.png
image.png

如何创建虚拟DOM

本质上:
image.png
image.png
实际应用上:
React通过JSX,然后通过babel转化为createElement的形式
image.png
Vue通过Template,然后通过vue-loader转化
image.png

虚拟DOM优点

某些情况下,虚拟DOM快

  • 减少DOM操作:
    • 减少DOM操作的次数:虚拟DOM可以将多次操作合并为一次
    • 减少DOM操作的范围:借助DOM diff可以把重复多余的操作省掉
  • 跨平台:
    • 虚拟DOM不仅可以变成DOM,还可以变成小程序、IOS应用、安卓应用、因为虚拟DOM本质上只是一个JS对象

虚拟DOM缺点

需要额外的创建函数,React中用createElement,vue中用h函数,但可以通过JSX和vue template来简化写法,但严重依赖构建工具,如babel和vue loader

DOM diff

概念:
虚拟DOM的对比算法

把虚拟DOM想象成树形,即虚拟DOM树,DOM diff就是两颗虚拟DOM树的对比

  1. <div :class="x">
  2. <span v-if="y">string1</span>
  3. <span>string2</span>
  4. </div>

image.png

当数据变化时:
image.png
DOM diff:
标签类型不变,只需更新div对应的DOM属性
子元素不变,不更新

当数据变化时:
image.png
DOM diff:
div不变不用更新
子元素1标签不变,但children变了,更新DOM内容
子元素2不见了,删除对应的DOM

总结:
DOM diff 就是一个函数,称之为patch打补丁
patches = patch(oldVNode,newVNode)输入新旧节点,输出DOM操作
patches 就是要运行的DOM操作,可以长这样:

  1. [
  2. {type:'INSERT',vNode:...},
  3. {type:'TEXT',vNode:...},
  4. {type:'PROPS',propsPatch:[...]}
  5. ]

diff 逻辑

不一定每个节点都要diff,先diff根节点(上层节点),若上层节点已经不同,则直接替换
一个页面的节点大概是 两千到一万之间

  • Tree diff

将新旧两棵树逐层对比,找出哪些节点需要更新
如果节点是组件就看 Component diff
如果节点是标签就看 Element diff

  • Component diff

如果节点是组件,就先看组件类型
类型不同直接替换(删除旧的)
类型相同则diff属性
若组件嵌套,则继续 Tree diff(递归)

  • Element diff

如果节点是原生标签,则只看标签名
标签名不同直接替换,相同则diff属性
若标签嵌套,则继续 Tree diff(递归)

DOM diff问题

同级节点对比存在BUG,会出现识别错误的问题,如:
当数据变化时:
image.png
DOM diff:
div不变不用更新
子元素1标签不变,但children变了,更新DOM内容
子元素2不见了,删除对应的DOM
如上所示:实际上只删除了子元素1,但DOM diff会将子元素1更新为子元素2,然后删除右边子元素2

如v-if中key存在的必要,以及key为什么不能用index

v-if中的key

举例:
【1,2,3】
【1,3】
当我们不使用key或者key使用index(两者情况相同,因为默认情况就是把index作为key)
效果看似删除了2
但vue做了两件事:把2变成3,然后把3删除
使用index的情况:若删除了2,则3的index会往前移一位
因为计算机是通过遍历新旧两个虚拟节点来更新数组的,那如果在虚拟节点中标记了节点的唯一标识id,那DOM diff中