21-前端核心技术-VUE组合式API

第21章-前端核心技术-VUE组合式API

学习目标

  1. 掌握VUE响应式API的使用
  2. 掌握项目中常用开发技巧

VUE 组合式API概述

使用 (datacomputedmethodswatch) 组件选项来组织逻辑通常都很有效。然而,当我们的组件变得更大时,每个功能的代码都会被分布到(datacomputedmethodswatch) 等各个部分,不利于维护,功能封装性极差,这会导致组件难以阅读和理解。

使用组合式 API,首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup

setup 选项在组件创建**之前**执行,一旦 props 被解析,就将作为组合式 API 的入口。所有需要了解组件的创建过程,即生命周期。

VUE 生命周期

vue的组件整个生命周期过程如下:

网页捕获_28-9-2021_84520_

每个组件在被创建时都要经过一系列的初始化过程

例如:

设置数据监听 -> 编译模板 -> 将实例挂载到 DOM -> 在数据变化时更新 DOM 等。

同时在这个过程中也会运行一些叫做**生命周期钩子**的函数,这给了用户在不同阶段添加自己的代码的机会。

创建项目

1
2
3
4
# npm6.+
npm init vite@latest vue3 --template vue
# npm7.+
npm init vite@latest vue3 -- --template vue

下载依赖

npm i

在app.vue文件中引入各个组件

1
2
3
4
5
6
7
8
9
10
<script setup>
import Demo01 from "./components/Demo01.vue";
</script>

<template>
<Demo01></Demo01>
</template>

<style>
</style>

beforeCreate

  • 类型:Function

  • 详细:

在实例初始化**之后**、进行数据侦听和事件/侦听器的配置**之前**同步调用。所有data中的数据不能调用。

  • 实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
};
</script>

<template>
<p>VUE 生命周期钩子</p>
</template>

<style scoped></style>

created

  • 类型:Function

  • 详细:

在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。

  • 实例:
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
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
};
</script>

<template>
<p>VUE 生命周期钩子</p>
</template>

<style scoped></style>

beforeMount

  • 类型:Function

  • 详细:

在挂载开始之前被调用:相关的 render 函数首次被调用。将模板解析成渲染函数,准备渲染到页面上

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

  • 实例:
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
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
  beforeMount(){
    this.a = 'beforeMount'
    console.log("beforeMount在vue渲染页面之前"); // 正常使用
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
</template>

<style scoped></style>

mounted

  • 类型:Function

  • 详细:

在实例挂载完成后被调用,这时候传递给 app.mount 的元素已经被新创建的 vm.$el 替换了。如果根实例被挂载到了一个文档内的元素上,当 mounted 被调用时, vm.$el 也会在文档内。

注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等待整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick:

  • 实例:
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
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
  beforeMount(){
    this.a = 'beforeMount'
    console.log("beforeMount在vue渲染页面之前");
  },
  mounted() {
    console.log("mounted在vue渲染页面之后");
    this.$nextTick(function () {
      // 仅在整个视图都被渲染之后才会运行的代码
    })
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
</template>

<style scoped></style>

beforeUpdate

  • 类型:Function

  • 详细:

在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。

该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务器端进行。

  • 实例:
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
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
  beforeMount(){
    this.a = 'beforeMount'
    console.log("beforeMount在vue渲染页面之前");
  },
  mounted() {
    console.log("mounted在vue渲染页面之后");
    this.$nextTick(function () {
      // 仅在整个视图都被渲染之后才会运行的代码
    })
  },
  beforeUpdate(){
    console.log("beforeUpdate在vue组件页面每次有数据修改之前");
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
<p>修改输入框值,触发Update事件</p>
<input type="text" v-model="a">
</template>

<style scoped></style>

updated

  • 类型:Function

  • 详细:

在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。

当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性侦听器取而代之。

注意,updated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在 updated 内部使用 vm.$nextTick

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

  • 实例:
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
<script>
export default {
  data() {
    return {
      a: "默认值",
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
  beforeMount(){
    this.a = 'beforeMount'
    console.log("beforeMount在vue渲染页面之前");
  },
  mounted() {
    console.log("mounted在vue渲染页面之后");
    this.$nextTick(function () {
      // 仅在整个视图都被渲染之后才会运行的代码
    })
  },
  beforeUpdate(){
    console.log("beforeUpdate在vue组件页面每次有数据修改之前");
  },
  updated() {
    console.log("updated在vue组件页面每次有数据修改之后");
    this.$nextTick(function () {
      // 仅在整个视图都被重新渲染完毕之后才会运行的代码
    })
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
<p>修改输入框值,触发Update事件</p>
<input type="text" v-model="a">
</template>

<style scoped></style>

beforeUnmount

  • 类型:Function

  • 详细:

在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。通常用于清除 setTimeout 、setintval、addEventlistener等

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

  • 实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
export default {
  beforeMount(){
    timer = setInterval(() => {count.value++}, 1000)
    window.addEventListener('resize', onResize)
  },
  beforeUnmount(){
    clearInterval(timer)
    window.removeEventListener('resize', onResize)
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
<p>修改输入框值,触发Update事件</p>
<input type="text" v-model="a">
</template>

<style scoped></style>

unmounted

  • 类型:Function

  • 详细:

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。

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

activated

  • 类型:Function

  • 详细:

被 keep-alive 缓存的组件激活时调用。在一个多标签的界面中使用 is attribute 来切换不同的组件的时候被调用

1
2
vue-html
  <component :is="currentTabComponent"></component>

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

  • 实例:

C1

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default{
  activated(){
    console.log("activated在C1组件被激活");
  }
}
</script>

<template>
<p>测试组件1</p>
</template>

<style scoped></style>

C2

1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
export default{
  activated(){
    console.log("activated在C2组件被激活");
  }
}
</script>

<template>
<p>测试组件2</p>
</template>

<style scoped></style>

Demo01

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
<script>
import C1 from "./C1.vue";
import C2 from "./C2.vue";
export default {
  data() {
    return {
      a: "默认值",
      cname: 'C1'
    };
  },
  methods: {
    test: () => console.log("test"),
  },
  components:{
    C1,C2
  },
  beforeCreate() {
    // this?.test() // ypeError: this.test is not a function
    console.log("beforeCreate在vue对象创建之前", "a =", this?.a); // a = undefined
  },
  created() {
    this?.test() // 正常使用
    console.log("created在vue对象创建之后", "a =", this?.a); // 正常使用
  },
  beforeMount(){
    this.a = 'beforeMount'
    console.log("beforeMount在vue渲染页面之前");
  },
  mounted() {
    console.log("mounted在vue渲染页面之后");
    this.$nextTick(function () {
      // 仅在整个视图都被渲染之后才会运行的代码
    })
  },
  beforeUpdate(){
    console.log("beforeUpdate在vue组件页面每次有数据修改之前");
  },
  updated() {
    console.log("updated在vue组件页面每次有数据修改之后");
    this.$nextTick(function () {
      // 仅在整个视图都被重新渲染完毕之后才会运行的代码
    })
  }
};
</script>

<template>
<p>VUE 生命周期钩子</p>
<p>修改输入框值,触发Update事件</p>
<input type="text" v-model="a">
<div>
<button @click="cname = 'C1'">切换组件1</button>
<button @click="cname = 'C2'">切换组件2</button>
</div>
<keep-alive>
<component :is="cname"></component>
</keep-alive>
</template>

<style scoped></style>

deactivated

  • 类型:Function

  • 详细:

被 keep-alive 缓存的组件失活时调用。

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

  • 实例:

C1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default{
  activated(){
    console.log("activated在C1组件被激活");
  },
  deactivated(){
    console.log("deactivated在C1组件失活时");
  }
}
</script>

<template>
<p>测试组件1</p>
</template>

<style scoped></style>

C2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script>
export default{
  activated(){
    console.log("activated在C2组件被激活");
  },
  deactivated(){
    console.log("deactivated在C2组件失活时");
  }
}
</script>

<template>
<p>测试组件2</p>
</template>

<style scoped></style>

所有生命周期钩子的 this 上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。这意味着**你不应该使用箭头函数来定义一个生命周期方法** (例如 created: () => this.fetchTodos())。因为箭头函数绑定了父级上下文,所以 this 不会指向预期的组件实例,并且this.fetchTodos 将会是 undefined。

如:

1
2
3
4
5
6
7
8
9
Vue.createApp({
  data() {
    return { count: 1}
  },
  created() {
    // `this` 指向 vm 实例
    console.log('count is: ' + this.count) // => "count is: 1"
  }
})

VUE Setup函数

setup 选项在组件创建**之前**执行。 setup 是围绕 beforeCreatecreated 生命周期钩子运行的。执行 setup 时,组件实例尚未被创建。**无法访问**以下组件选项:

  • this

  • data

  • computed
  • methods

setup 函数时,它将接收两个参数:

  1. props
  2. context

Props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

1
2
3
4
5
6
7
8
export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

Context

传递给 setup 函数的第二个参数是 contextcontext 是一个普通的 JavaScript 对象,它暴露组件的三个 property:

attrsslotsemit 分别等同于 $attrs$slots$emit 实例 property。

1
2
3
4
5
6
7
8
9
10
11
12
export default {
  setup(props, context) {
    // Attribute (非响应式对象)
    console.log(context.attrs)
<span class="hljs-comment">// 插槽 (非响应式对象)</span>
<span class="hljs-built_in">console</span>.log(context.slots)

<span class="hljs-comment">// 触发事件 (方法)</span>
<span class="hljs-built_in">console</span>.log(context.emit)

}
}

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

1
2
3
4
5
6
// MyBook.vue
export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}

script setup

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。

要使用这个语法,需要将 setup 属性添加到 <script> 代码块上:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
// 变量
const msg = 'Hello!'

// 函数
function log() {
console.log(msg)
}
</script>

<template>
<div @click="log">{{ msg }}</div>
</template>

里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的代码会在**每次组件实例被创建的时候执行**。

import 导入的内容也会以同样的方式暴露。意味着可以在模板表达式中直接使用导入的 helper 函数,并不需要通过 methods 选项来暴露它:

1
2
3
4
5
6
7
<script setup>
import { capitalize } from './helpers'
</script>

<template>
<div>{{ capitalize('hello') }}</div>
</template>

<script setup> 范围里的值也能被直接作为自定义组件的标签名使用:将 组件看做被一个变量所引用。

1
2
3
4
5
6
7
<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
<MyComponent />
</template>

可以使用带点的组件标记,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:

1
2
3
4
5
6
7
8
9
<script setup>
import * as Form from './form-components'
</script>

<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>

useSlotsuseAttrs

<script setup> 使用 slotsattrs 的情况应该是很罕见的,因为可以在模板中通过 $slots$attrs 来访问它们。在你的确需要使用它们的罕见场景中,可以分别用 useSlotsuseAttrs 两个辅助函数:

1
2
3
4
5
6
<script setup>
import { useSlots, useAttrs } from 'vue'

const slots = useSlots()
const attrs = useAttrs()
</script>

生命周期钩子

可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup () 内部调用生命周期钩子:

选项式 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

这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

如:

1
2
3
4
5
6
7
export default {
  setup() {
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

defineProps 和 defineEmits

<script setup> 中必须使用 definePropsdefineEmits API 来声明 propsemits ,它们在 <script setup> 中直接用:

1
2
3
4
5
6
7
8
<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup code
</script>

特征:

  • definePropsdefineEmits 都是只在 <script setup> 中才能使用的**编译器宏**。他们不需要导入且会随着 <script setup> 处理过程一同被编译掉。
  • defineProps 接收与 props 选项相同的值,defineEmits 也接收 emits 选项同的值。
  • definePropsdefineEmits 在选项传入后,会提供恰当的类型推断。
  • 传入到 definePropsdefineEmits 的选项会从 setup 中提升到模块的范围。因此,传入的选项不能引用在 setup 范围中声明的局部变量。这样做会引起编译错误。但是,它*可以*引用导入的绑定,因为它们也在模块范围内。

setup() 内部,this 不是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

为了避免混淆,建议在setup函数中使用**生命周期钩子**,而不是直接使用生命周期函数。

expose

在 Vue 3.2 中新增的 expose 是一个函数,该函数允许通过公共组件实例暴露特定的 property。默认情况下,通过 ref、$parent$root 获取的公共实例等同于模板所使用的内部实例。调用 expose 将以指定的 property 创建一个独立的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const MyComponent = {
  setup(props, { expose }) {
    const count = ref(0)
    const reset = () => count.value = 0
    const increment = () => count.value++
<span class="hljs-comment">// 只有 reset 能被外部访问,例如通过 $refs</span>
expose({
  reset
})

<span class="hljs-comment">// 在组件内部,模板可以访问 count 和 increment</span>
<span class="hljs-keyword">return</span> { count, increment }

}
}

defineExpose

使用 <script setup> 的组件是**默认关闭**的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:

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

const a = 1
const b = ref(2)

defineExpose({
a,
b
})
</script>

当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)

VUE 组合式 Provide / Inject

setup() 中Provide / Inject也需要改变写法。

Provide

setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 来定义每个 property。

provide 函数允许你通过两个参数定义 property:

  1. name (<String> 类型)
  2. value

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'

export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole') // 给子组件提供值
provide('geolocation', { // 给子组件提供值
longitude: 90,
latitude: 135
})
}
}
</script>

inject

setup() 中使用 inject 时,也需要从 vue 显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。

inject 函数有两个参数:

  1. 要 inject 的 property 的 name
  2. 默认值 (**可选**)

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
import { inject } from 'vue'

export default {
setup() {
const userLocation = inject('location', '默认值') // 子组件获取值
const userGeolocation = inject('geolocation') // 子组件获取值

<span class="hljs-keyword">return</span> {
  userLocation,
  userGeolocation
}

}
}
</script>

VUE 响应式函数

响应式和组合式时两个完全不同的概念。响应式指的是数据变化会动态的变更相关的一切特性,一般用于将普通非响应式变量转变成响应式的。

ref

在 Vue 3.0 中,可以通过一个新的 ref 函数,用于将普通变量编程响应式的变量(即对象),并使任何响应式变量在任何地方起作用,如下所示:

1
2
3
import { ref } from 'vue'

const counter = ref(0)

ref 实际上是将接收参数包裹在一个带有 value 属性的对象中,然后可以使用该 value 属性访问或更改响应式变量的值:

1
2
3
4
5
6
7
8
9
import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

counter.value++
console.log(counter.value) // 1

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,NumberString 等基本类型是通过值而非引用传递的。

这个特性就像一切皆对象,面向对象编程。

(TS语法)有时我们可能需要为 ref 的内部值指定复杂类型。想要简洁地做到这一点,我们可以在调用 ref 覆盖默认推断时传递一个泛型参数:

1
2
3
const foo = ref<string | number>('foo') // foo 的类型:Ref<string | number>

foo.value = 123 // ok!

unref

unref 用户获取响应式变量的普通值,如果参数是一个 ref,则返回内部值,否则返回参数本身。

相当于是 val = isRef(val) ? val.value : val 的语法糖函数。

所以 isRef 函数用于判断变量是否是响应式变量

toRef

可以用来为源响应式对象上的某个 属性新创建一个 ref。然后,ref 可以被传递,它会保持对其源 property 的响应式连接。也就是还是指针,一改全改。

1
2
3
4
5
6
7
8
9
10
11
12
const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

当你要将 prop 的 ref 传递给复合函数时,toRef 很有用:

1
2
3
4
5
export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

即使源 属性不存在,toRef 也会返回一个可用的 ref。这使得它在使用可选 prop 时特别有用。

toRefs

可以用来为源响应式对象上的 全部 属性新创建 ref。返回结果对象的每个 属性 都是指向原始对象相应 属性的 ref上,也就是还有响应式的特征。但是返回结果对象是普通对象,方便展开/解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型:

{
foo: Ref<number>,
bar: Ref<number>
}
*/

// ref 和原始 property 已经“链接”起来了
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

当从组合式函数返回响应式对象时,toRefs 非常有用,这样组件就可以在不丢失响应性的情况下对返回的对象进行解构/展开:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

// 操作 state 的逻辑

// 返回时转换为ref
return toRefs(state)
}

export default {
setup() {
// 可以在不失去响应性的情况下解构
const { foo, bar } = useFeatureX()

<span class="hljs-keyword">return</span> {
  foo,
  bar
}

}
}

toRefs 只会为源对象中包含的 property 生成 ref。如果要为特定的 property 创建 ref,则应当使用 toRef

reactive

reactive 用于将对象中的每一个属性都变成响应式的。响应式转换是“深层”的——它影响所有嵌套 property。

如果源对象本来就是响应式的,reactive 将返回响应式对象的响应式副本,同时维持每个 ref 属性的响应性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const count = ref(1)
const obj = reactive({ count })

// ref 会被解包
console.log(obj.count === count.value) // true

// 它会更新 obj.count
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

// 它也会更新 count ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3

isReactive检查对象是否是由 reactive 创建的响应式代理。

1
2
3
4
5
6
7
8
9
import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true
  }
}

readonly

接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
// 用于响应性追踪
console.log(copy.count)
})

// 变更 original 会触发依赖于副本的侦听器
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!

isReadonly检查对象是否是由 readonly 创建的只读代理。

toRaw

返回 reactivereadonly始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。**不**建议保留对原始对象的持久引用。请谨慎使用。

1
2
3
4
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

markRaw

标记一个对象,使其永远不会转换为 proxy。返回对象本身。

1
2
3
4
5
6
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

computed

computed()函数接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

1
2
3
4
5
6
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者,接受一个具有 getset 函数的对象,用来创建可变的 ref 对象。

1
2
3
4
5
6
7
8
9
10
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0

watchEffect

watchEffect() 函数立即执行传入其中的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

1
2
3
4
5
6
7
8
9
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
count.value++
// -> logs 1
}, 100)

watch

watch API 侦听特定的数据源,并在单独的回调函数中执行处理函数。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。

  • 与 watchEffect 相比,watch 的特征:
  • 惰性地执行副作用;
  • 更具体地说明应触发侦听器重新运行的状态;
  • 能够访问被侦听状态的先前值和当前值。

侦听单一源

语法

1
2
3
watch(变量, (newValue, oldValue) => {
  /* ... */
})

侦听器数据源可以是一个具有返回值的 getter 函数,也可以直接是一个 ref:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})

侦听多个源

侦听器还可以使用数组以同时侦听多个源:

1
2
3
watch([foo, bar], ([newFoo, newBar], [oldFoo, oldar]) => {
  /* ... */
})

停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

1
2
3
4
5
6
const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

听响应式对象

使用侦听器来比较一个数组或对象的值,这些值是响应式的,要求它有一个由值构成的副本。

1
2
3
4
5
6
7
8
9
10
const numbers = reactive([1, 2, 3, 4])

watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers)
}
)

numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

尝试检查深度嵌套对象或数组中的 property 变化时,需要将 deep 选项设置为 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const state = reactive({ 
  id: 1,
  attributes: { 
    name: '',
  }
})

watch(
() => state,
(state, prevState) => {
console.log('not deep', state.attributes.name, prevState.attributes.name)
}
)

watch(
() => state,
(state, prevState) => {
console.log('deep', state.attributes.name, prevState.attributes.name)
},
{ deep: true }
)

state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"

作业

使用响应式API实现如下功能

image-20210927231848983

使用vue实现如下动态分页

image-20210928083040813

使用vue实现如下文件上传

image-20210928083809437

    </article>
posted @ 2021-11-20 16:08  柠檬色的橘猫  阅读(662)  评论(0)    收藏  举报