setup
setup 函数是一个新的组件选项。作为在组件内使用 Composition API 的入口点。
调用时机
创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用
模板中使用
如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文:
注意 setup 返回的 ref 在模板中会自动解开,不需要写 .value。
渲染函数 / JSX 中使用
setup 也可以返回一个函数,函数中也能使用当前 setup 函数作用域中的响应式数据:
import{h,ref,reactive}from'vue'exportdefault{setup(){constcount=ref(0)constobject=reactive({foo:'bar'})return()=>h('div',[count.value,object.foo])},}
参数
该函数接收 props 作为其第一个参数:
exportdefault{props:{name:String,},setup(props){console.log(props.name)},}
注意 props 对象是响应式的,watchEffect 或 watch 会观察和响应 props 的更新:
exportdefault{props:{name:String,},setup(props){watchEffect(()=>{console.log(`name is: `+props.name)})},}
然而不要解构 props 对象,那样会使其失去响应性:
exportdefault{props:{name:String,},setup({name}){watchEffect(()=>{console.log(`name is: `+name)// Will not be reactive!})},}
在开发过程中,props 对象对用户空间代码是不可变的(用户代码尝试修改 props 时会触发警告)。
第二个参数提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property。
constMyComponent={setup(props,context){context.attrs context.slots context.emit},}
attrs 和 slots 都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值:
constMyComponent={setup(props,{attrs}){// 一个可能之后回调用的签名functiononClick(){console.log(attrs.foo)// 一定是最新的引用,没有丢失响应性}},}
出于一些原因将 props 作为第一个参数,而不是包含在上下文中:
组件使用 props 的场景更多,有时候甚至只使用 props
将 props 独立出来作为第一个参数,可以让 TypeScript 对 props 单独做类型推导,不会和上下文中的其他属性相混淆。这也使得 setup 、 render 和其他使用了 TSX 的函数式组件的签名保持一致。
this的用法
this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。同时在 setup() 和 2.x 选项中使用 this 时将造成混乱。在 setup() 中避免这种情况的另一个原因是:这对于初学者来说,混淆这两种情况的 this 是非常常见的错误:
setup(){functiononClick(){this// 这里 `this` 与你期望的不一样!}}
类型定义
interfaceData{[key:string]:unknown}interfaceSetupContext{attrs:Data slots:Slotsemit:(event:string,...args:unknown[])=>void}functionsetup(props:Data,context:SetupContext):Data
提示
为了获得传递给 setup() 参数的类型推断,需要使用 defineComponent。
#响应式系统 API
#reactive
接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()
constobj=reactive({count:0})
响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。
类型定义
functionreactive
#ref
接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。
constcount=ref(0)console.log(count.value)// 0count.value++console.log(count.value)// 1
如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换。
模板中访问
当 ref 作为渲染上下文的属性返回(即在setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value:
作为响应式对象的属性访问
当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性:
constcount=ref(0)conststate=reactive({count,})console.log(state.count)// 0state.count=1console.log(count.value)// 1
注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref:
constotherCount=ref(2)state.count=otherCountconsole.log(state.count)// 2console.log(count.value)// 1
注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:
constarr=reactive([ref(0)])// 这里需要 .valueconsole.log(arr[0].value)constmap=reactive(newMap([['foo',ref(0)]]))// 这里需要 .valueconsole.log(map.get('foo').value)
类型定义
interfaceRef
有时我们可能需要为 ref 做一个较为复杂的类型标注。我们可以通过在调用 ref 时传递泛型参数来覆盖默认推导:
constfoo=ref
#computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
constcount=ref(1)constplusOne=computed(()=>count.value+1)console.log(plusOne.value)// 2plusOne.value++// 错误!
或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
constcount=ref(1)constplusOne=computed({get:()=>count.value+1,set:(val)=>{count.value=val-1},})plusOne.value=1console.log(count.value)// 0
类型定义
// 只读的functioncomputed
#readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的。
constoriginal=reactive({count:0})constcopy=readonly(original)watchEffect(()=>{// 依赖追踪console.log(copy.count)})// original 上的修改会触发 copy 上的侦听original.count++// 无法修改 copy 并会被警告copy.count++// warning!
#watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。
constcount=ref(0)watchEffect(()=>console.log(count.value))// -> 打印出 0setTimeout(()=>{count.value++// -> 打印出 1},100)
#停止侦听
当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时, 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
conststop=watchEffect(()=>{/* ... */})// 之后stop()
#清除副作用
有时副作用函数会执行一些异步的副作用, 这些响应需要在其失效时清除(即完成之前状态已改变了)。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参, 用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
副作用即将重新执行时
侦听器被停止 (如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)
watchEffect((onInvalidate)=>{consttoken=performAsyncOperation(id.value)onInvalidate(()=>{// id 改变时 或 停止侦听时// 取消之前的异步操作token.cancel()})})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect 中的方式),是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
constdata=ref(null)watchEffect(async()=>{data.value=awaitfetchData(props.id)})
我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。
#副作用刷新时机
Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:
在这个例子中:
count 会在初始运行时同步打印出来
更改 count 时,将在组件更新后执行副作用。
请注意,初始化运行是在组件 mounted 之前执行的。因此,如果你希望在编写副作用函数时访问 DOM(或模板 ref),请在 onMounted 钩子中进行:
onMounted(()=>{watchEffect(()=>{// 在这里可以访问到 DOM 或者 template refs})})
如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post'):
// 同步运行watchEffect(()=>{/* ... */},{flush:'sync',})// 组件更新前执行watchEffect(()=>{/* ... */},{flush:'pre',})
#侦听器调试
onTrack 和 onTrigger 选项可用于调试一个侦听器的行为。
当一个 reactive 对象属性或一个 ref 作为依赖被追踪时,将调用 onTrack
依赖项变更导致副作用被触发时,将调用 onTrigger
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:
watchEffect(()=>{/* 副作用的内容 */},{onTrigger(e){debugger},})
onTrack 和 onTrigger 仅在开发模式下生效。
类型定义
functionwatchEffect(effect:(onInvalidate:InvalidateCbRegistrator)=>void,options?:WatchEffectOptions):StopHandleinterfaceWatchEffectOptions{flush?:'pre'|'post'|'sync'onTrack?:(event:DebuggerEvent)=>voidonTrigger?:(event:DebuggerEvent)=>void}interfaceDebuggerEvent{effect:ReactiveEffect target:anytype:OperationTypes key:string|symbol|undefined}typeInvalidateCbRegistrator=(invalidate:()=>void)=>voidtypeStopHandle=()=>void
#watch
watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。
对比 watchEffect,watch 允许我们:
懒执行副作用;
更明确哪些状态的改变会触发侦听器重新运行副作用;
访问侦听状态变化前后的值。
侦听单个数据源
侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:
// 侦听一个 getterconststate=reactive({count:0})watch(()=>state.count,(count,prevCount)=>{/* ... */})// 直接侦听一个 refconstcount=ref(0)watch(count,(count,prevCount)=>{/* ... */})
侦听多个数据源
watcher 也可以使用数组来同时侦听多个源:
watch([fooRef,barRef],([foo,bar],[prevFoo,prevBar])=>{/* ... */})
与 watchEffect 共享的行为
watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致.
类型定义
// 侦听单数据源functionwatch
#生命周期钩子函数
可以直接导入 onXXX 一族的函数来注册生命周期钩子:
import{onMounted,onUpdated,onUnmounted}from'vue'constMyComponent={setup(){onMounted(()=>{console.log('mounted!')})onUpdated(()=>{console.log('updated!')})onUnmounted(()=>{console.log('unmounted!')})},}
这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。
组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。
与 2.x 版本生命周期相对应的组合式 API
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured
新增的钩子函数
除了和 2.x 生命周期等效项之外,组合式 API 还提供了以下调试钩子函数:
onRenderTracked
onRenderTriggered
两个钩子函数都接收一个 DebuggerEvent,与 watchEffect 参数选项中的 onTrack 和 onTrigger 类似:
exportdefault{onRenderTriggered(e){debugger// 检查哪个依赖性导致组件重新渲染},}
#依赖注入
provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。
import{provide,inject}from'vue'constThemeSymbol=Symbol()constAncestor={setup(){provide(ThemeSymbol,'dark')},}constDescendent={setup(){consttheme=inject(ThemeSymbol,'light'/* optional default value */)return{theme,}},}
inject 接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide 上下文中未找到该属性,则 inject 返回 undefined。
注入的响应性
可以使用 ref 来保证 provided 和 injected 之间值的响应:
// 提供者:constthemeRef=ref('dark')provide(ThemeSymbol,themeRef)// 使用者:consttheme=inject(ThemeSymbol,ref('light'))watchEffect(()=>{console.log(`theme set to: ${theme.value}`)})
如果注入一个响应式对象,则它的状态变化也可以被侦听。
类型定义
interfaceInjectionKey
Vue 提供了一个继承 Symbol 的 InjectionKey 接口。它可用于在提供者和消费者之间同步注入值的类型:
import{InjectionKey,provide,inject}from'vue'constkey:InjectionKey
如果使用字符串作为键或没有定义类型的符号,则需要显式声明注入值的类型:
constfoo=inject
#模板 Refs
当使用组合式 API 时,reactive refs 和 template refs 的概念已经是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样在 setup() 中声明一个 ref 并返回它: