关于前端:vue3实战完全掌握refreactive

3次阅读

共计 7532 个字符,预计需要花费 19 分钟才能阅读完成。

晓得大家应用 Vue3 的时候有没有这样的纳闷,“ref、rective 都能创立一个响应式对象,我该如何抉择?”,“为什么响应式对象解构之后就失去了响应式?应该如何解决?”明天咱们就来全面盘点一下 ref、reactive,置信看完你肯定会有不一样的播种,一起学起来吧!

reactive()

根本用法

在 Vue3 中咱们能够应用 reactive() 创立一个响应式对象或数组:

import {reactive} from 'vue'

const state = reactive({count: 0})

这个响应式对象其实就是一个 Proxy,Vue 会在这个 Proxy 的属性被拜访时收集副作用,属性被批改时触发副作用。

要在组件模板中应用响应式状态,须要在 setup() 函数中定义并返回。

<script>
import {reactive} from 'vue'

export default {setup() {const state = reactive({ count: 0})    return {state}  }}
</script>

<template>
  <div>{{state.count}}</div>
</template>

当然,也能够应用 <script setup><script setup> 中顶层的导入和变量申明能够在模板中间接应用。

<script setup>
import {reactive} from 'vue'

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

<template>
  <div>{{state.count}}</div>
</template>

响应式代理 vs 原始对象

reactive() 返回的是一个原始对象的 Proxy,他们是不相等的:

const raw = {}
const proxy = reactive(raw)

console.log(proxy === raw) // false

原始对象在模板中也是能够应用的,但批改原始对象不会触发更新。因而,要应用 Vue 的响应式零碎,就必须应用代理。

<script setup>
const state = {count: 0}
function add() {state.count++}
</script>

<template>
  <button @click="add">
    {{state.count}} <!-- 当点击 button 时,始终显示为 0 -->
  </button>
</template>

为保障拜访代理的一致性,对同一个原始对象调用 reactive() 会总是返回同样的代理对象,而对一个已存在的代理对象调用 reactive() 会返回其自身:

const raw = {}
const proxy1 = reactive(raw)
const proxy2 = reactive(raw)

console.log(proxy1 === proxy2) // true

console.log(reactive(proxy1) === proxy1) // true

这个规定对嵌套对象也实用。依附深层响应性,响应式对象内的嵌套对象仍然是代理:

const raw = {}
const proxy = reactive({nested: raw})
const nested = reactive(raw)

console.log(proxy.nested === nested) // true

shallowReactive()

在 Vue 中,状态默认都是深层响应式的。但某些场景下,咱们可能想创立一个 浅层响应式对象 ,让它仅在顶层具备响应性,这时候能够应用 shallowReactive()

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

// 状态本身的属性是响应式的
state.foo++

// 上层嵌套对象不是响应式的,不会按冀望工作
state.nested.bar++

留神:浅层响应式对象应该只用于组件中的根级状态。防止将其嵌套在深层次的响应式对象中,因为其外部的属性具备不统一的响应行为,嵌套之后将很难了解和调试。

reactive() 的局限性

reactive() 尽管弱小,但也有以下几条限度:

  1. 仅对对象类型无效(对象、数组和 MapSet 这样的汇合类型),而对 stringnumberboolean 这样的原始类型有效。
  2. 因为 Vue 的响应式零碎是通过属性拜访进行追踪的,如果咱们间接“替换”一个响应式对象,这会导致对初始援用的响应性连贯失落:

    <script setup>
    import {reactive} from 'vue'
    
    let state = reactive({count: 0})
    function change() {  // 非响应式替换
     state = reactive({count: 1})}
    </script>
    
    <template>
     <button @click="change">
       {{state}} <!-- 当点击 button 时,始终显示为 {"count": 0} -->
     </button>
    </template>
  3. 将响应式对象的属性赋值或解构至本地变量,或是将该属性传入一个函数时,会失去响应性:
const state = reactive({count: 0})

// n 是一个局部变量,和 state.count 失去响应性连贯
let n = state.count
// 不会影响 state
n++

// count 也和 state.count 失去了响应性连贯
let {count} = state
// 不会影响 state
count++

// 参数 count 同样和 state.count 失去了响应性连贯
function callSomeFunction(count) {
 // 不会影响 state
 count++
}
callSomeFunction(state.count)

为了解决以上几个限度,ref 闪耀退场了!

ref()

Vue 提供了一个 ref() 办法来容许咱们创立应用任何值类型的响应式 ref。

根本用法

ref() 将传入的参数包装为一个带有 value 属性的 ref 对象:

import {ref} from 'vue'

const count = ref(0)

console.log(count) // {value: 0}

count.value++
console.log(count.value) // 1

和响应式对象的属性相似,ref 的 value 属性也是响应式的。同时,当值为对象类型时,Vue 会主动应用 reactive() 解决这个值。

一个蕴含对象的 ref 能够响应式地替换整个对象:

<script setup>
import {ref} from 'vue'

let state = ref({count: 0})
function change() {
  // 这是响应式替换
  state.value = ref({count: 1})
}
</script>

<template>
  <button @click="change">
    {{state}} <!-- 当点击 button 时,显示为 {"count": 1} -->
  </button>
</template>

ref 从个别对象上解构属性或将属性传递给函数时,不会失落响应性:

参考 前端进阶面试题具体解答

const state = {count: ref(0)
}
// 解构之后,和 state.count 仍然放弃响应性连贯
const {count} = state
// 会影响 state
count.value++

// 该函数接管一个 ref, 和传入的值放弃响应性连贯
function callSomeFunction(count) {
  // 会影响 state
  count.value++
}
callSomeFunction(state.count)

ref() 让咱们能创立应用任何值类型的 ref 对象,并可能在不失落响应性的前提下传递这些对象。这个性能十分重要,常常用于将逻辑提取到 组合式函数 中。

// mouse.js
export function useMouse() {const x = ref(0)
  const y = ref(0)

  // ...
  return {x, y}
}
<script setup>
import {useMouse} from './mouse.js'
// 能够解构而不会失去响应性
const {x, y} = useMouse()
</script>

ref 的解包

所谓解包就是获取到 ref 对象上 value 属性的值。罕用的两种办法就是 .valueunref()unref() 是 Vue 提供的办法,如果参数是 ref,则返回 value 属性的值,否则返回参数自身。

ref 在模板中的解包

当 ref 在模板中作为顶层属性被拜访时,它们会被主动解包,不须要应用 .value。上面是之前的例子,应用 ref() 代替:

<script setup>
import {ref} from 'vue'

const count = ref(0)
</script>

<template>
  <div>
    {{count}} <!-- 无需 .value -->
  </div>
</template>

还有一种状况,如果文本插值({{}})计算的最终值是 ref,也会被主动解包。上面的非顶层属性会被正确渲染进去。

<script setup>
import {ref} from 'vue'

const object = {foo: ref(1) }

</script>

<template>
  <div>
    {{object.foo}} <!-- 无需 .value -->
  </div>
</template>

其余状况则不会被主动解包,如:object.foo 不是顶层属性,文本插值({{}})计算的最终值也不是 ref:

const object = {foo: ref(1) }

上面的内容将不会像预期的那样工作:

<div>{{object.foo + 1}}</div>

渲染的后果会是 [object Object]1,因为 object.foo 是一个 ref 对象。咱们能够通过将 foo 改成顶层属性来解决这个问题:

const object = {foo: ref(1) }
const {foo} = object
<div>{{foo + 1}}</div>

当初后果就能够正确地渲染进去了。

ref 在响应式对象中的解包

当一个 ref 被嵌套在一个响应式对象中,作为属性被拜访或更改时,它会主动解包,因而会体现得和个别的属性一样:

const count = ref(0)
const state = reactive({count})

console.log(state.count) // 0

state.count = 1
console.log(state.count) // 1

只有当嵌套在一个深层响应式对象内时,才会产生解包。当 ref 作为 浅层响应式对象 的属性被拜访时则不会解包:

const count = ref(0)
const state = shallowReactive({count})

console.log(state.count) // {value: 0} 而不是 0

如果将一个新的 ref 赋值给一个曾经关联 ref 的属性,那么它会替换掉旧的 ref:

const count = ref(1)
const state = reactive({count})

const otherCount = ref(2)
state.count = otherCount

console.log(state.count) // 2
// 此时 count 曾经和 state.count 失去连贯
console.log(count.value) // 1

ref 在数组和汇合类型的解包

跟响应式对象不同,当 ref 作为响应式数组或像 Map 这种原生汇合类型的元素被拜访时,不会进行解包。

const books = reactive([ref('Vue 3 Guide')])
// 这里须要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里须要 .value
console.log(map.get('count').value)

toRef()

toRef 是基于响应式对象上的一个属性,创立一个对应的 ref 的办法。这样创立的 ref 与其源属性放弃同步:扭转源属性的值将更新 ref 的值,反之亦然。

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

const fooRef = toRef(state, 'foo')

// 更改源属性会更新该 ref
state.foo++
console.log(fooRef.value) // 2

// 更改该 ref 也会更新源属性
fooRef.value++
console.log(state.foo) // 3

toRef() 在你想把一个 prop 的 ref 传递给一个组合式函数时会很有用:

<script setup>
import {toRef} from 'vue'

const props = defineProps(/* ... */)

// 将 `props.foo` 转换为 ref,而后传入一个组合式函数
useSomeFeature(toRef(props, 'foo'))
</script>

toRef 与组件 props 联合应用时,对于禁止对 props 做出更改的限度仍然无效。如果将新的值传递给 ref 等效于尝试间接更改 props,这是不容许的。在这种场景下,你能够思考应用带有 getsetcomputed 代替。

留神:即便源属性以后不存在,toRef() 也会返回一个可用的 ref。这让它在解决可选 props 的时候十分有用,相比之下 toRefs 就不会为可选 props 创立对应的 refs。上面咱们就来理解一下 toRefs

toRefs()

toRefs() 是将一个响应式对象上的所有属性都转为 ref,而后再将这些 ref 组合为一个一般对象的办法。这个一般对象的每个属性和源对象的属性放弃同步。

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

// 相当于
// const stateAsRefs = {//   foo: toRef(state, 'foo'),
//   bar: toRef(state, 'bar')
// }
const stateAsRefs = toRefs(state)

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

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

从组合式函数中返回响应式对象时,toRefs 相当有用。它能够使咱们解构返回的对象时,不失去响应性:

// feature.js
export function useFeature() {
  const state = reactive({
    foo: 1,
    bar: 2
  })

  // ...
  // 返回时将属性都转为 ref
  return toRefs(state)
}
<script setup>
import {useFeature} from './feature.js'
// 能够解构而不会失去响应性
const {foo, bar} = useFeature()
</script>

toRefs 只会为源对象上已存在的属性创立 ref。如果要为还不存在的属性创立 ref,就要用到下面提到的 toRef

以上就是 ref、reactive 的具体用法,不晓得你有没有新的播种。接下来,咱们来探讨一下响应式原理。

响应式原理

Vue2 的限度

大家都晓得 Vue2 中的响应式是采⽤ Object.defineProperty() , 通过 getter / setter 进行属性的拦挡。这种形式对旧版本浏览器的反对更加敌对,但它有泛滥毛病:

  • 初始化时只会对已存在的对象属性进行响应式解决。也是说新增或删除属性,Vue 是监听不到的。必须应用非凡的 API 解决。
  • 数组是通过笼罩原型对象上的 7 个⽅法进行实现。如果通过下标去批改数据,Vue 同样是无奈感知的。也要应用非凡的 API 解决。
  • 无奈解决像 MapSet 这样的汇合类型。
  • 带有响应式状态的逻辑不不便复用。

Vue3 的响应式零碎

针对上述情况,Vue3 的响应式零碎横空出世了!Vue3 应用了 Proxy 来创立响应式对象,仅将 getter / setter 用于 ref,完满的解决了上述几条限度。上面的代码能够阐明它们是如何工作的:

function reactive(obj) {
  return new Proxy(obj, {get(target, key) {track(target, key)
      return target[key]
    },
    set(target, key, value) {target[key] = value
      trigger(target, key)
    }
  })
}

function ref(value) {
  const refObject = {get value() {track(refObject, 'value')
      return value
    },
    set value(newValue) {
      value = newValue
      trigger(refObject, 'value')
    }
  }
  return refObject
}

不难看出,当将一个响应性对象的属性解构为一个局部变量时,响应性就会“断开连接”。因为对局部变量的拜访不会触发 get / set 代理捕捉。

咱们回到响应式原理。在 track() 外部,咱们会查看以后是否有正在运行的副作用。如果有,就会查找到存储了所有追踪了该属性的订阅者的 Set,而后将以后这个副作用作为新订阅者增加到该 Set 中。

// activeEffect 会在一个副作用就要运行之前被设置
let activeEffect

function track(target, key) {if (activeEffect) {const effects = getSubscribersForProperty(target, key)
    effects.add(activeEffect)
  }
}

副作用订阅将被存储在一个全局的 WeakMap<target, Map<key, Set<effect>>> 数据结构中。如果在第一次追踪时没有找到对相应属性订阅的副作用汇合,它将会在这里新建。这就是 getSubscribersForProperty() 函数所做的事。

trigger() 之中,咱们会再次查找到该属性的所有订阅副作用。这一次咱们全副执行它们:

function trigger(target, key) {const effects = getSubscribersForProperty(target, key)
  effects.forEach((effect) => effect())
}

这些副作用就是用来执行 diff 算法,从而更新页面的。

这就是响应式零碎的大抵原理,Vue3 还做了编译器的优化,diff 算法的优化等等。不得不拜服尤大大,把 Vue 的响应式零碎又晋升了一个台阶!

正文完
 0