1. Vue3 中重用代码的方式有哪些?
组件、组合式函数与指令:
-
组件是主要的构建模块
-
组合式函数则侧重于有状态的逻辑
-
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑
注:只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用
v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
2. 自定义指令
自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
数字化管理平台
Vue3+Vite+VueRouter+Pinia+Axios+ElementPlus
Vue权限系统案例
个人博客
一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。指令钩子及参数详见
组件内自定义指令:
-
在
<script setup>
中(组合式API),任何以v
开头的驼峰式命名的变量都可以被用作一个自定义指令。<script setup> // 在模板中启用 v-focus const vFocus = { mounted: (el) => el.focus() } </script> <template> <input v-focus /> </template>
-
在没有使用
<script setup>
的情况下(选项式API),自定义指令需要通过directives
选项注册。export default { setup() { /*...*/ }, directives: { // 在模板中启用 v-focus focus: { /* ... */ } } }
全局注册自定义指令:
-
全局通过 app.directive(dname, options) 注册
const app = createApp({}) // 使 v-focus 在所有组件中都可用 app.directive('focus', { /* ... */ })
3. 单页面应用 (SPA)
单页面应用 (Single-Page application,缩写为 SPA)是一种不仅可以控制整个页面,还可以抓取新数据,并在无需重新加载的前提下处理页面切换的应用程序。
特性:应用在前端需要具有丰富的交互性、较深的会话和复杂的状态逻辑。
4. 什么是组合式API?
组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它是一个概括性的术语,涵盖了以下方面的 API:
- 响应式 AP:例如
ref()
和reactive()
,使我们可以直接创建响应式状态、计算属性和侦听器。 - 生命周期钩子:例如
onMounted()
和onUnmounted()
,使我们可以在组件各个生命周期阶段添加逻辑。 - 依赖注入:例如
provide()
和inject()
,使我们可以在使用响应式 API 时,利用 Vue 的依赖注入系统。
在 Vue 3 中,组合式 API 基本上都会配合 <script setup>
语法在单文件组件中使用。
特别注意:组合式 API 并不是函数式编程。组合式 API 是以 Vue 中数据可变的、细粒度的响应性系统为基础的,而函数式编程通常强调数据不可变。
5. 组合式 API有什么优势?
-
更好的逻辑复用
组合式 API 最基本的优势是它使我们能够通过组合函数来实现更加简洁高效的逻辑复用。在选项式 API 中我们主要的逻辑复用机制是 mixins,而组合式 API 解决了 mixins 的所有缺陷(不清晰的数据来源、命名空间冲突、隐式的跨 mixin 交流)
-
更灵活的代码组织
我们无需再为了一个逻辑关注点在不同的选项块间来回滚动切换。此外,我们现在可以很轻松地将这一组代码移动到一个外部文件中,不再需要为了抽象而重新组织代码,大大降低了重构成本,这在长期维护的大型项目中非常关键。
-
更好的类型推导
组合式 API 主要利用基本的变量和函数,它们本身就是类型友好的。用组合式 API 重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多! -
更小的生产包体积
<script setup>
形式书写的组件模板被编译为了一个内联函数,和<script setup>
中的代码位于同一作用域。被编译的模板可以直接访问<script setup>
中定义的变量,这对代码压缩更友好。注:本地变量的名字可以被压缩,但对象的属性名则不能。选项式 API 需要依赖
this
上下文对象访问属性,而
6. JavaScript 中数据劫持方式 与 Vue响应式工作原理
在 JavaScript 中有两种劫持 property 访问的方式:getter / setters 和 Proxies。
Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象,仅将 getter / setter 用于 ref。
相关伪代码演示:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
- 在
track()
内部,我们会检查当前是否有正在运行的副作用。如果有,我们会查找到一个存储了所有追踪了该属性的订阅者的 Set,然后将当前这个副作用作为新订阅者添加到该 Set 中。-> 收集依赖- 在
trigger()
之中,我们会再查找到该属性的所有订阅副作用。但这一次我们需要执行它们。-> 触发依赖
7. reactive()
的局限性
- 当你将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”,因为对局部变量的访问不再触发 get / set 代理捕获。
- 从
reactive()
返回的代理尽管行为上表现得像原始对象,但我们通过使用===
运算符还是能够比较出它们的不同。
8. Vue 渲染机制
Vue 的渲染系统基于"虚拟DOM"这个概念构建,可以实现将一份模板转换为真实的 DOM 节点,并对这些节点实现高效地更新。
-
虚拟 DOM
虚拟 DOM (Virtual DOM,简称 VDOM) 是一种编程概念,意为将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,然后将真实的 DOM 与之保持同步。
// vnode 是一个纯 JavaScript 的对象 (一个“虚拟节点”),包含了创建实际元素所需的所有信息 const vnode = { type: 'div', props: { id: 'hello' }, children: [ /* 更多 vnode */ ] }
一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载 (mount)。
如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5zd1JXtT-1684820952556)(./%E9%A1%B9%E7%9B%AE%E6%8F%92%E4%BB%B6.assets/render-pipeline.03805016.png)]
-
静态提升:
在重新渲染时不会再次创建和比对静态内容,Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode。当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。
这些静态内容在初次挂载后会缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的
cloneNode()
方法来克隆新的 DOM 节点,这会非常高效。<div> <div>foo</div> <!-- 需提升 --> <div>bar</div> <!-- 需提升 --> <div>{{ dynamic }}</div> </div>
-
树结构打平
组件需要重渲染时,只需要遍历这个打平的树而非整棵树。模板中任何的静态部分都会被高效地略过。
9. 通过 name 注册的组件能直接引入吗?
如果一个组件是用名字注册的,不能直接导入 (例如,由一个库全局注册),可以使用 resolveComponent()
来解决这个问题。
10. Vue template 标签不可以使用 v-show/v-for
-
v-show
v-show 是通过 display 来控制标签进行渲染的,但是 template 标签在 vue 解析后是不会显示在页面上的,是虚拟 Dom,所以无法使用 v-show。
反之 v-if 是可以使用在 template 标签上,因为 v-if 是条件渲染,只要满足 v-if 后的条件就可以完成渲染。<template v-if="showTag"></template>
-
v-for
同理,v-for也因为虚拟dom的原因,不能在template标签上使用(v-for的时候,自动根据要遍历的数组渲染dom)、<div v-for="(item, index) in forArray"></div>
11. reactive 定义的响应式失效问题
问题代码:
通过 reactive 定义一个响应式数组,网络请求返回的数据,赋值给数组之后,页面上的数据并没有更新。
const arr = reactive([])
console.log("赋值前arr:",arr)
// 模拟异步请求
setTimeout(() => {
// Mock 数据
const res = Array.from({ length: 100}).map((_,i) => i + 1)
// 直接赋值丢失了响应性,失败
arr = res
console.log("重新赋值后arr:",arr)
}, 1000)
分析:
在vue3使用proxy,对于对象或数组都不能直接将整个数据赋值。上面 reactive 声明的响应式对象被 arr 代理,操作代理对象需要有代理对象的前缀,此时的 res 直接把值赋值给了 arr ,使得 arr 失去了响应式。
解决方案:
-
使用 ref 函数定义响应式数据(推荐)
const arr = ref([]) arr.value = [10,20,30,40,50]
-
作为一个响应式对象的属性
let state = reactive({ list: [] }) const arr = [10,20,30,40,50] state.list = arr
-
使用数组中的 push、unshift、splice 等可响应式修改数组数据的方法
let list = reactive([]) const arr = [10,20,30,40,50] arr.forEach(v => { list.push(v) })
或
let list = reactive([]) const arr = [10,20,30,40,50] list.push(...arr)
九、Vite + Vue3 按需引入 Vant 或 Element 等UI库文件抽取
按需引入的组件,如果全都放到 main.js 中,会显得文件可读性很差。所以,一般情况下,我们都会将其剥离到一个单独的文件中,对于处理方式跟 Vue2 方式有所不同。
在 main.js 同级的根目录下创建 vant 文件夹,文件夹中创建 index.js 文件,代码如下:
// 1. 引入你需要的组件
import {
Button,
DropdownMenu,
DropdownItem,
Col,
Row,
Search,
Icon,
List,
Cell,
Tabbar,
TabbarItem
} from 'vant';
// 2. 引入组件样式(可以放到main.js中引入)
import 'vant/lib/index.css';
// 3. 导出你需要的组件去 main.js 中注册(关键的步骤)
export default {
install: function (app) {
app.use(Button);
app.use(DropdownMenu);
app.use(DropdownItem);
app.use(Col);
app.use(Row);
app.use(Search);
app.use(Icon);
app.use(List);
app.use(Cell);
app.use(Tabbar);
app.use(TabbarItem);
}
}
main.js 中引入这个文件
import { createApp } from 'vue'
import App from './App.vue'
// 引入 vant 组件
import vant from './vant'
const app = createApp(App)
app.use(vant)
app.mount('#app')
移动端适配插件 postcss-px-to-viewport
yarn add postcss-px-to-viewport -D
vue.config.js 配置
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// 适配移动端的插件
import pxtovw from "postcss-px-to-viewport"
// 如果是其他的文件,我们就按照我们UI的宽度来设置viewportWidth,即750。
const loder_pxtovw = pxtovw({
viewportWidth: 750,// UI设计稿的宽度
viewportUnit: 'vw',// 指定需要转换成的视窗单位,默认vw
exclude: [/node_modules\/vant/i], // 设置忽略文件,用正则做目录名匹配
// unitToConvert: 'px', // 要转化的单位
// unitPrecision: 6, // 转换后的精度,即小数点位数
// propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
// fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
// selectorBlackList: ['ignore-'], // 指定不转换为视窗单位的类名,
// minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
// mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
// replace: true, // 是否转换后直接更换属性值
// landscape: false // 是否处理横屏情况
})
// 如果读取的是vant相关的文件,viewportWidth就设为375。但是经过实践,vant部分组件需要兼容处理的地方比较多,这里先注释了
const vant_pxtovw = pxtovw({
viewportWidth: 375,
viewportUnit: 'vw',
exclude: [/^(?!.*node_modules\/vant)/] //忽略除vant之外的
})
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
],
css: {
postcss: {
plugins: [
vant_pxtovw,
loder_pxtovw
]
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})