红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

Vue3 学习笔记 4:生命周期

2025年9月7日 6点热度 0人点赞 0条评论

ref 属性

如果需要获取一个 DOM 元素,传统的 JS 方式是:

<template>
    <div class="person">
        <h1>中国</h1>
        <h2 id="city">北京</h2>
        <h3>大兴区</h3>
        <button @click="showDom">显示dom元素</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
const showDom = () => {
    console.log(document.getElementById('city'))
}
​
</script>

在这个简单示例中这样做没问题,但在正式项目中就可能存在一个问题。在 Vue 工程中,是通过一个根组件加载诸多子组件,所以很容易出现不同的组件中包含同样 id 的 HTML 标签,比如在根组件App.vue中:

<template>
    <!-- HTML -->
    <h1 id="city">Hello World</h1>
    <Person></Person>
</template>

现在再点击按钮输出的就是App.vue中的<h1>标签,因为它先加载。

为了避免这种情况出现,vue 提供一个ref属性,结合ref方法可以获取对应的 DOM 元素:

<template>
    <div class="person">
        <h1>中国</h1>
        <h2 ref="city">北京</h2>
        <h3>大兴区</h3>
        <button @click="showDom">显示dom元素</button>
    </div>
</template>
​
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const city = ref()
const showDom = () => {
    console.log(city.value)
}
​
</script>

并且 HTML 标签的ref属性仅在当前组件中有效,不会存在前文描述的问题。

ref属性不仅可以用于获取 HTML 标签对应的 DOM 元素,还可以获取 vue 组件的实例对象:

<template>
    <!-- HTML -->
    <h1 id="city">Hello World</h1>
    <Person ref="person"></Person>
    <button @click="showPerson">show person</button>
</template>
​
<script setup lang="ts" name="App">
import Person from './Person2.vue'
import { ref } from 'vue'
const person = ref()
const showPerson = () => {
    console.log(person.value)
}
</script>

但默认情况下获取到的组件实例中不包含任何方法和数据,如果父组件要通过这种方式访问子组件的数据或方法,就需要在子组件中暴露:

<script setup lang="ts" name="Person">
import { ref } from 'vue'
const city = ref()
const showDom = () => {
    console.log(city.value)
}
​
defineExpose({
    showDom
})
</script>

此时父组件就可以调用子组件暴露的方法(或数据):

<script setup lang="ts" name="App">
import Person from './Person2.vue'
import { ref } from 'vue'
const person = ref()
const showPerson = () => {
    console.log(person.value)
    person.value.showDom()
}
</script>

vue3 如此设计,vue2 不需要暴露父组件就可以使用子组件的数据和方法。

自定义类型

用普通的 JS 方式定义一个对象:

<script setup lang="ts" name="Person">
const person = {
    id: 1,
    name: 'zhang san',
    age: 20
}
</script>

这样做对类型没有任何限制,也就没有类型检查,会引发常见的属性命名错误或在其它组件中引用时 IDE 无法智能联想提示属性名等问题。

可以在文件src\types\index.ts中定义一个接口用于描述这种类型:

export interface Person {
  id: number;
  name: string;
  age: number;
}

在需要定义类型的代码中导入并使用这种类型进行约束:

<script setup lang="ts" name="Person">
import {Person} from '@/types'
const person: Person = {
    id: 1,
    name: 'zhang san',
    age: 20
}
console.log(person)
</script>

在 VSCode 中导入类型可能报错:

image-20250907170708150

这是因为在 TS 规范中,导入类型和数据或方法不同,需要使用type进行标记:

import {type Person} from '@/types'

定义一个Person类型的数组:

const persons: Array<Person> = [{
    id: 1,
    name: 'zhang san',
    age: 20
}, {
    id: 2,
    name: 'lisi',
    age: 18
}, {
    id: 3,
    name: 'wangwu',
    age: 16
}]

Array<Person>是使用泛型数组的方式定义类型。除了这种方式还可以:

const persons: Person[] = [{
    id: 1,
    name: 'zhang san',
    age: 20
}, {
    id: 2,
    name: 'lisi',
    age: 18
}, {
    id: 3,
    name: 'wangwu',
    age: 16
}]

还可以将 Person 数组定义为一个自定义类型:

export type PersonList = Array<Person>;

使用自定义类型进行约束:

<script setup lang="ts" name="Person">
import { type Person, type PersonList } from '@/types'
// ...
const persons: PersonList = [{
    id: 1,
    name: 'zhang san',
    age: 20
}, {
    id: 2,
    name: 'lisi',
    age: 18
}, {
    id: 3,
    name: 'wangwu',
    age: 16
}]
</script>

props

props是定义在组件上的属性,父组件可以通过它传递信息给子组件:

<template>
    <!-- HTML -->
    <Person ref="person" msg="hello"></Person>
</template>

<script setup lang="ts" name="App">
import Person from './Person5.vue'
</script>

子组件需要通过方法defineProps接收:

<template>
    <div class="person">
      <p>{{ msg }}</p>
    </div>
</template>

<script setup lang="ts" name="Person">
import { defineProps } from 'vue';
defineProps(['msg'])
</script>

defineProps接收一个数组,数组中的值就是子组件接收的props的名称。与此同时,如上面展示的,接收到的属性可以直接在模版中使用。

defineProps这种以defineXXX方式命名的 vue 方法被称作宏函数,是不需要通过import导入的,因此这里可以省略:

<script setup lang="ts" name="Person">
// import { defineProps } from 'vue';
defineProps(['msg'])
</script>

需要注意的是,如果传递的不是常量,是变量,需要使用:定义props属性:

<template>
    <!-- HTML -->
    <Person ref="person" msg="hello" :persons="persons"></Person>
</template>

<script setup lang="ts" name="App">
import Person from './Person5.vue'
const persons = [{
    id: 1,
    name: '张三',
    age: 18
},{
    id: 2,
    name: '李四',
    age: 19
},
{
    id: 3,
    name: '王五',
    age: 20
}]
</script>

实际上就是通过v-bind:xxx定义了一个值是表达式的props属性。

子组件接收和使用的方式是相似的:

<template>
    <div class="person">
      <p>{{ msg }}</p>
      <ul>
        <li v-for="person in persons" :key="person.id">
          {{ person.name }}
        </li>

      </ul>
    </div>
</template>

<script setup lang="ts" name="Person">
// import { defineProps } from 'vue';
defineProps(['msg','persons'])
</script>

如果需要在代码中使用 props 而非模版中,可以通过defineProps的返回值:

const props = defineProps(['msg','persons'])
console.log(props.msg)

当然,也可以通过props传递响应式数据:

<template>
    <!-- HTML -->
    <Person ref="person" msg="hello" :persons="persons"></Person>
</template>

<script setup lang="ts" name="App">
import Person from './Person6.vue'
import { reactive } from 'vue';
const persons = reactive([{
    id: 1,
    name: '张三',
    age: 18
},{
    id: 2,
    name: '李四',
    age: 19
},
{
    id: 3,
    name: '王五',
    age: 20
}])
</script>

可以添加类型约束:

import {type PersonList} from '@/types'
const persons:PersonList = reactive([{
    id: 1,
    name: '张三',
    age: 18
},{
    id: 2,
    name: '李四',
    age: 19
},
{
    id: 3,
    name: '王五',
    age: 20
}])

这样做不会报错,但不推荐,更推荐的方式是通过 vue 函数的泛型添加类型约束:

const persons = reactive<PersonList>([{
    id: 1,
    name: '张三',
    age: 18
},{
    id: 2,
    name: '李四',
    age: 19
},
{
    id: 3,
    name: '王五',
    age: 20
}])

类似的,子组件(接收方)同样可以以泛型的方式添加类型约束:

<script setup lang="ts" name="Person">
import { type PersonList } from '@/types';
const props = defineProps<{
  msg: string;
  persons: PersonList;
}>()
console.log(props.msg)
</script>

值得注意的是,此时defineProps方法的参数可以省略。

可以将某个props设置为可选的:

const props = defineProps<{
  msg?: string;
  persons: PersonList;
}>()

此时父组件可以传也可以不传:

<Person ref="person" :persons="persons"></Person>

此时通常需要在父组件没有传递props时提供一个默认值,可以使用withDefaults函数:

<script setup lang="ts" name="Person">
import { type PersonList } from '@/types';
import { withDefaults } from 'vue';
const props = withDefaults(defineProps<{
  msg?: string;
  persons: PersonList;
}>(), {
  msg: () => 'hello world',
})
console.log(props.msg)
</script>

withDefaults函数的第二个方法接收一个对象,其属性可以指定对应props的默认值,需要注意的是其值只能是一个匿名函数,返回值就是props的默认值。

生命周期

vue2

为了对比 vue2 和 vue3 的生命周期,需要先创建一个 vue2 项目。

先安装 vue2 脚手架创建工具 vue-cli:

npm install -g @vue/cli

安装要求是 node.js 版本 >= 18 且 <= 22,推荐使用 22,推荐使用 node.js 版本管理工具 nvm 切换 node.js 的版本。

使用工具创建 vue2 项目:

vue create vue2_test

注意需要在出现提示后选择 vue2 项目。

如果运行命令提示找不到命令,需要通过npm config get prefix查看 node.js 的安装目录,然后确保其中包含一个vue.cmd和vue.ps文件,并确保这个目录在环境变量path中。

创建一个简单的示例项目:

<template>
    <div>
        <h1>{{ sum }}</h1>
        <button @click="add">sum++</button>
    </div>
</template>
<script>
/* eslint-disable */
export default {
    name: 'Person',
    methods: {
        add() {
            this.sum++
        }
    },
    data() {
        return {
            sum: 0
        }
    }
}
</script>

组件在内存中创建前后会调用的两个生命周期钩子:

    // 创建前
    beforeCreate() {
        console.log('beforeCreate')
    },
    // 创建完成
    created() {
        console.log('created')
    },

组件被挂载到父组件后才能在页面显示,挂载前后的生命周期钩子:

    // 挂载前
    beforeMount() {
        console.log('beforeMount')
    },
    // 挂载完成
    mounted() {
        console.log('mounted')
    },

如果组件的数据发生变化(比如这里点击按钮让响应式数据自增),会调用生命周期钩子:

    // 更新前
    beforeUpdate() {
        console.log('beforeUpdate')
    },
    // 更新完成
    updated() {
        console.log('updated')
    },

组件被销毁的前后会调用生命周期钩子:

    // 销毁前
    beforeDestroy() {
        console.log('beforeDestroy')
    },
    // 销毁完成
    destroyed() {
        console.log('destroyed')
    }

可以在父组件中编写一个逻辑,以触发子组件的销毁:

<template>
  <div id="app">
  <person v-if="show"/>
  <button @click="destroyPerson">destroyPerson</button>
  </div>
</template>

<script>
import Person from './components/Person.vue'
export default {
  name: 'App',
  components: {
    Person
  },
  data() {
    return {
      show: true
    }
  },
  methods: {
    destroyPerson() {
      this.show = false
    }
  }
}
</script>

vue3

与vue2类似,同样创建一个简单示例:

<template>
  <div class="person">
    <h1>{{ sum }}</h1>
    <button @click="add">sum++</button>
  </div>
</template>

<script setup lang="ts" name="Person">
import {ref} from 'vue'
const sum = ref(0)
const add = () => {
  sum.value++
}
</script>

与vue2不同的是,vue3中没有创建前和创建后的钩子,因为setup方法本身就相当于创建过程,因此可以这样表示创建前和创建后的时刻:

<script setup lang="ts" name="Person">
import {ref} from 'vue'
console.log("创建前")
const sum = ref(0)
const add = () => {
  sum.value++
}
console.log("创建后")
</script>

挂载前和挂载后的钩子:

onBeforeMount(() => {
  console.log("挂在前")
})
onMounted(() => {
  console.log("挂载后")
})

更新前和更新后的钩子:

onBeforeUpdate(() => {
  console.log("更新前")
})
onUpdated(() => {
  console.log("更新后")
})

组件卸载前后(相当于 vue2 的销毁前后,只是称呼不同)的生命周期钩子:

onBeforeUnmount(() => {
  console.log("卸载前")
})
onUnmounted(() => {
  console.log("卸载后")
})

同样的,利用v-if在父组件触发子组件的销毁以观察卸载的生命周期钩子被触发:

<template>
    <!-- HTML -->
    <Person v-if="show"/>
    <button @click="show=false">destroy person</button>
</template>

<script setup lang="ts" name="App">
import Person from './Person7.vue'
import { ref } from 'vue'
const show = ref(true)
</script>

最后需要说明的是,父子组件的加载顺序是先加载子组件,在子组件加载完毕后才加载父组件,而根组件最后完成加载。

hooks

看一个简单示例:

<template>
  <div class="person">
    <h1>{{ sum }}</h1>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="add">sum++</button>
    <button @click="addList">list++</button>
  </div>
</template>

<script setup lang="ts" name="Person">
import { ref, reactive } from 'vue'
const sum = ref(0)
const list = reactive<number[]>([])
const add = () => {
  sum.value++
}
const addList = () => {
  list.push(list.length + 1)
}
</script>

这个子组件中有两部分功能,一部分是让sum自增,另一部分是展示一个从1开始的等差数列。现在这个示例中两部分代码的数据和方法是混杂在一起的,可以通过 hooks 实现模块化封装。

将sum自增部分的数据和方法抽取并封装到src\hooks\useList.ts:

import { reactive } from "vue";
export default () => {
  const list = reactive<number[]>([]);
  const addList = () => {
    list.push(list.length + 1);
  };
  return { list, addList };
};

可以看到,数据和方法都封装到一个匿名函数中,通过默认暴露的方式提供给外部调用,其数据和方法都以返回值的方式提供给调用方。

类似的,等差数列的部分代码封装到src\hooks\useSum.ts:

import { ref } from "vue";
export default () => {
  const sum = ref(0);
  const add = () => {
    sum.value++;
  };
  return { sum, add };
};

hooks 文件习惯以useXXX的方式命名。

重构子组件,导入两个 hooks 文件,并使用其中的方法结合解构赋值的方式引入数据和方法:

<template>
  <div class="person">
    <h1>{{ sum }}</h1>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="add">sum++</button>
    <button @click="addList">list++</button>
  </div>
</template>

<script setup lang="ts" name="Person">
import useSum from './hooks/useSum'
import useList from './hooks/useList'
const {sum, add} = useSum()
const {list, addList} = useList()
</script>

这种方式的组织代码的好处是可以让相同功能的代码组织在一个 hooks 文件中,不仅包含数据和方法,如果有必要,还可以将相应的计算属性、监视、生命周期钩子都写在一起。比如默认就加载三个等差数列的元素:

import { reactive, onMounted } from "vue";
export default () => {
  const list = reactive<number[]>([]);
  const addList = () => {
    list.push(list.length + 1);
  };
  onMounted(() => {
    addList();
    addList();
    addList();
  });
  return { list, addList };
};

The End.

参考资料

  • 尚硅谷Vue3入门到实战

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: vue
最后更新:2025年9月7日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号