3.Composition API
# setup
setup是vue新增的一个选项,它是组件内使用Composition API的入口。
注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value【setup 内部需要.value】
# 调用时机
setup是在创建vue组件实例并完成props的初始化之后执行,也是在beforeCreate钩子之前执行,这意味着setup中无法使用其它option(如data)中的变量,而其它option可以使用setup中返回的变量。 如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
# 模板使用
如果setup返回一个对象,这个对象的所有属性会合并到template的渲染上下文中,也就是说可以在template中使用setup返回的对象的属性。
<template>
<div>{{ count }} {{ object.foo }}</div>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
// expose to template
return {
count,
object
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 与Render Function/jsx结合使用
setup也可以返回render function,使用方法与vue2中的render方法类似。
import { h, ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
return () => h('div', [count.value, object.foo])
}
}
2
3
4
5
6
7
8
9
10
# 参数
setup有两个参数,第一个参数为props,是组件的所有参数,与vue2中一样,参数是响应式的,改变会触发更新;第二个参数是setup context,包含emit等属性。
const MyComponent = {
setup(props, context) {
context.attrs
context.slots
context.emit
}
}
2
3
4
5
6
7
props 作为其第一个参数
注意 props 对象是响应式的,watchEffect 或 watch 会观察和响应 props 的更新
不要解构 props 对象,那样会使其失去响应性
export default {
props: {
name: String,
},
setup(props) {
console.log(props.name)
watchEffect(() => {
console.log(`name is: ` + props.name)
})
},
}
2
3
4
5
6
7
8
9
10
11
第二个参数提供了一个上下文对象【从原来 2.x 中 this 选择性地暴露了一些 property(attrs/emit/slots)】
attrs 和 slots 都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值
setup(props, { attrs }) {
// 一个可能之后回调用的签名
function onClick() {
console.log(attrs.name) // 一定是最新的引用,没有丢失响应性
}
}
2
3
4
5
6
# this的使用
this is not available inside setup(). Since setup() is called before 2.x options are resolved, this inside setup() (if made available) will behave quite differently from this in other 2.x options. Making it available will likely cause confusions when using setup() along other 2.x options. Another reason for avoiding this in setup() is a very common pitfall for beginners
v3中同时支持Options API 与Composition API,由于setup()会在Option API 解析之前调用,而此时setup()内部的this指向与Option API中的完全不同,非常可能引起混乱。 故而,setup中不使用this上下文。
# 响应式系统API
# reactive
reactive函数接收一个对象,并返回一个对这个对象的响应式代理。它与v2中的Vue.obserable()是等价的
Proxy对象是目标对象的一个代理器,任何对目标对象的操作(实例化,添加/删除/修改属性等等),都必须通过该代理器。因此我们可以把来自外界的所有操作进行拦截和过滤或者修改等操作
响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象
因为是组合函数【对象】,所以必须始终保持对这个所返回对象的引用以保持响应性【不能解构该对象或者展开】例如 const { x, y } = useMousePosition()或者return { ...useMousePosition() }
toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref【把对象转成了ref】。
function useMousePosition() {
const pos = reactive({
x: 0,
y: 0,
})
return toRefs(pos)
}
// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()
2
3
4
5
6
7
8
9
10
# ref
ref函数接收一个用于初始化的值并返回一个响应式的和可修改的ref对象。该ref对象存在一个value属性,value保存着ref对象的值。
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
2
3
4
5
如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换
ref 作为 reactive 对象的 property 被访问或修改时,将自动解套 .value
const count = ref(0)
/*当做reactive的对象属性----解套*/
const state = reactive({
count,
})
/* 不需要.value*/
console.log(state.count) // 0
/*修改reactive的值*/
state.count = 1
/*修改了ref的值*/
console.log(count.value) // 1
2
3
4
5
6
7
8
9
10
11
12
//注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref
/*创建一个新的ref*/
const otherCount = ref(2)
/*赋值给reactive的旧的ref,旧的会被替换掉*/
state.count = otherCount
/*修改reactive会修改otherCount*/
console.log(state.count) // 2
/*修改reactive会count没有被修改 */
console.log(count.value) // 1
2
3
4
5
6
7
8
9
10
11
嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套【自由数据类型是Object才会解套,array map set weakmap weakset集合类 访问 ref 时,不会自动解套】
const arr = reactive([ref(0)])
// 这里需要 .value
console.log(arr[0].value)
const map = reactive(new Map([['foo', ref(0)]]))
// 这里需要 .value
console.log(map.get('foo').value)
2
3
4
5
6
7
# readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的【返回一个永远不会变的只读代理】【场景可以参数比对等】
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 依赖追踪
console.log(copy.count)
})
// original 上的修改会触发 copy 上的侦听
original.count++
// 无法修改 copy 并会被警告
copy.count++ // warning!
2
3
4
5
6
7
8
9
10
11
12
13
14
# reactive响应式系统工具集
# isProxy
检查一个对象是否是由 reactive 或者 readonly 方法创建的代理
import {ref,reactive,isProxy,readonly} from'vue'
export default {
setup(){
let num = ref(5);
let single = readonly({
name:'tom'
});
let obj = reactive({
age:5
})
let numFlag = isProxy(num); //false
let singleFlag = isProxy(single); //true
let objFlag = isProxy(obj); //true
return{
numFlag,
singleFlag,
objFlag
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# isReactive
检查一个对象是否是由 reactive 创建的响应式代理
import { reactive, isReactive } from 'vue'
const state = reactive({
name: 'John'
})
console.log(isReactive(state)) // -> true。
2
3
4
5
如果这个代理是由 readonly 创建的,但是又被 reactive 创建的另一个代理包裹了一层,那么同样也会返回 true
//检查一个对象是否是由 reactive 创建的响应式代理
import { reactive, isReactive, readonly } from 'vue'
const state = reactive({
name: 'John'
})
// 用readonly创建一个只读响应式对象plain
const plain = readonly({
name: 'Mary'
})
//readonly创建的,所以isReactive为false
console.log(isReactive(plain)) // -> false
// reactive创建的响应式代理对象包裹一层readonly,isReactive也是true,isReadonly也是true
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# isReadonly
检查一个对象是否是由 readonly 创建的只读代理
import {ref,reactive,isReadonly,readonly} from 'vue'
let num = ref(5);
let single = readonly({
name:'tom'
});
let obj = reactive({
age:5
})
console.log(isReadonly(num)) //false
console.log(isReadonly(single)) //true
console.log(isReadonly(obj)) //false
2
3
4
5
6
7
8
9
10
11
# reactive高级响应式系统API
# toRaw
返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发更改。不建议一直持有原始对象的引用【不建议赋值给任何变量】。请谨慎使用
被toRaw之后的对象是没有被代理/跟踪的的普通对象
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
console.log(toRaw(reactiveFoo) !== reactiveFoo) // true
2
3
4
5
# markRaw
显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。
【markRaw传入对象,返回的值是永远不会被转为响应式代理的】
const foo = markRaw({
name: 'Mary'
})
console.log(isReactive(reactive(foo))) // false
2
3
4
被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
2
markRaw注意点
markRaw和 shallowXXX 一族的 API允许选择性的覆盖reactive或者readonly 默认创建的 "深层的" 特性【响应式】/或者使用无代理的普通对象
设计这种「浅层读取」有很多原因
一些值的实际上的用法非常简单,并没有必要转为响应式【例如三方库的实例/省市区json/Vue组件对象】
当渲染一个元素数量庞大,但是数据是不可变的,跳过 Proxy 的转换可以带来性能提升
这些 API 被认为是高级的,是因为这种特性仅停留在根级别,所以如果你将一个嵌套的,没有 markRaw 的对象设置为 reactive 对象的属性,在重新访问时,你又会得到一个 Proxy 的版本,在使用中最终会导致标识混淆的严重问题:执行某个操作同时依赖于某个对象的原始版本和代理版本(标识混淆在一般使用当中应该是非常罕见的,但是要想完全避免这样的问题,必须要对整个响应式系统的工作原理有一个相当清晰的认知)。
const foo = markRaw({
nested: {},
})
const bar = reactive({
// 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
nested: foo.nested,
})
console.log(foo.nested === bar.nested) // false
2
3
4
5
6
7
8
9
10
# shallowReactive
只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样【第一层是响应式代理,深层次只保留原样(不具备响应式代理)】
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性是响应式的【第一层次响应式】
state.foo++
// ...但不会深层代理【深层次不是响应式】(渲染性能)
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
2
3
4
5
6
7
8
9
10
11
12
# shallowReadonly
类似于shallowReactive,区别是:
- 第一层将会是响应式代理【第一层修改属性会失败】,属性为响应式
- 深层次的对象属性可以修改,属性不是响应式
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性会失败
state.foo++
// ...但是嵌套的对象是可以变更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套属性依然可修改
2
3
4
5
6
7
8
9
10
11
12
# ref 响应式系统工具集
# isRef
检查一个值是否为一个 ref 对象
# unref
unref是val = isRef(val) ? val.value : val 的语法糖
unref(ref(0))===unref(0)===0 //返回number
function useFoo(x: number | Ref<number>) {
const unwrapped = unref(x) // unwrapped 一定是 number 类型
}
2
3
# toRef
toRef 可以用来为一个 reactive 对象的属性【某个属性区别toRefs每一个属性】创建一个 ref。这个 ref 可以被传递并且能够保持响应性
const state = reactive({
foo: 1,
bar: 2,
})
//reactive获取单个属性转为ref【fooRef只是一个代理】
const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
2
3
4
5
6
7
8
9
10
11
12
13
# toRefs
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
// ref 对象 与 原属性的引用是 "链接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2
stateAsRefs.foo.value++
console.log(state.foo) // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
可以通过toRefs返回可解构的reactive,因为toRefs包裹之后返回一一对应的ref属性
function useFeatureX() {
const state = reactive({
foo: 1,
bar: 2,
})
// 对 state 的逻辑操作
// 返回时将属性都转为 ref
return toRefs(state)
}
export default {
setup() {
// 可以解构,不会丢失响应性
const { foo, bar } = useFeatureX()
return {
foo,
bar,
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ref 高级响应式系统API
# customRef
用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个一个带有 get 和 set 属性的对象【实际上就是手动 track追踪 和 trigger触发响应】
以下代码可以使得v-model防抖
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
/*初始化手动追踪依赖讲究什么时候去触发依赖收集*/
track()
return value
},
set(newValue) {
/*修改数据的时候会把上一次的定时器清除【防抖】*/
clearTimeout(timeout)
timeout = setTimeout(() => {
/*把新设置的数据给到ref数据源*/
value = newValue
/*再有依赖追踪的前提下触发响应式*/
trigger()
}, delay)
},
}
})
}
setup() {
return {
/*暴露返回的数据加防抖*/
text: useDebouncedRef('hello'),
}
}
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
# shallowRef
创建一个 ref ,将会追踪它的 .value 更改操作,但是并不会对变更后的 .value 做响应式代理转换(即变更不会调用 reactive)
前面我们说过如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换,通过shallowRef创建的ref,将不会调用reactive【对象不会是响应式的】
const refOne = shallowRef({});
refOne.value = { id: 1 };
refOne.id == 20;
console.log(isReactive(refOne.value),refOne.value);//false { id: 1 }
2
3
4
# triggerRef 【与shallowRef配合】
手动执行与shallowRef相关的任何效果
const shallow = shallowRef({
greet: 'Hello, world'
})
// 第一次运行打印 "Hello, world"
watchEffect(() => {
console.log(shallow.value.greet)
})
// 这不会触发效果,因为ref是shallow
shallow.value.greet = 'Hello, universe'
// 打印 "Hello, universe"
triggerRef(shallow)
2
3
4
5
6
7
8
9
10
11
12
13
14