vue3为啥推荐使用ref而不是reactive

3,572 阅读8分钟

在 Vue 3 中,refreactive 都是用于声明响应式状态的工具,但它们的使用场景和内部工作机制有所不同。Vue 3 推荐使用 ref 而不是 reactive 的原因主要涉及到以下几个方面:

  1. 简单的原始值响应式处理

    • ref 更适合处理简单的原始值(如字符串、数字、布尔值等),而 reactive 更适合处理复杂的对象或数组。
  2. 一致性和解构

    • 使用 ref 时,解构不会丢失响应性,因为 ref 会返回一个包含 .value 属性的对象。而 reactive 对象在解构时会丢失响应性。
  3. 类型推导和代码提示

    • ref 更容易与 TypeScript 配合使用,提供更好的类型推导和代码提示。

示例代码

以下是一个详细的代码示例,演示为什么在某些情况下推荐使用 ref 而不是 reactive

使用 ref 的示例
import { ref } from 'vue';

export default {
  setup() {
    // 使用 ref 声明响应式状态
    const count = ref(0);

    function increment() {
      count.value++;
    }

    return {
      count,
      increment
    };
  }
};
使用 reactive 的示例
import { reactive } from 'vue';

export default {
  setup() {
    // 使用 reactive 声明响应式状态
    const state = reactive({
      count: 0
    });

    function increment() {
      state.count++;
    }

    return {
      state,
      increment
    };
  }
};

解构问题

使用 ref 解构
import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    // 解构时不会丢失响应性
    const { value: countValue } = count;

    return {
      countValue,
      increment
    };
  }
};
使用 reactive 解构
import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({
      count: 0
    });

    function increment() {
      state.count++;
    }

    // 解构时会丢失响应性
    const { count } = state;

    return {
      count,
      increment
    };
  }
};

代码解释

  1. 使用 ref

    • ref 返回一个包含 .value 属性的对象,因此在模板中使用时需要通过 .value 访问实际值。
    • 解构时,可以直接解构 .value 属性,不会丢失响应性。
  2. 使用 reactive

    • reactive 适用于复杂的对象或数组,返回一个代理对象。
    • 直接解构 reactive 对象的属性会丢失响应性,因为解构后得到的属性是原始值,不再是响应式的。

总结

  • 简单值:对于简单的原始值(如字符串、数字、布尔值等),推荐使用 ref,因为它更简洁,并且在解构时不会丢失响应性。
  • 复杂对象:对于复杂的对象或数组,推荐使用 reactive,因为它可以更方便地处理嵌套属性的响应性。
  • 一致性ref 在解构时不会丢失响应性,而 reactive 在解构时会丢失响应性,这使得 ref 在某些情况下更为可靠。

通过理解 refreactive 的不同使用场景和内部工作机制,可以更好地选择适合的工具来管理 Vue 3 应用中的响应式状态。

补充回复

tips:上面是基础讲解,我看有两个老哥评论了ref和reactive都是一样的,下面我会详细说一下,详细讲解在Vue 3中为何推荐使用ref而不是reactive,从原理层面分析,并通过详细的代码示例进行说明。

Vue 3中的响应式系统概述

Vue 3引入了全新的响应式系统,基于Proxy实现,极大地增强了其响应式能力。主要通过两个API来创建响应式数据:

  1. ref:用于创建持有单个值的响应式引用,适用于基本类型以及希望保持引用透明性的场景。
  2. reactive:用于创建深层响应式对象,适用于复杂的数据结构,如对象和数组。

为什么推荐使用ref而不是reactive

1. 响应性跟踪的精确性

ref提供了更精确的响应性跟踪。每个ref都有其独立的响应式依赖,这意味着当ref的值变化时,只有依赖于该ref的组件会重新渲染。

<template>
  <div>
    <p>计数器:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'RefExample',
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment,
    };
  },
});
</script>

2. 更好的类型推断

使用ref时,TypeScript能够更好地推断出具体的类型,尤其是对于基本类型(如numberstring等),这在使用reactive时可能需要额外的类型声明。

<template>
  <div>
    <p>姓名:{{ name }}</p>
    <input v-model="name" placeholder="输入姓名" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'RefTypeExample',
  setup() {
    const name = ref<string>('张三');

    return {
      name,
    };
  },
});
</script>

3. 避免响应式对象的泄漏

使用reactive创建的对象是深层响应式的,这在某些情况下可能导致不必要的响应式对象嵌套,增加内存消耗。而ref只包裹单一值,避免了这种情况。

<template>
  <div>
    <p>用户姓名:{{ user.name }}</p>
    <input v-model="user.name" placeholder="输入姓名" />
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  name: 'ReactiveLeakExample',
  setup() {
    const user = reactive({
      name: '李四',
    });

    return {
      user,
    };
  },
});
</script>

在上述示例中,user对象是一个深层响应式对象,可能会带来不必要的响应式追踪。而使用ref可以避免这种情况。

<template>
  <div>
    <p>用户姓名:{{ name }}</p>
    <input v-model="name" placeholder="输入姓名" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'RefNoLeakExample',
  setup() {
    const name = ref<string>('李四');

    return {
      name,
    };
  },
});
</script>

4. 更好的兼容性与组合

ref可以与其他组合式API更好地协作,如computedwatch,提供更灵活的响应式操作。

<template>
  <div>
    <p>原始计数:{{ count }}</p>
    <p>计数的双倍:{{ doubleCount }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue';

export default defineComponent({
  name: 'ComputedRefExample',
  setup() {
    const count = ref(0);
    const doubleCount = computed(() => count.value * 2);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      doubleCount,
      increment,
    };
  },
});
</script>

5. 更清晰的读写语义

ref通过.value进行访问和修改,能够明确区分响应式引用和值本身,增加代码的可读性。

<template>
  <div>
    <p>消息:{{ message }}</p>
    <input v-model="message" placeholder="输入消息" />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'RefSemanticExample',
  setup() {
    const message = ref<string>('Hello Vue 3');

    const updateMessage = (newMessage: string) => {
      message.value = newMessage;
    };

    return {
      message,
      updateMessage,
    };
  },
});
</script>

refreactive的原理解析

ref的实现原理

ref本质上是对基本类型数据的包装,通过在内部使用Proxy来实现响应式。每当.value被访问或修改时,都会触发依赖追踪和更新。

import { track, trigger } from './effect';

export function ref<T>(initialValue: T) {
  let _value = initialValue;
  
  const r = {
    get value() {
      track(r, 'get', 'value');
      return _value;
    },
    set value(newVal) {
      if (newVal !== _value) {
        _value = newVal;
        trigger(r, 'set', 'value');
      }
    },
  };

  return r;
}

reactive的实现原理

reactive通过Proxy对整个对象进行代理,深度劫持其所有属性。当任意属性被访问或修改时,都会触发相应的依赖追踪和更新。

import { track, trigger } from './effect';

export function reactive<T extends object>(target: T): T {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      track(target, 'get', key);
      return typeof result === 'object' && result !== null ? reactive(result) : result;
    },
    set(target, key, value, receiver) {
      const oldValue = (target as any)[key];
      const result = Reflect.set(target, key, value, receiver);
      if (oldValue !== value) {
        trigger(target, 'set', key);
      }
      return result;
    },
  });
}

区别总结

  • 作用范围
    • ref适用于基本类型和需要明确引用的场景。
    • reactive适用于复杂的对象和数组结构。
  • 响应式追踪
    • ref只追踪.value的变化。
    • reactive追踪对象所有属性的变化。
  • 类型推断
    • ref更适合与TypeScript结合,提供更好的类型检查。
    • reactive在深层对象中类型推断可能较为复杂。

详细代码示例

以下是一个综合示例项目,展示了在不同场景下使用refreactive的区别和优势。

项目结构

src/
├── components/
│   ├── RefCounter.vue
│   ├── ReactiveCounter.vue
│   ├── RefVsReactive.vue
├── services/
│   └── dataService.ts
├── store/
│   ├── useCounterStore.ts
├── App.vue
└── main.ts

1. 使用ref实现计数器

<template>
  <div>
    <h2>Ref计数器</h2>
    <p>计数值:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

export default defineComponent({
  name: 'RefCounter',
  setup() {
    const count = ref<number>(0);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment,
    };
  },
});
</script>

<style scoped>
h2 {
  color: #42b983;
}
</style>

2. 使用reactive实现计数器

<template>
  <div>
    <h2>Reactive计数器</h2>
    <p>计数值:{{ state.count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, reactive } from 'vue';

export default defineComponent({
  name: 'ReactiveCounter',
  setup() {
    const state = reactive({
      count: 0,
    });

    const increment = () => {
      state.count++;
    };

    return {
      state,
      increment,
    };
  },
});
</script>

<style scoped>
h2 {
  color: #35495e;
}
</style>

3. 对比refreactive

<template>
  <div>
    <RefCounter />
    <ReactiveCounter />
    <hr />
    <h2>Ref与Reactive对比</h2>
    <p>Ref count: {{ refCount }}</p>
    <p>Reactive count: {{ reactiveState.count }}</p>
    <button @click="incrementBoth">同时增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, reactive } from 'vue';
import RefCounter from './RefCounter.vue';
import ReactiveCounter from './ReactiveCounter.vue';

export default defineComponent({
  name: 'RefVsReactive',
  components: {
    RefCounter,
    ReactiveCounter,
  },
  setup() {
    const refCount = ref<number>(0);
    const reactiveState = reactive({
      count: 0,
    });

    const incrementBoth = () => {
      refCount.value++;
      reactiveState.count++;
    };

    return {
      refCount,
      reactiveState,
      incrementBoth,
    };
  },
});
</script>

<style scoped>
h2 {
  color: #ff9800;
}
hr {
  margin: 20px 0;
}
</style>

4. 数据服务

import { ref } from 'vue';

export function useDataService() {
  const data = ref<string>('初始数据');

  const updateData = (newData: string) => {
    data.value = newData;
  };

  return {
    data,
    updateData,
  };
}

5. 全局状态管理(使用ref

import { ref } from 'vue';
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', () => {
  const count = ref<number>(0);

  const increment = () => {
    count.value++;
  };

  return {
    count,
    increment,
  };
});

6. 应用主组件

<template>
  <div id="app">
    <h1>Vue 3 `ref` vs `reactive` 示例</h1>
    <RefVsReactive />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import RefVsReactive from './components/RefVsReactive.vue';

export default defineComponent({
  name: 'App',
  components: {
    RefVsReactive,
  },
});
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  margin-top: 60px;
}
</style>

7. 主入口文件

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

const app = createApp(App);

app.use(createPinia());

app.mount('#app');

扩展示例:表单处理与状态管理

表单组件(使用ref

<template>
  <div>
    <h2>Ref表单</h2>
    <form @submit.prevent="handleSubmit">
      <label for="name">姓名:</label>
      <input id="name" v-model="name.value" type="text" />
      <br/>
      <label for="age">年龄:</label>
      <input id="age" v-model.number="age.value" type="number" />
      <br/>
      <button type="submit">提交</button>
    </form>
    <p>提交的数据:{{ submittedData }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';

interface FormData {
  name: string;
  age: number;
}

export default defineComponent({
  name: 'RefForm',
  setup() {
    const name = ref<string>('');
    const age = ref<number>(0);
    const submittedData = ref<FormData | null>(null);

    const handleSubmit = () => {
      submittedData.value = {
        name: name.value,
        age: age.value,
      };
      // 重置表单
      name.value = '';
      age.value = 0;
    };

    return {
      name,
      age,
      submittedData,
      handleSubmit,
    };
  },
});
</script>

<style scoped>
h2 {
  color: #8e44ad;
}
label {
  display: inline-block;
  width: 50px;
}
input {
  margin-bottom: 10px;
}
</style>

状态管理组件(使用refPinia

<template>
  <div>
    <h2>Pinia计数器</h2>
    <p>全局计数值:{{ counterStore.count }}</p>
    <button @click="counterStore.increment">增加</button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { useCounterStore } from '../store/useCounterStore';

export default defineComponent({
  name: 'CounterStore',
  setup() {
    const counterStore = useCounterStore();

    return {
      counterStore,
    };
  },
});
</script>

<style scoped>
h2 {
  color: #2ecc71;
}
</style>

结论

通过以上详细的原理解析与代码示例,可以看到在Vue 3中使用ref相比于reactive具有以下优势:

  1. 更精确的响应性追踪:避免不必要的依赖更新,提高性能。
  2. 更好的类型推断:尤其在使用TypeScript时,ref能够提供更准确的类型检查。
  3. 避免响应式对象的泄漏:减少内存消耗,提升应用性能。
  4. 更清晰的语义.value的使用使代码更具可读性和可维护性。
  5. 更好的组合性:与其他组合式API如computedwatch更好地协同工作。

尽管reactive在处理复杂对象时非常有用,但在很多情况下,ref提供了更为简洁和高效的解决方案。因此,推荐在适当的场景下优先考虑使用ref