06 【生命周期 模板引用】

06 【生命周期 模板引用】

1.生命周期

vue2

image-20220629211626515

vue3

lifecycle_2

  • Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
    • beforeDestroy改名为 beforeUnmount
    • destroyed改名为 unmounted
  • Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
    • beforeCreate===>setup()
    • created=======>setup()
      在setup比beforeCreate还先执行
    • beforeMount ===>onBeforeMount
    • mounted=======>onMounted
    • beforeUpdate===>onBeforeUpdate
    • updated =======>onUpdated
    • beforeUnmount ==>onBeforeUnmount
    • unmounted =====>onUnmounted

关于两个销毁生命周期,可以在组件实例上用v-if打成

所有罗列在下面的 API 都应该在组件的 setup() 阶段被同步调用。相关细节请看指南 - 生命周期钩子

1.1 onBeforeMount#

注册一个钩子,在组件被挂载之前被调用。

  • 类型

    1
    function onBeforeMount(callback: () => void): void
  • 详细信息

    当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。

    这个钩子在服务器端渲染期间不会被调用。

1.2 onMounted#

注册一个回调函数,在组件挂载完成后执行。

  • 类型

    1
    function onMounted(callback: () => void): void
  • 详细信息

    组件在以下情况下被视为已挂载:

    • 其所有同步子组件都已经被挂载 (不包含异步组件或 <Suspense> 树内的组件)。
    • 其自身的 DOM 树已经创建完成并插入了父容器中。注意仅当根容器在文档中时,才可以保证组件 DOM 树也在文档中。

    这个钩子通常用于执行需要访问组件所渲染的 DOM 树相关的副作用,或是在服务端渲染应用中用于确保 DOM 相关代码仅在客户端执行。

    这个钩子在服务器端渲染期间不会被调用。

  • 示例

    通过模板引用访问一个元素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <script setup>
    import { ref, onMounted } from 'vue'

    const el = ref()

    onMounted(() => {
    el.value // <div>
    })
    </script>

    <template>
    <div ref="el"></div>
    </template>

1.3 onBeforeUpdate#

注册一个钩子,在组件即将因为响应式状态变更而更新其 DOM 树之前调用。

  • 类型

    1
    function onBeforeUpdate(callback: () => void): void
  • 详细信息

    这个钩子可以用来在 Vue 更新 DOM 之前访问 DOM 状态。在这个钩子中更改状态也是安全的。

    这个钩子在服务器端渲染期间不会被调用。

1.4 onUpdated#

注册一个回调函数,在组件因为响应式状态变更而更新其 DOM 树之后调用。

  • 类型

    1
    function onUpdated(callback: () => void): void
  • 详细信息

    父组件的更新钩子将在其子组件的更新钩子之后调用。

    这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。

    这个钩子在服务器端渲染期间不会被调用。

    警告

    不要在 updated 钩子中更改组件的状态,这可能会导致无限的更新循环!

  • 示例

    访问更新后的 DOM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <script setup>
    import { ref, onUpdated } from 'vue'

    const count = ref(0)

    onUpdated(() => {
    // 文本内容应该与当前的 `count.value` 一致
    console.log(document.getElementById('count').textContent)
    })
    </script>

    <template>
    <button id="count" @click="count++">{{ count }}</button>
    </template>

1.5 onBeforeUnmount#

注册一个钩子,在组件实例被卸载之前调用。

  • 类型

    1
    function onBeforeUnmount(callback: () => void): void
  • 详细信息

    当这个钩子被调用时,组件实例依然还保有全部的功能。

    这个钩子在服务器端渲染期间不会被调用。

1.6 onUnmounted#

注册一个回调函数,在组件实例被卸载之后调用。

  • 类型

    1
    function onUnmounted(callback: () => void): void
  • 详细信息

    一个组件在以下情况下被视为已卸载:

    • 其所有子组件都已经被卸载。
    • 所有相关的响应式作用 (渲染作用以及 setup() 时创建的计算属性和侦听器) 都已经停止。

    可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。

    这个钩子在服务器端渲染期间不会被调用。

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <script setup>
    import { onMounted, onUnmounted } from 'vue'

    let intervalId
    onMounted(() => {
    intervalId = setInterval(() => {
    // ...
    })
    })

    onUnmounted(() => clearInterval(intervalId))
    </script>

1.7 onErrorCaptured#

注册一个钩子,在捕获了后代组件传递的错误时调用。

  • 类型

    1
    2
    3
    4
    5
    6
    7
    function onErrorCaptured(callback: ErrorCapturedHook): void

    type ErrorCapturedHook = (
    err: unknown,
    instance: ComponentPublicInstance | null,
    info: string
    ) => boolean | void
  • 详细信息

    错误可以从以下几个来源中捕获:

    • 组件渲染
    • 事件处理器
    • 生命周期钩子
    • setup() 函数
    • 侦听器
    • 自定义指令钩子
    • 过渡钩子

    这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。

    你可以在 errorCaptured() 中更改组件状态来为用户显示一个错误状态。注意不要让错误状态再次渲染导致本次错误的内容,否则组件会陷入无限循环。

    这个钩子可以通过返回 false 来阻止错误继续向上传递。请看下方的传递细节介绍。

    错误传递规则

    • 默认情况下,所有的错误都会被发送到应用级的 app.config.errorHandler (前提是这个函数已经定义),这样这些错误都能在一个统一的地方报告给分析服务。
    • 如果组件的继承链或组件链上存在多个 errorCaptured 钩子,对于同一个错误,这些钩子会被按从底至上的顺序一一调用。这个过程被称为“向上传递”,类似于原生 DOM 事件的冒泡机制。
    • 如果 errorCaptured 钩子本身抛出了一个错误,那么这个错误和原来捕获到的错误都将被发送到 app.config.errorHandler
    • errorCaptured 钩子可以通过返回 false 来阻止错误继续向上传递。即表示“这个错误已经被处理了,应当被忽略”,它将阻止其他的 errorCaptured 钩子或 app.config.errorHandler 因这个错误而被调用。

1.8 onActivated#

注册一个回调函数,若组件实例是 `` 缓存树的一部分,当组件被插入到 DOM 中时调用。

这个钩子在服务器端渲染期间不会被调用。

1.9 onDeactivated#

注册一个回调函数,若组件实例是 KeepAlive 缓存树的一部分,当组件从 DOM 中被移除时调用。

这个钩子在服务器端渲染期间不会被调用。

1.10 汇总

  • this的数据:除了beforeCreate,其他12个生命周期的 this 都能获得组件实例对应初始化完成的内容。
  • beforeUpdate() 永远只在所有父子组件的 mounted() 之后才会触发!
  • 例子里的生命周期根据功能进行了划分,生命周期的顺序都标了数字!
  • setup() 调用时间,与调用时间相关功能的讲解。onMounted之类的生命周期函数必须同步调用!onXxxxx()就不讲解了,在对应的选项API生命周期名的函数之前执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<template lang="">
<keep-alive>
<edit></edit>
</keep-alive>
<div>
这是登陆页面{{username}}{{password}}
</div>
<button @click='$router.back()'>返回</button>
<button @click='changePass()'>changePass</button>
</template>
<script lang="ts">
import edit from '@/components/edit.vue'
import { defineComponent,onMounted,getCurrentInstance } from 'vue'
export default defineComponent({
name:'login',
props:['ceshi'],
data(){
return{
username:'author',
password:'123456'
}
},
components:{edit},
methods: {
changePass(){
this.password = '奇蛋物语'
},
},
setup(props,context){
// 在创建组件实例时,在初始 prop 解析之后立即调用 setup。在生命周期方面,它是在 beforeCreate 钩子之前调用的。
//! 在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。
let vuePrototype = getCurrentInstance().appContext.config.globalProperties

let componentInstance = getCurrentInstance().proxy // 初始组件实例对象,但其实没什么用!
console.log(componentInstance.$props,componentInstance.$nextTick); //! 在setup中只能访问到实例方法、实例property的内容!以及实例对象原型链上的内容。
onMounted(()=>{console.log('在setup内注册生命周期必须在setup内同步调用')})
console.log('0 setup');// 写0是代表 setup 不算生命周期,讲解setup对理解生命周期也有好处就写出来了。
},
beforeCreate(){
// 在实例初始化之后同步调用。但此时还未初始化完成!!
// 在实例 `进行数据侦听和事件/侦听器的配置` 之前同步调用。
//! 该生命周期的 this 只能获取到组件实例初始化完成前的内容!
//> 简单点说:只能拿到实例方法、实例property的内容!以及实例对象原型链上的内容。
console.log(
this.username, // undefined,组件未实例化完成,所以拿不到!!
this.$props, // 若父组件有传,则有数据
'1 beforeCreate'
)
},
created(){console.log(this,'2 created')},
beforeMount(){
console.log(this,'3 beforeMount')
},
renderTracked({ key, target, type }){
// 组件template引用了响应式数据,则会触发renderTracked生命周期,并告诉你模板`跟踪了/引用了`哪些响应式数据。
//! 仅限本地开发环境生效的生命周期
console.log({ key, target, type },'4 renderTracked') // 只需要关注这3个,总共只有4个属性,另外一个是默认的副作用函数
},
mounted(){
// 在mounted里修改组件响应式数据,则会触发数据变更的组件生命周期
// 也就是说:在 mounted 生命周期前对响应式数据进行`同步修改`,是不会触发数据变更的组件生命周期
console.log(this,'5 mounted')
this.changePass()
},
activated(){console.log(this,'7 activated')},

// 组件响应式数据变更时触发的生命周期,从组件 mounted 生命周期开始,有响应式数据变更操作,则会执行。
renderTriggered(){console.log(this,'6 renderTriggered')}, //! 仅限本地开发环境生效的生命周期
beforeUpdate(){console.log(this,'8 beforeUpdate')},
updated(){console.log(this,'9 updated')},

// 销毁组件 或 组件缓存失活时触发,若这2个状态同时有,则按照下列 10 11 12的顺序触发!!
beforeUnmount(){console.log(this,'10 beforeUnmount')}, // 在`销毁当前组件前`触发
deactivated(){console.log(this,'11 deactivated')},
unmounted(){console.log(this,'12 unmounted')}, // 当前组件销毁时,在其他激活的组件mounted生命周期前,才会触发当前销毁组件的unmounted

errorCaptured(err, instance, info){
// 捕获来自后代组件的错误时被调用。( 注意是当前的组件所有后代组件! )
// 此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。
console.log(err, instance, info,'errorCaptured')
// 此钩子可以返回 false 以阻止该错误继续向上传播。
return false // 如果我在main.ts里对 vue实例 设置了 `app.config.errorHandler=()=>{}`,此处return false则不会触发errorHandler
},
})
</script>

image-20220810151058920

父组件的前4个声明周期执行完后,开始执行子组件的前7个生命周期,执行完毕后,再接着开始执行父组件的mounted()生命周期。

2.模板引用

虽然 Vue 的声明性渲染模型为你抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。要实现这一点,我们可以使用特殊的 ref attribute:

1
<input ref="input">

ref 是一个特殊的 attribute,和 v-for 章节中提到的 key 类似。它允许我们在一个特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。这可能很有用,比如说在组件挂载时将焦点设置到一个 input 元素上,或在一个元素上初始化一个第三方库。

2.1 访问模板引用

为了通过组合式 API 获得该模板 ref,我们需要声明一个同名的 ref:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<input ref="input" />
</template>

<script setup>
import { ref, onMounted } from 'vue'

// 声明一个 ref 来存放该元素的引用
// 必须和模板 ref 同名
const input = ref(null)

onMounted(() => {
input.value.focus()
})
</script>

如果不使用 <script setup>,需确保从 setup() 返回 ref:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<input ref="input" />
</template>

<script>
import Demo from './components/Demo.vue';
import { onMounted, ref } from 'vue';
export default {
components: { Demo },
name: 'App',
setup() {
// 声明一个 ref 来存放该元素的引用
// 必须和模板 ref 同名
const input = ref(null)

onMounted(() => {
input.value.focus()
})
return {
input
}
},
};
</script>

注意,你只可以在组件挂载后才能访问 ref。如果你想在模板中的表达式上访问 input,在初次渲染时会是 null。这是因为在初次渲染前这个元素还压根不存在呢!

2.2v-for 中的模板引用#

需要 v3.2.25 及以上版本

当在 v-for 中使用模板引用时,对应的 ref 中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script setup>
import { ref, onMounted } from 'vue'

const list = ref([1, 2, 3])

const itemRefs = ref([])

onMounted(() => {
alert(itemRefs.value.map(i => i.textContent))
})
</script>

<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>

在演练场中尝试一下

应该注意的是,ref 数组并不保证与源数组相同的顺序。

2.3 函数模板引用

除了使用字符串值作名字,ref attribute 还可以绑定为一个函数,会在每次组件更新时都被调用。该函数会收到元素引用作为其第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--  当动态绑定时,我们可以将 ref 定义为回调函数,显式地传递元素或组件实例 -->
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);

onMounted(() => {
console.log(inputRef.value);
inputRef.value.focus();
});
</script>

<template>
<input
:ref="
el => {
inputRef = el;
} /* 将 el 赋值给一个数据属性或 ref 变量 */
"
/>
</template>

注意我们这里需要使用动态的 :ref 绑定才能够传入一个函数。当绑定的元素被卸载时,函数也会被调用一次,此时的 el 参数会是 null。你当然也可以绑定一个组件方法而不是内联函数。

配置v-for

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref, onMounted, reactive } from 'vue';
const list = reactive([1, 2, 3]);
const divs = ref([]);

onMounted(() => {
console.log(divs.value);
});
</script>

<template>
<div
v-for="(item, i) in list"
:ref="
el => {
if (el) divs[i] = el;
}
"
>
{{ item }}
</div>
</template>

2.4 组件上的 ref

模板引用也可以被用在一个子组件上。这种情况下引用中获得的值的是组件实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, onMounted } from 'vue';
import Child from './components/Child.vue';

const child = ref(null);

onMounted(() => {
// child.value 是 <Child /> 组件的实例
console.log(child.value);
});
</script>

<template>
<Child ref="child" />
</template>

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。

有一个例外的情况,使用了 <script setup> 的组件是默认私有的:一个父组件无法访问到一个使用了 <script setup> 的子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<input type="text" />
</template>

<script>
export default {
name: 'Child',
};
</script>

<script setup>
const data = [1, 2, 3];
const data2 = {
name: 'ds',
};
defineExpose([data, data2]);
</script>

当父组件通过模板引用获取到了该组件的实例时,得到的实例类型为 { a: number, b: number } (ref 都会自动解包,和一般的实例一样)。

TypeScript 用户请参考:为组件的模板引用标注类型

2.5 defineExpose详解

vue3新特性,如果是options api类型的组件,不声明 expose 时,默认暴露当前组件实例的全部内容,声明了 expose 选项, expose 数组内标记的才会暴露。(expose:[]则什么都不暴露,注意这个问题。也可以利用这个特性提高组件使用的规范。)

1
2
3
4
5
6
export default defineComponent({
expose:['nameA',...],// 可以 expose 当前实例的任何内容
methods:{
nameA(){}
}
})

<script setup> setup语法糖的情况下,默认是封闭的,需要获取当前组件里的内容的话,必须显式expose出去

1
2
3
4
5
6
7
<script lang='ts' setup>
import {ref} from 'vue';
let refData = ref('data')
defineExpose({
refData,
})
</script>

TS类型:

1
2
3
4
5
6
7
8
9
10
11
12
import { ref,Ref } from "vue";
//> defineExpose
interface exFace {
ex1:Ref<string>,
ex2?:number
}
let ex1 = ref('1')
let exObj:exFace = {
ex1,
}
// 源码类型: const defineExpose: (exposed?: Record<string, any>) => void
defineExpose(exObj)

06 【生命周期 模板引用】
https://flepeng.github.io/021-frontend-04-Vue-01-course-vue3-06-【生命周期-模板引用】/
作者
Lepeng
发布于
2023年8月1日
许可协议