美文网首页Vue
Vue3 组合式 API 的基础 —— setup

Vue3 组合式 API 的基础 —— setup

作者: Lia代码猪崽 | 来源:发表于2021-01-12 10:39 被阅读0次

    参考文档

    组合式 API 基础 - Vue3中文文档
    Setup - Vue3中文文档

    一、setup 是什么?

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

    观看 Vue Mastery 上的免费 setup 视频。

    PS: 个人觉得这个视频讲的挺好的。

    二、在哪里使用 setup ?

    setup 是一个组件选项,所以像别的组件选项一样,写在组件导出的对象里。

    <script>
      export default {
        name: "App",
        setup() {
          // ...
    
          return {
            // ...
          }
        },
      }
    </script>
    

    三、使用 setup 的正确姿势

    官方文档如此描述:
    setup 选项应该是一个接受 propscontext 的函数。
    此外,我们从 setup 返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。

    个人觉得可以理解为:

    1. setup 选项应该为一个函数
    2. setup 选项函数接受两个参数: propscontext
    3. setup 选项函数需要返回要暴露给组件的内容

    setup 函数的参数

    1. setup 函数中的第一个参数 —— props

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

    // MyBook.vue
    
    export default {
      props: {
        title: String
      },
      setup(props) {
        console.log(props.title)
      }
    }
    
    

    WARNING:

    但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
    如果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来安全地完成此操作。

    // MyBook.vue
    
    import { toRefs } from 'vue'
    
    setup(props) {
        const { title } = toRefs(props)
    
        console.log(title.value)
    }
    

    2. setup 函数中的第二个参数 —— context

    context 上下文是一个普通的 JavaScript 对象,它暴露三个组件的 property:

    // MyBook.vue
    export default {
      setup(props, context) {
        // Attribute (非响应式对象)
        console.log(context.attrs)
    
        // 插槽 (非响应式对象)
        console.log(context.slots)
    
        // 触发事件 (方法)
        console.log(context.emit)
      }
    }
    

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

    // MyBook.vue
    export default {
      setup(props, { attrs, slots, emit }) {
        ...
      }
    }
    

    attrsslots 是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.xslots.x 的方式引用 property。请注意,与 props 不同,attrsslots 是非响应式的。如果你打算根据 attrsslots 更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。

    setup 函数的返回值

    1. setup 函数的返回值 —— 对象

    如果 setup 返回一个对象,则可以在组件的模板中像传递给 setupprops property 一样访问该对象的 property:

    <!-- MyBook.vue -->
    <template>
      <!-- 模板中使用会被自动解开,所以不需要 .value  -->
      <div>{{ readersNumber }} {{ book.title }}</div>
    </template>
    
    <script>
      import { ref, reactive } from 'vue'
    
      export default {
        setup() {
          const readersNumber = ref(0)
          const book = reactive({ title: 'Vue 3 Guide' })
    
          // expose to template
          return {
            readersNumber,
            book
          }
        }
      }
    </script>
    
    

    注意,从 setup 返回的 refs 在模板中访问时是被自动解开的,因此不应在模板中使用 .value

    2. setup 函数的返回值 —— 渲染函数

    setup 还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:

    // MyBook.vue
    
    import { h, ref, reactive } from 'vue'
    
    export default {
      setup() {
        const readersNumber = ref(0)
        const book = reactive({ title: 'Vue 3 Guide' })
        // Please note that we need to explicitly expose ref value here
        return () => h('div', [readersNumber.value, book.title])
      }
    }
    

    新的 setup 组件选项在创建组件之前执行,一旦 props 被解析,并充当合成 API 的入口点。

    三、setup 函数内部一般不使用 this

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

    四、响应式系统 API

    1. reactive

    reactive() 接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

    const obj = reactive({ count: 0 })
    

    响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。

    <template>
      <div id="app">{ state.count }</div>
    </template>
    
    <script>
    import { reactive } from 'vue'
    export default {
      setup() {
        // state 现在是一个响应式的状态
        const state = reactive({
          count: 0,
        })
      }
    }
    </script>
    

    2. ref

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

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

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

    • 模板中访问

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

      <template>
        <div>{{ count }}</div>
      </template>
      
      <script>
        export default {
          setup() {
            return {
              count: ref(0),
            }
          },
        }
      </script>
      
    • 作为响应式对象的属性访问

      当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性:

      const count = ref(0)
      const state = reactive({
        count,
      })
      
      console.log(state.count) // 0
      
      state.count = 1
      console.log(count.value) // 1
      

      注意如果将一个新的 ref 分配给现有的 ref, 将替换旧的 ref:

      const otherCount = ref(2)
      
      state.count = otherCount
      console.log(state.count) // 2
      console.log(count.value) // 1
      

      注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 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)
      
    • 类型定义

      interface Ref<T> {
        value: T
      }
      
      function ref<T>(value: T): Ref<T>
      

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

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

    3. computed

    使用响应式 computed API 有两种方式:

    1. 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
    const count = ref(1)
    const plusOne = computed(() => count.value + 1)
    
    console.log(plusOne.value) // 2
    
    plusOne.value++ // 错误!
    
    1. 传入一个拥有 getset 函数的对象,创建一个可手动修改的计算状态。
    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
    
    
    • 类型定义

      // 只读的
      function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>
      
      // 可更改的
      function computed<T>(options: {
        get: () => T
        set: (value: T) => void
      }): Ref<T>
      
      

    4. readonly

    传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理。一个只读的代理是“深层的”,对象内部任何嵌套的属性也都是只读的。

    const original = reactive({ count: 0 })
    
    const copy = readonly(original)
    
    watchEffect(() => {
      // 依赖追踪
      console.log(copy.count)
    })
    
    // original 上的修改会触发 copy 上的侦听
    original.count++
    
    // 无法修改 copy 并会被警告
    copy.count++ // warning!
    

    5. watchEffect

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

    const count = ref(0)
    
    watchEffect(() => console.log(count.value))
    // -> 打印出 0
    
    setTimeout(() => {
      count.value++
      // -> 打印出 1
    }, 100)
    
    
    1. 停止侦听

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

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

    const stop = watchEffect(() => {
      /* ... */
    })
    
    // 之后
    stop()
    
    1. 清除副作用

    有时副作用函数会执行一些异步的副作用, 这些响应需要在其失效时清除(即完成之前状态已改变了)。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参, 用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

    • 副作用即将重新执行时

    • 侦听器被停止 (如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时)

    watchEffect((onInvalidate) => {
      const token = performAsyncOperation(id.value)
      onInvalidate(() => {
        // id 改变时 或 停止侦听时
        // 取消之前的异步操作
        token.cancel()
      })
    })
    

    我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect 中的方式),是因为返回值对于异步错误处理很重要。

    在执行数据请求时,副作用函数往往是一个异步函数:

    const data = ref(null)
    watchEffect(async () => {
      data.value = await fetchData(props.id)
    })
    
    

    我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。

    1. 副作用刷新时机

    Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:

    <template>
      <div>{{ count }}</div>
    </template>
    
    <script>
      export default {
        setup() {
          const count = 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',
      }
    )
    
    
    1. 侦听器调试

    onTrackonTrigger 选项可用于调试一个侦听器的行为。

    • 当一个 reactive 对象属性或一个 ref 作为依赖被追踪时,将调用 onTrack

    • 依赖项变更导致副作用被触发时,将调用 onTrigger

    这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:

    watchEffect(
      () => {
        /* 副作用的内容 */
      },
      {
        onTrigger(e) {
          debugger
        },
      }
    )
    
    

    onTrackonTrigger 仅在开发模式下生效。

    • 类型定义

      function watchEffect(
        effect: (onInvalidate: InvalidateCbRegistrator) => void,
        options?: WatchEffectOptions
      ): StopHandle
      
      interface WatchEffectOptions {
        flush?: 'pre' | 'post' | 'sync'
        onTrack?: (event: DebuggerEvent) => void
        onTrigger?: (event: DebuggerEvent) => void
      }
      
      interface DebuggerEvent {
        effect: ReactiveEffect
        target: any
        type: OperationTypes
        key: string | symbol | undefined
      }
      
      type InvalidateCbRegistrator = (invalidate: () => void) => void
      
      type StopHandle = () => void
      
      

    6. watch

    watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。

    • 对比 watchEffectwatch 允许我们:

      • 懒执行副作用;
      • 更明确哪些状态的改变会触发侦听器重新运行副作用;
      • 访问侦听状态变化前后的值。
    • 侦听单个数据源

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

      // 侦听一个 getter
      const state = reactive({ count: 0 })
      watch(
        () => state.count,
        (count, prevCount) => {
          /* ... */
        }
      )
      
      // 直接侦听一个 ref
      const count = ref(0)
      watch(count, (count, prevCount) => {
        /* ... */
      })
      
      
    • 侦听多个数据源

      watcher 也可以使用数组来同时侦听多个源:

      watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
        /* ... */
      })
      
      
    • watchEffect 共享的行为

      watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机侦听器调试 等方面行为一致.

    • 类型定义

      // 侦听单数据源
      function watch<T>(
        source: WatcherSource<T>,
        callback: (
          value: T,
          oldValue: T,
          onInvalidate: InvalidateCbRegistrator
        ) => void,
        options?: WatchOptions
      ): StopHandle
      
      // 侦听多数据源
      function watch<T extends WatcherSource<unknown>[]>(
        sources: T
        callback: (
          values: MapSources<T>,
          oldValues: MapSources<T>,
          onInvalidate: InvalidateCbRegistrator
        ) => void,
        options? : WatchOptions
      ): StopHandle
      
      type WatcherSource<T> = Ref<T> | (() => T)
      
      type MapSources<T> = {
        [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never
      }
      
      // 共有的属性 请查看 `watchEffect` 的类型定义
      interface WatchOptions extends WatchEffectOptions {
        immediate?: boolean // default: false
        deep?: boolean
      }
      
      

    #生命周期钩子函数

    可以直接导入 onXXX 一族的函数来注册生命周期钩子:

    未完待续。。。

    相关文章

      网友评论

        本文标题:Vue3 组合式 API 的基础 —— setup

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