Vue3 - Composition API
Vue3 - Composition API
前言:
Composition API
官网的解释是,一组附加的,基于函数的api,允许灵活组合组件的逻辑。本文首先讲述vue3
相比vue2
变更的地方,然后再逐一讲解常用的Cmposition API
。
篇幅较长,如果想直接查看composition api
,而不想看vue3与vue2的变更, 点这里
安装指引
下面讲述vue3
如何安装和使用:
安装
vite
npm init vite-app hello-vue3 # 或者 yarn create vite-app hello-vue3
vue-cli v4.5.0 以上版本
npm install -g @vue/cli # 或者 yarn global add @vue/cli
vue create hello-vue3
# 然后选择vue3的预设
使用
import { createApp } from 'vue'
import MyApp from './MyApp.vue'
const app = createApp(MyApp)
app.mount('#app')
Vue2 与 Vue3 的差异
下面讲述Vue3破坏性变更的地方
全局API
全局api已迁移至 createApp()
创建的实例下
2.x全局API | 3.x实例API(app) |
---|---|
Vue.config.production | 已经删除 |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
use
const app = createApp(MyApp)
app.use(VueRouter)
component & directive
const app = createApp(MyApp)
app.component('button-counter', {
data: () => ({
count: 0
}),
template: '<button @click="count++">Clicked {{ count }} times.</button>'
})
app.directive('focus', {
mounted: el => el.focus()
})
app.mount('#app')
provide & inject
// 在入口文件
app.provide('guide', 'Vue 3 Guide')
// 在子组件中
export default {
inject: {
book: {
from: 'guide'
}
},
template: `<div>{{ book }}</div>`
}
plugin
const plugin = {
install: app => {
app.directive('focus', {
mounted: el => el.focus()
})
}
}
tree shaking
tree shaking主要作用是打包项目时会将用不到的方法不打包进项目中,这样得以优化项目体积。
import { nextTick } from 'vue'
nextTick(() => {
// something DOM-related
})
以前的api改为es模块导入,以及vShow,transition等内部助手方法,都启用了摇树优化(tree shaking),只有实际用到的才会被打包进去。
模板指令
下面讲述模板指令相关破坏性变更的地方:
v-model
v-model
prop 和 event 默认的名称已经更改
<ChildComponent v-model="pageTitle" />
<!-- 上下等价 -->
<!-- value-> modelValue -->
<!-- input-> update:modelValue -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
同时,v-model.sync
的修饰符也已经删除, v-model支持绑定不同的数据,可以作为其替代。
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />
<!-- 上下等价 -->
<ChildComponent
:title="pageTitle"
@update:title="pageTitle = $event"
:content="pageContent"
@update:content="pageContent = $event"
/>
key
-
key
在v-if/v-else/v-else-if
分支上不再需要,因为vue会自动生成唯一key - 如果你手动提供
key
,则每个分支必须使用唯一key
,你不再可以有意使用它来强制重用分支。 -
<template v-for>
key
应放置在<template>
标签上(而不是其子标签上)。
<!-- Vue 2.x -->
<template v-for="item in list">
<div v-if="item.isVisible" :key="item.id">...</div>
<span v-else :key="item.id">...</span>
</template>
<!-- Vue 3.x -->
<template v-for="item in list" :key="item.id">
<div v-if="item.isVisible">...</div>
<span v-else>...</span>
</template>
v-if & v-for
如果用于同一元素,v-if则优先级高于v-for
- 在2.x中,当在同一元素上使用v-if和v-for时,v-for将优先使用。
- 在3.x中,v-if优先级始终高于v-for。
v-bind
v-bind的绑定顺序将影响渲染结果。
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>
规则是,绑定相同属性,在后边的具有最高优先级。(在2.x中,单个属性的优先级,比v-bind高)。
ref
ref的用法发生改变,需要一个变量来接收dom对象的引用, 不再自动合并到$refs
<div v-for="item in list" :ref="setItemRef"></div>
export default {
data() {
return {
itemRefs: [] // 存储节点引用
}
},
methods: {
setItemRef(el) {
this.itemRefs.push(el)
}
},
beforeUpdate() {
this.itemRefs = []
},
updated() {
console.log(this.itemRefs)
}
}
Components
组件方面发生变更的地方
functional components
函数式组件:
- 在2.x中,函数式组件的性能提升现在可以忽略不计,因此我们建议仅使用有状态组件
- 只能使用接收一个普通的函数来创建函数式组件,参数包括props和context(即,slots,attrs,emit)
-
<template>
与 单文件组件中的 functional选项已经删除
通过函数创建:
import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
通过单文件组件创建
<template>
<component
v-bind:is="`h${$props.level}`"
v-bind="$attrs"
// v-on="listeners" listeners现在作为$attrs的一部分传递,可以删除
/>
</template>
<script>
export default {
props: ['level']
}
</script>
async components
异步组件发生改变的地方:
- 新增了
defineAsyncComponent
方法定义异步组件 - component 选项重命名为 loader
- 加载函数本身不接收resolve和reject传递参数,必须返回Promise
2.x中:
const asyncPage = () => import('./NextPage.vue')
3.x中:
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 无选项
const asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))
// 带选项
const asyncPageWithOptions = defineAsyncComponent({
loader: () => import('./NextPage.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
Render Function
渲染函数,这个改变不会影响使用<template>
的用户,不用render api
的可以略过。
render函数api
有以下改变:
- h函数以全局导入的方式替代render函数参数传递的方式
- vnode的props的格式变的扁平化
2.x中:
export default {
render(h) {
return h('div')
}
}
3.x中:
import { h, reactive } from 'vue' // 手动导入h
export default {
setup(props, { slots, attrs, emit }) {
const state = reactive({
count: 0
})
function increment() {
state.count++
}
// 返回一个渲染函数
return () =>
h(
'div', // 节点名
{
onClick: increment // 节点属性
},
state.count // 子节点
)
}
}
vnode的属性结构(h的第二个参数):
// 2.x
{
staticClass: 'button',
class: {'is-outlined': isOutlined },
staticStyle: { color: '#34495E' },
style: { backgroundColor: buttonColor },
attrs: { id: 'submit' },
domProps: { innerHTML: '' },
on: { click: submitForm },
key: 'submit-button'
}
// 3.x Syntax
{
class: ['button', { 'is-outlined': isOutlined }],
style: [{ color: '#34495E' }, { backgroundColor: buttonColor }],
id: 'submit',
innerHTML: '',
onClick: submitForm,
key: 'submit-button'
}
注册组件,2.x中:
// 假设有个ButtonCounter的自定义组件
export default {
render(h) {
return h('button-counter')
}
}
3.x中
import { h, resolveComponent } from 'vue'
export default {
setup() {
const ButtonCounter = resolveComponent('button-counter') // 先解析组件
return () => h(ButtonCounter) // 再传递组件
}
}
slots
变化:
- this.$slots 现在将slots公开为功能
- this.$scopedSlots 已经移除
在渲染函数中使用:
// 2.x 语法
h(LayoutComponent, [
h('div', { slot: 'header' }, this.header),
h('div', { slot: 'content' }, this.content)
])
// 3.x 语法
h(LayoutComponent, {}, {
header: () => h('div', this.header),
content: () => h('div', this.content)
})
编程方式使用:
// 2.x 语法
this.$scopedSlots.header
// 3.x 语法
this.$slots.header()
自定义元素
有以下发生改变:
- 自定义元素的白名单,在编译器选项中配置。
- is属性的使用,仅限于保留组件的标签名
- 新增指令v-is,解决html 元素限制。
自定义元素白名单
如果想要指定vue之外的自定义元素(比如web组件),以plastic-button为例
<plastic-button></plastic-button>
2.x中
Vue.config.ignoredElements = ['plastic-button'] // 将plastic-button列入白名单
3.x中有两种方式可选,一种是作为编译选项配置,一种是运行时配置:
// webpack的vue-loader里配置
rules: [
{
test: /\.vue$/,
use: 'vue-loader',
options: {
compilerOptions: {
isCustomElement: tag => tag === 'plastic-button' // 指定组件加入白名单
}
}
}
// ...
]
// 运行时配置
const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
定制内置元素
<button is="plastic-button">Click Me!</button>
在2.x中,它被解释为使用is的值作为组件的name去解析并渲染plastic-button组件,但该做法阻止了原生button元素的行为。
在3.x中,仅当使用<component>
标签的时候,is才会和2.x中的用法相同。
<component is="plastic-button"/>
在普通组件上使用,它的行为类似与普通属性:
<foo is="bar" />
- 2.x行为: 渲染bar组件
- 3.x行为: 渲染foo组件,并传递is属性。
在普通元素上使用:
<button is="plastic-button">Click Me!</button>
- 2.x行为:渲染plastic-button组件。
- 3.x行为:通过调用呈现原生按钮
// 创建了web组件plastic-button的实例,但保留了button的特性
document.createElement('button', { is: 'plastic-button' })
v-is In-Dom模板解析
In-Dom模板,主要用于需要遵守html特定元素的解析规则的情况,比如<ul>
,<ol>
,<table>
和<select>
有什么元素可以在其内部出现的限制,以及一些元素,如<li>
,<tr>
和<option>
只能出现某些其他元素中。
2.x中通常是这样使用:
<table>
<tr is="blog-post-row"></tr>
</table>
而3.x则改成了v-is:
<table>
<!-- 注意v-is是指令,里面接受的是表达式,要填字符串,必须加引号‘’ -->
<tr v-is="'blog-post-row'"></tr>
</table>
其他变化
-
destroyed
生命周期改名为unmounted
-
beforeDestroy
生命周期改名为beforeUnmount
-
props
的defalut
工厂函数不再支持this
上下文访问import { inject } from 'vue' export default { props: { theme: { default (props) { // props 是通过组件传递的原始属性 // before any type / default coercions // 仅能使用inject来访问provide注入的属性 return inject('theme', 'default-theme') } } } }
- 自定义指令的生命周期api改为跟组件一致
- bind → beforeMount
- inserted → mounted
- beforeUpdate 新增,在元素更新前调用
- update 已经移除,因为与updated类似
- componentUpdated → updated
- beforeUnmount 新增,卸载元素之前立即调用
- unbind -> unmounted
于是最终形成的样子如下:
const MyDirective = { beforeMount(el, binding, vnode, prevVnode) { // const vm = vnode.context 2.x中访问组件实例 const vm = binding.instance // 3.x中访问组件的实例 }, mounted() {}, beforeUpdate() {}, updated() {}, beforeUnmount() {}, // new unmounted() {} }
- data选项只能使用函数声明方式
<script> import { createApp } from 'vue' createApp({ data() { return { apiKey: 'a1b2c3' } } }).mount('#app') </script>
- data选项的合并行为被改变,只合并已有的属性。
结果如下:const Mixin = { data() { return { user: { name: 'Jack', id: 1 } } } } const CompA = { mixins: [Mixin], data() { return { user: { id: 2 } } } }
// 2.x中的合并结果 { user: { id: 2, name: 'Jack' } } // 3.x中的合并结果 { user: { id: 2 } }
- transition过渡类重命名,v-enter-> v-enter-from , v-leave-> v-leave-from
<!-- 实现渐入渐出效果 --> .v-enter-from, .v-leave-to { opacity: 0; } .v-leave-from, .v-enter-to { opacity: 1; }
- 监听数组时,仅当替换整个数组的时候才会触发,如果需要监听内部值的改变,则需要指定
deep
选项。watch: { bookList: { handler(val, oldVal) { console.log('book list changed') }, deep: true }, }
- 没有携带特殊指令(v-if/else-if/else, v-for, 或者 v-slot)的
<template>
标签,将被视作原生的<template>
html标签,这将导致不会渲染其内部的内容。 - 在vue2.x中,应用根容器的
outerHTML
会被根组件替换(如果根组件没有template
或者render
选项,则最终编译为一个模板)。而vue3.x现在使用根容器的innerHTML
代替。这意味着根容器自身不再作为模板的一部分。举个例子:<body id="body"> <div id="app"></div> </body> <!-- vue2.x会将容器div一起替换了,而vue3,只会把app组件的内容替换到innerhtml里 --> <!-- 这也是vue2.x为什么不能把body作为app根元素的原因,因为body会消失= =! -->
移除的API
以下为移除的API
keycode
由于KeyboardEvent.keyCode已弃用, 因此vue3不再支持该功能。
- v-on不再支持使用数字(即keyCodes)作为修饰符
- config.keyCodes不再受支持
<!-- 不支持 -->
<input v-on:keyup.13="submit" />
<!-- 支持 -->
<input v-on:keyup.delete="confirmDelete" />
Vue.config.keyCodes = { // 不支持
f1: 112
}
Events API
$on
、$off
、$once
不再支持,官方建议使用第三方库mitt或者tiny-emitter替换。$emit
仍作为现有api,触发父组件事件而受支持。
filters
filters选项已被移除,在vue3中不再受支持(因为该语法打破了{}
内只是javascript的假设),官方建议用方法或者computed代替。
如果你想使用全局过滤器,那么可以这么做:
// main.js
const app = createApp(App)
app.config.globalProperties.$filters = {
currencyUSD(value) {
return '$' + value
}
}
<template>
<h1>Bank Account Balance</h1>
<p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
使用这种方法时,你只能使用方法,而不能使用计算属性。因为后者仅在单个组件的上下文中定义才有意义。
Inline Template Attribute
内联模板不再受支持:
<!-- 这个没用过= =! -->
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>
官方建议用script标签或者slot代替。具体用法见官网
$destroy
$destroy
实例方法。用户不应再手动管理各个Vue组件的生命周期。
支持库
目前所有的官方库和工具都支持vue3,但其中大多数仍处于beta(公测)状态。官方计划在2020年底之前稳定并切换所有项目以使用dist标签。
Vue CLI
从v4.5.0开始,vue-cli现在提供了内置选项,可以在创建新项目时选择Vue 3预设。
Vue Router
vue router 4.0 提供vue3的支持,并且具有许多重大更改。
Vuex
Vuex 4.0通过与3.x大致相同的API提供了Vue 3支持。唯一的重大变化是插件的安装方式
Composition API
下面开始正式讲解常用的组合式API。
reactive
reactive 基本等价于2.x中的Vue.observable(),返回一个响应式对象,就像2.x中定义在data选项里的数据一样,最终都会被转换成响应式对象。基于 ES2015 的 Proxy 实现。
import { reactive } from 'vue'
// state 现在是一个响应式的状态
const state = reactive({
count: 0,
})
ref
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性.value
const count = ref(0) // 相当于返回{value:0}
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
可能有些同学会问了,既然reactive和ref都能创建响应式对象,他们之间有什么区别呢,或者说各自使用在哪种场景呢?下面看一个例子:
// 风格 1: 将变量分离
let x = ref(0)
let y = ref(0)
function updatePosition(e) {
x.value = e.pageX
y.value = e.pageY
}
// --- 与下面的相比较 ---
// 风格 2: 单个对象
const pos = reactive({
x: 0,
y: 0,
})
function updatePosition(e) {
pos.x = e.pageX
pos.y = e.pageY
}
从上可以看出,他们的使用符合js的值类型和引用类型的概念:
- ref 适合基础类型值
- reactive 适合对象类型的值
有人会说,既然这样,那为什么不把变量全塞对象里直接用reactive呢?这是因为对象解构的时候,数据会丢失响应式特性,如下:
const pos = reactive({
x: 0,
y: 0,
})
function updatePosition(e) {
// 解构对象,导致响应式丢失,相当于重新将值赋给了一个变量,之后的更改不会改变原属性的值
let {x,y} = pos
x = e.pageX
y = e.pageY
}
正因为此,官方提供了toRefs
与toRef
的函数,来将一个响应式对象的基础类型属性值转换为ref对象,这才不可避免的有了ref的概念。
const state = reactive({
foo: 1,
bar: 2,
})
const fooRef = toRef(state, 'foo') // 转换单个的foo属性为ref对象
fooRef.value++
console.log(state.foo) // 2
state.foo++
console.log(fooRef.value) // 3
const state = reactive({
foo: 1,
bar: 2,
})
const stateAsRefs = toRefs(state) // 转换state对象的所有属性为ref对象
/*
stateAsRefs 的类型如下:
{
foo: Ref<number>,
bar: Ref<number>
}
*/
注意:ref对象在以下情况会自动解套,也就是,不需要写.value也能访问值。
- 当嵌套在reactive Object 中
- 当作为setup的返回值返回
setup
setup是组件的新选项,作为在组件内使用 Composition API 的入口点。
-
调用时机
创建组件实例,初始化props → 调用setup → beforeCreate -
模板中使用
<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' })
// 暴露给模板
return {
count,
object,
}
},
}
</script>
注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value。
- 参数
第一个参数收传递给组件的属性,第二个参数,从原来的this
上下文选择性暴露了一些属性。
export default {
props: {
name: String,
},
setup(props, ctx) { // 不要解构props,会导致其失去响应式
watchEffect(() => {
console.log(`name is: ` + props.name)
})
conosole.log(ctx)
// context.attrs
// context.slots
// context.emit
},
}
-
this
的用法
this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。
watchEffect
预期接收一个含有副作用的函数,仅当该过程中用到的响应式状态发生改变时,会重新执行该函数。
import { reactive, watchEffect } from 'vue'
const state = reactive({
count: 0,
})
onMounted(()=>{
// 立即执行一次,之后会在state.count发生改变的时候执行,组件卸载的时候销毁
watchEffect(() => {
document.body.innerHTML = `count is ${state.count}`
})
})
watchEffect
回调的执行时机:
- 立即执行传入的一个函数,并响应式追踪其依赖
- 依赖变更时重新运行该函数(里面用到的响应式属性发生变更时)
- 清除副作用
组件卸载时候会自动停止侦听器,当然也有显式调用停止的方式:
// 同步的方式
const stop = watchEffect(() => {
/* ... */
})
// 之后
stop()
// 如果是回调里有异步,可以用回调的参数onInvalidate去取消监听
const data = ref(0)
watchEffect((onInvalidate) => { // 立即执行,其后data改变,组件更新后执行
console.log(data.value)
const timer = setInterval(()=>{
data.value ++
},1000)
// 第一次初始化时候不执行该回调,仅注册回调,data改变时以及停止侦听时,会触发该回调
onInvalidate(() => {
// 取消定时器
clearInterval(timer)
})
})
// output: 0 1
onInvalidate 触发时机
- 副作用即将重新执行时(也就是追踪的依赖发生改变时)
- 侦听器被停止时(如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)
watch
watch
API 完全等效于 2.x this.$watch
(以及 watch
中相应的选项)。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
-
对比
watchEffect
,watch
的作用:- 懒执行副作用(回调);
- 更明确哪些状态的改变会触发侦听器重新运行副作用;
- 能够侦听到数据变化的新值与旧值。
-
watch
的数据源watch
的数据源可以是一个或多个拥有返回值的 getter 函数,也可以是 ref:// 侦听一个 getter const state = reactive({ count: 0 }) watch( () => state.count, // 返回count的getter (count, prevCount) => { // 回调,新值旧值 /* ... */ } ) // 直接侦听一个 ref const count = ref(0) watch(count, (count, prevCount) => { // 监听ref /* ... */ }) // 监听多个属性,参数以数组方式传递即可 watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ })
清除副作用与watchEffect
类似,不同的地方就是onInvalidate会作为回调的第三个参数传入。
const data = ref(0)
watch(data, (newData, oldData, onInvalidate) => {
console.log(newData.value)
onInvalidate(() => {
// 取消定时器
clearInterval(timer)
})
})
computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误用法,由于默认只设置了getter!
传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
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
readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。只读代理是“深只读”,对象内部任何嵌套的属性也都是只读的。
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 依赖追踪
console.log(copy.count)
})
// original 上的修改会触发 copy 上的侦听
original.count++
// 无法修改 copy 并会被警告
copy.count++ // warning!
生命周期钩子函数
直接导入onXXX
即可使用周期函数:
import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
setup() {
// beforeMount
onMounted(() => {
console.log('mounted!')
})
// beforeUpdate
onUpdated(() => {
console.log('updated!')
})
// beforeUnmount
onUnmounted(() => {
console.log('unmounted!')
})
},
}
注意,这些生命周期钩子函数只能在setup中使用,因为他们依赖当前组件的实例。
组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的watch
和computed
也将自动删除。
与2.x相比:
- beforeCreate -> 使用 setup()
- created -> 使用 setup()
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- updated -> onUpdated
- beforeDestroy -> onBeforeUnmount
- destroyed -> onUnmounted
- errorCaptured -> onErrorCaptured
新增的钩子函数
- onRenderTracked
- onRenderTriggered
两个钩子函数都接收一个 DebuggerEvent:
export default {
onRenderTriggered(e) {
debugger
// 检查哪个依赖性导致组件重新渲染
},
}
依赖注入
// 提供者:
const themeRef = ref('dark')
provide(ThemeSymbol, themeRef)
// 使用者:
const theme = inject(ThemeSymbol, ref('light'))
watchEffect(() => {
console.log(`theme set to: ${theme.value}`)
})
模板Refs
<template>
<!-- 将div的引用赋值给root -->
<div ref="root"></div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象
console.log(root.value) // 打印出 <div/>
})
return {
root,
}
},
}
</script>
配合 render 函数 / JSX 的用法
export default {
setup() {
const root = ref(null)
// 使用render函数渲染
return () =>
h('div', {
ref: root,
})
// 使用 JSX , 有木有感觉跟react很像:)
return () => <div ref={root} />
},
}
在v-for
中使用:
<template>
<div v-for="(item, i) in list" :ref="el => { divs[i] = el }">
{{ item }}
</div>
</template>
<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// 确保在每次变更之前重置引用
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs,
}
},
}
</script>
响应式系统工具集
API | 用途 |
---|---|
isRef | 检查一个值是否为一个 ref 对象 |
isProxy | 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理 |
isReactive | 检查一个对象是否是由 reactive 创建的响应式代理。 |
isReadonly | 检查一个对象是否是由 readonly 创建的只读代理。 |
unref | val = isRef(val) ? val.value : val 的语法糖 |
toRef | toRef 可以用来为一个 reactive 对象的属性创建一个 ref。 |
toRefs | 把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref |
具体用法见官网
高级响应式系统API
customRef
customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应。
<template>
<input v-model="text" />
</template>
<script>
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get() {
track() // 调用track收集依赖
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger() // 调用trigger,触发响应
}, delay)
},
}
})
}
export default {
setup() {
return {
text: useDebouncedRef('hello'),
}
},
}
</script>
markRaw
显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。作用有点类似Object.freeze
, 去除响应式。
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
shallowReactive
与reactive
类似,唯一的区别就是只创建“浅代理”,嵌套对象不会变成响应式。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性是响应式的
state.foo++
// ...但不会深层代理
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
shallowReadonly
与readonly
类似,唯一的区别就是只限制“浅只读”。嵌套对象仍然可以赋值。
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2,
},
})
// 变更 state 的自有属性会失败
state.foo++
// ...但是嵌套的对象是可以变更的
isReadonly(state.nested) // false
state.nested.bar++ // 嵌套属性依然可修改
shallowRef
与ref
类似,唯一的区别只是“浅引用” ,只会追踪它的 .value
更改操作,但是如果赋值的是一个对象,则该对象不是可响应,并且后续的对象的属性更改均不会触发视图响应。
const foo = shallowRef({})
foo.value.a = 1 // 这个a也不会响应到视图上去
isReactive(foo.value) // false
// 更改对操作会触发响应
foo.value = []
// 但上面新赋的这个对象并不会变为响应式对象,只是会同步这个值,视图上会同步显示[]
isReactive(foo.value) // false
const bar = shallowRef(0)
bar.value ++ // 1 , 这个是响应式的
toRaw
返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。简单来说就是返回代理之前的原始对象。
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
组合式API的应用
前面已经讲述了常用Composition API
,下面再看看,在实际使用中如何提取重用逻辑的。
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// 组件中使用
import { useMousePosition } from './mouse'
export default {
setup() {
const { x, y } = useMousePosition() // 官方推荐组合函数命名,以use打头,= =!莫名有点像hook
// 其他逻辑...
return { x, y }
},
}
可以看到,使用这种方式的好处在于,可以将组件中任意一段逻辑提取出来复用。并且通过规范的命名,还能看出来这个函数的功能是做什么的,易于维护,不再像2.x那样,只是一堆选项配置的堆砌,无法直白的看出,某个地方具体作用。
上一篇: 浅析iis7.5安装配置php环境
下一篇: 网络爬虫-微博主页内容(Ajax接口)
推荐阅读
-
windows10下安装TensorFlow Object Detection API的步骤
-
Android指纹识别API讲解,一种更快更好的用户体验
-
详解Laravel5.6 Passport实现Api接口认证
-
Java网络编程实现HTTP协议(Socket API)
-
C#实现快递api接口调用方法
-
微信提现API实现(企业付款到个人)
-
vue axios封装及API统一管理的方法
-
android monkey自动化测试改为java调用monkeyrunner Api
-
使用 Spring Boot 2.0 + WebFlux 实现 RESTful API功能
-
HTML5中5个简单实用的API(第二篇,含全屏、可见性、拍照、预加载、电池状态)