【vue学习笔记】vue单组件

本文是阅读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元素、子组件上,获取它们响应式变化的值。获取值时注意几点:

  1. 必须通过 xxx.value 才能正确操作到挂载的 DOM 元素或组件;
  2. 视图渲染完毕后再执行 DOM 或组件的相关操作(需要放到生命周期的 onMounted 或者 nextTick 函数里;
  3. 该变量必须 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在业务场景中的运用:

  1. 先用reactive定义一个源数据,所有的数据更新,都是修改这个对象对应的值,按照对象的写法去维护你的数据;
  2. 再通过toRefs定义一个给template用的对象,它本身不具备响应性但是它的字段全部是ref变量;
  3. 在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 到数据变更的时候,需要取消这些操作,这个函数的作用就用于此,会在以下情况调用这个清理函数:

  1. watcher即将重新运行的时候;
  2. watcher被停止(组件被卸载或者被手动停止监听 );

watchEffect

这里注意watch只有在数据变化时才会执行监听,而watchEffect会默认执行一次,然后在数据变化时再监听。

数据的计算

vue数据的计算采用computed API,通过现有的响应式数据,去计算得到新的响应式变量。
定义出的computed变量,也是和ref变量一样,需要xx.value才能获取到它的值,但区别在于,computed变量的value是只读的。
一般不需要显示定义computed出来的变量类型,defineComponent会自动推导。

computed变量优势:

  1. 使用缓存储存计算的数据,等到下次访问时能快速给出;
  2. 和ref变量一样使用xx.value获取数据,书写风格统一;

computed变量劣势:

  1. 不能用于非响应式数据(比如时间)的计算;
  2. 只读,不能直接赋值;

如果一定要对computed变量手动复制,可以使用其setter属性更新数据:

// 注意这里computed接收的入参已经不再是函数
const foo = computed({
  // 这里需要明确的返回一个值
  get() {
	// ...
	},
	// 这里接收一个参数,代表修改 foo 时,赋值下来的新值
	set(newValue) {
  	// ...
  },
})

此时使用xx.value=所赋的值,computed变量就会用其set方法更新数据。

computed变量的应用场景:

  1. 数据的拼接和计算:在已有数据上进行综合计算。
  2. 复用组件的动态数据:根据接口返回不同的url,无需重新渲染UI组件。
  3. 获取多级对象的值:类似v-if,若多级对象的某级字段不存在,则作相应的异常处理。
  4. 不同类型的数据转换:将用户在组件中输入的数据,用set方法实时处理。

指令

指令是vue模板语法里的特殊标记,统一以v-开头(比如v-html),它以简单的方式实现了常用的JavaScript表达式功能,当表达式的值改变的时候,响应式地作用到DOM上。

有两个指令有别名:

  1. v-on的别名是@,使用@click等价于V-on:click;
  2. v-bind的别名是:,使用:src等价于v-bind:src;

自定义指令分局部注册和全局注册两种方式:

  1. 局部注册在单个组件内定义并使用,分对象式和函数式写法(函数式更简单);
  2. 全局注册只需在入口文件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>的区别:

  1. 如果单纯只使用<style module>,那么在绑定样式的时候,是默认使用style对象来操作的;
  2. 必须显示的指定绑定到某个样式,比如$style,才能生效;
  3. 如果单纯的绑定style,并不能得到“把全部样式名直接绑定”的期望结果;
  4. 如果你指定的className是短横杆命名,比如.user-name,那么需要通过$style['user-name']去绑定;

使用CSS预处理器

CSS预处理器用来解决CSS的低复用性,弥补了直接写CSS的一些缺憾3

  1. 语法不够强大,比如无法嵌套书写导致模块化开发中需要书写很多重复的选择器;
  2. 没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。

CSS预处理器主要有Sass\Less\Stylus,直接在style标签里定义lang属性,指定使用哪个预处理器,就可以编写对应的预处理器代码。

参考


  1. Vue3 入门指南与实战案例 ↩︎

  2. vue插槽(slot)详解 ↩︎

  3. 浅谈css预处理器,Sass、Less和Stylus ↩︎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值