本文是阅读1所做的笔记。
组件的生命周期
vue 3组件的生命周期写法称为Composition API(组合式API)。
vue 3组件的生命周期的函数统一放在setup里运行:onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted、onErrorCaptued。
被包含在<keep-alive>中的组件,相比正常的会多出两个生命周期钩子函数:onActivated、onDeactivated。
defineComponent
defineComponent是vue3推出的一个API,用来自动对TypeScript进行类型推导(省去手动类型推导的过程)。
setup()
steup是vue3推出的compostion API之一,是vue3组件生命周期的启用入口。
steup(props,context)的入参:props是传承自父组件的数据,context是本组件的执行上下文;如果一个定义不在props中定义,那么就会存在于context.attrs中;context.slots是插槽,context.emit是触发事件(可以用emit('xxx')
代替context.emit('xxx')
)。
setup()模板如下:
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
// 业务代码写这里...
return {
// 需要给 template 用的数据、函数放这里 return 出去...
}
},
})
组件的基本写法
vue3官方推荐defineComponent+Composition API的写法。
响应式数据的变化
1. 响应性
所谓响应性,官方文档是这样解释的:响应性是一种允许我们以声明式的方式去适应变化的编程范例。简单地可以理解为,具有响应性的数据,如果在某处地方被使用,这个数据如果发生了变化,那么使用的地方也会相应更新。
例如,有一个数据a,然后b使用了数据a,如果a在某处发生变化了,会触发b使用a的地方的更新。如果还不理解,再具体一点,vue data 选项里定义的就是响应性的数据,你在模板中使用了这些数据,然后这些数据在某处发生了变化,你模板里面的数据是不是也会相应更新?这就是响应性。vue3 通过 Proxy 和依赖&副作用收集 实现这种响应性,相比起vue3的Proxy,vue2是使用defineProperty实现的。
2. 副作用effect
关于副作用,官方文档是这么描述的:Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。
文档说得有点抽象,可以这么简单地理解,副作用就是记录[使用了响应式数据的地方]且可能需要被重新执行的一种机制,在vue3里面体现为一个函数包裹器,里面便包裹了使用了响应式数据的地方。也就是说,如果我们跟踪了副作用依赖于哪些响应式数据,在这些数据做出了变化之后,我们便可以触发副作用的执行,达到更新的目的。
你可以发现,其实computed,watch,甚至使用了数据的模板,都可以理解为跟副作用相关,因为他们都依赖于某些响应式的数据。
3. ref、reactive
ref,reactive可以创建响应式的数据,可以理解为setup里面与原来data选项相对应的功能,一旦有地方使用到了这些数据,就会被包裹进副作用里,并在这些数据修改的时候运行副作用更新。
vue3使用Proxy的getter/setter实现数据的响应性。
响应式API之ref
ref用来定义响应式数据(随状态变化而变化的数据),定义方式是用<>包裹类型定义,紧跟ref API之后:
// 单类型
const msg = ref<string>('Hello World!');
// 多类型
const phoneNumber = ref<number | string>(13800138000);
不同类型的ref定义有些差异:基本类型、对象、普通数组、对象数组。
ref也可以挂在html语言的DOM元素、子组件上,获取它们响应式变化的值。获取值时注意几点:
- 必须通过 xxx.value 才能正确操作到挂载的 DOM 元素或组件;
- 视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里;
- 该变量必须 return 出去才可以传给template使用。
ref会将其标记的变量变为对象,因此必须用xx.value才能获取变量的值。
响应式API之reactive
reactive也是用于定义响应式数据,但只适用于对象、数组。
reactive对象读取字段的值或修改值的时候,与普通对象是一样的(不像ref对象那样需要xx.value取值)。
但是一些不良操作可能会使reactive数组失去响应性:重置数组;对reactive定义的对象进行解构(解构(解构赋值)是一种表达式,将数组或者对象中的数据赋予另一个变量[10])。
响应式API之toRef与toRefs
toRef、toRefs都是用来将reactive对象转为ref对象的,前者转换某个字段,后者转换所有字段,当修改转换后ref对象的字段时,转换前的reactive对象相应字段会同步更新。
注意使用toRef最好不要转换原reactive不存在的字段。
注意toRef与toRefs在业务场景中的运用:
- 先用reactive定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法去维护你的数据;
- 再通过toRefs定义一个给template用的对象,它本身不具备响应性但是它的字段全部是ref变量;
- 在return的时候,对toRefs对象进行解构,这样导出去就是各个字段对应的ref变量,而不是一整个reactive对象;(return给模板的时候,注意是否有相同命名的变量存在,不然排在后面的同名变量会覆盖前面的)
函数的定义和使用
vue3的函数都是放在setup()中定义的,注意执行时放在对应的生命周期中,比如OnMounted。
如果是暴露给模板通过click、change等行为除法的函数,需要把函数名在setup里进行return才可以在模板里使用。
数据的监听
数据的监听即是对组件的监听,比如监听它的路由变化、参数变化等。
watch
watch API分基础用法和批量监听,该API接受3个入参:source(必传) 数据源,callback (必传)监听到变化后执行的回调函数,options(可选) 一些监听选项。watch返回一个可以用来停止监听的函数。
watch API 代码:
import { watch } from 'vue'
// 基本用法
watch(
source, // 必传,要监听的数据源
callback, // 必传,监听到变化后要执行的回调函数
// options // 可选,一些监听选项
)
1. 监听源
watch的监听源必须是:1、响应式变量(Ref);2、计算数据(ComputedRef);3、getter函数(()=>T);
注意如果监听响应性对象中的某个字段(这种情况下对象本身是响应式,但它的字段不是),就需要在return中写成getter函数返回需要监听的字段,比如()=>foo.bar。
2. 回调函数
// watch 第 2 个入参的 TS 类型
// ...
export declare type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
// ...
注意回调函数在watch批量监听时的简化写法。
如果有多个数据源监听,并且监听到变化后执行的行为一样,可以用批量监听。
注意回调函数只有在数据源发生变化时,才会被执行。
3. 监听的选项
deep控制是否开启深度监听,默认不开启,且默认对ref对象不起作用(可以手动开启监听)。
immediate让回调函数不等数据源的变化就立马执行,即watch所监听的ref对象一旦初始化,就触发回调函数(immediate不能在第一次会调时就取消该数据源的监听)。
flush控制是在渲染的什么时候进行回调函数的执行。
4. 停止监听
watch函数会返回一个停止监听的函数(一般来说不太关心这个)。
5. 监听效果清理
有时 watch 的回调会执行异步操作,当 watch 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:
- watcher即将重新运行的时候;
- watcher被停止(组件被卸载或者被手动停止监听 );
watchEffect
这里注意watch只有在数据变化时才会执行监听,而watchEffect会默认执行一次,然后在数据变化时再监听。
数据的计算
vue数据的计算采用computed API,通过现有的响应式数据,去计算得到新的响应式变量。
定义出的computed变量,也是和ref变量一样,需要xx.value才能获取到它的值,但区别在于,computed变量的value是只读的。
一般不需要显示定义computed出来的变量类型,defineComponent会自动推导。
computed变量优势:
- 使用缓存储存计算的数据,等到下次访问时能快速给出;
- 和ref变量一样使用xx.value获取数据,书写风格统一;
computed变量劣势:
- 不能用于非响应式数据(比如时间)的计算;
- 只读,不能直接赋值;
如果一定要对computed变量手动复制,可以使用其setter属性更新数据:
// 注意这里computed接收的入参已经不再是函数
const foo = computed({
// 这里需要明确的返回一个值
get() {
// ...
},
// 这里接收一个参数,代表修改 foo 时,赋值下来的新值
set(newValue) {
// ...
},
})
此时使用xx.value=所赋的值,computed变量就会用其set方法更新数据。
computed变量的应用场景:
- 数据的拼接和计算:在已有数据上进行综合计算。
- 复用组件的动态数据:根据接口返回不同的url,无需重新渲染UI组件。
- 获取多级对象的值:类似v-if,若多级对象的某级字段不存在,则作相应的异常处理。
- 不同类型的数据转换:将用户在组件中输入的数据,用set方法实时处理。
指令
指令是vue模板语法里的特殊标记,统一以v-开头(比如v-html),它以简单的方式实现了常用的JavaScript表达式功能,当表达式的值改变的时候,响应式地作用到DOM上。
有两个指令有别名:
- v-on的别名是@,使用@click等价于V-on:click;
- v-bind的别名是:,使用:src等价于v-bind:src;
自定义指令分局部注册和全局注册两种方式:
- 局部注册在单个组件内定义并使用,分对象式和函数式写法(函数式更简单);
- 全局注册只需在入口文件main.ts中定义,任意组件都可以使用它。(见“开发本地vue专属插件一节”)
注意:自定义指令还有一个deep选项,如果自定义指令用于一个有嵌套属性的对象,并且需要在嵌套属性更新的时候触发beforeUpdate和updated钩子,那么需要将这个选项设置为true才能够生效:
<template>
<div v-foo="foo"></div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
export default defineComponent({
directives: {
foo: {
beforeUpdate(el, binding) {
console.log('beforeUpdate', binding)
},
updated(el, binding) {
console.log('updated', binding)
},
mounted(el, binding) {
console.log('mounted', binding)
},
// 需要设置为 true ,如果是 false 则不会触发
deep: true,
},
},
setup() {
// 定义一个有嵌套属性的对象
const foo = reactive({
bar: {
baz: 1,
},
})
// 2s 后修改其中一个值,会触发 beforeUpdate 和 updated
setTimeout(() => {
foo.bar.baz = 2
console.log(foo)
}, 2000)
return {
foo,
}
},
})
</script>
插槽
参考2,Vue实现了一套内容分发的API,将元素作为承载分发内容的出口,这是vue文档上的说明。具体来说,slot就是可以让你在组件内添加内容的空间。举个例子:
//子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button> </button>
</div>
</template>
//父组件:(引用子组件 ebutton)
<template>
<div class= 'app'>
<ebutton> </ebutton>
</div>
</template>
如果直接想要在父组件中的<ebutton></ebutton> 中添加内容,是不会在页面上渲染的。那么我们如何使添加的内容能够显示呢?在子组件内添加slot 即可。
//子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button></button>
<slot></slot> //slot 可以放在任意位置。(这个位置就是父组件添加内容的显示位置)
</div>
</template>
子组件可以在任意位置添加slot , 这个位置就是父组件添加内容的显示位置。
简言之,插槽就是父组件内部定义的内容通过slot替换子组件默认的内容。
1. 编译作用域
上面我们了解了,slot 其实就是能够让我们在父组件中添加内容到子组件的‘空间’。我们可以添加父组件内任意的data值,比如这样:
//父组件:(引用子组件 ebutton)
<template>
<div class= 'app'>
<ebutton> {{ parent }}</ebutton>
</div>
</template>
new Vue({
el:'.app',
data:{
parent:'父组件'
}
})
使用数据的语法完全没有变,但是,我们能否直接使用子组件内的数据呢?显然不行。
// 子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button> </button>
<slot></slot>
</div>
</template>
new Vue({
el:'.button',
data:{
child:'子组件'
}
})
// 父组件:(引用子组件 ebutton)
<template>
<div class= 'app'>
<ebutton> {{ child }}</ebutton>
</div>
</template>
2. 后备内容(子组件默认值)
所谓的后背内容,其实就是slot的默认值,有时我没有在父组件内添加内容,那么 slot就会显示默认值,如:
//子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button> </button>
<slot> 这就是默认值 </slot>
</div>
</template>
3. 具名插槽
有时候,也许子组件内的slot不止一个,那么我们如何在父组件中,精确的在想要的位置,插入对应的内容呢? 给插槽命一个名即可,即添加name属性。
//子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button> </button>
<slot name= 'one'> 这就是默认值1</slot>
<slot name='two'> 这就是默认值2 </slot>
<slot name='three'> 这就是默认值3 </slot>
</div>
</template>
父组件通过v-slot : name 的方式添加内容:
//父组件:(引用子组件 ebutton)
<template>
<div class= 'app'>
<ebutton>
<template v-slot:one> 这是插入到one插槽的内容 </template>
<template v-slot:two> 这是插入到two插槽的内容 </template>
<template v-slot:three> 这是插入到three插槽的内容 </template>
</ebutton>
</div>
</template>
当然 vue 为了方便,书写 v-slot:one 的形式时,可以简写为 #one 。
4. 作用域插槽
通过slot 我们可以在父组件为子组件添加内容,通过给slot命名的方式,我们可以添加不止一个位置的内容。但是我们添加的数据都是父组件内的。上面我们说过不能直接使用子组件内的数据,但是我们是否有其他的方法,让我们能够使用子组件的数据呢? 其实我们也可以使用v-slot的方式:
//子组件 : (假设名为:ebutton)
<template>
<div class= 'button'>
<button> </button>
<slot name= 'one' :value1='child1'> 这就是默认值1</slot> //绑定child1的数据
<slot :value2='child2'> 这就是默认值2 </slot> //绑定child2的数据,这里我没有命名slot
</div>
</template>
new Vue({
el:'.button',
data:{
child1:'数据1',
child2:'数据2'
}
})
//父组件:(引用子组件 ebutton)
<template>
<div class= 'app'>
<ebutton>
// 通过v-slot的语法 将插槽 one 的值赋值给slotonevalue
<template v-slot:one = 'slotonevalue'>
{{ slotonevalue.value1 }}
</template>
// 同上,由于子组件没有给slot命名,默认值就为default
<template v-slot:default = 'slottwovalue'>
{{ slottwovalue.value2 }}
</template>
</ebutton>
</div>
</template>
总结来说就是:
- 首先在子组件的slot上动态绑定一个值( :key=‘value’)
- 然后在父组件通过v-slot : name = ‘values ’的方式将这个值赋值给 values
- 最后通过{{ values.key }}的方式获取数据
CSS样式与处理器
vue组件样式的基础写法,即在vue文件中创建一个style标签,就可在里面写CSS代码了。
动态绑定CSS
1. 使用:class动态修改样式名
可以在DOM元素的标签中使用:class属性,动态绑定一到多个样式(预先在script文件中定义好的)。
最常见的场景,应该就是导航、选项卡了,比如你要给一个当前选中的选项卡做一个突出高亮的状态,那么就可以使用:class来动态绑定一个样式。
2. 使用:style动态修改内联样式
如果你觉得使用class需要提前先写样式,再去绑定样式名有点繁琐,有时候只想简简单单的修改几个样式,那么你可以通过:style来处理。
样式表的组件作用域
由于CSS没有作用域的概念,有可能一旦写了某个样式,造成全局污染。vue组件有两种方案可以避免这种污染问题,一个是vue2的<style scoped>,一个是vue3的<style module>。
注意<style scoped>和<style module>的区别:
- 如果单纯只使用<style module>,那么在绑定样式的时候,是默认使用style对象来操作的;
- 必须显示的指定绑定到某个样式,比如
$style
,才能生效; - 如果单纯的绑定style,并不能得到“把全部样式名直接绑定”的期望结果;
- 如果你指定的className是短横杆命名,比如.user-name,那么需要通过
$style['user-name']
去绑定;
使用CSS预处理器
CSS预处理器用来解决CSS的低复用性,弥补了直接写CSS的一些缺憾3:
- 语法不够强大,比如无法嵌套书写导致模块化开发中需要书写很多重复的选择器;
- 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。
CSS预处理器主要有Sass\Less\Stylus,直接在style标签里定义lang属性,指定使用哪个预处理器,就可以编写对应的预处理器代码。