MVC设计模式与MVVM设计模式
使用Vue框架开发前端项目,最大的优势就是再也不用进行复杂的DOM操作了,我们只要关心数据的变化即可,Vue框架会帮我们把复杂的DOM进行渲染,这背后都要归功于他的设计思想,即MVVM设计模式。
了解MVVM设计模式之前,有必要先了解一下MVC设计模式,MVVM模式是在MVC模式基础上演变而来的。
最早的MVC设计模式是出现在后端开发中,主要目的就是让视图层与数据层分离,职责更加清晰,方便开发等等,例如:Spring MVC、ASP.NET MVC等等。
随着Ajax技术的流行,前后端分离开发越来越流行,前端需要处理复杂的视图与数据,迫使前端也急需一种设计模式来进行分层处理,所以MVC设计模式开始进入前端领域。
早期比较经典的前端MVC框架就是backbone.js,但是前后端还是有很大差异的,所以对传统MVC做了一些改良。
backbone.js存在的问题:
- 数据流混乱,尤其是多视图多数据场景下
- 控制层单薄,可有可无
于是2009年Angular.js横空出世,带来了全新的MVVM设计模式,让开发者眼前一亮,除了M和V层以外,就是这个VM层啦,即:viewModel层。MVVM设计模式的核心思想就是不让Model和View这两层直接通信,而是通过VM层来连接。
MVVM设计模式比MVP模式的优势:
- ViewModel能够观察到数据的变化,并对视图对应的内容进行自动更新
- ViewModel能够监听到视图的变化,并能够通知数据发生变化
虽然最早提出MVVM模式的是Angular.js,但是Vue把MVVM设计模式发扬光大了,Vue也成为了当下最主流的前端框架之一。
Vue官网上的一段话:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示组件实例。
MVVM 模型中 M 和 V 不能直接操作,需要VM层加持。但Vue比较灵活,可以直接去操作原生DOM,也就是直接去操作V层。
选项式API的编程风格与优势
选项式API,即:options API
let vm = createApp({
methods: {},
computed: {},
watch: {},
data(){},
mounted(){}
})
这种写法的优势:
- 只有一个参数,不会出现参数顺序的问题,随意调整配置的位置
- 非常清晰,语法化特别强
- 非常适合添加默认值的
-
声明式渲染及响应式数据实现原理
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:
<div id="counter">
Counter: {{ counter }}
</div>
const Counter = {
data() {
return {
counter: 0
}
}
}
Vue.createApp(Counter).mount('#counter')
声明式编程:不需要编写具体是如何实现的,直接调用声明就可以实现功能。SQL就是比较经典的声明式语言:
SELECT * from user WHERE username = xiaoming
for(var i=0;i<user.length;i++)
{
if(user[i].username == "xiaoming")
{
print("find");
break;
}
}
注意:数据是通过 {{ }} 模板语法来完成的,模板语法支持编写JS表达式
响应式数据实现的原理:利用JS的Proxy对象。Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,其实就是直接监控值的修改,当值发生改变的时候,可以监控到。
<div id="app"></div>
<script>
let data = new Proxy(
{
message: "hello",
},
{
get(target) {
console.log("get")
return target.message
},
set(target, key, value) {
console.log("set")
app.innerHTML = value;
},
}
)
app.innerHTML = data.message;
setTimeout(() => {
data.message = "hi"
}, 2000)
</script>
指令系统与事件方法及传参处理
指令系统就是通过自定义属性实现的一套功能,也是声明式编程的体现。
通常在标签上添加 v-* 字样,常见的指令有:
- v-bind -> 操作标签属性,可通过 : 简写
- v-on -> 操作事件,可通过 @ 简写
<div id="app">
<p :title="message">这是一个段落</p>
<button @click=" message = 'hi' ">点击</button>
</div>
{{ message }}
<script>
let vm = Vue.createApp({
data(){
return {
message: 'hello'
}
}
}).mount('#app')
</script>
如何添加事件方法,通过methods选项API实现,并且Vue框架已经帮我们帮事件传参机制处理好了。
<div id="app">
<button @click="toClick($event, 123)">点击</button>
</div>
<script>
let vm = Vue.createApp({
methods: {
toClick(ev, num){
}
}
}).mount('#app')
</script>
计算属性与侦听器区别与原理
计算属性
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以过于复杂的逻辑可以移植到计算属性中进行处理。
<div id="app">
{{ reverseMessage }}
</div>
<script>
let vm = Vue.createApp({
data(){
return {
message: 'hello'
}
},
computed: {
reverseMessage(){
return this.message.split('').reverse().join('')
}
}
}).mount('#app')
</script>
计算属性与方法比较像,如下所示:
<div id="app">
{{ reverseMessageMethod() }}<br>
{{ reverseMessageMethod() }}<br>
{{ reverseMessage }}<br>
{{ reverseMessage }}<br>
</div>
<script>
let vm = Vue.createApp({
data(){
return {
message: 'hello world'
}
},
methods: {
reverseMessageMethod(){
console.log(1);
return this.message.split(' ').reverse().join(' ');
}
},
computed: {
reverseMessage(){
console.log(2);
return this.message.split(' ').reverse().join(' ');
}
}
}).mount('#app');
</script>
计算属性跟方法相比,具备缓存的能力,而方法不具备缓存,所以上面代码执行完,会弹出两次1和一次2。
注意:默认是只读的,一般不会直接更改计算属性,如果想更改也是可以做到的,通过Setter写法实现,官方地址。
既然计算属性编写的是一个函数,而调用的时候以函数名的形式进行使用,其实实现起来也不是特别难的事情:
let computed = {
num(){
return 123;
}
}
let vm = {}
for(let attr in computed){
Object.defineProperty(vm, attr, {
value: computed[attr]()
})
}
侦听器
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。侦听器的目的:侦听器用来观察和响应Vue实例上的数据变动,类似于临听机制+事件机制。当有一些数据需要随着其它数据变化而变化时,就可以使用侦听器。
<div id="app">
{{ message }}
</div>
<script>
let vm = Vue.createApp({
data(){
return {
message: 'hello'
}
},
watch: {
message(newVal, oldVal){
}
}
}).mount('#app')
</script>
有时候,计算属性 和 侦听器 往往能实现同样的需求,那么他们有何区别呢?
- 计算属性适合:多个值去影响一个值的应用;而侦听器适合:一个值去影响多个值的应用
- 侦听器支持异步的程序,而计算属性不支持异步的程序
条件渲染与列表渲染及注意点
条件渲染
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 falsy 假值(即除 false、0、-0、0n、“”、null、undefined 和 NaN 以外皆为真值)。
<div id="app">
<div v-if="isShow">aaaaa</div>
<div v-else>bbbbb</div>
</div>
<script>
let vm = Vue.createApp({
data(){
return {
isShow: 0
}
}
}).mount('#app');
</script>
列表渲染
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。
<div id="app">
<div v-for="item, index in list">{{ item }}, {{ index }}</div>
<div v-for="value, key, index in info">{{ value }}, {{ key }}, {{ index }}</div>
<div v-for="item in num">{{ item }}</div>
<div v-for="item in text">{{ item }}</div>
</div>
<script>
let vm = Vue.createApp({
data(){
return {
list: ['a', 'b', 'c'],
info: { username: 'xiaoming', age: 20 },
num: 10,
text: 'hello'
}
}
}).mount('#app');
</script>
条件渲染与列表渲染需要注意的点
- 列表渲染需要添加key属性,用来跟踪列表的身份
- v-if 和 v-for 尽量不要一起使用,可利用计算属性来完成筛选这类功能(因为v-if优先级高于v-for,这样v-if拿不到v-for中的item属性)
- template标签起到的作用,形成一个整体容器
class样式与style样式的三种形态
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind
处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
- 字符串
- 数组
- 对象
let vm = Vue.createApp({
data() {
return {
myClass1: 'box box2',
myClass2: ['box', 'box2'],
myClass3: { box: true, box2: true },
myStyle1: 'background: red; color: white;',
myStyle2: ['background: red', 'color: white'],
myStyle3: { background: 'red', color: 'white' },
}
},
}).mount("#app")
数组和对象的形式要比字符串形式更加的灵活,也更容易控制变化。
表单处理与双向数据绑定原理
表单是开发过程中经常要进行操作的,一般需要收集表单数据,发送给后端,或者把后端的数据进行回显等。在Vue中是通过v-model指令来操作表单的,可以非常灵活的实现响应式数据的处理。
<div id="app">
<input type="text" v-model="message"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
data() {
return {
message: 'hello'
}
}
}).mount("#app")
</script>
尽管有些神奇,但 v-model
本质上不过是语法糖。可通过value属性 + input事件来实现同样的效果。
<div id="app">
<input type="text" :value="message" @input="message = $event.target.value"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
data() {
return {
message: 'hello'
}
}
}).mount("#app")
</script>
v-model除了可以处理输入框以外,也可以用在单选框、复选框、以及下拉菜单中。
<div id="app">
<input type="checkbox" v-model="fruits" value="苹果">苹果<br>
<input type="checkbox" v-model="fruits" value="西瓜">西瓜<br>
<input type="checkbox" v-model="fruits" value="哈密瓜">哈密瓜<br>
{{ fruits }
<input type="radio" v-model="gender" value="女">女<br>
<input type="radio" v-model="gender" value="男">男<br>
{{ gender }}
<select v-model="city">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="杭州">杭州</option>
</select>
{{ city }}
</div>
<script>
let vm = Vue.createApp({
data(){
return {
fruits: ['西瓜', '哈密瓜'],
gender: '男',
city: '杭州'
}
}
}).mount('#app');
</script>
生命周期钩子函数及原理分析
每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
就是工厂的流水线,每个工人站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作。
简单来说生命周期钩子函数就是回调函数,在Vue的某个时机去调用对应的回调函数。就像定时器一样,谁调用的定时器的回调函数呢?其实就是定时器内部在调用的。
setTimeout(()=>{
console.log('2秒后被执行了');
}, 2000)
官方提供的生命周期图示
生命周期可划分为三个部分:
- 初始阶段:beforeCreate、created、beforeMount、mounted
- 更新阶段:beforeUpdate、updated
- 销毁阶段:beforeUnmout、unmounted
注:一般在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。因为这样可以更短的时间去响应数据。
搜索关键词加筛选条件的综合案例
案例图示如下
准备好案例的JSON数据
[
{
"id": 1,
"name": "小明",
"gender": "女",
"age": 20
},
{
"id": 2,
"name": "小强",
"gender": "男",
"age": 18
},
{
"id": 3,
"name": "大白",
"gender": "女",
"age": 25
},
{
"id": 4,
"name": "大红",
"gender": "男",
"age": 22
}
]
参考代码
<style>
.active-gender{
background: red;
}
</style>
<div id="app">
<input type="text" v-model="message">
<button :class="activeGender('全部')" @click="handleGender('全部')">全部</button>
<button :class="activeGender('男')" @click="handleGender('男')">男</button>
<button :class="activeGender('女')" @click="handleGender('女')">女</button>
<ul>
<li v-for="item in filterList" :key="item.id">{{ item.name }}, {{ item.gender }}, {{ item.age }}</li>
</ul>
</div>
<script>
let vm = Vue.createApp({
data() {
return {
list: [],
message: '',
gender: '全部'
}
},
created(){
fetch('./02-data.json').then((res)=> res.json()).then((res)=>{
this.list = res;
})
},
computed: {
filterList(){
return this.list
.filter((v)=> v.name.includes(this.message))
.filter((v)=> v.gender === this.gender || '全部' === this.gender);
}
},
methods: {
activeGender(gender){
return { 'active-gender': this.gender === gender };
},
handleGender(gender){
this.gender = gender;
}
}
}).mount('#app');
</script>
组件的概念及组件的基本使用方式
组件的概念
组件是带有名称的可复用实例,通常一个应用会以一棵嵌套的组件树的形式来组织,比如:页头、侧边栏、内容区等组件。
组件可以拥有自己独立的结构,样式,逻辑。这样对于后期的维护是非常方便的。下面给出评分组件与按钮组件的抽离过程。
组件的命名方式与规范
- 定义组件可通过驼峰、短线两种风格定义
- 调用组件推荐短线方式
<div id="app">
<my-head></my-head>
</div>
<script>
let app = Vue.createApp({
data(){
return {
}
}
})
app.component('my-head', {
template: `
<header>
<div>{{ message }}</div>
<h2>logo</h2>
<ul>
<li>首页</li>
<li>视频</li>
<li>音乐</li>
</ul>
</header>`,
data(){
return {
message: 'hello world'
}
}
});
let vm = app.mount('#app');
</script>
根组件
app容器可以看成根组件,所以根组件跟普通组件都具备相同的配置信息,例如:data、computed、methods等等选项。
<div id="app">
<my-head></my-head>
</div>
<script>
// 根组件
let RootApp = {
data(){
return {
}
}
};
// MyHead组件
let MyHead = {
template: `
<header>
<div>{{ message }}</div>
<h2>logo</h2>
<ul>
<li>首页</li>
<li>视频</li>
<li>音乐</li>
</ul>
</header>
`
};
let app = Vue.createApp(RootApp)
app.component('MyHead', MyHead);
let vm = app.mount('#app');
</script>
根组件与MyHead组件形成了父子组件。
局部组件与全局组件
局部组件只能在指定的组件内进行使用,而全局组件可以在容器app下的任意位置进行使用。
组件之间是如何进行互相通信的
上一个小节中,我们了解了组件是可以组合的,那么就形成了父子组件,父子组件之间是可以进行通信的, 那么为什么要通信呢?主要是为了让组件满足不同的需求。
父子通信
最常见的通信方式就是父传子,或者子传父,那么父传子通过props实现,而子传父则通过emits自定义事件实现。
<div id="app">
<my-head :count="count" @custom-click="handleClick"></my-head>
</div>
<script>
let app = Vue.createApp({
data(){
return {
count: 10
}
},
methods: {
handleClick(data){
console.log(data);
}
}
})
app.component('MyHead', {
props: ['count'],
emits: ['custom-click'],
template: `
<header>
<div>{{ count }}</div>
<h2>logo</h2>
<ul>
<li>首页</li>
<li>视频</li>
<li>音乐</li>
</ul>
</header>`,
mouted(){
this.$emit('custom-click', 'MyHead Data')
}
});
let vm = app.mount('#app');
</script>
父子通信需要注意的点
- 组件通信的props是可以定义类型的,在运行期间会进行检测
- 组件之间的数据是单向流动的,子组件不能直接修改传递过来的值
- 但是有时候也需要数据的双向流动,可利用v-model来实现
组件的属性与事件是如何进行处理的
有时候组件上的属性或事件并不想进行组件通信,那么Vue是如何处理的呢?
组件的属性与事件
默认不通过props接收的话,属性会直接挂载到组件容器上,事件也是如此,会直接挂载到组件容器上。可通过 inheritAttrs 选项阻止这种行为,通过指定这个属性为false,可以避免组件属性和事件向容器传递。可通过 $attrs 内置语法,给指定元素传递属性和事件,代码如下:
<div id="app">
<my-head title="hello world" class="box" @click="handleClick"></my-head>
</div>
<script>
let app = Vue.createApp({
data(){
return {
}
},
methods: {
handleClick(ev){
console.log(ev.currentTarget);
}
}
})
app.component('MyHead', {
template: `
<header>
<h2 v-bind:title="$attrs.title">logo</h2>
<ul v-bind:class="$attrs.class">
<li>首页</li>
<li>视频</li>
<li>音乐</li>
</ul>
</header>
`,
mounted(){
console.log( this.$attrs ); // 也可以完成父子通信操作
},
inheritAttrs: false // 阻止默认的属性传递到容器的操作
});
let vm = app.mount('#app');
</script>
$attrs也可以实现组件之间的间接通信。
组件的内容是如何组合与分发处理的
在前面的小节中,我们学习了组件之间的通信,让组件之间实现了不同的需求,我们通过给组件添加不同的属性来实现。那么在Vue中如何去传递不同的组件结构呢?这就涉及到了组件内容的分发处理。
插槽slot
在Vue中是通过插槽slot方式来进行分发处理的,Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 元素作为承载分发内容的出口。
<div id="app">
<my-head>
<p>logo</p>
</my-head>
</div>
<script>
let app = Vue.createApp({
data(){
return {
message: 'hello'
}
}
})
app.component('MyHead', {
data(){
return {
};
},
template: `
<header>
<slot></slot>
</header>`,
});
let vm = app.mount('#app');
</script>
组件内的结构,即<p>logo</p>
会被分发到<slot></slot>
所在的区域。
内容分发与插槽的注意点
- 渲染作用域 -> 插槽只能获取当前组件的作用域
- 具名插槽 -> 处理多个插槽的需求,通过v-slot指令实现,简写为#
- 作用域插槽 -> 希望插槽能访问子组件的数据
完整代码如下:
<div id="app">
<my-head>
<template #title>
<p>logo, {{ message }}, {{ count }}</p>
</template>
<template #list="{ list }">
<ul>
<li v-for="item in list">{{ item }}</li>
</ul>
</template>
</my-head>
</div>
<script>
let app = Vue.createApp({
data(){
return {
message: 'hello'
}
}
})
app.component('MyHead', {
data(){
return {
count: 123,
list: ['首页', '视频', '音乐']
};
},
template: `
<header>
<slot name="title"></slot>
<hr>
<slot name="list" :list="list"></slot>
</header>
`,
});
let vm = app.mount('#app');
</script>
# 仿Element Plus框架的el-button按钮组件实现
本小节利用前面学习的组件通信知识,来完成一个仿Element Plus框架的el-button按钮组件实现。仿造的地址:uhttps://element-plus.org/zh-CN/component/button.html
实现需求
- 按钮类型
- 按钮尺寸
- 按钮文字
- 添加图标
完整代码如下:
<style>
.el-button{
display: inline-flex;
justify-content: center;
align-items: center;
line-height: 1;
height: 32px;
white-space: nowrap;
cursor: pointer;
background-color: #ffffff;
border: 1px solid #dcdfe6;
border-color: #dcdfe6;;
color: #606266;
-webkit-appearance: none;
text-align: center;
box-sizing: border-box;
outline: none;
transition: .1s;
font-weight: 500;
user-select: none;
vertical-align: middle;
padding: 8px 15px;
font-size: 14px;
border-radius: 4px;
}
.el-button--primary{
color: white;
background-color: #409eff;
}
.el-button--success{
color: white;
background-color: #67c23a;
}
.el-button--large{
height: 40px;
padding: 12px 19px;
font-size: 14px;
}
.el-button--small{
height: 24px;
padding: 5px 11px;
font-size: 12px;
border-radius: 3px;
}
</style>
<link rel="stylesheet" href="./iconfont/iconfont.css">
<script src="../vue.global.js"></script>
<div id="app">
<el-button>default</el-button>
<el-button type="primary" size="small">Primary</el-button>
<el-button type="success" size="large">Success</el-button>
<el-button type="success" size="large">
<template #icon>
<i class="iconfont iconfangdajing"></i>
</template>
Success
</el-button>
</div>
<script>
let ElButton = {
data(){
return {
buttonClass: {
'el-button': true,
[`el-button--${this.type}`]: this.type !== '',
[`el-button--${this.size}`]: this.size !== ''
}
}
},
props: {
type: {
type: String,
default: ''
},
size: {
type: String,
default: ''
}
},
template: `
<button :class="buttonClass">
<slot name="icon"></slot>
<slot></slot>
</button>`
};
let vm = Vue.createApp({
data(){
return {
}
},
components: {
ElButton
}
}).mount('#app');
</script>
单文件组件SFC及Vue CLI脚手架的安装使用
Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的模板、逻辑 与 样式封装在单个文件中。
为什么要使用 SFC
使用 SFC 必须使用构建工具,但作为回报带来了以下优点:
- 使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件
- 让本来就强相关的关注点自然内聚
- 预编译模板,避免运行时的编译开销
- 组件作用域的 CSS
- 在使用组合式 API 时语法更简单
- 通过交叉分析模板和逻辑代码能进行更多编译时优化
- 更好的 IDE 支持,提供自动补全和对模板中表达式的类型检查
- 开箱即用的模块热更新 (HMR) 支持
如何支持SFC
可通过项目脚手架来进行支持,Vue支持Vite脚手架和Vue CLI脚手架。这里我们先来介绍Vue CLI的基本使用方式。
# 安装
npm install -g @vue/cli
# 创建项目
vue create vue-study
# 选择default
default (babel, eslint)
# 启动脚手架
npm run serve
通过localhost:8080进行访问。
脚手架文件的组成
- src/main.js -> 主入口模块
- src/App.vue -> 根组件
- src/components -> 组件集合
- src/assets -> 静态资源
单文件的代码组成
- template -> 编写结构
- script -> 编写逻辑
- style -> 编写样式
其中style中的scoped属性,可以让样式成为局部的,不会影响到其他组件,只会作用于当前组件生效,同时在脚手架下支持常见的文件进行模块化操作,例如:图片、样式、.vue文件等。
脚手架原理之webpack处理html文件和模块打包
为了更好的理解项目脚手架的使用,我们来学习一下webpack工具,因为脚手架的底层就是基于webpack工具实现的。
安装
webpack工具是基于nodejs的,所以首先要有nodejs环境,其次需要下载两个模块,一个是代码中使用的webpack模块,另一个是终端中使用的webpack-cli模块。
npm install --save-dev webpack
npm install --save-dev webpack-cli
配置文件
通过编写webpack.config.js文件,来编写webpack的配置信息,完成工具的操作行为。webpack最大的优点就是可以把模块化的JS文件进行合并打包,这样可以减少网络请求数,具体是通过entry和output这两个字段来完成的。
// webpack.config.js
module.exports = {
entry: {
main: './src/main.js'
},
output: {
path: __dirname + '/dist',
clean: true
}
}
在终端中进行nodejs脚本build的调用,这样去进行webpack执行,需要我们自己配置一下package.json的脚本。
npm run build # -> webpack
这样在项目目录下就产生了一个 /dist 文件夹,里面有合并打包后的文件。那么我们该如何预览这个文件呢?一般可通过html文件进行引入,然后再通过浏览器进行访问。
但是html的编写还需要我们自己引入要预览的JS文件,不是特别的方便,所以是否可以做到自动完成html的操作呢?答案是可以利用webpack工具的插件HtmlWebpackPlugin来实现。
这样HtmlWebpackPlugin插件是需要安装的,通过npm i HtmlWebpackPlugin
来完成。
// webpack.config.js
module.exports = {
...,
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'vue-study'
}),
new VueLoaderPlugin()
]
}
脚手架原理之webpack启动服务器和处理sourcemap
启动服务环境
目前我们的webpack是没有服务环境的,那么如何启动一个web服务器呢?可以通过webpack-dev-server模块,下载使用即可。
npm install webpack-dev-server
安装好后,再package.json中配置scripts脚本,"serve": "webpack-dev-server"
,然后运行serve脚本。这样就会启动一个http://localhost:8080的服务。
当开启了web服务后,咱们的/dist文件可以不用存在了,服务会把dist的资源打入到内存中,这样可以大大加快编译的速度,所以/dist文件夹可以删除掉了,不影响服务的启动和访问。
处理sourcemap
socurcemap启动映射文件的作用,可以通过浏览器查找到原始的文件,这样对于调试是非常有帮助的,配置如下:
module.exports = {
devtool: 'inline-source-map'
}
# 脚手架原理之webpack处理样式模块和图片模块
loader预处理文件
在模块化使用中,默认只会支持JS文件,那么怎么能够让其他类型的文件,例如:css文件、图片文件、json文件等等都可以当作模块化进行使用呢?这就需要使用loader技术。
支持css模块化
可以通过安装,css-loader
和style-loader
这两个模块来支持css模块化操作。其中css-loader
作用是让css文件能够import方式进行使用,而style-loader
的作用是把css代码抽离到<style>
标签中,这样样式就可以在页面中生效。
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
}
]
}
}
注意数组的顺序是先执行后面再执行前面,所以先写style-loader
再写css-loader
,这样就可以通过import './assets/common.css'
在main.js中进行使用。
图片模块
同样的情况,如果让webpack支持图片模块化也要使用对应的loader,不过在最新版本的webpack中已经内置了对图片的处理,所以只需要配置好信息就可以支持图片模块化。
module.exports = {
module: {
rules: [
...,
{
test: /\.(png|jpg|gif)$/i,
type: 'asset/resource'
}
]
}
}
脚手架原理之webpack处理单文件组件及loader转换
处理单文件组件
目前我们的webpack还不支持对.vue文件的识别,也不支持.vue模块化使用,所以需要安装一些模块来实现。
npm install vue @vue/complier-sfc vue-loader
vue模块主要是为了让vue功能生效。@vue/complier-sfc是对单文件组件的支持。vue-loader是把单文件组件进行转换。下面看一下webpack的完整配置,如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
entry: {
main: './src/main.js'
},
output: {
path: __dirname + '/dist',
clean: true
},
devtool: 'inline-source-map',
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg|gif)$/i,
type: 'asset/resource'
},
{
test: /\.vue$/i,
use: ['vue-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
title: 'vue-study'
}),
new VueLoaderPlugin()
],
mode: 'development'
};
通过配置操作后,目前已经可以完成一个小型的脚手架,支持模块化文件,也支持.vue文件的使用,还可以启动web服务器。
仿Element Plus的el-rate评分组件实现(单文件组件)
插件的安装
在完成本小节案例之前,先安装一下vsCode和chrome的相关插件。
- vsCode Plugin : Vue Language Features (Volar)
- vsCode Plugin : Vue VSCode Snippets
- Chrome Plugin: Vue.js devtools
这些插件都有助于vue框架的使用。下面就看一下我们要做的案例吧。
前面我们仿Element Plus实现了一个按钮组件,不过没有在脚手架的环境下,本小节会在脚手架的环境下完成一个仿Element Plus的el-rate评分组件实现。仿造组件的地址:element-plus.org/zh-CN/compo…
实现需求
- 最大分值
- 选中分值
- 事件交互
<template>
<ul class="rate">
<li v-for="index in max" :key="index" @mouseenter=" $emit('update:modelValue', index) " @mouseleave=" $emit('update:modelValue', initValue) " @click=" initValue = index "><i :class="rateClass(index)"></i></li>
</ul>
</template>
<script>
import '@/assets/iconfont/iconfont.css'
export default {
data(){
return {
initValue: this.modelValue
}
},
props: {
max: {
type: Number,
default: 5
},
modelValue: {
type: Number,
default: 0
}
},
emits: ['update:modelValue'],
methods: {
rateClass(index){
return {
iconfont: true,
'icon-xingxing': true,
active: this.modelValue >= index
}
}
}
}
</script>
<style scoped>
.rate{
display: flex;
list-style: none;
}
.rate i{
font-size: 30px;
color: #ccc;
}
.rate .active{
color: blueviolet;
}
</style>
调用评分组件,如下:
<template>
<h2>hello vue</h2>
<el-rate v-model="value1"></el-rate>{{ value1 }}
<el-rate :max="6" v-model="value2"></el-rate>{{ value2 }}
</template>
<script>
import ElRateVue from './components/ElRate.vue'
export default {
name: 'App',
data(){
return {
value1: 0,
value2: 3
}
},
components: {
ElRate: ElRateVue
}
}
</script>
ref属性在元素和组件上的分别使用
ref属性
前面我们介绍过,Vue是基于MVVM设计模式进行实现的,视图与数据不直接进行通信,但是Vue并没有完全遵循这一原则,而是允许开发者直接进行原生DOM操作。
在Vue中可通过ref属性来完成这一行为,通过给标签添加ref属性,然后再通过vm.$refs
来获取DOM,代码如下:
<template>
<div>
<h2>ref属性</h2>
<button @click="handleClick">点击</button>
<div ref="elem">aaaaaaaaa</div>
<div ref="elem2">bbbbbbbbb</div>
</div>
</template>
<script>
export default {
methods: {
handleClick(){
// ref属性添加到元素身上,可以获取到当前元素的原生DOM
console.log( this.$refs.elem );
console.log( this.$refs.elem2 );
}
}
}
</script>
除了可以把ref属性添加给DOM元素外,还可以把ref属性添加给组件,这样可以获取到组件的实例对象,可以间接的实现组件之间的通信,代码如下:
<template>
<div>
<h2>ref属性</h2>
<my-head ref="elem3"></my-head>
</div>
</template>
<script>
import MyHead from '@/2_头部组件.vue'
export default {
methods: {
handleClick(){
// ref属性添加到组件身上,可以获取到当前组件的vm对象(实例对象)
console.log( this.$refs.elem3 );
console.log( this.$refs.elem3.message );
this.$refs.elem3.handleMessage('根组件的数据');
//$refs 也可以实现间接的父子通信
}
}
}
</script>
2_头部组件.vue
文件:
<template>
<div>
hello myhead
</div>
</template>
<script>
export default {
data(){
return {
message: '头部组件的消息'
}
},
methods: {
handleMessage(data){
console.log(data);
}
}
}
</script>
利用nextTick监听DOM更新后的情况
本小节我们将学习一下nextTick方法,它的主要作用是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
默认情况下,数据的更新会产生一个很小的异步延迟,所以直接再数据改变后取获取DOM是得不到DOM更新后的结果,而得到的是DOM更新前的结果。
<template>
<div>
<h2>hello nextTick</h2>
<div ref="elem">{{ message }}</div>
</div>
</template>
<script>
export default {
data(){
return {
message: 'hello world'
}
},
mounted(){
setTimeout(()=>{
this.message = 'hi vue';
console.log( this.$refs.elem.innerHTML ); // 'hello world'
}, 2000)
}
}
</script>
如何才能得到DOM更新后的结果呢,可以有两种方案,第一种就是利用生命周期updated这个钩子函数,第二种就是利用我们讲的nextTick方法,支持两种风格即回调和promise。
<template>
<div>
<h2>hello nextTick</h2>
<div ref="elem">{{ message }}</div>
</div>
</template>
<script>
export default {
data(){
return {
message: 'hello world'
}
},
mounted(){
setTimeout(()=>{
this.message = 'hi vue';
/* this.$nextTick(()=>{
console.log( this.$refs.elem.innerHTML ); // 'hi vue'
}) */
this.$nextTick().then(()=>{
console.log( this.$refs.elem.innerHTML ); // 'hi vue'
})
}, 2000)
},
updated(){
console.log( this.$refs.elem.innerHTML ); // 'hi vue'
}
}
</script>
自定义指令与自定义全局属性及应用场景
除了核心功能默认内置的指令 (例如 v-model 和 v-show),Vue 也允许注册自定义指令,来实现一些封装功能。
自定义指令的实现
首先我们先来实现一个简单的v-color
指令,用于给元素添加背景色,代码如下:
<template>
<div>
<h2>自定义指令</h2>
<div @click="handleClick" v-color="color">aaaaaaa</div>
</div>
</template>
<script>
export default {
data(){
return {
color: 'red'
}
},
//创建局部的自定义指令
directives: {
/* color: {
mounted(el, binding){
el.style.background = binding.value
},
updated(el, binding){
el.style.background = binding.value
}
} */
color: (el, binding) => {
el.style.background = binding.value
}
}
}
</script>
这里的回调函数是指令中mounted生命周期和updated生命周期的简写方式。
下面我们来完成一个实际可以应用的指令,按钮权限指令,一般情况下这种指令不会局部使用,而是全局使用,所以可以通过vue来实现一个全局的按钮权限指令,代码如下:
// main.js
app.directive('auth', (el, binding) => {
let auths = ['edit', 'delete'];
let ret = auths.includes(binding.value);
if(!ret){
el.style.display = 'none';
}
});
// demo.vue
<template>
<button v-auth="'edit'">编辑</button>
</template>
自定义全局属性
添加一个可以在应用的任何组件实例中访问的全局 property,这样在引入一些第三方模块的时候,就不用每一次进行import操作,而是直接通过this对象去访问全局属性即可,下面举一个例子,实现一个http的全局属性。
// main.js
app.config.globalProperties.$http = http;
//demo.vue
<script>
export default {
created(){
this.$http.get();
}
}
</script>
复用组件功能之Mixin混入
Mixin混入
本小节我们来了解一下mixin混入,它是选项式API的一种复用代码的形式,可以非常灵活的复用功能。
// mymixin.js
const myMixin = {
data(){
return {
message: '复用的数据'
}
},
computed: {
message2(){
return '复用的数据2'
}
}
};
export {
myMixin
}
// mymixin.vue
<template>
<div>
<h2>mixin混入</h2>
<div>{{ message }}</div>
<div>{{ message2 }}</div>
</div>
</template>
<script>
import { myMixin } from '@/mymixin.js'
export default {
mixins: [myMixin]
}
</script>
这样就可以直接在.vue中使用这些混入的功能。当然这种方式是局部混入写法,也可以进行全局混入的写法,代码如下:
// main.js
import { myMixin } from '@/mymixin.js'
app.mixin(myMixin)
mixin存在的问题也是有的,那就是不够灵活,不支持传递参数,这样无法做到差异化的处理,所以目前比较推荐的复用操作还是选择使用组合式API中的use函数来完成复用的逻辑处理。后面章节我们会学习到这种组合式API的应用。
插件的概念及插件的实现
插件是自包含的代码,通常向 Vue 添加全局级功能。例如:全局方法、全局组件、全局指令、全局mixin等等。基于Vue的第三方模块都是需要通过插件的方式在Vue中进行生效的,比如:Element Plus、Vue Router、Vuex等等。
// myplugin.js
import * as http from '@/http.js'
export default {
install(app, options){
console.log(options);
app.config.globalProperties.$http = http;
app.directive('auth', (el, binding) => {
let auths = ['edit', 'delete'];
let ret = auths.includes(binding.value);
if(!ret){
el.style.display = 'none';
}
});
app.component('my-head', {
template: `<div>hello myhead</div>`
})
}
}
// main.js 让插件生效
import myplugin from './myplugin.js'
app.use(myplugin, { info: '配置信息' })
可以看到,让插件生效的语法为app.use
,这样就可以跟Vue结合到一起,所以插件就可以独立出去,成为第三方模块。
# Element Plus框架的安装与使用
前面小节中介绍了自定义插件的实现,那么本小节来看下一比较出名的第三方插件Element Plus如何安装与使用。
Element Plus框架
Element Plus是一套基于PC端的组件库,可以直接应用到很多管理系统的后台开发中,使用前需要先下载安装,除了下载组件库以外,最好也下载组件库对应的icon图标模块,如下:
npm install element-plus @element-plus/icons-vue
接下来把element plus完整引入到Vue中,包装全局组件,全局样式。
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
基本使用方式
element plus中提供了非常多的常见组件,例如:按钮、评分、表单控件等等。
<template>
<div>
<h2>element plus</h2>
<el-button @click="handleClick" type="primary" icon="Clock">Primary</el-button>
<el-button @click="handleClick2" type="success">Success</el-button>
<el-rate v-model="value1" />
<el-icon><Clock /></el-icon>
</div>
</template>
<script>
import { ElMessage, ElNotification } from 'element-plus';
export default {
data(){
return {
value1: 3
}
},
mounted(){
setTimeout(()=>{
this.value1 = 5;
}, 2000)
},
methods: {
handleClick(){
ElMessage.success('提示成功的消息');
},
handleClick2(){
ElNotification({
title: '邮件',
message: '您今日的消费记录'
});
}
}
}
</script>
除了常见的组件外,element plus中也提供了一些逻辑组件,这些逻辑组件是可以直接在JavaScript中进行使用,例如:ElMessage,ElNotification等方法。
transition动画与过渡的实现
在Vue中推荐使用CSS3来完成动画效果。当在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。
transition动画
Vue中通过两个内置的组件来实现动画与过渡效果,分别是:<transition>
和<transition-group>
,代码如下:
<template>
<div>
<h2>hello transition</h2>
<button @click=" isShow = !isShow ">点击</button>
<transition name="slide" mode="out-in">
<div v-if="isShow" class="box"></div>
<div v-else class="box2"></div>
</transition>
</div>
</template>
<script>
export default {
data(){
return {
isShow: true
}
}
}
</script>
<style scoped>
.box{
width: 200px;
height: 200px;
background: skyblue;
}
.box2{
width: 200px;
height: 200px;
background: pink;
}
.slide-enter-from{
opacity: 0;
transform: translateX(200px);
}
.slide-enter-to{
opacity: 1;
transform: translateX(0);
}
.slide-enter-active{
transition: 1s;
}
.slide-leave-from{
opacity: 1;
transform: translateX(0);
}
.slide-leave-to{
opacity: 0;
transform: translateX(200px);
}
.slide-leave-active{
transition: 1s;
}
</style>
其中<transition>
组件通过name
属性去关联CSS中的选择器,CSS中的选择器主要有6种,分别:
v-enter-from
:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。v-enter-active
:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。v-enter-to
:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是v-enter-from
被移除的同时),在过渡或动画完成之后移除。v-leave-from
:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。v-leave-active
:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。v-leave-to
:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是v-leave-from
被移除的同时),在过渡或动画完成之后移除。
默认情况下,进入和离开在两个元素身上是同时执行的,如果想改变其顺序,需要用到
mode
属性,其中out-in
表示先离开再进入,而in-out
表示先进入再离开。
动态组件与keep-alive组件缓存
动态组件
动态组件可以实现在同一个容器内动态渲染不同的组件,依一个内置组件<component>
的is
属性的值,来决定使用哪个组件进行渲染。
<template>
<div>
<h2>动态组件</h2>
<button @click=" nowCom = 'my-com1' ">组件1</button>
<button @click=" nowCom = 'my-com2' ">组件2</button>
<button @click=" nowCom = 'my-com3' ">组件3</button>
<component :is="nowCom"></component>
</div>
</template>
<script>
import MyCom1 from '@/13_MyCom1.vue'
import MyCom2 from '@/14_MyCom2.vue'
import MyCom3 from '@/15_MyCom3.vue'
export default {
data(){
return {
nowCom: 'my-com1'
}
},
components: {
'my-com1': MyCom1,
'my-com2': MyCom2,
'my-com3': MyCom3
}
}
</script>
keep-alive组件
当我们点击的时候,就会进行组件的切换。在每次切换的过程中都会重新执行组件的渲染,这样组件操作的行为就会还原,而我们如何能够保证组件不变呢?可以利用<keep-alive>
对组件进行缓存,这样不管如何切换,都会保持为初始的组件渲染,这样可以很好的保留之前组件的行为。
组件的切换也可以配合<transition>
完成动画的切换。
<template>
<div>
<h2>动态组件</h2>
<button @click=" nowCom = 'my-com1' ">组件1</button>
<button @click=" nowCom = 'my-com2' ">组件2</button>
<button @click=" nowCom = 'my-com3' ">组件3</button>
<transition name="slide" mode="out-in">
<keep-alive>
<component :is="nowCom"></component>
</keep-alive>
</transition>
</div>
</template>
异步组件与Suspense一起使用
异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。
在上一个小节的动态组件的基础上,进行异步组件的演示。首先可以打开chrome浏览器的network网络,可以观察到在动态组件切换的时候,network网络中没有进行任何请求的加载,这证明了在初始的时候,相关的动态组件就已经加载好了。
所以对于大型项目来说,如果能实现按需载入的话,那么势必会对性能有所提升,在Vue中主要就是利用defineAsyncComponent来实现异步组件的。
<script>
import { defineAsyncComponent } from 'vue'
export default {
data(){
return {
nowCom: 'my-com1'
}
},
components: {
'my-com1': defineAsyncComponent(() => import('@/MyCom1.vue')),
'my-com2': defineAsyncComponent(() => import('@/MyCom2.vue')),
'my-com3': defineAsyncComponent(() => import('@/MyCom3.vue'))
}
}
</script>
Suspense组件
由于异步组件是点击切换的时候才去加载的,所以可能会造成等待的时间,那么这个时候可以配合一个loading效果,在Vue中提供了一个叫做<Suspense>
的组件用来完成loading的处理。
<template>
<suspense>
<component :is="nowCom"></component>
<template #fallback>
<div>loading...</div>
</template>
</suspense>
</template>
# 跨组件间通信方案 Provide_Inject
跨组件通信方案
正常情况下,我们的组件通信是需要一级一级的进行传递,通过父子通信的形式,那么如果有多层嵌套的情况下,从最外层把数据传递给最内层的组件就非常的不方便,需要一级一级的传递下来,那么如何才能方便的做到跨组件通信呢?
可以采用Provide 和 inject 依赖注入的方式来完成需求,代码如下:
// provide.vue
<script>
export default {
provide(){
return {
message: 'hello provide',
count: this.count,
getInfo(data){
console.log(data);
}
}
}
}
</script>
// inject.vue
<template>
<div>
hello inject, {{ message }}, {{ count }}
</div>
</template>
<script>
export default {
inject: ['message', 'getInfo', 'count'],
mounted(){
this.getInfo('hello inject');
}
}
</script>
Provide与Inject注意点
- 保证数据是单向流动的,从一个方向进行数据的修改
- 如果要传递响应式数据,需要把provide改造成工厂模式发送数据
Teleport实现传送门功能
Teleport组件
Teleport可以实现传送门功能,也就是说逻辑属于当前组件中,而结构需要在组件外进行渲染,例如:按钮模态框组件。
// 模态框.vue
<template>
<div>
<button @click=" isShow = true ">点击</button>
<teleport to="body">
<div v-if="isShow">模态框</div>
</teleport>
</div>
</template>
<script>
export default {
data(){
return {
isShow: false
}
}
}
</script>
// 调用模态框.vue
<template>
<div>
<h2>传送门</h2>
<my-modal></my-modal>
</div>
</template>
<script>
import MyModal from '@/模态框.vue'
export default {
components: {
'my-modal': MyModal
}
}
</script>
逻辑组件
但是往往我们需要的并不是普通组件的调用方式,而是逻辑组件的调用方式,那么如何实现逻辑组件呢?代码如下:
// 定义逻辑组件,modal.js
import { createApp } from 'vue';
import ModalVue from '@/模态框.vue';
function modal(){
let div = document.createElement('div');
createApp(ModalVue).mount(div);
document.body.append(div);
}
export default modal;
// 调用逻辑组件
<template>
<div>
<h2>传送门</h2>
<button @click="handleClick">点击</button>
</div>
</template>
<script>
import modal from '@/modal.js'
export default {
methods: {
handleClick(){
modal();
}
}
}
</script>
13-虚拟DOM与render函数及Diff算法
虚拟DOM
Vue框架帮我们完成了大量的DOM操作,那么在底层Vue并没有直接操作真实的DOM,因为真实的DOM直接去操作是非常好性能的,所以最好在JS环境下进行操作,然后在一次性进行真实DOM的操作。
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* 更多 vnode */
]
}
那么在Vue中是如何把<template>
模板中的字符串编译成虚拟DOM的呢?需要用到内置的render函数,这个函数可以把字符串转换成虚拟DOM。
Diff算法
当更新的时候,一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。
而两个虚拟DOM进行对比的时候,需要加入一些算法提高对比的速度,这个就是Diff算法。
在脚手架下我们推荐使用
<template>
来完成结构的编写,那么也可以直接通过render函数进行虚拟DOM的创建,代码如下:
<template>
<div>
<h2>render</h2>
</div>
</template>
<script>
import { h } from 'vue';
export default {
render(){
return h('div', h('h2', 'render2'))
}
}
</script>
<style scoped>
</style>
setup方法与script_setup及ref响应式
setup方法与script_setup
在Vue3.1版本的时候,提供了setup方法;而在Vue3.2版本的时候,提供了script_setup属性。那么setup属性比setup方法对于操作组合式API来说会更加的简易。
<template>
<div>
<h2>setup方法</h2>
{{ count }}
</div>
</template>
// Vue3.1
<script>
export default {
setup () {
let count = 0;
return {
count
}
}
}
</script>
// Vue3.2
<script setup>
let count = 0;
</script>
setup方法是需要把数据进行return后,才可以在template标签中进行使用,而setup属性方式定义好后就可以直接在template标签中进行使用。
ref响应式
下面来学习一下,如何在组合式API中来完成数据的响应式操作,通过的就是ref()方法,需要从vue模块中引入这个方法后才可以使用。
<script setup>
import { ref } from 'vue';
let count = ref(0); // count -> { value: 0 }
//count += 1; //✖
count.value += 1; // ✔
</scirpt>
count数据的修改,需要通过count.value的方式,因为vue底层对响应式数据的监控必须是对象的形式,所以我们的count返回的并不是一个基本类型,而是一个对象类型,所以需要通过count.value进行后续的操作,那么这种使用方式可能会添加我们的心智负担,还好可以通过Volar插件可以自动完成.value的生成,大大提升了使用方式。
那么现在count就具备了响应式变化,从而完成视图的更新操作。
那么ref()方法还可以关联原生DOM,通过标签的ref属性结合到一起,也可以关联到组件上。
<template>
<div>
<h2 ref="elem">setup属性方式</h2>
</div>
</template>
<script setup>
import { ref } from 'vue';
let elem = ref();
setTimeout(()=>{
console.log( elem.value ); //拿到对应的原生DOM元素
}, 1000)
</script>
事件方法_计算属性 reactive_toRefs
事件方法与计算属性
下面看一下在组合式API中是如何实现事件方法和计算属性的。
<template>
<div>
<button @click="handleChange">点击</button>
{{ count }}, {{ doubleCount }}
</div>
</template>
<script setup>
import { computed, ref } from 'vue';
let count = ref(0);
let doubleCount = computed(()=> count.value * 2)
let handleChange = () => {
count.value += 1;
};
</script>
事件方法直接就定义成一个函数,计算属性需要引入computed方法,使用起来是非常简单的。
reactive与toRefs
reactive()方法是组合式API中另一种定义响应式数据的实现方式,它是对象的响应式副本。
<template>
<div>
<h2>reactive</h2>
{{ state.count }}
</div>
</template>
<script setup>
import { reactiv} from 'vue';
let state = reactive({
count: 0,
message: 'hi vue'
})
state.count += 1;
</script>
reactive()方法返回的本身就是一个state对象,那么在修改的时候就不需要.value操作了,直接可以通过state.count的方式进行数据的改变,从而影响到视图的变化。
ref()和reactive()这两种方式都是可以使用的,一般ref()方法针对基本类型的响应式处理,而reactive()针对对象类型的响应式处理,当然还可以通过toRefs()方法把reactive的代码转换成ref形式。
<template>
<div>
<h2>reactive</h2>
{{ state.count }}, {{ count }}
</div>
</template>
<script setup>
import { reactive, toRefs } from 'vue';
let state = reactive({
count: 0
})
let { count } = toRefs(state); // let count = ref(0)
setTimeout(() => {
//state.count += 1;
count.value += 1;
}, 1000)
</script>
生命周期_watch_watchEffect
生命周期钩子函数
在学习选项式API的时候,我们学习了生命周期钩子函数,那么在组合式API中生命周期又是如何使用的呢?下面我们从图中先看一下对比的情况吧。
那么具体的区别如下:
- 组合式中是没有beforeCreate和created这两个生命周期,因为本身在组合式中默认就在created当中,直接定义完响应式数据后就可以直接拿到响应式数据,所以不需要再有beforeCreate和created这两个钩子
- 组合式的钩子前面会有一个on,类似于事件的特性,那就是可以多次重复调用
<script>
import { onMounted, ref } from 'vue';
let count = ref(0);
onMounted(()=>{
console.log( count.value );
});
onMounted(()=>{
console.log( count.value );
});
onMounted(()=>{
console.log( count.value );
});
</script>
watch与watchEffect
这里先说一下watchEffect的用法,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
watchEffect常见特性:
- 一开始会初始触发一次,然后所依赖的数据发生改变的时候,才会再次触发
- 触发的时机是数据响应后,DOM更新前,通过flush: 'post' 修改成DOM更新后进行触发
- 返回结果是一个stop方法,可以停止watchEffect的监听
- 提供一个形参,形参主要就是用于清除上一次的行为
<template>
<div>
<h2>watchEffect</h2>
<div>{{ count }}</div>
</div>
</template>
<script setup>
import { ref, watchEffect } from 'vue';
let count = ref(0);
// const stop = watchEffect(()=>{
// console.log(count.value);
// }, {
// flush: 'post'
// })
// setTimeout(()=>{
// stop();
// }, 1000)
// setTimeout(()=>{
// count.value += 1;
// }, 2000)
watchEffect((cb)=>{
console.log(count.value);
cb(()=>{
//更新前触发和卸载前触发,目的:清除上一次的行为(停止上一次的ajax,清除上一次的定时器)
console.log('before update');
})
})
setTimeout(()=>{
count.value += 1;
}, 2000)
</script>
再来看一下watch侦听器的使用方式,如下:
<script setup>
import { ref, watch } from 'vue';
let count = ref(0);
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal);
})
setTimeout(()=>{
count.value = 1;
}, 2000)
</script>
那么watch与watchEffect的区别是什么呢?
- 懒执行副作用
- 更具体地说明什么状态应该触发侦听器重新运行
- 访问侦听状态变化前后的值
跨组件通信方案provide_inject
依赖注入
在Vue中把跨组件通信方案provide_inject也叫做依赖注入的方式,前面我们在选项式中也学习了它的基本概念,下面看一下在组合式API中改如何使用。
// provide.vue
<template>
<div>
<my-inject></my-inject>
</div>
</template>
<script setup>
import MyInject from '@/inject.vue'
import { provide, ref, readonly } from 'vue'
//传递响应式数据
let count = ref(0);
let changeCount = () => {
count.value = 1;
}
provide('count', readonly(count))
provide('changeCount', changeCount)
setTimeout(()=>{
count.value = 1;
}, 2000)
</script>
//inject.vue
<template>
<div>
<div>{{ count }}</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
let count = inject('count')
let changeCount = inject('changeCount')
setTimeout(()=>{
changeCount();
}, 2000);
</script>
依赖注入使用的时候,需要注意的点:
-
不要在inject中修改响应式数据,可利用回调函数修改
-
为了防止可设置成 readonly
复用组件功能之use函数
为了更好的组合代码,可以创建统一规范的use函数,从而抽象可复用的代码逻辑。利用use函数可以达到跟mixin混入一样的需求,并且比mixin更加强大。
// useCounter.js
import { computed, ref } from 'vue';
function useCounter(num){
let count = ref(num);
let doubleCount = computed(()=> count.value * 2 );
return {
count,
doubleCount
}
}
export default useCounter;
<template>
<div>
<h2>use函数</h2>
<div>{{ count }}, {{ doubleCount }}</div>
</div>
</template>
<script setup>
import useCounter from '@/compotables/useCounter.js'
let { count, doubleCount } = useCounter(123);
setTimeout(()=>{
count.value += 1;
}, 2000);
</script>
通过useCounter函数的调用,就可以得到内部return出来的对象,这样就可以在.vue文件中进行功能的使用,从而实现功能的复用逻辑。