vue3.0--组合式API

作者: 裘马轻狂大帅 | 来源:发表于2020-08-20 10:08 被阅读0次

    setup

    setup 函数是一个新的组件选项。作为在组件内使用 Composition API 的入口点。

    调用时机

    创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用

    模板中使用

    如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文:

    <template><div>{{ count }} {{ object.foo }}</div></template><script>import{ref,reactive}from'vue'exportdefault{setup(){constcount=ref(0)constobject=reactive({foo:'bar'})// 暴露给模板return{count,object,}},}</script>

    注意 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<Textendsobject>(raw:T):T

    #ref

    接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。

    constcount=ref(0)console.log(count.value)// 0count.value++console.log(count.value)// 1

    如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换。

    模板中访问

    当 ref 作为渲染上下文的属性返回(即在setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value:

    <template><div>{{ count }}</div></template><script>exportdefault{setup(){return{count:ref(0),}},}</script>

    作为响应式对象的属性访问

    当 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<T>{value:T}functionref<T>(value:T):Ref<T>

    有时我们可能需要为 ref 做一个较为复杂的类型标注。我们可以通过在调用 ref 时传递泛型参数来覆盖默认推导:

    constfoo=ref<string|number>('foo')// foo 的类型: Ref<string | number>foo.value=123// 能够通过!

    #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<T>(getter:()=>T):Readonly<Ref<Readonly<T>>>// 可更改的functioncomputed<T>(options:{get:()=>Tset:(value:T)=>void}):Ref<T>

    #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 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:

    <template><div>{{ count }}</div></template><script>exportdefault{setup(){constcount=ref(0)watchEffect(()=>{console.log(count.value)})return{count,}},}</script>

    在这个例子中:

    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<T>(source:WatcherSource<T>,callback:(value:T,oldValue:T,onInvalidate:InvalidateCbRegistrator)=>void,options?:WatchOptions):StopHandle// 侦听多数据源functionwatch<TextendsWatcherSource<unknown>[]>(sources:Tcallback:(values:MapSources<T>,oldValues:MapSources<T>,onInvalidate:InvalidateCbRegistrator)=>void,options?:WatchOptions):StopHandletypeWatcherSource<T>=Ref<T>|(()=>T)typeMapSources<T>={[KinkeyofT]:T[K]extendsWatcherSource<inferV>?V:never}// 共有的属性 请查看 `watchEffect` 的类型定义interfaceWatchOptionsextendsWatchEffectOptions{immediate?:boolean// default: falsedeep?:boolean}

    #生命周期钩子函数

    可以直接导入 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<T>extendsSymbol{}functionprovide<T>(key:InjectionKey<T>|string,value:T):void// 未传,使用缺省值functioninject<T>(key:InjectionKey<T>|string):T|undefined// 传入了默认值functioninject<T>(key:InjectionKey<T>|string,defaultValue:T):T

    Vue 提供了一个继承 Symbol 的 InjectionKey 接口。它可用于在提供者和消费者之间同步注入值的类型:

    import{InjectionKey,provide,inject}from'vue'constkey:InjectionKey<string>=Symbol()provide(key,'foo')// 类型不是 string 则会报错constfoo=inject(key)// foo 的类型: string | undefined

    如果使用字符串作为键或没有定义类型的符号,则需要显式声明注入值的类型:

    constfoo=inject<string>('foo')// string | undefined

    #模板 Refs

    当使用组合式 API 时,reactive refs 和 template refs 的概念已经是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样在 setup() 中声明一个 ref 并返回它:

    <template><divref="root"></div></template><script>import{ref,onMounted}from'vue'exportdefault{setup(){constroot=ref(null)onMounted(()=>{// 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象console.log(root.value)// <div/>})return{root,}},}</script>

    这里我们将 root 暴露在渲染上下文中,并通过 ref="root" 绑定到 div 作为其 ref。 在 Virtual DOM patch 算法中,如果一个 VNode 的 ref 对应一个渲染上下文中的 ref,则该 VNode 对应的元素或组件实例将被分配给该 ref。 这是在 Virtual DOM 的 mount / patch 过程中执行的,因此模板 ref 仅在渲染初始化后才能访问。

    ref 被用在模板中时和其他 ref 一样:都是响应式的,并可以传递进组合函数(或从其中返回)。

    配合 render 函数 / JSX 的用法

    exportdefault{setup(){constroot=ref(null)return()=>h('div',{ref:root,})// 使用 JSXreturn()=><div ref={root}/>},}

    在 v-for 中使用

    模板 ref 在 v-for 中使用 vue 没有做特殊处理,需要使用函数型的 ref(3.0 提供的新功能)来自定义处理方式:

    <template><divv-for="(item, i) in list":ref="el => { divs[i] = el }">{{ item }}</div></template><script>import{ref,reactive,onBeforeUpdate}from'vue'exportdefault{setup(){constlist=reactive([1,2,3])constdivs=ref([])// 确保在每次变更之前重置引用onBeforeUpdate(()=>{divs.value=[]})return{list,divs,}},}</script>

    #响应式系统工具集

    #unref

    如果参数是一个 ref 则返回它的 value,否则返回参数本身。它是 val = isRef(val) ? val.value : val 的语法糖。

    functionuseFoo(x:number|Ref<number>){constunwrapped=unref(x)// unwrapped 一定是 number 类型}

    #toRef

    toRef 可以用来为一个 reactive 对象的属性创建一个 ref。这个 ref 可以被传递并且能够保持响应性。

    conststate=reactive({foo:1,bar:2,})constfooRef=toRef(state,'foo')fooRef.value++console.log(state.foo)// 2state.foo++console.log(fooRef.value)// 3

    当您要将一个 prop 中的属性作为 ref 传给组合逻辑函数时,toRef 就派上了用场:

    exportdefault{setup(props){useSomeFeature(toRef(props,'foo'))},}

    #toRefs

    把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。

    conststate=reactive({foo:1,bar:2,})conststateAsRefs=toRefs(state)/*

    stateAsRefs 的类型如下:

    {

      foo: Ref<number>,

      bar: Ref<number>

    }

    */// ref 对象 与 原属性的引用是 "链接" 上的state.foo++console.log(stateAsRefs.foo.value)// 2stateAsRefs.foo.value++console.log(state.foo)// 3

    当想要从一个组合逻辑函数中返回响应式对象时,用 toRefs 是很有效的,该 API 让消费组件可以 解构 / 扩展(使用 ... 操作符)返回的对象,并不会丢失响应性:

    functionuseFeatureX(){conststate=reactive({foo:1,bar:2,})// 对 state 的逻辑操作// 返回时将属性都转为 refreturntoRefs(state)}exportdefault{setup(){// 可以解构,不会丢失响应性const{foo,bar}=useFeatureX()return{foo,bar,}},}

    #isRef

    检查一个值是否为一个 ref 对象。

    #isProxy

    检查一个对象是否是由 reactive 或者 readonly 方法创建的代理。

    #isReactive

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

    如果这个代理是由 readonly 创建的,但是又被 reactive 创建的另一个代理包裹了一层,那么同样也会返回 true。

    #isReadonly

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

    #高级响应式系统 API

    #customRef

    customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个带有 get 和 set 属性的对象。

    使用自定义 ref 实现带防抖功能的 v-model :

    <inputv-model="text"/>

    functionuseDebouncedRef(value,delay=200){lettimeoutreturncustomRef((track,trigger)=>{return{get(){track()returnvalue},set(newValue){clearTimeout(timeout)timeout=setTimeout(()=>{value=newValuetrigger()},delay)},}})}exportdefault{setup(){return{text:useDebouncedRef('hello'),}},}

    类型定义

    functioncustomRef<T>(factory:CustomRefFactory<T>):Ref<T>typeCustomRefFactory<T>=(track:()=>void,trigger:()=>void)=>{get:()=>Tset:(value:T)=>void}

    #markRaw

    显式标记一个对象为“永远不会转为响应式代理”,函数返回这个对象本身。

    constfoo=markRaw({})console.log(isReactive(reactive(foo)))// false// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的constbar=reactive({foo})console.log(isReactive(bar.foo))// false

    注意

    markRaw 和下面的 shallowXXX 一族的 API 允许你可选择性的覆盖 reactive readonly 默认 "深层的" 特性,或者使用无代理的普通对象。设计这种「浅层读取」有很多原因,比如:

    一些值的实际上的用法非常简单,并没有必要转为响应式,比如某个复杂的第三方类库的实例,或者 Vue 组件对象

    当渲染一个元素数量庞大,但是数据是不可变的,跳过 Proxy 的转换可以带来性能提升。

    这些 API 被认为是高级的,是因为这种特性仅停留在根级别,所以如果你将一个嵌套的,没有 markRaw 的对象设置为 reactive 对象的属性,在重新访问时,你又会得到一个 Proxy 的版本,在使用中最终会导致标识混淆的严重问题:执行某个操作同时依赖于某个对象的原始版本和代理版本。

    constfoo=markRaw({nested:{},})constbar=reactive({// 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有nested:foo.nested,})console.log(foo.nested===bar.nested)// false

    标识混淆在一般使用当中应该是非常罕见的,但是要想完全避免这样的问题,必须要对整个响应式系统的工作原理有一个相当清晰的认知。

    #shallowReactive

    只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样。

    conststate=shallowReactive({foo:1,nested:{bar:2,},})// 变更 state 的自有属性是响应式的state.foo++// ...但不会深层代理isReactive(state.nested)// falsestate.nested.bar++// 非响应式

    #shallowReadonly

    只为某个对象的自有(第一层)属性创建浅层的只读响应式代理,同样也不会做深层次、递归地代理,深层次的属性并不是只读的。

    conststate=shallowReadonly({foo:1,nested:{bar:2,},})// 变更 state 的自有属性会失败state.foo++// ...但是嵌套的对象是可以变更的isReadonly(state.nested)// falsestate.nested.bar++// 嵌套属性依然可修改

    #shallowRef

    创建一个 ref ,将会追踪它的 .value 更改操作,但是并不会对变更后的 .value 做响应式代理转换(即变更不会调用 reactive)

    constfoo=shallowRef({})// 更改对操作会触发响应foo.value={}// 但上面新赋的这个对象并不会变为响应式对象isReactive(foo.value)// false

    #toRaw

    返回由 reactive 或 readonly 方法转换成响应式代理的普通对象。这是一个还原方法,可用于临时读取,访问不会被代理/跟踪,写入时也不会触发更改。不建议一直持有原始对象的引用。请谨慎使用。

    constfoo={}constreactiveFoo=reactive(foo)console.log(toRaw(reactiveFoo)===foo)// true

    相关文章

      网友评论

        本文标题:vue3.0--组合式API

        本文链接:https://www.haomeiwen.com/subject/ydhsjktx.html