Vue
watch 和 computed 和 method 的区别
思路:先翻译单词,再阐述作用,最后强行找不同。
- computed 和 methods 相比,最大区别是 computed 有缓存:如果 computed 属性依赖的属性没有变化,那么 computed 属性就不会重新计算。methods 则是看到一次计算一次。
- watch 和 computed 相比,computed是计算属性,watch是侦听器;computed 是计算出一个属性,它会根据你所依赖的数据动态显示新的计算结果。计算结果会被缓存,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算,而 watch 是当侦听的 data 的数据变化,执行回调,即可能是做别的事情(如上报数据)
vue的data为什么是一个方法
因为组件是用来复用的,而对象是引用类型,如果组件中的data是对象的话,组件被多次复用就会创建多个实例,那么多个实例中的数据会互相影响且冲突;
如果data是一个方法的话,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的data属性就不会互相影响。
Vue生命周期函数
每个组件都有生命周期,一旦一个组件被创建出来,就会在对应时机回调对应钩子函数
vue2中:11个
请求放在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钩子中
ps:其实每个钩子都可以请求数据,只是看需求
Vue 如何实现组件间通信
参考:https://juejin.cn/post/6887709516616433677#heading-20
父子组件:
- 使用
props/$emit
通过监听和触发事件来通信- 子组件使用 props 用于接收来自父组件通过v-bind传递的数据
- 子组件使用 $emit 来触发父组件v-on在子组件上绑定相应事件的监听
v-slot
- 父向子传值:父组件通过{{ message }}将父组件的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需要的参数是一致的
其中:
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>
vue3里的ref和reactive
ref 可以理解为 针对 基本数据类型,但会封装为 响应式对象
reactive 可以理解为 针对 对象,一定程度上可以替代ref
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中:
<ul>
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">
{{ user.name }}
</li>
</template>
</ul>
注意:使用template标签则不会在页面创建DOM,使用div标签则会创建DOM
特殊:若要筛选子元素,则用计算属性,来返回过滤或排序后的数组或对象
在计算属性不适用的情况下 (例如,在嵌套的 v-for 循环中) 可以使用方法:
Vuex
当项目庞大以后,在多人维护同一个项目时,如果使用事件总线进行全局通信,容易让全局的变量的变化难以预测。于是有了Vuex的诞生
Vuex 是一个专为 Vue.js 应用程序开发的状态管理工具。
五个核心概念,分别是什么,怎么用
State/Getter/Mutation/Action/Module
Vuex是Vue项目的全局状态管理工具,它有几个核心概念分别是:store:存储状态的容器;state:状态;mutation:同步改变状态的方法;getter:store的计算属性;action:异步改变状态的方法;Module:将 store 进行模块化管理。
复杂——更容易维护
简单——更容易理解
Vue-router
是vue.js官方的路由管理器,适用于构建单页应用。
重定向和别名
重定向可用于设置默认路径,当用户访问根目录 / 的时候重定向到 home首页 可使用 path:'/',redirect :'/home'
将 / 别名为 /home,意味着当用户访问 /home 时,URL 仍然是 /home,但会被匹配为用户正在访问 /
const routes = [{ path: '/', component: Homepage, alias: '/home' }]
注意:嵌套路由中设置默认路径则path为空即 path:''
hash模式和history模式
区别
- 一个用的hash,一个用的html5的history API
- 一个不需要后端nginx配合(把所有的html请求都重定向到index),一个需要
vue-router 默认 hash 模式,若要修改为history模式:
vue2中:
const router = new VueRouter({
mode: 'history',
routes: [...]
})
vue3中:
createRouter({
history: createWebHistory(),
// 其他配置...
})
//大多数 web 应用程序都应该使用 createWebHistory,但它要求正确配置服务器。
你还可以使用 createWebHashHistory 的基于 hash 的历史记录,
它不需要在服务器上进行任何配置,但是搜索引擎根本不会处理它,在 SEO 上表现很差
实际上是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面
嵌套路由
要点:
作用:监听路由跳转的过程,在跳转的过程(进入路由、离开路由)做一些操作
全局守卫
全局前置守卫
应用场景:实现登录控制
router.beforeEach((to,from,next) => {
if(to.path === '/login') return next() //前往登录页面直接放行
if(to.path === '受控页面' && 没有登录) next('/login') //需要登录信息并且没有登录
next() //不需要登录的
})
假如:目前页面只有一个title定义在index.html中,我们的需求是页面跳转到对应组件,则title渲染对应组件的title,如何做?
解决方法:
方法一:(页面较多的情况下不易维护)
每个组件都有生命周期,在组件内部声明钩子,在对应时机则会自动回调对应钩子函数,传入document.title = ‘xxx’
方法二:
导航守卫
1、在对应组件route中配置meta元信息
注意:嵌套路由中只需要在最外层配置meta,并且在matched[0]拿到
2、利用导航守卫router.beforeEach
获取路由的值:
console.log(this.$route.metched)可以拿到当前路由对象中一系列匹配路径的所有参数对象
注意:next()必须要在前置守卫函数内部调用!
执行效果依赖next方法的调用参数
next()
默认进行管道中的下一个钩子next(false)
终端当前导航next('/') 或者 next({path:'/'})
跳转到指定地址,常用于用户未登录进行跳转的应用场景
全局后置钩子
路由独享守卫
组件内的守卫
补充:路由元信息
将任意信息附加到路由上,如过渡名称、谁可以访问路由等。这些事情可以通过接收属性对象的meta属性来实现,并且它可以在路由地址和导航守卫上都被访问到。定义路由的时候你可以这样配置 meta 字段:
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
]
一个路由匹配到的所有路由记录会暴露为 $route 对象(还有在导航守卫中的路由对象)的$route.matched 数组。
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
})
keep-alive
保留 路由跳转间组件的状态
因为每次路由跳转,即切换不同组件实际上 是销毁和创建新的组件的过程
keep-alive是vue内置的组件
它有两个非常重要的属性:
- include 字符串或正则表达式,包含
- exclude 字符串或正则表达式,不包含
以下两个函数,只有当该组件被保持了状态使用了keep-alive时候,才是有效的,即<keep-alive><router-view/></keep-alive>
路由懒加载
vue router 打包文件
当打包构建应用时,JavaScript包会变得非常大,影响页面加载
如果能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,就更高效了
因为包过大的话,从服务器获取包的过程就变长了,客户端会出现短暂空白页面
路由懒加载即用到对应组件的时候再从服务器获取对应组件
主要作用是将路由对应的组件打包成一个个的js代码块
只有在这个路由被访问到的时候,才加载对应的组件
说出常用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等
hash和history模式实现原理
路由配置一般有两种模式,一是hash模式,一是html5的history模式
路由跳转之间传递参数
需要传递大量数据则用query对象
编程式路由也是传递query对象
query 和params区别
路由组件传参
布尔模式:在index路由文件中配置对应routes路由的属性,在需要组件传参的routes上添加props:true
虚拟DOM和DOM diff
前置:DOM操作慢?这是相对而言的
- vue是基于DOM的库,它是利用DOM操作DOM的,如果直接操作DOM,那DOM比Vue更快
- JS用时是很快的,只不过浏览器在渲染页面的时候让页面不可交互
- 规模小的时候虚拟DOM是快的,但规模大的时候虚拟DOM开销也很大
虚拟DOM
概念:
一个能代表DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素们,以及其他属性
React和Vue中的虚拟DOM:
如何创建虚拟DOM
本质上:
实际应用上:
React通过JSX,然后通过babel转化为createElement的形式
Vue通过Template,然后通过vue-loader转化
虚拟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树的对比
<div :class="x">
<span v-if="y">string1</span>
<span>string2</span>
</div>
当数据变化时:
DOM diff:
标签类型不变,只需更新div对应的DOM属性
子元素不变,不更新
当数据变化时:
DOM diff:
div不变不用更新
子元素1标签不变,但children变了,更新DOM内容
子元素2不见了,删除对应的DOM
总结:
DOM diff 就是一个函数,称之为patch打补丁
patches = patch(oldVNode,newVNode)输入新旧节点,输出DOM操作
patches 就是要运行的DOM操作,可以长这样:
[
{type:'INSERT',vNode:...},
{type:'TEXT',vNode:...},
{type:'PROPS',propsPatch:[...]}
]
diff 逻辑
不一定每个节点都要diff,先diff根节点(上层节点),若上层节点已经不同,则直接替换
一个页面的节点大概是 两千到一万之间
- Tree diff
将新旧两棵树逐层对比,找出哪些节点需要更新
如果节点是组件就看 Component diff
如果节点是标签就看 Element diff
- Component diff
如果节点是组件,就先看组件类型
类型不同直接替换(删除旧的)
类型相同则diff属性
若组件嵌套,则继续 Tree diff(递归)
- Element diff
如果节点是原生标签,则只看标签名
标签名不同直接替换,相同则diff属性
若标签嵌套,则继续 Tree diff(递归)
DOM diff问题
同级节点对比存在BUG,会出现识别错误的问题,如:
当数据变化时:
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中