在此之前也做了好多个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>
-
-
绑定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"
<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-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">
<label for="intro">
自我介绍
<textarea name="intro" id="intro" cols="30" rows="10" v-model="intro"></textarea>
</label>
<h2>intro: {{intro}}</h2>
<label for="agree">
<input id="agree" type="checkbox" v-model="isAgree"> 同意协议
</label>
<h2>isAgree: {{isAgree}}</h2>
<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>
<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>
<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,默认情况下其实就是在组件中提供modelValue
props, 并且定义update:modelValue
事件。
如果我们想要在表单元素上使用v-model来代替上面的input事件中的属性操作。我们可以借助computed来实现,并且提供getter, setter方法。
<input v-model="updateModelValue">
props: {
modelValue: String
},
emits: ["update:modelValue"],
computed: {
updateModelValue: {
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"
}
}
<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() {
this.test = "llm zh"
}
},
computed: {
test: {
get() {
return this.name
},
set(value) {
this.name = value
}
}
}
Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢?
事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已
watch监听器
如果我们需要监听数据变化,然后做一些逻辑处理,就需要用到watch
了。
如何使用? 默认情况下,监听器只能监听本身数据的变化,内部属性的变化是不能被监听的(对于对象来说)
watch: {
a(val, oldVal) {
console.log(`new: ${val}, old: ${oldVal}`)
}
}
如果想要监听内部数据(数组或者对象)的变化,我们可以将监听写成一个对象,并且传入一个deep: true
属性,来让他深度监听,不管内部嵌套多深,都会被监听到。这里需要注意的是,监听函数的新旧值是一模一样的,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。如果想要使用旧数据,我们需要自己拷贝副本。
watch: {
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',
function handle2(val, oldVal) {
console.log('handle2 triggered')
},
{
handler: function handle3(val, oldVal) {
console.log('handle3 triggered')
}
}
]
}
我们还可以单独监听一个对象中的特定属性值的变化。注意:监听函数拿到的新旧值依旧是一样的。都是改变后的新值。而且是整个对象。而非单独监听的这个属性的值。
watch: {
'c.d': function (val, oldVal) {
}
}
如果我们想要监听数组中对象属性值的变化,我们不可以像上面那种监听方法,我们或者通过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中的组件其实很简单,官网讲的很详细。
v3.cn.vuejs.org/guide/compo…[4]
但是有很多需要注意的地方。接下来我们就介绍一下:
props约束
-
当传递的是对象或者数组,我们指定默认值必须是一个工厂函数。并且返回默认值对象和数组。
-
我们可以通过数组来表示可以是多个类型。
-
我们还可以通过
validator
检验函数来自定义约束类型。 -
Prop 的大小写命名,最好使用
-
链接命名。
非props属性处理
当我们传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为 非Prop的Attribute。
-
当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中
- 如果我们不希望组件的根元素继承attribute,可以在组件中设置
inheritAttrs: false
。-
禁用attribute继承的常见情况是需要将attribute应用于根元素之外的其他元素。
-
我们可以通过 $attrs来访问所有的 非props的attribute。
-
- 多个根节点的attribute
-
多个根节点的attribute如果没有显示的绑定,那么会报警告,我们必须手动的指定要绑定到哪一个属性上。
-
子组件向父组件传参
我们可以通过emits
来对传递的事件参数进行校验,如果出现不符合的,将会出现警告。
如果我们徐想要校验参数,直接写数组就行。
emits: ["add", "sub", "addN"]
全局事件总线
主要用在非父子组件传递参数。
Vue3从实例中移除了 on、on、off 和 $once 方法,所以我们如果希望继续使用全局事件总线,要通过第三方的库mitt
。
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
module.exports = { plugins: [ require("postcss-preset-env") ] }
-
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
标签作为占位,让外部决定到底显示什么样的元素。
具体使用可以访问官网。
v3.cn.vuejs.org/guide/compo…[5]
下面我们来介绍插槽使用的需要注意什么。
注意事项
-
如果想要给插槽做样式定义,我们需要给
slot
标签包裹上一个div
元素,如果直接在slot标签上写class,那么插槽替换的内容将替代完整的slot
插槽。 -
除了默认插槽外,我们传入内容是都需要在
template
标签上指定插槽的名称。 -
可以通过
v-slot:[SlotName]
方式动态绑定一个名称。 -
插槽不能访问提供插槽的组件中的属性。
-
如果我们渲染插槽的时候,需要用到子组件中的数据,我们就可以通过作用域插槽来将数据传递到父组件进行使用。数据放在一个对象中,并且将作为
v-slot
指令的值。
<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,
delay: 200,
timeout: 3000,
suspensible: false,
*
* @param {*} error 错误信息对象
* @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试
* @param {*} fail 一个函数,指示加载程序结束退出
* @param {*} attempts 允许的最大重试次数
*/
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
retry()
} else {
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,
}
}
<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 {
}
</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属性值中定义上面两个属性即可。
.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;
}
.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')
gsap.from(el, {
scale: 0,
x: 200,
onComplete: done,
})
},
leave(el, done) {
console.log('leave')
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>
其他动画知识,请访问官网,讲的非常详细。
v3.cn.vuejs.org/guide/trans…[6]
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方法被劫持了而已。
const info1 = {name: "zh"};
const readonlyInfo1 = readonly(info1);
const info2 = reactive({
name: "zh"
})
const readonlyInfo2 = readonly(info2);
const info3 = ref("zh");
const readonlyInfo3 = readonly(info3);
toRefs
: 将传入的对象变成ref对象
当我们想要对reactive对象做解构的时候,直接解构,将使数据失去响应式。如果我们用toRefs将其包裹后解构,数据依然是响应式的。这种做法相当于已经在reactive中的属性和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化
const info = reactive({ name: 'zh', age: 22 })
let { name, age } = toRefs(info)
toRef
: 将指定传入的对象那个属性变成ref对象
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法。
const info = reactive({ name: 'zh', age: 22 })
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';
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对象。
const fullName = computed(() => firstName.value + " " + lastName.value);
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
的监听。或者组件卸载后自动停止监听。
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++;
if (age.value > 30) {
stop();
}
}
watchEffect的执行时机。默认情况下,watchEffect是在视图更新之前执行副作用函数。如果我们想要改变他的执行时机,怎么改变呢?
watchEffect还可以传入第二个参数,为一个对象。设置flush
顺序性就可以改变watchEffect的执行时机。
flush: 'pre'(默认,在视图更新前执行)
'post'(在视图更新后执行)
'sync'(同步触发,会出现问题,少用)
const title = ref(null);
watchEffect(() => {
console.log(title.value);
}, {
flush: "post"
})
watchEffect还可以清除副作用
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。那么上一次的网络请求应该被取消掉,这个时候我们就可以清除上一次的副作用。
在我们给watchEffect
传入的函数被回调时,其实可以获取到一个参数:onInvalidate, 当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数。我们可以在传入的回调函数中,执行一些清楚工作。就类似于节流函数。
watchEffect((onInvalidate) => {
const timer = setTimeout(() => {
console.log("网络请求成功~");
}, 2000)
onInvalidate(() => {
clearTimeout(timer);
console.log("onInvalidate");
})
});
watch
watch的API完全等同于组件watch选项的Property:
-
watch需要侦听特定的数据源,并在回调函数中执行副作用。
-
默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调。 监听单个数据源
watch侦听函数的数据源有两种类型:
const info = reactive({ name: 'zh', age: 20 })
watch(info, (newValue, oldValue) => {
console.log('newValue:', newValue, 'oldValue:', oldValue)
})
监听多个值
可以将多个源放在数组中。
const info = reactive({ name: 'zh', age: 20 })
const name = ref('zh')
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',
},
})
watch(
() => ({ ...info }),
(newInfo, oldInfo) => {
console.log(newInfo, oldInfo)
},
{
}
)
const changeData = () => {
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来接收传递的数据。
由于我们向下传递的数据,不需要子孙组件修改,只允许我们自己修改,然后影响下层组件,所以。可以使用readonly
API来传递只读数据。
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')
如果我们真的想要在子孙组件中修改数据,我们可以提供一个函数,接收子孙组件的数据,然后在该祖先组件中修改。
const updateName = (e, val) => {
console.log('e---------------', e, val)
name.value = val
}
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(
'div',
{},
[
'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
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'},
{
first: (slotProps) => h('span', slotProps.first),
second: (slotProps) => h('span', slotProps.second),
}
)
},
以上只是一些个人实验,如果想要了解更多,请访问官网 v3.cn.vuejs.org/guide/rende…[7]
在vue中使用jsx
@vue/babel-plugin-jsx[8]
首先我们需要安装@vue/babel-plugin-jsx
/ @vitejs/plugin-vue-jsx
插件,然后在babel配置文件中配置。
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方法。
export default {
install(app) {
app.config.globalProperties.$name = "zh"
}
}
export default function(app) {
console.log(app);
}
const app = createApp(App);
app.use(plugin)