• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 响应式基础

    reactive()

    我们可以使用reactive()函数创建一个响应式对象或数组

    import { reactive } from 'vue'
    
    const state = reactive({ count: 0 })
    

    响应式对象,其实是JavaScript Proxy,其行为表现与一般对象相似。不同之处:Vue 能够跟踪响应式对象属性的访问与更改操作。

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

    import { reactive } from 'vue'
    
    export default {
      // `setup` 是一个专门用于组合式 API 的特殊钩子
      setup() {
        const state = reactive({ count: 0 })
    
        // 暴露 state 到模板
        return {
          state
        }
      }
    }
    
    template
    <div>{{ state.count }}</div>
    

    我们也可以,在同一个作用域下,定义一个更新state函数,并作为一个方法state一起暴露出去:

    import { reactive } from 'vue'
    
    export default {
      setup() {
        const state = reactive({ count: 0 })
    
        function increment() {
          state.count++
        }
    
        // 不要忘了同时暴露 increment 函数
        return {
          state,
          increment
        }
      }
    }
    

    暴露的方法,通常会被用作事件监听器:

    <button @click="increment">
      {{ state.count }}
    </button>
    


    <script setup>

    setup()函数中手动暴露状态和方法可能非常繁琐。幸运的是,你可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用<script setup>来简化大量样板代码。

    <script setup>
    import { reactive } from 'vue'
    
    const state = reactive({ count: 0 })
    
    function increment() {
      state.count++
    }
    </script>
    
    <template>
      <button @click="increment">
        {{ state.count }}
      </button>
    </template>
    

    <script setup>中的顶层的导入变量声明,在同一组件的模板中,自动直接使用。


    DOM 更新时机

    当你更改响应式状态后,DOM 也会自动更新。然而,你得注意 DOM 的更新并不是同步的。相反,Vue 将缓冲它们直到更新周期的“next tick”执行,以确保无论你进行了多少次状态更改,每个组件都只需要更新一次

    若要等待一个状态改变后的 DOM 更新完成,你可以使用nextTick()这个全局 API:

    import { nextTick } from 'vue'
    
    function increment() {
      count.value++
      nextTick(() => {
        // 访问更新后的 DOM
      })
    }
    


    深层响应性

    在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。

    import { reactive } from 'vue'
    
    const obj = reactive({
      nested: { count: 0 },
      arr: ['foo', 'bar']
    })
    
    function mutateDeeply() {
      // 以下都会按照期望工作
      obj.nested.count++
      obj.arr.push('baz')
    }
    

    你也可以直接创建一个浅层响应式对象。它们仅在顶层具有响应性,一般仅在某些特殊场景中需要。


    响应式代理 vs.原始对象

    值得注意的是,reactive()返回的是一个源对象的Proxy,它和源对象是不全等的(!==):

    const raw = {}
    const proxy = reactive(raw)
    
    // 代理和原始对象不是全等的
    console.log(proxy === raw)  // false
    

    只有代理是响应式的,更改原始的对象不会触发更新。因此,使用 Vue 的响应式系统的最佳实践是仅使用你声明对象的代理版本

    为保证访问代理的一致性,对同一个对象调用reactive()会总是返回同样的代理,而对代理调用reactive()则会返回它自己:

    const raw = {}
    const proxy = reactive(raw)
    const proxy2 = reactive(raw)
    
    // 在同一个对象上调用 reactive() 会返回相同的代理
    console.log(proxy === proxy2)  // true
    
    // 在一个代理上调用 reactive() 会返回它自己
    console.log(reactive(proxy) === proxy)  // true
    

    这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理:

    const proxy = reactive({})
    
    const raw = {}
    proxy.nested = raw
    
    console.log(proxy.nested === raw)  // false
    


    reactive()的局限性

    reactive() API 有两条限制:

    • 仅对对象类型有效(对象数组MapSet这样的集合类型),而对stringnumberboolean这样的基础类型无效。
    • 因为 Vue 的响应式系统是通过对象的属性(property)访问进行追踪的,因此我们必须始终保持对该响应式对象的相同引用。这意味着我们不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失。更新响应式对象属性的值,不会丧失响应性。
    let state = reactive({ count: 0 })
    
    // 把已经创建的具备响应式的变量 state ,重新赋值,state 会失去响应性。
    state = reactive({ count: 1 })
    

    同时,这也意味着,当我们将响应式对象的属性赋值给一个新变量时,此新变量失去响应性。

    const state = reactive({ count: 0 })
    
    //把具备响应性 state.count 赋值给变量 n,那么 n 不具备响应性
    let n = state.count
    
    // 变量 n 自增加,并不影响 state.count 状态
    n++
    

    解构响应式对象,至本地变量时,解构出来的变量失去响应性。

    const state = reactive({ count: 0 })
    // count 也和 state.count 失去了响应性连接
    let { count } = state
    
    // 不会影响原状态
    count++
    


    将响应式对象的任何属性传入一个函数时,此对象将会失去响应性

    const state = reactive({ count: 0 })
    // 更换响应式对象的属性的值,其对象的响应性不会丢失。
    state.count = 10
    
    // 对响应式对象传入一个函数后,state 对象会丧失响应性,并且将无法跟踪 state.count 的变化
    callSomeFunction(state.count)
    



    ref()

    为了解决reactive()带来的限制,我们提供了另一个ref()方法来帮我们创建响应式的ref,它可以装载任何值类型

    import { ref } from 'vue'
    
    const count = ref(0)
    

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

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

    和响应式对象的属性类似,ref.value属性也是响应式的。同时,当值为对象类型时,会用reactive()自动转换它的.value


    一个包含对象类型值的ref,可以响应式地替换整个对象:

    const objectRef = ref({ count: 0 })
    
    // 这是响应式的替换
    objectRef.value = { count: 1 }
    

    ref被传递给函数或是从一般对象上被解构时,不会丢失响应性:

    const obj = {
      foo: ref(1),
      bar: ref(2)
    }
    
    // 该函数接收一个 ref 需要通过 .value 取值。但它会保持响应性。
    callSomeFunction(obj.foo)
    
    // 解构赋值,仍然是响应式的
    const { foo, bar } = obj
    

    一言以蔽之,ref()使我们能创造一种对对任意值的“引用”,并能够在不丢失响应性的前提下,传递这些引用。这个功能非常重要,因为它经常用于将逻辑提取到组合函数中。


    ref 在模板中的解包

    ref在模板中作为顶层property被访问时,它们会被自动“解包”,所以不需要使用.value。下面是之前的计数器例子,用ref()代替:

    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    
    function increment() {
      count.value++
    }
    </script>
    
    <template>
      <button @click="increment">
        {{ count }} <!-- no .value needed -->
      </button>
    </template>
    


    请注意,仅当ref是模板渲染上下文的顶层属性时才适用自动“解包”,访问深层级的ref则不会解包:

    js
    const object = { foo: ref(1) }
    
    template
    {{ object.foo + 1 }}   <!-- 无法自动解包 -->
    

    foo是顶层属性,但object.foo不是。上面的表达式,不会像预期的那样工作。

    渲染的结果会是一个[object Object],因为object.foo是一个ref对象。我们可以通过让foo成为顶层 property 来解决这个问题:

    js
    const object = { foo: ref(1) }
    const { foo } = object
    
    template
    {{ foo + 1 }}   <!-- 自动解包 渲染结果将是 2-->
    


    需要注意的是,如果一个ref是文本插值(即一个{{}}符号)计算的最终值,它也将被解包。因此下面的渲染结果将为1

    {{ object.foo }}
    

    这只是文本插值的一个方便功能,相当于{{object.foo.value}}


    ref 在响应式对象中的解包

    当一个ref作为一个响应式对象的 property 被访问或更改时,它会自动解包,因此会表现得和一般的 property 一样:

    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
    
    // 原来的 ref 现在已经和 state.count 脱去联系
    console.log(count.value)   // 1
    

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


    数组和集合类型的 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)
    


    响应性语法糖

    不得不对ref使用.value是一个受限于 JavaScript 语言限制的缺点。不过在编译期间,自动在合适的位置上添加上.value可以改进开发体验。Vue 提供了一个语法糖,可以在编译时作相应转换,使得我们可以像这样书写上面的计数器示例:

    <script setup>
    let count = $ref(0)
    
    function increment() {
      // no need for .value
      count++
    }
    </script>
    
    <template>
      <button @click="increment">{{ count }}</button>
    </template>
    

    请注意它仍处于实验性阶段,在最终提案落地前仍可能发生改动。