在此之前也做了好多个vue3项目,这次通过coderwhy老师的视频,系统的学习一下vue3。
项目一:仿知乎项目 github.com/zhang-glitc…
项目二: 数据大屏项目: github.com/zhang-glitc…
项目三: 自己构建的个人blog: github.com/zhang-glitc…
如何使用Vue呢?
Vue的本质,就是一个JavaScript的库。
-
方式一:在页面中通过CDN的方式来引入;
<script src="https://unpkg.com/vue@next"></script> -
方式二:下载Vue的JavaScript文件,并且自己手动引入;
-
方式三:通过npm包管理工具安装使用它;
-
方式四:直接通过Vue CLI创建项目,并且使用它; 简单使用 我们可以调用
Vue.createApp()来创建一个应用实例,并通过mount将其挂载到指定的dom上。
<div id="app"></div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const options = {
template: '<h2>Hello Vue3</h2>'
}
const app = Vue.createApp(options);
app.mount("#app")
</script>
模板语法
React的开发模式:
-
React使用的jsx,所以对应的代码都是编写的类似于js的一种语法;
-
之后通过Babel将jsx编译成 React.createElement 函数调用;
Vue也支持jsx的开发模式:
-
但是大多数情况下,使用基于HTML的模板语法;
-
在模板中,允许开发者以声明式的方式将DOM和底层组件实例的数据绑定在一起;
-
在底层的实现中,Vue将模板编译成虚拟DOM渲染函数。
Mustache双大括号语法
如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。
-
并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中;
-
当data中的数据发生改变时,对应的内容也会发生更新。
-
当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。
指令
渲染内容相关指令
-
v-once用于指定元素或者组件只渲染一次:-
当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过;
-
该指令可以用于性能优化;
-
如果是子节点,也是只会渲染一次。即使用v-once的标签中的内容都只会渲染一次。
-
-
v-text用于更新元素的 textContent- 等价于
{{}}。 - 并且他会覆盖标签中的任何内容。
- 等价于
-
v-html用于将html字符串当做html渲染到页面,这个指令一般在个人blog渲染文章用的比较多。-
我们通过
{{}}展示html字符串时,vue并不会对其进行特殊的解析。仍然渲染成html字符串。 -
如果我们希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示;
-
-
v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签:- 跳过不需要编译的节点,加快编译的速度;
属性相关指令
v-bind动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。- 缩写
: - 修饰符
.camel将 kebab-case attribute 名转换为 camelCase。.prop- 将一个绑定强制设置为一个 DOM property。.attr- 将一个绑定强制设置为一个 DOM attribute。
- 绑定class有两种方式:
- 对象语法: 传入一个对象作为class的值。key为class属性值,value为一个boolean,看key是否绑定到该元素的class。
如果想使用data中定义的变量作为class值,我们需要使用动态属性绑定
[],下面的title将被看作是一个变量,而不是一个字符串。
<div :class="{active: isActive, [title]: true}">对象形式添加class</div>- 数组语法: 传入一个数组作为class的值。数组中的每个元素作为class属性值。如果遇到元素是表达式或者对象,那么就看其值是否是true。就被添加到class上。 注意如果是元素值不加上引号,那么他将会去定义的data中查找是否有该变量值。例如下面的title
<div :class="['abc', title, isActive ? 'active': '', {active: isActive}]"> 数组形式添加class </div> - 对象语法: 传入一个对象作为class的值。key为class属性值,value为一个boolean,看key是否绑定到该元素的class。
如果想使用data中定义的变量作为class值,我们需要使用动态属性绑定
- 绑定style:某些样式我们需要根据数据动态来决定。
- CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名
- 对象语法:等同于写内联的css样式。只不过属性必须是字符串。如果不是字符串,那么他将被当做成变量,然后将去data中查找是否有该变量。
<div :style="{color: finalColor, 'font-size': '30px'}">对象形式</div>- 数组语法:基本不用。就是将键值对的样式对象当做元素放在数组中。
<div :style="[style1Obj, style2Obj]">数组形式</div> data() { return { message: "Hello World", style1Obj: { color: 'red', fontSize: '30px' }, style2Obj: { textDecoration: "underline" } } } - 动态绑定自定义属性。通过
:[自定义属性]的形式。
<div :[name]="value">动态绑定自定义属性</div> data() { return { name: "cba", value: "kobe" } }- 将对象数据映射到dom元素的属性。这个一般用于将
inheritAttrs: false后,将父元素传入的非props属性挂载到指定的dom上。v-bind="$attrs"
// 这里的info将被写到到div上,成为div的一个个属性 <div v-bind="info">将对象数据映射到dom元素的属性</div> <div :="info">将对象数据映射到dom元素的属性</div> data() { return { info: { name: "zh", age: 20 } } }- 缩写
事件指令
v-on: 用于绑定事件监听。- 简写:
@ - 修饰符
-
.stop- 调用 event.stopPropagation()。 -
.prevent- 调用 event.preventDefault()。 -
.capture- 添加事件侦听器时使用 capture 模式。 -
.self- 只当事件是从侦听器绑定的元素本身触发时才触发回调。 -
.{keyAlias}- 仅当事件是从特定键触发时才触发回调。 -
.once- 只触发一次回调。 -
.lef- 只当点击鼠标左键时触发。 -
.right- 只当点击鼠标右键时触发。 -
.middle- 只当点击鼠标中键时触发。 -
.passive- { passive: true } 模式添加侦听器
-
- 开发时基本上都是绑定一个function,但是如果需要绑定多个函数,我们就需要传入一个对象。
<div v-on="{click: btn1Click, mousemove: mouseMove}"></div> <div @="{click: btn1Click, mousemove: mouseMove}"></div>-
当通过methods中定义方法,以供@click调用时,需要注意参数问题:
- 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。
但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
- 情况二:如果需要同时传入某个参数,同时需要event时,可以通过
$event传入事件。
- 简写:
条件渲染相关指令
v-if- v-if是惰性的。
- 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉。
- 当条件为true时,才会真正渲染条件块中的内容。
- 如果想要多个dom同时显示或者隐藏,我们可以将v-if写在
template标签上,并且让其包裹该多个dom元素。
v-else(配合v-if使用)v-else-if(配合v-if使用)v-show- v-show是不能添加在
template标签上 - v-show不可以和v-else一起使用。
- 本质是通过设置css的
display的属性值来显示或者隐藏元素的。
- v-show是不能添加在
列表渲染指令
-
v-for- 它既可以遍历对象也可以遍历数组
- 格式:
-
"value in object / Array / Number"; -
"(value, key) in object / Array / Number"; -
"(value, key, index) in object";
-
- v-for同时也支持数字的遍历。
- 可以使用template来对多个元素进行包裹,而不是使用div来完成。
- 需要结合key来使用。 v-for中的key是什么作用?
-
key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。
-
如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。
-
而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素。
VNode是什么?
VNode的全称是Virtual Node,也就是虚拟节点。事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode,VNode的本质是一个JavaScript的对象。
虚拟DOM?
如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree。然后就组成了虚拟DOM。
下面我们来看一个小案例
vue中的diff算法
没有添加key的处理过程
添加key的处理过程
表单指令
v-model: 用于表单数据和提供的数据双向绑定。- 在表单
<input>、<textarea>及<select>元素上创建双向数据绑定。 - 它会根据控件类型自动选取正确的方法来更新元素。
- 它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理。
- 他的本质就是监听
input事件,并且通过事件对象将值赋值给提供的数据。 - 修饰符
.lazy: 将v-model的事件绑定从input转变为change事件。.number: 将v-model绑定的值转化为数字.trim: 将v-model绑定的值两边的空格去除。
- 如果是复选框和多选框,
v-model将给选中的值加入到绑定的数组中。并且每个选项都必须设置value属性。
- 在表单
<div id="app"></div>
<template id="my-app">
<!-- 1.绑定textarea -->
<label for="intro">
自我介绍
<textarea name="intro" id="intro" cols="30" rows="10" v-model="intro"></textarea>
</label>
<h2>intro: {{intro}}</h2>
<!-- 2.checkbox -->
<!-- 2.1.单选框 -->
<label for="agree">
<input id="agree" type="checkbox" v-model="isAgree"> 同意协议
</label>
<h2>isAgree: {{isAgree}}</h2>
<!-- 2.2.多选框 -->
<span>你的爱好: </span>
<label for="basketball">
<input id="basketball" type="checkbox" v-model="hobbies" value="basketball"> 篮球
</label>
<label for="football">
<input id="football" type="checkbox" v-model="hobbies" value="football"> 足球
</label>
<label for="tennis">
<input id="tennis" type="checkbox" v-model="hobbies" value="tennis"> 网球
</label>
<h2>hobbies: {{hobbies}}</h2>
<!-- 3.radio -->
<span>你的爱好: </span>
<label for="male">
<input id="male" type="radio" v-model="gender" value="male">男
</label>
<label for="female">
<input id="female" type="radio" v-model="gender" value="female">女
</label>
<h2>gender: {{gender}}</h2>
<!-- 4.select -->
<span>喜欢的水果: </span>
<select v-model="fruit" multiple size="2">
<option value="apple">苹果</option>
<option value="orange">橘子</option>
<option value="banana">香蕉</option>
</select>
<h2>fruit: {{fruit}}</h2>
</template>
<script src="../js/vue.js"></script>
<script>
const App = {
template: '#my-app',
data() {
return {
intro: "Hello World",
isAgree: false,
hobbies: ["basketball"],
gender: "",
fruit: "orange"
}
}
}
Vue.createApp(App).mount('#app');
</script>
在组件中使用v-model指令
我们在表单元素中很容易的使用v-model来做双向绑定。他的原理是通过v-bind:value的数据绑定和@input的事件监听
如果我们想要在自定义组件中使用v-model呢?该如何实现呢?
<!-- 组件上使用v-model -->
<hy-input v-model="message"></hy-input>
// 等加入上面
<hy-input :modelValue="message" @update:model-value="message = $event"></hy-input>
其实在组件中使用v-model,默认情况下其实就是在组件中提供modelValueprops, 并且定义update:modelValue事件。
如果我们想要在表单元素上使用v-model来代替上面的input事件中的属性操作。我们可以借助computed来实现,并且提供getter, setter方法。
// 通过原生的双向绑定实现
<input v-model="updateModelValue">
props: {
modelValue: String
},
emits: ["update:modelValue"],
computed: {
updateModelValue: {
// 当改变modelValue时,就是调用setter方法
set(value) {
this.$emit("update:modelValue", value);
},
get() {
return this.modelValue;
}
}
},
如果我们想要自定义props来实现在组件上使用v-model,我们需要给v-model传递自定义属性名。
<hy-input v-model:title="title"></hy-input>
data() {
return {
title: "title"
}
}
// 这里绑定的computed提供的属性
<input v-model="updateTitle">
props: {
title: String
},
emits: ["update:title"],
computed: {
updateTitle: {
set(value) {
this.$emit("update:title", value);
},
get() {
return this.title;
}
}
}
当我们想在自定义组件中绑定多个属性(即使用多个v-model)时,我们就需要通过上面自定义props绑定名来实现了。
<hy-input v-model="message" v-model:title="title"></hy-input>
data() {
return {
message: "message",
title: "title"
}
}
<input v-model="updateModelValue">
<input v-model="updateTitle">
props: {
modelValue: String,
title: String
},
emits: ["update:modelValue", "update:title"],
computed: {
updateModelValue: {
set(value) {
this.$emit("update:modelValue", value);
},
get() {
return this.modelValue;
}
},
updateTitle: {
set(value) {
this.$emit("update:title", value);
},
get() {
return this.title;
}
}
}
optionsAPI
computed计算属性
我们知道,在模板中可以直接通过插值语法显示一些data中的数据。但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示。
- 需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示
- 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算,在模板中放入太多的逻辑会让模板过重和难以维护。所以需要使用计算属性。
- 如果多个地方都使用到,那么会有大量重复的代码,将它抽离到计算属性中,可以得到重用。 其实,我们也可以通过methods来实现这些逻辑,那为什么要用计算属性呢?他们有什么区别呢?
- 调用逻辑函数的时候,计算属性不需要写
(), 但是methods需要写() - 计算属性方法多次使用会有缓存,只会执行一次,再调用就会使用缓存的结果。当引用的数据发生变化他会重新结算结果,并缓存。但是methods方法不会存在缓存,每次调用对应的方法,都会重新执行一遍。
计算属性的
getter和setter方法
计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。但是,如果我们确实想设置计算属性的值呢? 这个时候我们也可以给计算属性设置一个setter的方法,并且调用计算属性函数时,可以传入值。
methods: {
handleName() {
// 改变计算属性值,然后值传递到set方法中作为参数
this.test = "llm zh"
}
},
computed: {
test: {
get() {
return this.name
},
set(value) {
// console.log(value)
this.name = value
}
}
}
Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?
事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已
watch监听器
如果我们需要监听数据变化,然后做一些逻辑处理,就需要用到watch了。
如何使用? 默认情况下,监听器只能监听本身数据的变化,内部属性的变化是不能被监听的(对于对象来说)
watch: {
// 侦听顶级 property
a(val, oldVal) {
console.log(`new: ${val}, old: ${oldVal}`)
}
}
如果想要监听内部数据(数组或者对象)的变化,我们可以将监听写成一个对象,并且传入一个deep: true属性,来让他深度监听,不管内部嵌套多深,都会被监听到。这里需要注意的是,监听函数的新旧值是一模一样的,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。如果想要使用旧数据,我们需要自己拷贝副本。
watch: {
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler(val, oldVal) {
console.log('c changed')
},
deep: true
},
}
如果我们想要立即执行监听器,我们就需要传递一个immediate: true属性。
watch: {
// 该回调将会在侦听开始之后被立即调用
e: {
handler(val, oldVal) {
console.log('e changed')
},
immediate: true
},
}
我们还可以对一个属性传入多个监听函数。他将被依次调用
watch: {
// 你可以传入回调数组,它们会被逐一调用
f: [
'handle1', // mothods中定义的方法
function handle2(val, oldVal) {
console.log('handle2 triggered')
},
{
handler: function handle3(val, oldVal) {
console.log('handle3 triggered')
}
/* ... */
}
]
}
我们还可以单独监听一个对象中的特定属性值的变化。注意:监听函数拿到的新旧值依旧是一样的。都是改变后的新值。而且是整个对象。而非单独监听的这个属性的值。
watch: {
// 侦听单个嵌套 property
'c.d': function (val, oldVal) {
// do something
}
}
如果我们想要监听数组中对象属性值的变化,我们不可以像上面那种监听方法,我们或者通过deep: true来深度监听,或者在子组件中监听传递的数组中的每一项
我们还可以调用
this.$watch()来监听。并且他会返回一个函数,用于取消监听。
-
第一个参数是要侦听的源。
-
第二个参数是侦听的回调函数callback。
-
第三个参数是额外的其他选项,比如deep、immediate。
const unwatch = this.$watch("info",
function (newInfo, oldInfo) {
console.log(newInfo, oldInfo);
},
{
deep: true,
immediate: true
}
)
// 调用它将取消监听
unwatch()
mixins 混入
组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。这个属性对于vue2中代码抽离和复用,非常有效。但是vue3中我们可以使用另外的方式来对代码进行抽离复用
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
- Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能。
- 一个Mixin对象可以包含任何组件选项。
- 当组件使用Mixin对象时,所有Mixin对象的选项将被 混合 进入该组件本身的选项中。 如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?
这里分成不同的情况来进行处理;
-
情况一:如果是data函数的返回值对象 如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据。
-
情况二:混入生命周期钩子函数
生命周期的钩子函数会被合并到数组中,都会被调用。
- 情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
- 比如都有methods选项,并且都定义了方法,那么它们都会生效;
- 但是如果对象的key相同,那么会取组件对象的键值对; 如果每个组件都需要用到一段相同的逻辑,那么我们就可以使用全局混入。
- 全局的Mixin可以使用 应用app的方法 mixin 来完成注册。
- 一旦注册,那么全局混入的选项将会影响每一个组件。
app.mixin(混入的对象)
混入的代码和该组件中本身的代码执行顺序:全局混入 > 混入 > 自身组件中的代码。
Vue的组件化
我们将一个完整的页面分成很多个组件,每个组件都用于实现页面的一个功能块,而每一个组件又可以进行细分,而组件本身又可以在多个地方进行复用。前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件。
vue中的组件其实很简单,官网讲的很详细。
但是有很多需要注意的地方。接下来我们就介绍一下:
props约束
- 当传递的是对象或者数组,我们指定默认值必须是一个工厂函数。并且返回默认值对象和数组。
- 我们可以通过数组来表示可以是多个类型。
- 我们还可以通过
validator检验函数来自定义约束类型。 - Prop 的大小写命名,最好使用
-链接命名。
非props属性处理
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute。
- 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中
- 如果我们不希望组件的根元素继承attribute,可以在组件中设置
inheritAttrs: false。- 禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素。
- 我们可以通过 $attrs来访问所有的 非props的attribute。
- 多个根节点的attribute
- 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上。
- 多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上。
子组件向父组件传参
我们可以通过emits来对传递的事件参数进行校验,如果出现不符合的,将会出现警告。
如果我们徐想要校验参数,直接写数组就行。
emits: ["add", "sub", "addN"]
全局事件总线
主要用在非父子组件传递参数。
Vue3从实例中移除了 off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库mitt。
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
注册和监听事件
// 发送事件
emitter.emit("字符串事件名", 参数)
// 定义事件
emitter.on('字符串事件名', 回调函数)
// 监听所有事件
emitter.on('*', (事件类型, 对应事件传递的参数) => {})
移除事件
// 移除所有事件
emitter.all.clear()
// 移除指定事件
emitter.off("事件名", 移除事件的引用)
vite的简单介绍
下一代前端开发与构建工具。
他是解决上一代构建工具的问题:
- 在实际开发中,我们编写的代码往往是不能被浏览器直接识别的,比如ES6、TypeScript、Vue文件等等。所以我们必须通过构建工具来对代码进行转换、编译,类似的工具有webpack、rollup、parcel。
- 随着项目越来越大,需要处理的JavaScript呈指数级增长,模块越来越多。
- 构建工具需要很长的时间才能开启服务器,HMR也需要几秒钟才能在浏览器反应出来。
- 开发阶段不需要对代码做过多的适配,并且将浏览器不能识别的文件都转化为esModule文件,提升构建速度,开发效率提升。当项目打包时,在对项目做适配。
它主要由两部分组成:
- 一个开发服务器,它基于原生ES模块提供了丰富的内建功能,HMR的速度非常快速;
- 一套构建指令,它使用
rollup打开我们的代码,并且它是预配置的,可以输出生成环境的优化过的静态资源;
如果我们不借助于其他工具,直接使用ES Module来开发有什么问题呢?
- 首先,当加载一个库时,加载了这个库的所有依赖模块的js代码,对于浏览器发送请求是巨大的消耗。
- 其次,我们的代码中如果有TypeScript、less、vue等代码时,浏览器并不能直接识别。
多以上述问题就需要vite来解决。
现在先安装vite
npm install vite –g #全局安装
npm install vite –D #局部安装
Vite对css的支持
- vite可以直接支持css的处理
- vite可以直接支持css预处理器,比如less,sass
- 但是需要安装less,sass编译器
npm install less -D npm install sass -D - vite直接支持postcss的转换:
- 只需要安装postcss,并且配置
postcss.config.js的配置文件即可
npm install postcss postcss-preset-env -D// postcss.config.js module.exports = { plugins: [ require("postcss-preset-env") ] } - 只需要安装postcss,并且配置
vite对Typescript的支持
- vite对TypeScript是原生支持的,它会直接使用ESBuild来完成编译:
- 只需要直接导入即可。
如果我们查看浏览器中的请求,会发现请求的依然是ts的代码:
这是因为vite中的服务器Connect会对我们的请求进行转发,获取ts编译后的代码,给浏览器返回,浏览器可以直接进行解析。 注意:在vite2中,已经不再使用Koa了,而是使用Connect来搭建的服务器
Vite对vue的支持
- Vue 3 单文件组件插件支持:@vitejs/plugin-vue
- Vue 3 JSX 插件支持:@vitejs/plugin-vue-jsx
- Vue 2 插件支持:underfin/vite-plugin-vue2 在vite.config.js中配置插件:
const vue = require('@vitejs/plugin-vue')
module.exports = {
plugins: [
vue()
]
}
上述配置完成后,我们引入.vue文件,并启动项目,会报错。这时候需要我们安装@vue/compiler-sfc插件即可。
Vite脚手架工具
执行以下命令即可创建一个完整的vue项目。
npm install @vitejs/create-app -g
create-app 项目名
插槽
插槽的作用
通过props传递给组件一些数据,让组件来进行展示,但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的div、span等等这些元素。我们可以定义插槽,让外部可以自定义展示的内容和元素。
插槽的使用
插槽的使用过程其实是抽取共性、预留不同。我们会将共同的元素、内容依然在组件内进行封装。同时会将不同的元素使用slot标签作为占位,让外部决定到底显示什么样的元素。
具体使用可以访问官网。
下面我们来介绍插槽使用的需要注意什么。
注意事项
- 如果想要给插槽做样式定义,我们需要给
slot标签包裹上一个div元素,如果直接在slot标签上写class,那么插槽替换的内容将替代完整的slot插槽。 - 除了默认插槽外,我们传入内容是都需要在
template标签上指定插槽的名称。 - 可以通过
v-slot:[SlotName]方式动态绑定一个名称。 - 插槽不能访问提供插槽的组件中的属性。
- 如果我们渲染插槽的时候,需要用到子组件中的数据,我们就可以通过作用域插槽来将数据传递到父组件进行使用。数据放在一个对象中,并且将作为
v-slot指令的值。
// 这里的item, index属性将会被放入一个对象中传递给父组件。
<slot :item="item" :index="index"></slot>
动态组件
我们如果需要根据条件切换组件,我们就可以使用component标签。并指定一个is属性。其中is属性的值:
- 可以是通过component函数注册的组件。
- 在一个组件对象的components对象中注册的组件。
所以切换组件,我们就可以改变
is属性中的值。并且,我们还可以将组件中的props和事件写入
component组件进行传递。当渲染对应的组件时,就会将props和事件传入到对应的组件中。
<component :is="currentTab"
name="zh"
:age="20"
@pageClick="pageClick">
</component>
组件缓存
默认情况下,我们每次离开一个组件时,该组件都会被销毁,有时候,我们希望保持组件的状态。所以就需要使用keep-alive组件来包裹住需要缓存的组件。只要被包裹后,该组件中的状态就不会消失。
keep-alive有一些属性:(这些一般用于动态组件,路由使用。单个组件包裹一般不需要)
- include - string | RegExp | Array。只有名称匹配的组件会被缓存。
- exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
- max - number | string。最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁。
include 和 exclude prop 允许组件有条件地缓存:
- 二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。
- 匹配首先检查组件自身的 name 选项。
如果我们想要在组件缓存进入和离开之前做一些事情的时候,我们不能调用create, unmounted钩子函数,但是vue给我们内置了用activated和deactivated这两个生命周期钩子函数。
activated() {
console.log("about activated");
},
deactivated() {
console.log("about deactivated");
}
异步组件
如果我们的项目过大了,对于某些组件我们希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中给我们提供了一个函数:defineAsyncComponent。
defineAsyncComponent接受两种类型的参数:
- 类型一:工厂函数,该工厂函数需要返回一个Promise对象。
defineAsyncComponent(() => import("./AsyncCategory.vue"))
上面的import函数返回的就是一个promise对象。是es6的语法。
- 类型二:接受一个对象类型,对异步函数进行配置。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
// 工厂函数
loader: () => import('./Foo.vue')
// 加载异步组件时要使用的组件
loadingComponent: LoadingComponent,
// 加载失败时要使用的组件
errorComponent: ErrorComponent,
// 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)
delay: 200,
// 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件
// 默认值:Infinity(即永不超时,单位 ms)
timeout: 3000,
// 定义组件是否可挂起 | 默认值:true
suspensible: false,
/**
*
* @param {*} error 错误信息对象
* @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
* @param {*} fail 一个函数,指示加载程序结束退出
* @param {*} attempts 允许的最大重试次数
*/
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
// 请求发生错误时重试,最多可尝试 3 次
retry()
} else {
// 注意,retry/fail 就像 promise 的 resolve/reject 一样:
// 必须调用其中一个才能继续错误处理。
fail()
}
}
})
结合Suspense组件使用
与之结合使用后,他会忽略提供的异步组件中的加载、错误、延迟和超时选项。 suspense组件,具有两个插槽。而且两个插槽只能有一个直接子节点。
- default默认插槽,用来显示异步组件。
- fallback插槽,用来显示加载时的组件。
<suspense>
<template #default>
// 异步组件
<async-category></async-category>
</template>
<template #fallback>
<loading></loading>
</template>
</suspense>
teleport组件
在组件化开发中,我们封装一个组件A,在另外一个组件B中使用。那么组件A中template的元素,会被挂载到组件B中template的某个位置。最终我们的应用程序会形成一颗DOM树结构。
但是某些情况下,我们希望组件不是挂载在这个组件树上的,可能是移动到Vue app之外的其他位置。比如移动到body元素上,或者我们有其他的div#app之外的元素上。这个时候我们就可以通过teleport来完成。
Teleport是什么呢?
它是一个Vue提供的内置组件,类似于react的Portals。teleport翻译过来是心灵传输、远距离运输的意思。
它有两个属性:
- to:指定将其中的内容移动到的目标元素,可以使用选择器。
- disabled:是否禁用 teleport 的功能。 但是他仍然是使用方的子组件。仍然可以传递props。同一个目标父组件可以挂载多个teleport传递的组件。按先后顺序插入。
<teleport to="#zh">
<hello-world :name="helloVal"></hello-world>
</teleport>
<teleport to="#zh">
<p>另外一个组件</p>
</teleport>
components: {
HelloWorld,
},
setup() {
const helloVal = ref('hello')
return {
helloVal,
}
}
// HelloWorld.vue
<div>
<h2>{{name}}</h2>
</div>
props: ['name']
}
vue生命周期
每个组件都可能会经历从创建、挂载、更新、卸载等一系列的过程。在这个过程中的某一个阶段,用于可能会想要添加一些属于自己的代码逻辑(比如组件创建完后就请求一些服务器数据)。
但是我们如何可以知道目前组件正在哪一个过程呢?
Vue给我们提供了组件的生命周期函数,生命周期钩子的 this 上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。
动画
Vue中为我们提供一些内置组件和对应的API来完成动画,利用它们我们可以方便的实现过渡动画效果。
没有动画的情况下,整个内容的显示和隐藏会非常的生硬:
如果我们希望给单元素或者组件实现过渡动画,可以使用 transition 内置组件来完成动画。
Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡:
- 条件渲染 (使用 v-if)条件展示 (使用 v-show)
- 动态组件
- 组件根节点
当插入或删除包含在 transition 组件中的元素时,Vue 将会做以下处理:
- 自动嗅探目标元素是否应用了CSS过渡(transition)或者动画(animation),如果有,那么在恰当的时机添加/删除 CSS类名。
- 如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用。
- 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行。将不会有动画效果。
<button @click="isShow = !isShow">显示/隐藏</button>
<transition name="zh">
<h2 v-if="isShow">Hello World</h2>
</transition>
<style scoped>
.zh-enter-from,
.zh-leave-to {
opacity: 0;
}
.zh-enter-to,
.zh-leave-from {
opacity: 1;
}
// 为添加过度属性,将不会出现动画效果。
.zh-enter-active,
.zh-leave-active {
/* transition: opacity 2s ease; */
}
</style>
class属性添加时机
过渡动画class
Vue就是帮助我们在这些class之间来回切换完成的动画:
v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。 当我们没有对transition组件命名时,即给出name属性,那么它将默认使用v当做name属性值。即所有的class是以v-作为默认前缀。给定name属性后,将会根据name属性值多为class前缀。
css动画和css过度
vue中,完成动画我们需要借助动画animation和过度transition来完成。我们需要在 v--enter-active 和 v-leave-active class属性值中定义上面两个属性即可。
// css过度
.zh-enter-from,
.zh-leave-to {
opacity: 0;
}
.zh-enter-to,
.zh-leave-from {
opacity: 1;
}
.zh-enter-active,
.zh-leave-active {
transition: opacity 2s ease;
}
// css动画
.zh-enter-active {
animation: bounce 1s ease;
}
.zh-leave-active {
animation: bounce 1s ease reverse;
}
@keyframes bounce {
0% {
transform: scale(0)
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
transition组件属性
name属性
为了设置class前缀。
type属性
Vue为了知道过渡的完成,内部是在监听 transitionend 或 animationend,到底使用哪一个取决于元素应用的CSS规则:
- 如果我们只是使用了其中的一个,那么Vue能自动识别类型并设置监听。 但是如果我们同时使用了过渡和动画呢?
- 并且在这个情况下可能某一个动画执行结束时,另外一个动画还没有结束。
- 在这种情况下,我们可以设置
type属性为animation或者transition来明确的告知Vue监听的类型。并且可以通过duration显式的指定动画时间。
<transition name="zh" type="transition" :duration="{enter: 800, leave: 1000}">
<h2 class="title" v-if="isShow">Hello World</h2>
</transition>
duration属性
设置过度动画的时间。
duration可以设置两种类型的值:
- pnumber类型:同时设置进入和离开的过渡时间。
- pobject类型:分别设置进入和离开的过渡时间。 注意:显式设置的值会覆盖css过度和动画中指定的值。
mode属性
默认情况下,进入和离开同时发生。如果想改变默认状态。我们就需要添加mode属性。
in-out: 新元素先进行过渡,完成之后当前元素过渡离开。out-in: 当前元素先进行过渡,完成之后新元素过渡进入。 大多数情况下,我们需要前一个动画结束时,后一个动画开始。所以需要使用out-in
appear属性
默认情况下,首次渲染的时候是没有动画的,如果我们希望给他添加上去动画,那么就可以增加appear;ture。
css属性
当我们通过js来操作动画的时候,我们就不需要vue来检测css中的动画了。所以需要将css设置为false。默认情况下css: true。
JavaScript 钩子
当我们想要在动画执行的各个阶段,做一些事情。我们就可以使用这个钩子。
- @before-enter="beforeEnter",执行到v-enter-from阶段
- @enter="enter",执行到v-enter-active
- @after-enter="afterEnter",执行到v-enter-to阶段
- @enter-cancelled="enterCancelled"
- @before-leave="beforeLeave",执行到v-leave-to阶段
- @leave="leave",执行到v-leave-active阶段
- @after-leave="afterLeave",执行到v-leave-to阶段
- @leave-cancelled="leaveCancelled",执行到v-enter-to阶段
当只用 JavaScript 过渡的时候,在
enter和leave钩中必须使用done进行回调。否则,它们将被同步调用,过渡会立即完成。这时,我们可以添加:css="false",让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
上面的钩子可以和一些js动画库来实现动画。例如jsap库。 它可以通过JavaScript为CSS属性、SVG、Canvas等设置动画,并且是浏览器兼容的。其中有两个比较重要的API来实现动画。
jsap.from(el, options): 表示动画从什么状态开始。jsap.to(el, options): 表示动画以什么状态结束。 其中el表示动画作用的元素。options表示动画css属性。
// 进入动画
enter(el, done) {
console.log('enter')
// from是表示开始的位置
gsap.from(el, {
scale: 0,
x: 200,
onComplete: done,
})
},
// 离开动画
leave(el, done) {
console.log('leave')
// to表示结束的位置
gsap.to(el, {
scale: 0,
x: 200,
onComplete: done,
})
},
我们来使用jsap库实现一个滚动数字动画。
<template>
<div class="app">
<input type="number" step="100" v-model="counter">
<h2>当前计数: {{showNumber.toFixed(0)}}</h2>
</div>
</template>
<script>
import gsap from 'gsap';
export default {
data() {
return {
counter: 0,
showNumber: 0
}
},
watch: {
counter(newValue) {
gsap.to(this, {duration: 1, showNumber: newValue})
}
}
}
</script>
其他动画知识,请访问官网,讲的非常详细。
composition API
一下使用的API都需要在vue中导入。
Options API的弊端
在Vue2中,我们编写组件的方式是Options API:
- Options API的一大特点就是在对应的属性中编写对应的功能模块。比如data定义数据、methods中定义方法、computed中定义计算属性、watch中监听属性改变,也包括生命周期钩子;
但是这种代码有一个很大的弊端:
- 当我们实现某一个功能时,这个功能对应的代码逻辑会被拆分到各个属性中。
- 当我们组件变得更大、更复杂时,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散。
- 尤其对于那些一开始没有编写这些组件的人来说,这个组件的代码是难以阅读和理解的(阅读组件的其他人)。
composition API介绍
composition API的容器
其中composition API都是写在我们setup函数中的。并且在setup函数中是不能使用this的,因为vue内部再调用setup函数的时候没有绑定this。
下面我们就来研究一些setup函数。
- 它主要有两个参数:
- 第一个参数:
props。组件接收的属性 - 第二个参数:
context。组件上下文对象- attrs:所有的非prop的attribute。
- slots:父组件传递过来的插槽(这个在以渲染函数返回时会有作用)。
- emit:当我们组件内部需要发出事件时会用到emit。
- expose:当通过ref获取该组件时,向外暴露的一些setup中的数据。 那我们如何定义响应式数据呢?
- 第一个参数:
composition API处理数据
reactive: 将多个数据变成响应式数据
- 当我们使用reactive函数处理我们的数据之后,数据再次被使用时就会进行依赖收集。
- 当数据发生改变时,所有收集到的依赖都是进行对应的响应式操作(比如更新界面)。
- 事实上,我们编写的data选项,也是在内部交给了reactive函数将其编程响应式对象的。
const state = reactive({
counter: 100,
name: 'zh'
})
ref: 将单个数据变成响应式。
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:如果我们传入一个基本数据类型(String、Number、Boolean)会报一个警告。所以我们需要使用ref。
- ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值,这就是ref名称的来源。
- 它内部的值是在ref的 value 属性中被维护的。
- 在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用。
- 在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式。
注意:ref对象在模板中的解包是浅层的解包
readonly: 返回一个传入的对象的只读代理
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改。例如我们想要将provide提供数数据传递给子孙组件,我们就可以使用readonly,让其是只读的,不能再子孙组件中修改。
该API返回普通对象, ref对象, reactive对象的只读代理。
- readonly返回的对象都是不允许修改的。
- 但是经过readonly处理的原来的对象是允许被修改的。
- 其实本质上就是readonly返回的对象的setter方法被劫持了而已。
// 1.普通对象
const info1 = {name: "zh"};
const readonlyInfo1 = readonly(info1);
// 2.响应式的对象reactive
const info2 = reactive({
name: "zh"
})
const readonlyInfo2 = readonly(info2);
// 3.响应式的对象ref
const info3 = ref("zh");
const readonlyInfo3 = readonly(info3);
toRefs: 将传入的对象变成ref对象
当我们想要对reactive对象做解构的时候,直接解构,将使数据失去响应式。如果我们用toRefs将其包裹后解构,数据依然是响应式的。这种做法相当于已经在reactive中的属性和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化
const info = reactive({ name: 'zh', age: 22 })
// 1.toRefs: 将reactive对象中的所有属性都转成ref, 建立链接
let { name, age } = toRefs(info)
toRef: 将指定传入的对象那个属性变成ref对象
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法。
const info = reactive({ name: 'zh', age: 22 })
// 将info对象中的age属性变成ref对象。
let age = toRef(info, 'age')
isProxy
- 检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
- 检查对象是否是由 reactive创建的响应式代理。
- 如果该代理是 readonly 创建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true。
isReadonly
- 检查对象是否是由 readonly 创建的只读代理。
toRaw
- 返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
- 创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
- 创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
unref
- 如果我们想要获取一个ref引用中的value,那么也可以通过unref方法。
- 如果参数是一个 ref,则返回内部值,否则返回参数本身。
- 这是 val = isRef(val) ? val.value : val 的语法糖函数。
isRef
- 判断值是否是一个ref对象。
shallowRef
- 创建一个浅层的ref对象。只有修改了ref对象,他才是响应式的。如果修改内部对象,将不是响应式的。 这个api和shallowReactive不一样。后者是将传入的对象第一层变成一个响应式的,修改第一层对象属性依旧是可以做到响应式的。但是这个api只是修改ref对象才会是响应式的。
const info = shallowRef({ name: 'zh' })
const changeInfo = () => {
// 只有这样修改才是响应式的
info.value = { name: 'llm' }
// 这样改不是响应式的
info.value.name = 'llm'
}
triggerRef
- 手动触发和 shallowRef 相关联的副作用。
const info = shallowRef({name: "zh"})
const changeInfo = () => {
info.value.name = "llm";
// 让其是响应式的
triggerRef(info);
}
customRef: 自定义ref对象
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
- 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数。
- 并且应该返回一个带有 get 和 set 的对象。
import { customRef } from 'vue';
// 自定义ref
export default function(value, delay = 300) {
let timer = null;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timer);
timer = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}
})
}
// 直接使用
const message = debounceRef("Hello World");
获取当前组件上下文 getCurrentInstance
由于setup函数中,没有绑定this。所以我们获取不到this,即当前组件对象。
如果我们想要获取呢?
vue提供了getCurrentInstance, 可以让我们获取当前组件对象。调用该API即可。
如果想要获取组件提供的全局属性。我们需要获取全局对象。
getCurrentInstance().appContext.config.globalProperties
计算属性 computed
当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理。
computed可以传入两种参数:
- 接收一个getter函数,并为 getter 函数指定返回值
- 接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象。当修改computed时非常有用。
- computed返回一个ref对象。
// 1.用法一: 传入一个getter函数
// computed的返回值是一个ref对象
const fullName = computed(() => firstName.value + " " + lastName.value);
// 2.用法二: 传入一个对象, 对象包含getter/setter
const fullName = computed({
get: () => firstName.value + " " + lastName.value,
set(newValue) {
const names = newValue.split(" ");
firstName.value = names[0];
lastName.value = names[1];
}
});
监听数据 watch / watchEffect
当数据变化时执行某一些操作。我们可以通过watch / watchEffect来监听。
二者的区别:
watchEffect不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行,而watch只能监听指定的属性而做出变更(v3开始可以同时指定多个)。watch可以获取到新值与旧值(更新前的值),而watchEffect是拿不到的。watchEffect如果存在的话,在组件初始化的时候就会执行一次用以收集依赖(与computed同理),而后收集到的依赖发生变化,这个回调才会再次执行,而watch不需要,因为他一开始就指定了依赖。watchEffect- 首先,watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖。
- 其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行。
- 并且返回一个函数,这个函数可以用来取消
watchEffect的监听。或者组件卸载后自动停止监听。
// watchEffect: 自动收集响应式的依赖
const name = ref("zh");
const age = ref(20);
const stop = watchEffect(() => {
console.log("name:", name.value, "age:", age.value);
});
const changeName = () => name.value = "llm"
const changeAge = () => {
age.value++;
// 当age属性大于30时,就取消watchEffect的监听。
if (age.value > 30) {
stop();
}
}
watchEffect的执行时机。默认情况下,watchEffect是在视图更新之前执行副作用函数。如果我们想要改变他的执行时机,怎么改变呢?
watchEffect还可以传入第二个参数,为一个对象。设置flush顺序性就可以改变watchEffect的执行时机。
flush: 'pre'(默认,在视图更新前执行)
'post'(在视图更新后执行)
'sync'(同步触发,会出现问题,少用)
const title = ref(null);
watchEffect(() => {
console.log(title.value); // 获取dom元素。
}, {
flush: "post" // 在视图更新后执行,会减少watchEffect无效的执行。因为第一次执行会获取null
})
watchEffect还可以清除副作用
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate, 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数。我们可以在传入的回调函数中,执行一些清楚工作。就类似于节流函数。
watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
onInvalidate(() => {
// 在这个函数中清除额外的副作用
clearTimeout(timer);
console.log("onInvalidate");
})
});
watch
watch的API完全等同于组件watch选项的Property:
- watch需要侦听特定的数据源,并在回调函数中执行副作用。
- 默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调。 监听单个数据源
watch侦听函数的数据源有两种类型:
- 一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref)。
- 直接写入一个可响应式的对象,reactive或者ref(比较常用的是ref)。
如果传入的是一个ref响应式对象或者getter并返回一个普通对象(通过...结构),监听函数中都是普通对象 / 普通值。而非ref, reactive对象。
如果传入的是一个reactive对象,那么监听函数中是reactive对象。并且新值和旧值都是一样的。
// 改变name
const info = reactive({ name: 'zh', age: 20 })
watch(info, (newValue, oldValue) => {
// newValue, oldValue都是reactive对象,并且返回的都是新值。
console.log('newValue:', newValue, 'oldValue:', oldValue)
})
监听多个值
可以将多个源放在数组中。
// 1.定义可响应式的对象
const info = reactive({ name: 'zh', age: 20 })
const name = ref('zh')
// 2.侦听器watch
watch(
[() => ({ ...info }), name],
([newInfo, newName], [oldInfo, oldName]) => {
console.log(newInfo, newName, oldInfo, oldName)
}
)
watch的选项
如果想要深度监听对象,我们就需要给watch传入第二个参数。用于深度监听或者立即执行。
默认情况下,watch对于监听展开的reactive对象不能深度监听,但是我们如果先改变第一层的属性即info.name,那么info.friend.name也会被改变。但是如果只改变info.friend.name,是不会触发watch回调的。只有配置了deep: true,才会被监听到。
const info = reactive({
name: 'zh',
age: 18,
friend: {
name: 'jcl',
},
})
// 2.侦听器watch
watch(
() => ({ ...info }),
(newInfo, oldInfo) => {
console.log(newInfo, oldInfo)
},
{
// deep: true,
// immediate: true,
}
)
const changeData = () => {
// info.name = 'llm'
info.friend.name = 'zheng'
}
但是对于直接监听reactive对象,他会自动深度监听,内部有设置deep: true。
const info = reactive({
name: 'zh',
age: 18,
friend: {
name: 'jcl',
},
})
watch(info, (newInfo, oldInfo) => {
console.log(newInfo, oldInfo)
})
const changeData = () => {
info.friend.name = 'zheng'
}
对于ref传入的对象,也是默认没有深度监听的。并且监听函数中参数都是一个对象。
const info = ref({
name: 'zh',
age: 18,
friend: {
name: 'jcl',
},
})
watch(
info,
(newInfo, oldInfo) => {
console.log(newInfo, oldInfo)
},
{
// 需要加上,才会深度监听
deep: true,
}
)
const changeData = () => {
info.value.friend.name = 'zheng'
}
生命周期
options API中生命周期和composition API中生命周期对比
| 选项式 API | Hook inside setup |
|---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
provide 和 inject
如果我们想要向子孙组件传递数据,我们就可以通过provide API来完成。用inject API来接收传递的数据。
由于我们向下传递的数据,不需要子孙组件修改,只允许我们自己修改,然后影响下层组件,所以。可以使用readonlyAPI来传递只读数据。
provide可以传入两个参数:
- 第一个是提供的属性名称
- 第二个是传入的数据 inject可以传入两个参数:
- 第一个是接收到provide传递的属性名
- 第二个是提供默认值
// 向下传递
const obj = reactive({
name: 'zh',
age: 20,
})
const name = ref('llm')
provide('obj', readonly(obj))
provide('name', readonly(name))
// 接收
let obj = inject('obj')
let name = inject('name')
如果我们真的想要在子孙组件中修改数据,我们可以提供一个函数,接收子孙组件的数据,然后在该祖先组件中修改。
// provide
//这个也传入到inject中,供其修改
const updateName = (e, val) => {
console.log('e---------------', e, val)
name.value = val
}
// inject
let updateName = inject('updateName')
<button @click="(e) => {updateName(e, '子孙组件中修改name的数据')}">改变name</button>
渲染函数 render
Vue推荐在绝大数情况下使用模板来创建你的HTML,然后一些特殊的场景,你真的需要JavaScript的完全编程的能力,这个时候你可以使用 渲染函数 ,它比模板更接近编译器。
Vue在生成真实的DOM之前,会将我们的节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚 拟DOM(VDOM)。
事实上,我们之前编写的 template 中的HTML 最终也是使用渲染函数生成对应的VNode。
那么,如果你想充分的利用JavaScript的编程能力,我们可以自己来编写 createVNode 函数,生成对应的VNode。
我们可以通过vue提供的h函数来实现。h() 函数是一个用于创建 vnode 的一个函数。其实准备的命名是 createVNode() 函数,但是为了简便在Vue将之简化为 h() 函数。
下面就是h函数的参数传递和用法。
h(
// {String | Object | Function} tag
// 一个 HTML 标签名、一个组件、一个异步组件、或
// 一个函数式组件。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 这会在模板中用到。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 VNode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
注意:如果没有props,那么通常可以将children作为第二个参数传入。如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入。
h函数的基本使用。h函数可以在两个地方使用:
- render函数选项中。作为render函数的返回值。如果想要使用事件。我们可以传入on+事件名的属性,函数作为他的值。
data() {
return {
counter: 0
}
},
render() {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${this.counter}`),
h("button", {
onClick: () => this.counter++
}, "+1"),
h("button", {
onClick: () => this.counter--
}, "-1"),
])
}
- setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode)。作为setup函数的返回值。
setup() {
const counter = ref(0);
return () => {
return h("div", {class: "app"}, [
h("h2", null, `当前计数: ${counter.value}`),
h("button", {
onClick: () => counter.value++
}, "+1"),
h("button", {
onClick: () => counter.value--
}, "-1"),
])
}
}
h函数中使用插槽。 可以通过三元运算符,提供默认插槽内容。
HelloWorld.vue
// 子组件提供插槽和插槽prop
setup() {
const instance = getCurrentInstance().ctx.$slots
return () =>
h('div', {}, [
instance.first
? instance.first({ first: 'first=========' })
: '默认插槽first',
instance.second
? instance.second({ second: 'second=========' })
: '默认插槽second',
])
},
// 使用渲染函数插槽
setup() {
return () =>
h(
HelloWorld,
{class: 'hello-world'},
{
// 向HelloWorld传入插槽内容,并使用提供的插槽数据
first: (slotProps) => h('span', slotProps.first),
second: (slotProps) => h('span', slotProps.second),
}
)
},
以上只是一些个人实验,如果想要了解更多,请访问官网 v3.cn.vuejs.org/guide/rende…
在vue中使用jsx
首先我们需要安装@vue/babel-plugin-jsx / @vitejs/plugin-vue-jsx插件,然后在babel配置文件中配置。
// babel.config.js
module.exports = {
presets: [
"@vue/cli-plugin-babel/preset"
],
plugins: [
"@vue/babel-plugin-jsx"
]
}
或者vite创建的项目vite.config.js中直接导入插件,然后在plugins调用。
使用时,需要在script标签中指定lang="jsx"。
在setup模板中使用jsx时,我们只需要定义一个函数,然后返回dom树结构,然后再在template模板中使用这个函数即可。
<template>
<jsxrender />
</template>
const a = ref("zh")
const jsxrender = () => (
<div>
{a.value}
</div>
)
使用jsx的好处
- 可以直接在jsx中得到使用变量的提示。
- 在为传递props时,编译时会报错。
- 也可以直接使用vue提供的指令。
- 可以很好的扩展当前组件。
自定义指令
在Vue的模板语法中我们学习过各种各样的指令:v-show、v-for、v-model等等,除了使用这些指令之外,Vue也允许我们来自定义自己的指令。用来复用代码,方便操作。
注意:在Vue中,代码的复用和抽象主要还是通过组件通常在某些情况下,你需要对DOM元素进行底层操作,这个时候就会用到自定义指令。
自定义指令分为两种:
- 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用。
- 自定义全局指令:app的 directive 方法,可以在任意组件中被使用。 下面我们来自定义一个自动获取焦点的指令。
局部指令: 直接在dom上通过v-focus使用即可
// 局部指令
directives: {
focus: {
mounted(el, bindings, vnode, preVnode) {
el.focus();
}
}
}
全局指令
app.directive("focus", {
mounted(el, bindings, vnode, preVnode) {
el.focus();
}
})
下面我们就来介绍一下自定义指令中的生命周期函数
一个指令定义的对象,Vue提供了如下的几个钩子函数: 注意: 这些生命周期函数名称和vue2有一些不同
created:在绑定元素的 attribute 或事件监听器被应用之前调用;beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;mounted:在绑定元素的父组件被挂载后调用;beforeUpdate:在更新包含组件的 VNode 之前调用;updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;beforeUnmount:在卸载绑定元素的父组件之前调用;unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次; 生命周期函数的参数:其中el, binding比较常用el: 指令绑定到的元素。这可用于直接操作 DOM。binding: 包含以下 property 的对象。instance:使用指令的组件实例。value:传递给指令的值。例如,在v-my-directive="1 + 1"中,该值为2。oldValue:先前的值,仅在beforeUpdate和updated中可用。值是否已更改都可用。arg:参数传递给指令 (如果有)。例如在v-my-directive:foo中,arg 为"foo"。modifiers:包含修饰符 (如果有) 的对象。例如在v-my-directive.foo.bar中,修饰符对象为{foo: true,bar: true}。dir:一个对象,在注册指令时作为参数传递。(就是提供的生命周期函数)
vnode: 上面作为 el 参数收到的真实 DOM 元素的蓝图。prevNode: 上一个虚拟节点,仅在beforeUpdate和updated钩子中可用。下面我们来封装一个格式化时间戳的指令
import dayjs from 'dayjs';
app.directive("format-time", {
// 做一些初始化操作
created(el, bindings) {
bindings.formatString = "YYYY-MM-DD HH:mm:ss";
// 当用户传入格式化字符串,我们将使用用户传入的格式
if (bindings.value) {
bindings.formatString = bindings.value;
}
},
mounted(el, bindings) {
const textContent = el.textContent;
let timestamp = parseInt(textContent);
if (textContent.length === 10) {
// 将时间戳转化为毫秒
timestamp = timestamp * 1000
}
el.textContent = dayjs(timestamp).format(bindings.formatString);
}
})
// 可以让用户自定义格式化
<h2 v-format-time="'YYYY/MM/DD'">{{timestamp}}</h2>
插件
通常我们向Vue全局添加一些功能时,会采用插件的模式,它有两种编写方式:
- 对象类型:一个对象,但是必须包含一个
install的函数,该函数会在安装插件时执行。install函数将接受两个参数,一是全局对象。二是用户传入的配置对象 - 函数类型:一个function,这个函数会在安装插件时自动执行。将接受两个参数,一是全局对象。二是用户传入的配置对象。
插件可以完成的功能没有限制,比如下面的几种都是可以的:
- 添加全局方法或者 property,通过把它们添加到 config.globalProperties 上实现。
- 添加全局资源:指令/过滤器/过渡等。
- 通过全局 mixin 来添加一些组件选。
- 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。 使用插件调用全局对象的use方法即可。并将插件对象传入给use方法。
// plugin.js
// 对象类型
export default {
install(app) {
app.config.globalProperties.$name = "zh"
}
}
// 函数类型
export default function(app) {
console.log(app);
}
// 使用
const app = createApp(App);
app.use(plugin)