欢迎来到我的博客

vuePress-theme-reco hbbaly    2021
欢迎来到我的博客

Choose mode

  • dark
  • auto
  • light
首页
时间轴
标签
分类
  • 前端
GitHub
author-avatar

hbbaly

31

Article

18

Tag

首页
时间轴
标签
分类
  • 前端
GitHub
  • 【vue3】Reactivity API && composition API

    • composition API
      • setup
      • Lifecycle Hooks
      • provide && inject
      • template refs
    • Reactivity API
      • reactive
      • readonly
      • isProxy
      • isReactive
      • isReadonly
      • shallowReadonly
      • shallowReactive
      • ref
      • toRef
      • toRefs
      • customRef
    • Computed and watch
      • computed
      • watchEffect
      • watch

【vue3】Reactivity API && composition API

vuePress-theme-reco hbbaly    2021

【vue3】Reactivity API && composition API


hbbaly 2020-10-12 11:23:00 Vue3

学习vue3, 主要是看着文档来一点点敲,给我的感触最大的就是借鉴hooks的思路, 给我们提供一种写代码的模式, 比如状态复用, 代码复用的思路,当初在学习react-hooks就看到过这个项目react-use , 在vue3中也是值得推荐用这种模式来写代码

官方文档

# composition API

# setup

创建组件实例,然后初始化 props ,紧接着就调用setup 函数。也就是说setup会在beforeCreate之前调用, 传递props及context

# 使用

<template>
  <div class="hello">
    <p>{{readersNumber}}---{{book}}</p>
  </div>
</template>

<script>
import { ref, reactive } from 'vue'
export default {
  setup() {
    const readersNumber = ref(0)
    const book = reactive({ title: 'Vue 3 Guide' })
    return {
      readersNumber,
      book
    }
  }
}
</script>

setup 返回的 ref 在模板中会自动解开,不需要写 .value

# props

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

props是响应式的,不要使用结构赋值,去获取props里面的值

export default {
  props: {
    msg: String,
  },
  // 这样写, msg就不是响应式数据了
  setup({ msg }) {
    watchEffect(() => {
      console.log(`message is: ` + msg) // Destructuring the `props` will cause the value to lose reactivity 
    })
  },
}

我们可以使用toRefs/toRef来赋值

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

// or

import { toRef } from 'vue'
setup(props) {
	const msg = toRef(props, 'msg')
	console.log(msg.value)
}

# context

提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property

export default {
  setup(props, context) {
    // Attributes (Non-reactive object)
    console.log(context.attrs)

    // Slots (Non-reactive object)
    console.log(context.slots)

    // Emit Events (Method)
    console.log(context.emit)
  }
}

context不是响应式数据,也就意味着可以使用结构赋值

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

attrs 和 slots 都是内部组件实例上对应项的代理,可以确保在更新后仍然是最新值。所以可以解构,无需担心后面访问到过期的值:

setup(props, { attrs }) {
  // 一个可能之后回调用的签名
  function onClick() {
    const { foo } = attrs // 这种情况会丢失响应性
    console.log(attrs.foo) // 一定是最新的引用,没有丢失响应性
  }
},

# 返回函数

setup 也可以返回一个函数,函数中也能使用当前 setup 函数作用域中的响应式数据:

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])
  }
}

# this

this 在 setup() 中不可用。由于 setup() 在解析 2.x 选项前被调用,setup() 中的 this 将与 2.x 选项中的 this 完全不同。

# Lifecycle Hooks

Options API Hook inside
beforeCreate setup
created setup
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

# provide && inject

provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。

# provide

  • 属性名

  • 属性值






 







 
 
 
 
 




<template>
  <MyMarker />
</template>

<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    provide('location', 'North Pole')
    provide('geolocation', {
      longitude: 90,
      latitude: 135
    })
  }
}
</script>

# Inject

  • 接受属性

— 默认值

inject 接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide 上下文中未找到该属性,则 inject 返回 undefined。






 
 










<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')

    return {
      userLocation,
      userGeolocation
    }
  }
}
</script>

# 注入响应性

可以使用 ref 来保证 provided 和 injected 之间值的响应














 
 
 
 
 
 
 
 




<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    provide('location', location)
    provide('geolocation', geolocation)
  }
}
</script>

如果注入一个响应式对象,则它的状态变化也可以被侦听。

有时我们需要更新注入组件内部的数据。在这种情况下,我们建议提供一种方法来改变反应性属性




















 
 
 



 




<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', location)
    provide('geolocation', geolocation)
    provide('updateLocation', updateLocation)
  }
}
</script>







 










<script>
import { inject } from 'vue'

export default {
  setup() {
    const userLocation = inject('location', 'The Universe')
    const userGeolocation = inject('geolocation')
    const updateUserLocation = inject('updateLocation')

    return {
      userLocation,
      userGeolocation,
      updateUserLocation
    }
  }
}
</script>

最后,如果要确保通过provide传递的数据不会被注入的组件更改,我们建议对provided属性使用readonly。
























 
 





<template>
  <MyMarker />
</template>

<script>
import { provide, reactive, readonly, ref } from 'vue'
import MyMarker from './MyMarker.vue

export default {
  components: {
    MyMarker
  },
  setup() {
    const location = ref('North Pole')
    const geolocation = reactive({
      longitude: 90,
      latitude: 135
    })

    const updateLocation = () => {
      location.value = 'South Pole'
    }

    provide('location', readonly(location))
    provide('geolocation', readonly(geolocation))
    provide('updateLocation', updateLocation)
  }
}
</script>

# template refs

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

<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // the DOM element will be assigned to the ref after initial render
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

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

# v-for 中使用ref

<template>
  <div v-for="(item, i) in list" :ref="el => { if (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([])

      // make sure to reset the refs before each update
      onBeforeUpdate(() => {
        divs.value = []
      })

      return {
        list,
        divs
      }
    }
  }
</script>

# Reactivity API

使用new Proxy(target, handler)来实现数据的响应

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop, receiver) {
    track(target, prop)
    return Reflect.get(...arguments)
  },
  set(target, key, value, receiver) {
    trigger(target, key)
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos

# reactive

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

import { reactive } from 'vue'

// reactive state
const state = reactive({
  count: 0
})

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

# readonly

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

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // works for reactivity tracking
  console.log(copy.count)
})

// mutating original will trigger watchers relying on the copy
original.count++

// mutating the copy will fail and result in a warning
copy.count++ // warning!

# isProxy

检查对象是不是由reactive or readonly生成代理对象。

# isReactive

检查对象是不是通过reactive生成代理对象

import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true
  }
}

如果readonly代理的对象是由reactive生成的, 也是回返回true














 
 



import { reactive, isReactive, readonly } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    // readonly proxy created from plain object
    const plain = readonly({
      name: 'Mary'
    })
    console.log(isReactive(plain)) // -> false

    // readonly proxy created from reactive proxy
    const stateCopy = readonly(state)
    console.log(isReactive(stateCopy)) // -> true
  }
}

# isReadonly

检查对象是不是通过readonly生成代理对象

# shallowReadonly

创建一个自己的属性可读的代理对象,嵌套对象的深层属性不起作用

const state = shallowReadonly({
  foo: 1,
  nested: {
    bar: 2
  }
})

// mutating state's own properties will fail
state.foo++
// ...but works on nested objects
isReadonly(state.nested) // false
state.nested.bar++ // works

# shallowReactive

和shallowReadonly类似

创建一个自己的属性响应的代理对象,嵌套对象的深层属性不起作用

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// mutating state's own properties is reactive
state.foo++
// ...but does not convert nested objects
isReactive(state.nested) // false
state.nested.bar++ // non-reactive

# ref

接受一个值并返回一个反应式和可变的ref对象。ref对象有一个指向内部值的.value。

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

count.value++
console.log(count.value) // 1
interface Ref<T> {
  value: T
}

function ref<T>(value: T): Ref<T>

有时我们可能需要为ref的内部值指定复杂类型。我们可以通过在调用ref重写默认推理时传递泛型参数来简洁地实现这一点

const foo = ref<string | number>('foo') // foo's type: Ref<string | number>

foo.value = 123 // ok!

如果泛型的类型未知,建议将ref强制转换为ref<T>

function useState<State extends string>(initial: State) {
  const state = ref(initial) as Ref<State> // state.value -> State extends string
  return state
}

# toRef

响应式对象上的属性创建引用ref

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

如果要将prop的ref传递给合成函数,则toRef非常有用

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  }
}

# toRefs

将响应式对象转化为普通对象,转化后的对象的属性指向原来响应式对象属性

const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// The ref and the original property is "linked"
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

当从组合函数返回响应式对象时,toRefs很有用,自定义组件在不损失返回对象响应式的情况下进行结构/扩展

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // logic operating on state

  // convert to refs when returning
  return toRefs(state)
}

export default {
  setup() {
    // can destructure without losing reactivity
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar
    }
  }
}

# customRef

创建自定义的ref,对其依赖项跟踪和更新进行显式控制。它需要一个工厂函数,该函数接收track和trigger函数作为参数,并应返回一个带有get和set的对象。

<input v-model="text" />
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>

type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

# Computed and watch

# computed

获取一个getter函数,并为getter返回的值返回一个不可变的reactive ref对象

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // error

使用带有get和set函数的对象来创建可写的ref对象

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
// read-only
function computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>

// writable
function computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>

# watchEffect

当依赖项改变时,立即运行

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

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

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

const stop = watchEffect(() => {
  /* ... */
})

// 之后
stop()

# 清除副作用

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

副作用即将重新执行时

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

watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

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

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

const data = ref(null)
watchEffect(async (onInvalidate) => {
  onInvalidate(() => { /* ... */ }) // we register cleanup function before Promise resolves
  data.value = await fetchData(props.id)
})

# 副作用刷新时机

Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。

如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post'):

// 同步运行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'sync',
  }
)

// 组件更新前执行
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'pre',
  }
)

# watch

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

对比 watchEffect,watch 允许我们:

  • 懒执行副作用;

  • 更明确哪些状态的改变会触发侦听器重新运行副作用;

  • 访问侦听状态变化前后的值。

# 侦听单个数据源

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

# 侦听多个数据源

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})