说说你对Vue的理解
- MVVM:VM层相当于MVC中的C层中的一部分特定职能的分离,即实时监控和读取数据层的修改并将其映射到视图层,所以VM层专干数据驱动视图的活;
- 响应式:与VM本质相同提法有区别,还是数据驱动视图,声明响应式的数据,当数据变化时自动驱动视图更新;
- 组件化开发:将页面分割成可以独立复用的小块(组件),每个组件都有有自己的模板、样式、数据与逻辑,主要目的是为了方便复用;
- 虚拟DOM:虚拟DOM是将真实DOM描述为JS对象,以便在数据状态变化时在内存中迅速递归比较出发生变化的节点(比较过程即diff算法),大致思路如下:
// 真实DOM
<div class="wrapper">
<h3>hello</h3>
</div>
// 虚拟DOM
{
type:"div",
attrs:{
class:"wrapper"
},
children:[
{
type:"h3",
attrs:{},
children:[
{
type:"text",
value:"hello"
}
]
}
]
}
- diff算法的作用:当数据变化时实现差量渲染,最大化使用上次渲染的缓存,但是需要提前比较出新老两个虚拟DOM的不同之处,在暴力递归比较的基础上做了很多优化,例如使用key去唯一标识特定的节点;
- SPA框架/单页面应用开发框架,当浏览器中的路由地址发生变化时,在一个单页面中切换组件,实现整站效果;
列表渲染中key的作用
- 用于diff算法的性能优化;
- 当新老虚拟DOM节点(vnode)的Key相同时,直接认定其整个DOM结构未发生变化,直接复用老的虚拟DOM的渲染结果,而不对该vnode进行深度递归比较
- 当新老虚拟DOM节点(vnode)的Key不同时,也不对该vnode进行深度递归比较,而是直接干掉旧的节点,根据数据重新渲染新的节点;
computed与watch的区别
- 共同点:都是侦听一手数据状态的变化,从而触发相应逻辑的手段;
- computed专注于基于一手数据状态去换算得到特定的二手数据,即计算属性;
- 当computed依赖的一手数据状态没有发生变化时,在组件更新时会直接复用上一次渲染的缓存而不会重新计算,以提升性能;
- computed中不应该产生副作用;
- 而watch是专门侦听特定的数据变化,去触发相应的副作用逻辑;
Vue生命周期有哪些
- 创建实例阶段(init options):beforeCreate + created
- 挂载阶段(渲染DOM):beforeMount + mounted
- 更新阶段(数据驱动视图变化):befoereUpdate + updated
- 卸载阶段(页面切换或渲染条件不再成立):beforeUnmount + unmounted
- Vue2中的卸载阶段:beforeDestroy + destroyed
- 最好能绘制下图:
父子组件的生命周期联动(不甚高频)
- 大原则:父组件搭台 + 子组件唱戏
- 创建与挂载阶段:父组件创建实例完毕并预备挂载 => 子组件创建实例并一一挂载完毕 => 父组件宣布整体挂载完毕;
parent: beforeCreate + created + beforeMount
child: beforeCreate + created + beforeMount + mounted
parent: mounted
- 更新阶段:父组件宣布预备更新 => 子组件一一更新完毕 => 父组件宣布整体更新完毕;
parent: beforeUpdate
child: beforeUpdate + updated
parent: updated
- 卸载阶段:父组件宣布预备卸载 => 子组件一一卸载 => 父组件宣布整体卸载完毕;
parent: beforeUnmount
child: beforeUnmount + unmounted
parent: unmounted
Vue有哪些常用的修饰符(不甚高频)
- 事件修饰符:stop,prevent,once,capture,self
- 键盘修饰符: enter,esc,ctrl,meta,shift
- 系统修饰符:exact
- vmodel修饰符: lazy,number,trim
- 自定义vmodel修饰符
- 自定义指令中的修饰符
vif与vshow的区别
- 当渲染条件不成立时,vif不渲染(DOM压根不存在),vshow不显示(display:none)
- 适合用vshow的场景:频繁显隐;
- 适合vif的场景:一锤子买卖(非VIP用户不给看【爽片专区】)
说说你对nexttick的理解(高频)
- 数据变化立刻完成,渲染跟进需要时间(异步)
- 数据变化后无法立即拿到DOM的更新状态
- nexttick(()=>/在这里就可以拿了/)
- nexttick中的callback在DOM渲染完毕的时候回调,此时就能拿到DOM的最新状态了
什么是具名插槽和作用域插槽
- 具名插槽:插槽位有很多,想要具体指定把哪个内容插在哪个插槽中,就需要指名道姓;
- 作用域插槽:父组件在自己的模板上部署子组件及其插槽内容时,只能访问到父组件自身作用域内的数据;要想将子组件自己的数据部署在插槽中,就需要用到作用域插槽;
子组件:
子组件暴露出去的数据会形成一个对象{myname,myage}
<slot name="a" :myname="子组件自己的myname数据" :myage="子组件自己的myage数据"></slot>
父组件:
<Child>
<!-- 将子组件暴露出来的数据对象起一个别名sp -->
<template #a="sp">
{{sp.myname}}/{{sp.myage}}
</template>
</Child>
Vue组件有哪些通信方式
- 父传子:props-down
- 子传父:event-up
- 祖传孙:provide + inject
- 主动暴露数据给父组件使用: expose
子组件
export default {
expose:{
myname,
myage,
sayHelloFn
}
}
父组件
<son ref="sonRef"/>
this.$refs.sonRef.sayHelloFn()
- 使用全局状态管理自由通信:需要共享的数据状态都在【具有响应式的中央状态仓库】(Vuex/pinia)中,并对外提供状态的CRUD接口,任何组件去修改数据状态,其它用到该状态的组件都具有响应式;根本不必刻意去通信;
Vue性能优化
缓存系列/最大程度使用缓存
- computed
- 动态组件与keepalive
- 本地缓存网络数据
- 服客双方配合使用HTTP的强缓与协缓;
偷懒系列
- 使用异步组件:需要用到某组件的时候再去加载
- 防抖节流(减少事件处理)
- v-model.lazy="xxx"(本质上不理input事件只理会change事件)
- 合理选用vif与vshow(能不渲染就不渲染)
- 避免产生无效回流(不要连续对一个DOM元素连做多次样式操作,集合起来做一次)
减少渲染与diff压力
- DOM结构尽量扁平化(幽灵标签)
- 数据结构尽量扁平化(可能涉及到深度侦听)
- 避免不必要的深度侦听
- 列表渲染时别忘了加key(diff时减少很多深度递归比较)
节约内存
- 避免内存泄露,组件卸载时将自创的全局变量、DOM事件侦听器、定时器、闭包统统释放;
提升算法水平,少造BUG...
对vmodel双向数据绑定的理解
- 在一对多通信中我们更推崇单向数据流,简单清晰好管理;
- 在一对一通信中使用双向数据流不存在复杂逻辑和难管理问题,因此合理使用能带来极大的遍历;
- 表单元素的v-model双向数据绑定实际上等价于
父子通信(props-down)
和子父通信(events-up)
的集成,其本质是一种语法糖;
<input type="text" v-model="text" />
<!-- 上面代码等价于↓ -->
<input :value="text" @input="e => text = e.target.value" />
- 当v-model作用在组件上时等价于
传递props modelValue
+处理自定义事件update:modelValue
<MyComponent v-model="searchText" />
<!-- 上面代码等价于↓ -->
<MyComponent
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
- 自定义vmodel参数时等价于
传递props xxx
+处理自定义事件update:xxx
<MyComponent v-model:xxx="bookTitle" />
对Props单向数据流的理解
- 父子通信中数据只能由父组件流向子组件,子组件不应直接修改父组件的数据(可以做到但会报警告),因为多个子组件同时修改父组件的数据会造成结构上的混乱;
- 子组件确实需要修改父组件数据时,应向父组件发送自定义事件,由父组件自己来修改该数据(子组件会获得同步响应);
- 在一对多通信中我们推崇单向数据流,简单清晰好管理;
- 特定数据的修改入口唯一化是科学的编程理念,政出多门,编程大忌、致乱之由也;
Vue有哪些逻辑复用方式
- Vue2中主要是
mixin
; - Vue3中主要是
自定义hook
; - mixin基本就是个垃圾,全局和局部到处都可以定义,相互冲突和覆盖,难以追踪和调试,因此并不好用;
- 自定义Hook很好很强大,是Vue3升级的最大初衷之一(或没有之一);
- 另外还有
自定义指令
和自定义插件
;
生命周期的父子联动
- 父组件搭台,子组件唱戏;
- 所有子组件都唱完,才算整体(父组件)唱完;
- parent: beforeCreate + created + beforeMount;//父组件搭台
- children: beforeCreate + created + beforeMount + mounted;//子组件把戏唱完
- parent: mounted;//整体唱戏完毕
- parent: beforeUpdate;//父组件搭台
- children: beforeUpdate + updated;//子组件把戏唱完
- parent: updated;//整体唱戏完毕
- parent: beforeUnmount;//父组件搭台
- children: beforeUnmount + unmounted;//子组件把戏唱完
- parent: unmounted;//整体唱戏完毕
为什么data选项是一个函数
- 为了形成封闭的闭包/作用域;
- 以确保每个组件的数据都相互独立,互不冲突,都不污染全局;
谈谈对Vuex的理解
- 实现数据和视图分离;
- 实现组件间的数据共享和自由通信;
- 实现全局数据缓存(缓在localStorage)(需要配置persistedState数据持久化插件);
- 单向数据流架构:组件派发action编辑数据=>action异步通信获取最新数据=>action提交mutation令其同步更新数据=>mutation同步更新state(devtool会知道)=>订阅state的组件自动被更新;
- 在一(中央数据仓库)对多(组件)的通信场景中,单向数据流简单清晰好管理;
- Vuex单向数据流图示:
Vue怎么缓存当前组件?缓存后想更新怎么办?
- keepalive包裹动态组件,参与动态替换的组件实例不会卸载;
- 可以在keepalive身上配置include(正选)、exclude(反选)
- 可以在keepalive身上配置max(最大缓存实例数),其底层使用LRU缓存算法,即当缓存实例数满时,最近最少使用的(LeastRecentUse)组件实例会被踢出缓存;
- 被切走的组件会回调deactived(失活)生命周期,被切入的组件会回调activated(激活)生命周期,分别可用于缓存和更新数据;
有没有封装过axios
- 必须有;
- 经典三层封装模型:实例层 => CRUD层=> 应用层,视图层只直接与应用层打交道;
- 实例层封装了axios实例(包含baseUrl,timeout等基础配置)+ 实例拦截器;
- 在请求拦截器中可以注入通用配置,例如登录token;(headers:{authorization:"Bearer tokenvalue"});
- 在响应拦截器中可以统一过滤数据,例如直接提取response.data;
- 拦截器还可以用于记录接口访问日志、统计数据与错误等;
- CRUD层封装了通用的POST/DELETE/PUT/GET请求,应用层只需要注入url,data,config即可;
- CRUD层通常还用于统一处理错误;
- 应用层调用CRUD层封装的增删改查方法,对视图层直接放出业务API,例如
- addGoods(goods)
- deleteGoods(gid)
- updateGoods(goods)
- getGoods(gid)
- getGoodsList(page)
Vue3有哪些更新?
@底层响应式原理更新
- 使用Proxy替代Vue2的数据劫持侦听/Object.defineProperty;
- Proxy机制:数据的每个叶子节点背后都是一个代理对象,访问数据一律通过其代理,这样一旦数据被更新,代理会直接通知相应的视图更新,而无需递归比较整棵数据树,从而极大地提升了响应式性能;
- 数据劫持机制的缺点:
- 性能垃圾,因为需要递归比较新老数据的所有叶子节点;
- 只能监听数据的get与set操作,这样当数据地址不发生变化时(例如删除对象的一个key,原地修改数组)数据的修改是无法被侦听的;(做了很多复杂的周边处理才填上这个坑)
- 底层响应式原理的替换并不直接体现在编码上;
@逻辑复用机制更新
- Vue2的主要(跨组件)逻辑复用方式是mixin;
- mixin就是个垃圾,全局和局部到处都可以定义,相互冲突和覆盖,难以追踪和调试,因此并不好用;
- Vue3的组合式API(compositionAPI)可以将响应式数据的定义逻辑、生命周期回调逻辑、数据侦听逻辑全部组合到【自定义Hook】中,轻松在组件间共享各种业务逻辑而不产生任何副作用;
- 独立的响应式数据的定义:ref定义单个地址、reactive定义响应式对象、toRefs将响应式对象打散为多个ref集合在一起的普通对象;
- 独立的生命周期逻辑:setup本身充当创建阶段,另外六大生命周期回调都以普通回调函数的方式存在:例如onMounted(()=>{组件挂载时做什么}),它们分别是:
- onBeforeMount/onMounted
- onBeforeUpdate/onUpdated
- onBeforeUnmount/onUnmounted;
- PS:在Vue2中卸载阶段的两个生命周期分别是beforeDestroy/destroyed;
- PS:注意keepalive管理的缓存实例有激活/失活两个生命周期activated/deactivated,它们在Vue3中对应的名字为: onActivated/onDeactivated;
- 独立的数据侦听逻辑:
const xxx = computed(()=>基于一手数据换算而来的二手数据)
用于创建独立存在的计算属性;const unwatch = watch(()=>侦听项,(nv,ov)=>副作用逻辑,{config})
和watchEffect(()=>自动收集依赖的副作用逻辑)
用于创建独立的数据变化侦听和副作用逻辑;
什么是自定义hook
- Vue3强大的跨组件逻辑复用方式;
- 通过将【响应式数据创建逻辑 + 生命周期回调逻辑 + 数据侦听逻辑】集成为一个函数,最终对外返回一组响应式数据;
- 通过这种方式,轻松在组件间共享业务逻辑,且没有任何额外副作用;
- 典型手写案例:useMouse, useCountDown, useAxios, useScroll...
什么是自定义指令
- Vue在元素上逻辑复用的主要方式;
- Vue指令格式:v-指令名:指令参数.指令修饰符="指令值";
- 指令定义方式:
app.directive(name,{
onMounted(el,binding){...},
onUpdated(el,binding){...},
})
app.directive(name,(el,binding)=>{
// 在元素的宿主组件onMounted和onUpdated时做什么
})
- 典型手写案例:v-pin, v-change...
案例:点击在组件外部
当使用Vue 3重构自定义指令时,需要按照新的Vue 3指令语法和API进行修改。下面是将上述click-outside
指令重构为Vue 3风格的示例:
// 创建一个自定义指令对象
export default const clickOutsideDirective = {
// 使用 `mounted` 钩子来替代 Vue 2 中的 `bind` 钩子
mounted(el, binding) {
const handleClickOutside = event => {
if (!el.contains(event.target)) {
binding.value();
}
};
document.addEventListener('click', handleClickOutside);
// 在元素销毁前移除事件监听器
el._ClickOutsideEvent = handleClickOutside;
},
// 使用 `unmounted` 钩子来替代 Vue 2 中的 `unbind` 钩子
unmounted(el) {
document.removeEventListener('click', el._ClickOutsideEvent);
delete el._ClickOutsideEvent;
}
};
全局注册指令
import COD from "@directives/ClickOutsideDirective"
app.directive("click-outside", COD);
在上面的示例中,我们使用Directive
类从自定义指令对象clickOutsideDirective
中创建了一个指令实例,并导出该指令实例。在实例中,我们使用了新的钩子函数mounted
和unmounted
来替代Vue 2中的bind
和unbind
钩子。
使用指令实例时,可以在模板中使用v-click-outside
指令,并将要执行的方法作为指令的值传递给它:
<template>
<div v-click-outside="closePopup"></div>
</template>
需要注意的是,在Vue 3中,指令不再支持修饰符,但可以使用参数来扩展指令的功能。
案例:触底加载下一页
先复习几个与滚动有关的概念
此处一个典型的自定义指令案例是实现无限滚动加载的指令。这个指令可以用于当滚动到页面底部时,自动加载更多数据。下面是一个基于Vue 3的示例:
// 在 Vue 3 中,需要单独导入 Directive 类
import { Directive } from 'vue';
// 创建一个自定义指令对象
const infiniteScrollDirective = {
// 使用 `mounted` 钩子来替代 Vue 2 中的 `bind` 钩子
mounted(el, binding) {
const handleScroll = () => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
// 滚动到页面底部时,触发绑定的处理函数
if (scrollTop + clientHeight >= scrollHeight) {
binding.value();
}
};
window.addEventListener('scroll', handleScroll);
// 在元素销毁前移除事件监听器
el._InfiniteScrollEvent = handleScroll;
},
// 使用 `unmounted` 钩子来替代 Vue 2 中的 `unbind` 钩子
unmounted(el) {
window.removeEventListener('scroll', el._InfiniteScrollEvent);
delete el._InfiniteScrollEvent;
}
};
// 创建一个指令实例,并导出
export const InfiniteScrollDirective = Directive.from(infiniteScrollDirective);
在上面的示例中,我们创建了一个名为infiniteScrollDirective
的自定义指令对象。在mounted
钩子中,我们使用addEventListener
监听scroll
事件,即滚动事件。每当页面滚动时,都会执行handleScroll
函数。
在handleScroll
函数中,我们通过获取document.documentElement
的scrollTop
、scrollHeight
和clientHeight
属性来判断是否滚动到页面底部。当滚动到底部时,我们触发自定义指令绑定的处理函数,即binding.value()
,来加载更多数据。
使用这个自定义指令时,可以在需要实现无限滚动加载的元素上使用v-infinite-scroll
指令,并将要执行的加载数据的方法作为指令的值传递给它:
<template>
<div v-infinite-scroll="loadMoreData"></div>
</template>
通过这个自定义指令,我们可以轻松地实现无限滚动加载数据的功能,提升用户体验和页面的交互性。用户只需滚动到页面底部,就可以自动加载更多数据,无需点击或其他操作。这种指令在处理大量数据时非常实用。
案例:鼠标覆盖改变样式
const hoverHandler = {
// v-hover="{ backgroundColor: 'green', color: 'white' }"
mounted(el, binding, vnode) {
// 存储原来的样式
const originalStyle = window.getComputedStyle(el)
const obj = {}
for (const key in binding.value) {
obj[key] = originalStyle[key]
}
// 鼠标覆盖时 使用binding定义的样式
el._mouseover = e => {
for (const key in binding.value) {
el.style[key] = binding.value[key]
}
}
// 鼠标移出时 恢复原来的样式
el._mouseout = e => {
for (const key in obj) {
el.style[key] = obj[key]
}
}
el.addEventListener(
"mouseover",
el._mouseover
)
el.addEventListener(
"mouseout",
el._mouseout
)
},
unmounted(el) {
el.removeEventListener(
"mouseover",
el._mouseover
)
el.removeEventListener(
"mouseout",
el._mouseout
)
delete el._mouseover
delete el._mouseout
}
}
export default hoverHandler
<div class="box1" v-hover="{ backgroundColor: 'green', color: 'white' }">
失败乃你之母
</div>
import hoverHandler from './directives/hover'
app.directive("hover", hoverHandler)
执行效果
你怎么理解Vue插件的
- 形式上是一个注册在app身上的带有install方法的对象;
- 在app被初始化的时候调用该对象的install方法可以成体系地做很多事情:
const i18nPlugin = {
install(app,options){
// 在app身上注册自定义指令
// 在app身上全局透传数据或配置
// 在app身上使用其它插件
// 为所有组件实例注入全局API
// app.config.globalProperties.$translate = ()=>{...}
}
}
app.use(i18nPlugin,options)
- Vuex、VueRouter以及各种Vue组件库都是以插件形式存在的;
- 在自定义组件库场景中尤为常见;
- 经典手写案例:i18nPlugin...
Vue有哪些路由模式,区别如何?
- 主要有Hash哈希模式(baseUrl/#/path)和H5历史记录模式(baseUrl/path);
- 哈希模式优点:浏览器直接识别为前端页面内跳转,没有额外的前后端通信过程,不对服务端产生额外压力;
- 哈希模式缺点:丑陋;不利于搜索引擎优化(SEO=SearchEnginOptimization);(搜索引擎爬虫认为各path与首页就是同一个页面,在爬完首页信息后就扬长而去)
- H5/历史记录模式优点:漂亮;利于SEO优化;
- H5/历史记录模式缺点:形成额外的前后端通信过程,一定程度上降低了性能,给服务端造成一定的额外负担;
- 历史记录模式相对来说更主流一些;
- 历史记录模式下服务端需要一点额外配置,以把前端路由打回给前端自己处理,以Nginx为例:
location / {
# 看看服务端有没有对应的API
# 如果没有则定性为前端路由(一脚踹回给/index.html即前端页面本身去处理)
# $uri(即/path)会回到前端的路由表进行前端路由匹配
try_files $uri $uri/ /index.html;
}
什么是路由懒加载
- 又叫路由的异步加载;
- 等到用户访问path时才历史去加载/下载对应的组件;
- 这样用户首次访问时只需要加载/下载首页组件即可,极大地提升了首屏加载性能;
- 路由异步加载/懒加载基本已经是日常项目开发的默认选项了;
- 其配置如下:
routes:[
{
path: "/about",
name: "about",
// 同步加载 客户端浏览器需要先完成组件内容的下载
// component: AboutView
/*
- 异步路由/路由的懒加载=用户什么时候访问/about就什么时候加载about组件对应的内容
- 首次用户浏览器只加载home组件
- 提高了首次加载的性能
*/
component: () => import("../views/AboutView.vue"),
},
]
什么是动态路由
- 路由中包含动态参数段,例如:/film/<电影id>
- 其配置如下:
routes:[
{
// 规定电影id为若干数字 例如/film/1234
path: "/film/:id(\\d+)",
name: "detail",
component: () => import("../views/DetailView.vue"),
},
]
- 组件内获取动态路由参数
this.$route.params["id"]
import {useRoute} from "vue-router"
const route = useRoute()
route.params["id"]
如何封装自己的组件
- 大前提:把什么都写死就没有复用价值可言
- 组件需要展示的数据由父组件通过
props
注入 - 当用户在子组件中交互(点击)时,交互的处理权限交还于父组件自行处理,通过向父组件发送自定义事件
events
实现 - 允许父组件通过覆盖
插槽
的方式给子组件注入内容和样式(子DOM) - 子组件自己可以暴露一些
便捷操作API
供父组件调用,例如轮播图组件可以暴露prev(),next(),startAutoPlay(),stopAutoPlay()
,主要用到expose
和ref
两个语法;
子组件对外暴露API
expose:["prev","next","startAutoPlay","stopAutoPlay"]
父组件通过ref得到子组件实例,再进一步调用其暴露出来的API
swiperRef.value.startAutoPlay()
- 巧用双向数据绑定
v-model:propName="dataName"
可以简化props+events
使用流程;
如何做自己的组件库
请参见 Vue发布自定义组件库