页面渲染优化
- html 不要嵌套过深 (减轻回流的压力)
- css 尽量使用精确的选择器 (减少回流的压力)
- 代码压缩 (减轻请求的压力)
- js 角度(js引擎线程和浏览器的渲染线程不会同时工作) 在script标签里面放 (async 异步加载 + 直接执行) 和 (defer 异步加载 + 延迟执行) 关键字
- 图片(懒加载,预加载,骨架屏,压缩,精灵图)
- 缓存
谈谈你对 vue 的理解
- 渐进式的单页应用框架 (根据项目的需求逐渐引入功能 use install 函数)
- MVVM 数据驱动页面更新 (model views views-model)
- 组件化 (可复用,方便调试)
- 指令
- 虚拟dom (跨端开发,减少dom操作,读取代码,生成虚拟dom树,给浏览器渲染)
- 生态完善
谈谈你对spa的理解
spa(单页面应用),整个项目只有一个页面,页面中的内容是动态的,以组件的形式展示,靠路由来映射匹配组件。
优点:
- 组件化开发,易于维护
- 页面切换快,体验好
- 前后端分离,提升开发效率
缺点:首屏加载时间长,不利于 SEO
可以通过服务端渲染 ssr 解决首屏加载时间长,不利于SEO的问题:
ssr服务端渲染原理:
- 创建 node 服务
- 读取 vue 组件
- 借助 vue 自带编译器函数编译 vue 组件,得到 AST
- 借助 vue 自带渲染器函数渲染 AST 得到 html
- 拼接 html 模板
- 发送响应
vue项目里面再启动一个node服务,读取需要第一个被展示出来的组件,将该组件由vue自带的编译器和渲染器,处理成代码块,最后拼接到一个html模板当中,响应给浏览器。
SPA 首屏加载优化
首屏加载慢的原因:
- 单页应用需要把所有页面的代码都执行完,首屏才加载
- 加载js脚本
- 网络延时
优化策略:
- 路由懒加载
- ssr 服务端渲染
- 骨架屏
- UI 框架按需加载
说说你对 vue 生命周期的理解
生命周期就是一个vue组件从创建到销毁的过程,其中官方打造了一系列的钩子函数。
-
setup = beforeCreate + created
-
onBeforeMount = beforeMount
-
onMounted = mounted
-
onBeforeUpdated = beforeUpdated
-
onUpdated = updated
-
onBeforeUnmount = beforeUnmount
-
onUnmounted = unmounted
-
onActived() 命中缓存会调用
-
onDeactivated()
-
捕获子组件错误时触发 onErrorCaptured
-
收集依赖时触发 onRenderTracked
-
触发依赖时触发 onRenderTriggered 响应式值变更,调试用的
当被keep-alive标签包裹后,里面的内容就被缓存,没有执行卸载操作
说说你对双向绑定的理解
vue中的双向绑定就是v-model
指令,修改数据,页面会同步更新,页面内容修改,数据也会同步更新。
原理:双向绑定由三个重要部分构成(MVVM):Model(模板层),View(视图层,浏览器的可视区域),ViewModel(vue框架的核心,将数据与视图关联起来)。
ViewModel:
- Observer(监听器):数据的代理
- Compiler(解析器):解析模板的指令
当vue组件被读取到时,将数据变成响应式以及解析模板指令,解析完指令会初始化视图,当数据源发生变化的时候就需要更新视图,通过Dep机制通知变更,通知watcher观察者,然后观察者去触发更新视图的函数。Dep充当数据与视图之间的中介,它知道哪些视图(Watcher)依赖于特定的数据,当数据变化时,能够通知到所有依赖该数据的视图。为了避免不必要的试图更新。
双向绑定的原理:
- 变量被处理成响应式的过程中会为变量做依赖收集,当变量的值变更时,触发setter,并执行依赖,导致试图更新。
- 视图更新相当于用户触发了 input 事件,修改响应式变量,进而又导致setter触发。
vue 组件通讯
list是一个数组。
父向子传值:
- 父组件用 v-bind : 传递给子组件,子组件用 defineProps 接收数据。
js
// parent.vue
<child :data="list"></child>
// child.vue
const props = defineProps({
data: {
type: String,
default: ''
}
})
console.log(props.data);
- 父组件 provide 数据,子组件 inject 注入 (全部子组件都能接收到,数据流向不明确,只能从上往下注入,在最外层的App.vue里面使用provide可以类似仓库效果)
js
// parent.vue
import { provide } from 'vue'
provide('list', list.value) // 向下提供数据 提供的是引用地址(子组件可以改)
provide('list', readonly(list.value)) // readonly接收一个对象,返回只读代理(不能改)
// child.vue
import { inject } from 'vue'
const list = inject('list') // 注入数据
子向父传值:
- 子组件通过 defineEmits 定义发布一个事件,父组件通过 v-on 订阅这个事件。
js
// parent.vue
<Child @add="handle" />
const handle = (val) => {
console.log(val) // hello
}
// child.vue
const emit = defineEmits(['add']) // 定义一个事件
const handle = () => {
emit('add', 'hello world') // 发布事件
}
- 子组件通过 defineExpose 暴露数据,父组件通过 ref 引用子组件中的数据
js
// parent.vue
<Child ref="childRef" />
const childRef = ref(null)
console.log(childRef.value); // 输出list的值
// childRef?.list 如果 childRef 有值执行 list,否则不执行
// :key 作用是Vue会根据该值来判断哪些元素是新的,哪些元素需要被重用,从而提高渲染效率
<li v-for="(item, index) in childRef?.list" :key="index">{{ item }}</li>
// child.vue
defineExpose({
list
})
- 父组件通过 v-model 绑定数据给子组件,子组件通过 defineProps接收,然后定义 'update:xxx' 事件,并直接修改父组件给过来的数据,但是一定要发布 'update:xxx' 事件
js
// parent.vue
<Child v-model:list="arr" />
const arr = ref(['html', 'css', 'js'])
// child.vue
const props = defineProps(['list']) // 接收父组件传过来的值
const emits = defineEmits(['update:abc']) // 定义 update: 事件
const add = () => {
const arr = props.list
arr.push('vue')
emits('update:abc', arr) // 发布 update: 事件
}
兄弟组件通讯:
- 在外部的 js 文件中定义响应式变量,同时引入到两个组件中,因为是响应式的,所以两个组件都可以修改这个变量,从而实现通讯。
js
// bus.js
export const list = ['html', 'css']
// child.vue
import { list } from './bus.js'
const add = () => {
list.push('child')
}
// parent.vue
import { list } from './bus.js'
const add = () => {
list.push('parent')
}
- pinia 状态管理库
v-if 和 v-show 的区别
- v-if是动态地向 DOM 树内添加或删除 DOM 元素,v-show 是通过 css 控制元素的 display属性
- v-if 控制的组件会触发生命周期,v-show 不会
- v-if 有更高的切换开销, v-show 会导致两次回流
v-if 和 v-for 可以一起使用吗?
- 在vue3中,v-if的优先级比v-for高
- 在vue2中,v-for的优先级比v-if高
data 为什么是一个函数,不能是一个对象?
如果 data 是一个对象,那么当该组件被多处使用时,会导致数据共享,会出现数据污染的问题,为了确保每个组件实例都有其独立的数据副本,避免数据污染,data 必须是一个函数,返回一个对象,每个组件实例都有自己的 data 对象
说说你对 vue 中 nextTick 的理解
nextTick 是一个异步函数,它的作用是在 DOM 更新后执行回调函数,延迟回调。
实现原理如下:
js
<script>
// 1. 什么时候执行 cb 回调函数
// 2. 执行resolve函数
function nextTick(cb) {
return new Promise(resolve => {
function fn() {
return () => {
cb()
resolve()
}
}
// 当 DOM 结构渲染完成后,再执行回调
if (typeof MutationObserver !== 'undefined') {
const observer = new MutationObserver(fn()) // 浏览器监听DOM的api
observer.observe(document.body, { // 监听dom结构更新
childList: true,
subtree: true
})
} else {//如果浏览器不支持`MutationObserver`,则使用`setTimeout`将回调函数延迟到下一个事件循环执行
setTimeout(fn(), 0)
}
})
}
const app = document.getElementById('app')
nextTick(() => {
console.log(app.innerHTML);
}).then(() => {
console.log(app.innerHTML, 'then');
})
app.addEventListener('click', () => {
app.innerHTML = 'hello world'
})
</script>
所以:
- 在拥有 MutationObserver 的浏览器中,通过 MutationObserver 来监听 DOM 的变化,触发会回调
- 在不支持 MutationObserver 的环境中,使用 setTimeout 来模拟 MutationObserver,当 DOM 变化时,触发回调
说说你对 slot 的理解
slot vue中的插槽功能,是组件中的一个占位符,用于接收该组件标签中的内容
- 匿名 slot
- 具名 slot
- 作用域 slot
- 条件 slot
js
// 匿名 slot
// layout.vue
<template>
<div class="head">
<slot>
xxxxxx
</slot>
</div>
<div class="body">
<div class="left"></div>
<div class="right"></div>
</div>
</template>
// App.vue
<template>
<layout>
<template v-slot>
<div>hello world</div>
<p>vue</p>
</template>
</layout>
</template>
js
// 具名 slot
// layout.vue
<template>
<div class="head">
<slot name="head">
</slot>
</div>
<div class="body">
<div class="left"></div>
<div class="right">
<slot name="right">
</slot>
</div>
</div>
</template>
// App.vue
<template>
<layout>
<template v-slot:head>
<div>hello world</div>
</template>
<template v-slot:right>
<p>vue</p>
</template>
</layout>
</template>
js
// 作用域 slot
// layout.vue
<template>
<div class="head">
<slot name="head" :user="{ name: '张三' }">
</slot>
</div>
</template>
// App.vue
<template>
<layout>
<template v-slot:head="props">
<div>{{ props.user.name }}</div>
</template>
</layout>
</template>
js
// 条件 slot
// layout.vue
<template>
// 如果有head就显示
<div class="head" v-if="$slots.head">
<slot name="head"></slot>
<slot name="left"></slot>
</div>
</template>
// App.vue
// 不显示
<template>
<layout>
<template v-slot:left>你好</template>
</layout>
</template>
应用场景:layout布局组件。
为什么要使用 key? index 做 key 有什么问题?
- key 大大提高了 diff 的效率,减少了不必要的渲染
- index 做 key 等同于没有 key,会导致性能问题
什么是虚拟 DOM?
用 js 对象作为树,使用对象的属性来描述节点的状态,用这个对象来描述真实的 DOM 树,这个对象就是虚拟 DOM。最少包含 tag,props,children 三个属性。
- 减少了真实 DOM 的操作带来页面渲染的性能开销 传给了 v8 引擎
- 抽象了原本的渲染过程,实现了跨平台开发的能力
diff 算法
diff 算法是一种对象的比较算法,效率很高,在拥有虚拟 DOM 的框架中被使用。
在vue中,拥有虚拟dom这个过程,所以,vue要先将模板代码编译成虚拟dom对象后,再拿虚拟dom对象生成真实的dom交给浏览器渲染,当响应式变量值发生改变时,会重新生成新的虚拟dom对象,为了考虑到降低渲染开销,需要用diff算法来比较,找出新的虚拟dom对象和老的dom对象的区别,把区别单独拎出来后去更新真实的dom结构。
原理:
- 比较旧 DOM 虚拟对象的根节点,如果根节点不同,直接替换整个 DOM 树
- 如果根节点相同,比较根节点的属性,如果不同,修改根节点的属性
- 如果根节点的属性相同,比较根节点的子节点,同层级的子节点进行比较,如果不同,替换该子节点以下的 DOM 树。
- 判断是不是文本节点,如果是文本节点,直接修改文本节点的内容
- 采用深度优先遍历对比子节点(对比的过程中,有旧没新,删除旧节点,反之增加新节点)
同层级的子节点比较:(双端队列)
- 设置新旧 Vnode 的头尾指针、
- 新旧头尾指针进行比较,向中间靠拢,这个过程包含 4 种情况,分别是:头头比,尾尾比,头尾比,尾头比
了解过 vue2 吗?vue3 和 vue2 的区别是什么?
- 速度更快
- 重写了虚拟 DOM 的实现,使用 proxy 代替了 Object.defineProperty
- 编译模板的优化,采用了静态提升策略,将静态节点缓存起来,减少了编译的时间
- 体积更小 (组合式api,实现按需分配的能力)
- 更易维护 (完全兼容 vue2 的语法,还可以搭配 vue3 模板使用)
- 更接近原生
- 更友好的 ts 支持