VUE
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
MVVM 和 MVC
mvc: Model、 View、 Controller
mvvm: model、 view、 modelView
MVC
- MVC 是一种架构思想,全称 Model-View-Controller,即模型-视图-控制器,MVC 是后端的开发思想,将数据从后端经过控制器层转向前端视图展示。
- View:视图层 ui 界面
- Model:模型 数据
- Controller:控制器 接受并处理用户请求通知 Model 改变,并将 Model 返回给 View
优点:
- 耦合性低:视图层和业务层分离,这样允许更改视图层代码而不用重新编译模型和控制器代码,改变其中一个不会影响到其他两个,所以这种设计思想有良好的松耦合的构件
- 重用性高:MVC 模式允许各种不同样式的视图来访问同一个服务的代码,因为多个视图能共享要给模型数据。
- 生命周期成本低:MVC 使开发和维护用户接口的技术含量降低。
- 部署快:使用 MVC 模式开发时间相当大的缩减,它使程序员集中精力在业务逻辑上,视图与业务逻辑分开。
- 可维护性高:分离视图层和业务层逻辑也是 WEB 应用更易于维护和修改.
- 有利于软件工程化管理:由于不同的层各司其职,每一层不同的应用具有某些特性,有利于通过工程化、工程化管理程序代码,可以使用控制器来连接不同的模型和视图去完成客户的需求.控制器可以根据用户需求选择模型进行处理,然后选择视图处理结果显示给用户
MVVM
- MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定,这使得 ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。
Vue.js 是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于 View 层。它的核心是 MVVM 中的 VM,也就是 ViewModel。ViewModel 负责连接 View 和 Model,保证视图和数据的一致性,这种轻量级的架构让前端开发更加高效、便捷。
- Model:数据模型,存放用于展示的数据,有的数据是写死的,大多数是从后端返回的数据
- View:视图,用于界面,在前端我们可以理解为 Dom 操作
- ViewModel:视图模型,可实现数据的双向绑定,连接 View 和 Model 的桥梁,当数据变化时,ViewModel 够监听到数据的变化(通过 Data Bindings),自动更新视图,而当用户操作视图,ViewModel 也能监听到视图的变化(通过 DOM Listeners),然后通知数据做改动,这就实现了数据的双向绑定
优点:
- 低耦合:视图 View 可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的 View 上,当 View 变化的时候 Model 可以不变,当 Model 变化的时候 View 也可以不变。
- 可重用性:可以把一些视图逻辑放在 ViewModel 里面,让很多 view 重用这段视图逻辑。
- 独立开发:开发人员可以专注业务逻辑和数据的开发,设计人员可以专注页面设计。
- 可测试:界面向来比较难预测时,测试可针对 ViewModel 来写。
MVC 和 MVVM 的区别
- 都是一种设计思想;
- MVC 后端用的多,而 MVVM 是前端设计思想;
- MVC 是单向通信,数据模型必须通过 Controller 层进行承上启下。MVVM 是将 View 和 Model 实现自动同步,当 Model 属性改变时,不用再自己手动操作 Dom 元素,提高页面渲染性能。
VUE 安装
// 通过vite初始化vue项目
// npm
npm init vite@latest
// yarn
yarn create vite
// 通过vue/cli初始化vue项目
npm install @vue/cli -g
vue create <project-name>
npm run dev
当我们执行这个命令时,会通过 package.json 文件的 scripts.json,执行相对应的指令。
为什么不直接执行 vite 呢?因为我们的电脑并没有配置过相关指令,因此无法执行。
其实我们在 npm install 的时候,会在 node-modules/.bin 创建好可执行文件, 该目录下的文件可以直接执行。
当我们执行 npm run dev 的时候,会通过软连接,查找该指令位于 node-modules 目录下 vite/bin 文件下 vite.js 文件。
所以我们执行 npm run xxx 的时候,会先到 node-modules/.bin 目录下查找对应的指令,然后在通过找到相对应的 js 文件执行。
注意:
- 查找规则是先从当前项目的 node-modules/.bin 目录下查找,如果找不到,再从全局的 node-modules/.bin 目录下查找。如果还找不到就会到全局环境变量中查找。
- 软连接:软连接就是 Windows 系统中的快捷方式,本质是一个文件,指向了 node-modules/.bin 目录下的 vite.js 文件。
数据的双向绑定 & 响应式原理
数据的双向绑定
v-model: 语法糖
<a-component v-model="title" :value="title" @input="handleInput" />
// a-component
model: {
value: 'input'
}
props: {
value: {
type: String,
default: ""
}
},
methods: {
handleInput() {
this.$emit('input', data)
}
}
VUE 响应式原理
vue2 响应式原理
- 数据劫持
- 利用
Object.defineProperty()来劫持各个属性的getter和setter,在数据变动时发布消息给订阅者,触发相应的监听回调。
- 依赖收集
- 每个组件实例都对应一个
watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
- 异步更新
- Vue 在更新 DOM 时是异步执行的。只要侦测到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,它只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的
Promise.then、MutationObserver和setImmediate,如果执行环境不支持,会采用setTimeout(fn, 0)代替。
vue3 响应式原理
- 利用 Proxy 实现
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
- 利用
Reflect实现
Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
- 利用
track和trigger实现
track在响应式对象的属性上访问时会触发,trigger在响应式对象的属性被修改时会触发。
vue2 和 vue3 的响应式原理区别
Object.defineProperty()只能对属性进行数据劫持,所以需要深度遍历整个对象。Proxy可以直接监听对象而非属性。Proxy可以直接监听数组的变化。Proxy有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty不具备的。Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。Proxy作为新标准受到浏览器厂商重点持续的性能优化。Proxy的 13 种拦截方法中,有些方法是没有Object.defineProperty()对应的操作的。
请详细说下你对 vue3 的 proxy 的理解
Proxy是 ES6 中新增的特性,它用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。Proxy对象可以理解为一个拦截器,它可以在底层对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy的定义
let p = new Proxy(target, handler);
target参数表示所要拦截的目标对象(上例是一个空对象),handler参数也是一个对象,用来定制拦截行为。
VUE 生命周期
vue 生命周期流程图:
vue2 生命周期
生命周期分为三大阶段
- 创建阶段:
BeforeCreate,Created,BeforeMount,Mounted - 更新阶段:
BeforeUpdate,Updated - 销毁阶段:
BeforeDestroy,Destroyed
- BeforeCreate:
new Vue(), 创建vue实例阶段 - Created: 创建数据和连接,
props、data、methods、computed、watch、 数据创建在实例上 - BeforeMount:
vDom生成虚拟节点,数据的操作和虚拟DOM的变更可操作,但不可涉及DOM的获取与操作 - Mounted:
DOM节点生成,将编译好的模板挂载到页面指定的容器中显示。 - BeforeUpdate: 状态更新之前执行函数,此时
data中的状态值是最新的,但是界面上显示的数据还是旧的,因为还没有开始重新渲染DOM节点。 - Updated: 此时
data中的状态值和界面上显示的数据都已经完成了跟新,界面已经被重新渲染好了!此阶段谨慎数据更新!!! - BeforeDestroy(): 实例被销毁之前,适用于清空
evenmtBus,setTimeout。 - Destroyed(): 实例销毁后调用,
Vue实例指示的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也都会被销毁。组件已经被完全销毁,此时组建中所有data、methods、以及过滤器,指令等,都已经不可用了。
vue3 生命周期
- 创建阶段:
setup,onBeforeMount,onMounted - 更新阶段:
onBeforeUpdate,onUpdated - 销毁阶段:
onBeforeUnmount,onUnmounted
- setup(): 开始创建组件之前,在
beforeCreate和created之前执行。创建的是data和method。 - onBeforeMount(): 组件挂载到节点上之前执行的函数。
- onMounted(): 组件挂载完成后执行的函数。
- onBeforeUpdate(): 组件更新之前执行的函数。
- onUpdated(): 组件更新完成之后执行的函数。
- onBeforeUnmount(): 组件卸载之前执行的函数。
- onUnmounted(): 组件卸载完成后执行的函数
- onActivated(): 被包含在中的组件,会多出两个生命周期钩子函数。被激活时执行。
- onDeactivated(): 比如从 A 组件,切换到 B 组件,A 组件消失时执行。
- onErrorCaptured(): 当捕获一个来自子孙组件的异常时激活钩子函数。
Watch 和 Computed
相同点
- 都依赖
vue的依赖收集 - 都是被依赖的变换触发,进行改变进而处理回调
不同点
computed有缓存,computed相对于函数性能更强,计算属性不受调用次数限制。watch无缓存,只在值变化的时候才能watch的到,watch更加专注于值的变化,添加immediate: true可以立即执行。computed多入单出,watch单入多处。computed不支持异步操作,watch支持异步操作。computed必有返回值,关注的是结果。watch关注的是过程。
问题一、computed 缓存和响应式是如何实现的?
computed依赖收集,收集的是响应式数据,当响应式数据变化时,会触发依赖收集的响应式数据,进而触发computed的重新计算。(在new Vue时,会调用initComputed方法,对computed进行初始化,之后调用createComputedGetter方法,对computed进行依赖收集。之后mounted)。- 劫持
getter,属性变化区分是否是最新状态。 target depend原值和依赖之间的关系链。
问题二、在 Vue 组件的初始化过程中,computed、watch 和 created 执行先后顺序是什么样的
vue 源码解释:
// 来源
// core/instance/init.js
initLifecycle(vm);
initEvents(vm);
initRender(vm);
// beforeCreate hook
callHook(vm, "beforeCreate", undefined, false /* setContext */);
// init injections before init children
initInjections(vm); // 初始化注入内容 inject
initState(vm);
initProvide(vm); // 初始化需要传递的 provide
// created hook
callHook(vm, "created");
// 实现
// core/instance/state.js
export function initState(vm: Component) {
const opts = vm.$options;
// 初始化 props
if (opts.props) initProps(vm, opts.props);
// Composition API
initSetup(vm);
// 初始化 methods
if (opts.methods) initMethods(vm, opts.methods);
// 初始化 data
if (opts.data) {
initData(vm);
} else {
const ob = observe((vm._data = {}));
ob && ob.vmCount++;
}
// 初始化计算属性
if (opts.computed) initComputed(vm, opts.computed);
// 初始化监听属性
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
computed:首先会初始化计算属性,Vue 会遍历组件中定义的所有计算属性,并计算它们的初始值。这些计算属性的初始化是在组件实例被创建时进行的。
watch:接着会初始化 watch 监听器,Vue 会遍历组件中定义的所有 watch 属性,并为每个属性设置对应的监听器。这些监听器可以监听数据的变化,并在数据变化时执行相应的操作。
created:最后执行 created 钩子函数,此时 Vue 实例已经被创建,但是挂载阶段还未开始。在 created 钩子函数中可以访问到组件的数据、计算属性和方法,但是无法访问到 $el,因为组件尚未被挂载到 DOM 中。
所以,总的来说,computed 先于 watch 和 created 执行,而 created 是最后执行的钩子函数。
指令
指令:
- v-text 用来显示文本
- v-html 用来展示富文本
- v-on 简写@ 用来给元素添加事件
- v-bind 简写: 用来绑定元素的属性 Attr
- v-model 双向绑定
- v-for 用来遍历元素
- v-on: 修饰符 冒泡案例
- v-once 性能优化只渲染一次
- v-memo 性能优化会有缓存, 3.2 版本新增
条件:
- v-if 用来控制元素的显示隐藏(切换真假 DOM)
- v-else-if 表示
v-if的else if块”。可以链式调用 - v-else
v-if条件收尾语句 - v-show 用来控制元素的显示隐藏(原理是通过
display: none/block来做的css切换)
自定义指令:directives
首先 vue3 自定义指令钩子函数发生变化,vue2 的写法在 vue3 中不支持。
vue2 地址:cn.vuejs.org/v2/guide/cu…
vue3 地址:staging-cn.vuejs.org/guide/reusa…
vue2 中钩子函数:
bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 inserted: 被绑定元素插入父节点时调用。
update:所用组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。
vue3 中的钩子函数 :
vue3 中自定义指令钩子函数和 vue2 中的组件生命周期相似,区别在于钩子函数携带参数。
函数及参数如下代码:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {},
};
// vue2写法如下:
// 注册一个全局自定义指令 `v-focus`
Vue.directive("focus", {
// 当被绑定的元素插入到 DOM 中时……
inserted: (el) => {
// 聚焦元素
el.focus();
},
});
// vue3写法:
// 注册一个全局自定义指令 `v-focus`
app.directive("focus", {
mounted: (el) => {
el.focus();
},
});
实现一个防抖指令
// 防抖函数
//利用闭包变量长期存储的特性,存储变量timer
function debounce(fn,delay){
let timer;
return function (...args){
// 有定时器则清除定时器
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn.apply(this,args)
},delay)
}
}
全局批量注册自定义指令,可以这样写。
import ...
const directives={
focus,
copy,
debounce
...
}
export default{
// vue官网有说,想使用Vue.use或者app.user(plugin),如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
install(app){
Object.keys(directives).forEach(item=>{
app.directive(item,directives[item])
})
}
}
// vue2写法:
const debounce = {
inserted: function (el, binding) {
// el 就是dom节点
let timer
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
binding.value()
}, 1000)
})
},
}
export default debounce
// vue3的写法
const debounce = {
mounted: (el, binding) => {
let timer
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
binding.value()
}, 1000)
})
},
}
export default debounce
实现一个权限指令 - vue3
import { Directive } from 'vue'
const user = 'xiaoxu'
const permission = [
'xiaoxu:stop.delete',
'xiaoxu:stop.create'
// ...
]
export const vHasPermission:Directive<HTMLElement, string> = (el, binding) => {
if (!permission.includes(`${user}:xiaoxu`)) {
el.style.display = 'none'
}
}
// 使用
import {vHasPermission} from './permission'
<button v-has-permission="stop.delete">删除</button>
<button v-has-permission="stop.create">创建</button>
v-memo:
v-memo: 官方文档:cn.vuejs.org/api/built-i…
// v-memo 在组件和元素都可以使用,主要是可以缓存,期望的类型是个数组,数组中可以放任何类型的值,但是值不能是响应式的。
// 如果数组里的每个值都与最后一次的渲染相同,那么他的更新将会被跳过,甚至虚拟 DOM 的 vnode 创建也将被跳过,提升了性能。
// tips:如果v-memo="[]" 传入的是一个空数组,那么他的效果和v-once 一样
<div v-memo="[val]"></div>
// v-memo 指令会在组件的 props 发生变化时才进行重新渲染,如果 props 没有发生变化,则会从缓存中获取最新的渲染结果,避免了不必要的重新渲染。
// 子组件
<template>
<div>
<h3>Child Component - {{ prop1 }}</h3>
<p>{{ prop2 }}</p>
<p>{{ prop3 }}</p>
</div>
</template>
<script>
export default {
props: ['prop1', 'prop2', 'prop3'],
};
</script>
// 父组件
<template>
<div>
<h2>Parent Component</h2>
<button @click="toggle">Toggle Child Prop</button>
<child-component v-memo :prop1="prop1" :prop2="prop2" :prop3="prop3" />
</div>
</template>
script>
import { ref } from 'vue';
export default {
setup() {
const prop1 = ref('Prop 1');
const prop2 = ref('Prop 2');
const prop3 = ref('Prop 3');
const toggle = () => {
prop3.value = prop3.value === 'Prop 3' ? 'Updated Prop 3' : 'Prop 3';
};
return {
prop1,
prop2,
prop3,
toggle,
};
}
};
</script>
// v-for使用参考官方文档
问题一、v-if 和 v-for 的优先级
- 2.x v-if > v-for
- 3.x v-for > v-if
事件修饰符: .stop, .prevent, .self, .capture, .once, .passive, .native, .sync
问题二、为何 vue 把事件设计在模板上而不是在 js 中
- 模版定位视图绑定的触发源头,通过触发源头寻找事件的逻辑,方便定位问题
- js 与视图绑定解藕,便于测试隔离
- vm 销毁,自动解绑事件,便于回收
组件化
一般组件和动态组件,动态组件实现原理是插件。
- 一般组件是在
Vue应用的模板中直接使用的静态组件。它们在模板中通过标签的形式声明和使用,并且在Vue实例创建时就已经存在。一般组件的使用非常简单,只需要在模板中直接引用即可。 - 动态组件则是在
Vue应用的模板中根据条件进行动态渲染的组件。它们通过Vue的动态组件语法来声明和使用。动态组件可以根据不同的条件决定渲染哪个组件。Vue提供了特定的组件标签(如<component>)和动态组件特性来实现动态组件的渲染。
动态组件的使用步骤:
- 在
Vue实例中定义一个或多个组件。 - 在
Vue应用的模板中通过<component>标签来声明动态组件,通过is属性来指定要渲染的组件。 - 在
Vue实例中,可以使用条件判断、计算属性等来动态修改is属性的值,从而实现根据条件渲染不同的组件。
提问: 在 vue 中 {{}} 的作用是什么?
在 Vue 中,{{}} 是用来进行插值的语法,也被称为双大括号语法或者 Mustache 语法。它可以将数据动态地插入到 HTML 模板中,实现数据绑定。
原理是通过 Vue 的数据绑定机制实现的。当 Vue 实例中的数据发生变化时,Vue 会自动更新插值表达式中对应的值,并重新渲染视图。这个过程是实现双向数据绑定的核心机制。
具体实现过程如下:
vue会解析模板中的插值表达式,将其转换为相应的JavaScript代码。vue会创建一个Observer对象,用于监听数据对象的变化。- 当数据对象发生变化时,
Observer会通知依赖收集器(Dep)更新视图。 Dep会通知指令(如插值表达式所在的v-text指令)更新DOM中的内容,从而实现数据的动态更新。
一般组件
- 一般组件是在
Vue应用的模板中直接使用的静态组件。它们在模板中通过标签的形式声明和使用,并且在Vue实例创建时就已经存在。 - 一般组件的使用非常简单,只需要在模板中直接引用即可。
- 一般组件的创建和注册过程是在
Vue实例创建时完成的,并且可以在整个应用中重复使用。
动态组件
异步加载组件 - cdn
// 此代码为实际开发中遇到问题,截取实现如下:
const { DxHeaderUrl } = themeConfig.value;
// 存在 dxHeader 对象才会展示dx
if (!DxHeaderUrl) return;
const DxHeader = defineAsyncComponent(async () => {
const APP_DX_ARCH_HEADER_URL = `${DxHeaderUrl}/header/v3/index.umd.js`;
return import(/* @vite-ignore */ APP_DX_ARCH_HEADER_URL)
.then(() => {
const HeaderCssUrl = `${DxHeaderUrl}/header/v3/style.css`;
setCss(HeaderCssUrl);
// cdn 异步导入后,此时组件存在于 window 中
const { DxHeader } = window.DxHeader || {};
return DxHeader;
})
.catch((err) => {
return false;
});
});
// 注册 css 样式
const setCss = (url) => {
const link = document.createElement("link");
link.href = url;
link.rel = "stylesheet";
const head = document.querySelector("head");
head.appendChild(link);
};
组件注册 - 插件形式
// components.js
import DxHeader from "./components/navBar/dx-header.vue";
import Message from "./components/message/message.vue";
import DxIcon from "./components/icon/icon.vue";
import "./assets/style/index.scss";
const components = [DxHeader, Message, DxIcon];
const install = (app: any) => {
components.forEach((comp) => {
app.component(comp.name, comp);
});
};
export default install;
// 使用
import Components from "./components";
vue.use(Components);
插槽 slot
- 默认插槽
- 具名插槽
- 作用域插槽
默认插槽
实现原理为:插槽聚合方式传递
// a.vue
<template>
<div>
<!-- 默认插槽 -->
<slot />
</div>
</template>
// b.vue
import A from './a.vue';
<template>
<A>
<div>插槽内容</div>
</A>
</template>
具名插槽
以 name 表示当前插槽的身份,从而在组件内部区分
原理:抽象成对象,通过 name(key)索引的 node 节点。(name 其实索引了一段单个解析的命名空间,node 独立由这个单个解析命名空间进行渲染)
// a.vue
<template>
<div>
<!-- 具名插槽 -->
<slot name="header" />
<div class="contain"></div>
<slot name="footer" />
</div>
</template>
// b.vue
import A from './a.vue';
<template>
<A>
<template v-slot:header>
<div>头部内容</div>
</template>
<template v-slot:footer>
<div>底部内容</div>
</template>
</A>
</template>
作用域插槽
外部组件可以访问到插槽内部的数据,从而实现数据的共享 主要作用: 数据传递
// a.vue
<template>
<div class="child">
<h3>这里是子组件</h3>
<slot :data="data"></slot>
</div>
</template>
export default {
data: function(){
return {
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
}
}
}
// 父组件
<template>
<div class="father">
<h3>这里是父组件</h3>
<!--第一次使用:用flex展示数据: class="tmpl"-->
<child>
<template slot-scope="user">
<div class="tmpl">
<span v-for="item in user.data">{{item}}</span>
</div>
</template>
</child>
<!--第二次使用:用列表展示数据-->
<child>
<template slot-scope="user">
<ul>
<li v-for="item in user.data">{{item}}</li>
</ul>
</template>
</child>
<!--第三次使用:直接显示数据-->
<child>
<template slot-scope="user">
{{user.data}}
</template>
</child>
<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
<child>
我就是模板
</child>
</div>
</template>
Mixin
- 将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。这样既可以减少代码冗余度,也可以让后期维护起来更加容易。
- 这里需要注意的是:提取的是逻辑或配置,而不是
HTML代码和CSS代码。其实大家也可以换一种想法:mixin就是组件中的组件,Vue组件化让我们的代码复用性更高,那么组件与组件之间还有重复部分,我们使用Mixin再抽离一遍。
export const mixin = {
data() {
return {
msg: "我是mixin",
};
},
compured: {},
beforeCreate() {
console.log("我是mixins 中的 beforeCreate钩子函数");
},
created() {
console.log("我是 mixins 中的 created 钩子函数");
},
mounted() {
console.log("我是 mixins 中的 mounted 函数");
},
methods: {
clickMe() {
console.log("我是mixins 中的点击事件");
},
},
};
// 使用mixin:
import { mixin } from "./views/mixin";
export default {
name: "mixinTest",
mixins: [mixin],
props: {},
data() {
return {
// msg: "我是app内部的msg",
};
},
beforeCreate() {
console.log("组件的 beforeCreate钩子函数");
},
created() {
console.log("组件的 crtead 钩子函数");
},
mounted() {
console.log("组件的 mounted 钩子函数");
},
};
常用问题解决方式
vue 发布订阅解决实际开发中的问题 - 事件总线
Vue 中的 EventBus 是一种事件总线,用于在组件之间进行通信。它的原理是通过创建一个全局的 Vue 实例作为中央事件总线,其他组件可以通过该实例来订阅和发布事件。
具体步骤如下:
创建一个全局的 Vue 实例,作为事件总线:const eventBus = new Vue();
在需要订阅事件的组件中,通过 eventBus.$on(eventName, callback)方法来订阅事件。其中,eventName是事件的名称,callback是事件触发时执行的回调函数。
在触发事件的组件中,通过eventBus.$emit(eventName, payload)方法来发布事件。其中,eventName 是事件的名称,payload 是传递给订阅者的数据。
在订阅事件的组件中,当事件被触发时,订阅者会执行相应的回调函数进行处理。
这种机制可以实现不同组件之间的解耦,使得组件之间可以方便地进行通信和数据传递。但同时也需要注意避免滥用 EventBus,因为过多的全局事件会导致代码维护和调试困难
vue2 实现
示例:
- 创建一个全局的
Vue实例,作为事件总线:const eventBus = new Vue();
import Vue from 'vue'
//向外部共享Vue实例
exprt default new Vue()
- 在传递数据的组件中通过
eventBus.$emit方法来发布事件.
import bus from "./eventBus.js";
export default {
data() {
return {
dmsg: "",
};
},
created() {
bus.$emit("shareData", (val) => {
this.dmsg = val;
});
},
};
- 在接收数据组件中通过
eventBus.$on(eventName, callback)方法来订阅事件.
import bus from "./eventBus.js";
export default {
data() {
return {
dmsg: "",
};
},
created() {
bus.$on("shareData", (val) => {
this.dmsg = val;
});
},
distroyed() {
// 组件销毁时,取消订阅
bus.$off("shareData");
},
};
vue3 实现
vue3.x 移除了 $on , $off , $emit 方法,需要使用第三方库 mitt 实现
移除原因:Vue3 移除了事件总线功能的主要原因是为了推动更好的组件通信模式。事件总线虽然在一些特定场景下有用,但它也存在一些问题。首先,事件总线使组件之间的通信变得隐式,组件之间的关系不够明确,导致代码理解和维护困难。其次,事件总线是全局的,在大型应用中可能会导致事件冲突和管理困难。最后,事件总线会导致性能问题,因为所有组件都监听同一个事件总线,而不仅仅是所需的组件。
Vue3 推荐使用新的组件通信方式,即通过 props 和自定义事件进行父子组件之间的通信,通过 Provide/Inject 和 Composition API 进行跨层级和跨组件之间的通信。这些方式更加显式和直观,并能够更好地进行组件间的解耦和管理。同时,这些新的通信方式也可以更好地支持静态类型检查和编辑器自动补全等工具的支持。
使用方式如下:
局部使用
// 引入mitt
npm install --save mitt
// 生成对应实例
import mitt from 'mitt'
const bus = mitt()
export default bus
// 使用
import bus from './eventBus.js'
export default{
data(){
return{
dmsg:''
}
},
created(){
// 订阅
bus.on('shareData',(val)=>{
this.dmsg=val
})
},
beforeUnmount(){
// 组件销毁时,取消订阅
bus.off('shareData')
}
}
// 发布
import bus from './eventBus.js'
export default{
data(){
return{
dmsg:''
}
},
created(){
// 订阅
bus.emit('shareData',dmsg)
},
}
全局使用
// main.ts
import mitt from 'mitt'
const Mit = mitt()
// typescript 注册
// 由于必须要拓展ComponentCustomProperties才能获得类型提示
declare module 'vue' {
export interface ComponentCustomProperties {
$Bus: typeof Mit
}
}
// 注册
app.config.globalProperties.$Bus = Mit
// A.vue
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const { $Bus } = instance?.proxy
$Bus.emit('xxx', options)
// B.vue
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const { $Bus } = instance?.proxy
$Bus.on('xxx', (options) => {})
// 监听全部事件
$Bus.on('*', (options) => {})
// 清除监听
$Bus.off('xxx')
// 清除全部
$Bus.alll.clear()
provide / inject
provide / inject 实现跨层级组件通信
在父子组件传递数据时,通常使用的是 props 和 emit,父传子时,使用的是 props ,如果是父组件传孙组件时,就需要先传给子组件,子组件再传给孙组件,如果多个子组件或多个孙组件使用时,就需要传很多次,会很麻烦。
像这种情况,可以使用 provide 和 inject 解决这种问题,不论组件嵌套多深,父组件都可以为所有子组件或孙组件提供数据,父组件使用 provide 提供数据,子组件或孙组件 inject 注入数据。同时兄弟组件之间传值更方便。
vue2.x
provide :是一个对象,里面是属性和值。如:
provide: {
info: "值";
}
如果 provide 需要使用 data 内的数据时,这样写就会报错。访问组件实例 property 时,需要将 provide 转换为返回对象的函数。
provide(){
return{
info: this.msg
}
}
inject :是一个字符串数组。如:
inject: ["info"];
接收上边 provide 提供的 info 数据,也可以是一个对象,该对象包含 from 和 default 属性,from 是可用做的注入内容中搜索用的 key,default 属性是指定默认值。
在 vue2 中 project / inject 应用:
//父组件
export default{
provide:{
info:"提供数据"
}
}
//子组件
export default{
inject:['info'],
mounted(){
console.log("接收数据:", this.info) // 接收数据:提供数据
}
}
vue3.x
在组合式 API 中使用 provide/inject,两个只能在 setup 期间调用,使用之前,必须从 vue 显示导入 provide/inject 方法。
provide 函数接收两个参数: provide( name,value )
import { provide } from "vue";
export default {
setup() {
provide("info", "值");
},
};
inject 函数有两个参数:
inject(name, default)
name:接收 provide 提供的属性名。
default:设置默认值,可选参数。
import { inject } from "vue";
export default {
setup() {
inject("info", "设置默认值");
},
};
完整示例:
//父组件代码
<script>
import { provide } from "vue"
export default {
setup(){
provide('info',"值")
}
}
</script>
//子组件 代码
<template>
{{info}}
</template>
<script>
import { inject } from "vue"
export default {
setup(){
const info = inject('info')
return{
info
}
}
}
</script>
VUE 2.x filter
filter 是一个纯函数。无法获取到组件实例状态
vue3 取消了 filter: 因为有更好用的 composition Api
// 全局使用
Vue.filter('xxx', function(value) {
// do something...
return filterValue
})
// 局部使用
filters: {
xxx: function(value) {
// do something...
return filterValue
}
}
结尾
此文章为个人学习使用vue中总结内容,如有意见分歧,欢迎补充交流。