面试题:在 Vue 中,如果变量名以 _ 或 $ 开头,会有什么问题?如何访问到这些值?

在 Vue 中,以 _$ 开头的变量名确实会有特殊行为,这源于 Vue 的设计决策。

🚫 问题:自动代理排除

Vue 会自动跳过_$ 开头的属性,不会将它们设置为响应式数据,也不会代理到 Vue 实例上。

示例演示

<template>
  <div>
    <p>普通属性: {{ normalProp }}</p>      <!-- 正常显示 -->
    <p>下划线属性: {{ _privateProp }}</p>  <!-- 不会显示 -->
    <p>美元属性: {{ $internalProp }}</p>   <!-- 不会显示 -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      normalProp: '我可以正常显示',
      _privateProp: '我不会被响应式处理',
      $internalProp: '我也不会被代理'
    }
  },
  mounted() {
    console.log(this.normalProp);    // "我可以正常显示"
    console.log(this._privateProp);  // undefined
    console.log(this.$internalProp); // undefined
    console.log(this.$data._privateProp); // "我不会被响应式处理"
  }
}
</script>

🔍 访问这些值的多种方式

方式一:通过 $data 直接访问

<template>
  <div>
    <p>通过 $data 访问: {{ $data._privateData }}</p>
    <button @click="updatePrivateData">更新私有数据</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      _privateData: '我是私有数据',
      $internalValue: '内部值'
    }
  },
  methods: {
    updatePrivateData() {
      // 通过 $data 访问和修改
      this.$data._privateData = '更新后的私有数据';
      this.$data.$internalValue = '更新后的内部值';

      console.log('_privateData:', this.$data._privateData);
      console.log('$internalValue:', this.$data.$internalValue);
    },

    accessInMethod() {
      // 在方法中访问
      const privateValue = this.$data._privateData;
      const internalValue = this.$data.$internalValue;

      return { privateValue, internalValue };
    }
  },
  computed: {
    computedFromPrivate() {
      // 在计算属性中访问
      return this.$data._privateData.toUpperCase();
    }
  }
}
</script>

方式二:使用计算属性包装

<template>
  <div>
    <p>私有数据: {{ privateData }}</p>
    <p>内部配置: {{ internalConfig }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      _privateData: '敏感信息',
      $appConfig: { theme: 'dark', version: '1.0.0' }
    }
  },
  computed: {
    // 使用计算属性暴露私有数据
    privateData() {
      return this.$data._privateData;
    },
    internalConfig() {
      return this.$data.$appConfig;
    }
  },
  methods: {
    updateConfig() {
      // 通过计算属性间接更新
      this.$data.$appConfig.theme = 'light';
    }
  }
}
</script>

方式三:在生命周期钩子中访问

<script>
export default {
  data() {
    return {
      _initialized: false,
      $cache: new Map()
    }
  },
  created() {
    // 在生命周期中直接操作 $data
    this.$data._initialized = true;
    this.$data.$cache.set('key', 'value');
  },
  mounted() {
    console.log('初始化状态:', this.$data._initialized);
    console.log('缓存内容:', this.$data.$cache.get('key'));
  }
}
</script>

💡 实际应用场景

场景一:私有状态管理

<template>
  <div>
    <p>页面加载次数: {{ accessCount }}</p>
    <p>最后访问时间: {{ lastAccess }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 公共可访问的数据
      accessCount: 0,
      lastAccess: null,

      // 私有内部状态
      _internalState: {
        startTime: null,
        sessionId: null
      },

      // 内部配置
      $performanceConfig: {
        trackTiming: true,
        logLevel: 'debug'
      }
    }
  },
  created() {
    this.$data._internalState.startTime = Date.now();
    this.$data._internalState.sessionId = this.generateSessionId();

    this.incrementAccessCount();
  },
  methods: {
    incrementAccessCount() {
      this.accessCount++;
      this.lastAccess = new Date().toLocaleString();

      // 记录私有日志
      this.$data._internalState.lastOperation = 'increment';
    },

    generateSessionId() {
      return 'session_' + Math.random().toString(36).substr(2, 9);
    },

    getInternalState() {
      // 提供受控的访问方式
      return {
        sessionId: this.$data._internalState.sessionId,
        runningTime: Date.now() - this.$data._internalState.startTime
      };
    }
  },
  computed: {
    // 暴露部分私有信息
    sessionInfo() {
      return `会话: ${this.$data._internalState.sessionId}`;
    }
  }
}
</script>

场景二:插件开发内部状态

<script>
export default {
  data() {
    return {
      // 公共API
      isLoading: false,
      data: null,

      // 插件内部状态(不暴露给用户)
      _$requestQueue: [],
      _$retryCount: 0,
      _$cache: new Map(),

      // 内部配置
      $_config: {
        timeout: 5000,
        maxRetries: 3,
        cacheTTL: 300000
      }
    }
  },
  methods: {
    async fetchData(url) {
      this.isLoading = true;

      // 使用内部队列管理
      this.$data._$requestQueue.push(url);

      try {
        const response = await this.makeRequest(url);
        this.data = response;

        // 更新内部缓存
        this.$data._$cache.set(url, {
          data: response,
          timestamp: Date.now()
        });

      } catch (error) {
        this.handleError(error);
      } finally {
        this.isLoading = false;
        this.$data._$requestQueue = this.$data._$requestQueue.filter(
          item => item !== url
        );
      }
    },

    makeRequest(url) {
      // 内部实现细节
      return fetch(url, { 
        timeout: this.$data.$_config.timeout 
      }).then(res => res.json());
    },

    // 内部方法,不暴露给模板
    handleError(error) {
      this.$data._$retryCount++;
      console.error('请求失败:', error);
    }
  }
}
</script>

场景三:混入(Mixin)中的私有属性

<script>
const PrivateDataMixin = {
  data() {
    return {
      _mixinPrivateState: '混入私有状态',
      $mixinInternalConfig: { enabled: true }
    }
  },
  methods: {
    // 混入的私有方法
    $_mixinInternalMethod() {
      console.log('内部方法被调用');
      return this.$data._mixinPrivateState;
    }
  }
}

export default {
  mixins: [PrivateDataMixin],
  data() {
    return {
      publicData: '公共数据',
      _componentPrivate: '组件私有'
    }
  },
  mounted() {
    // 访问混入的私有数据
    console.log('混入私有状态:', this.$data._mixinPrivateState);
    console.log('混入配置:', this.$data.$mixinInternalConfig);

    // 调用混入的私有方法
    const result = this.$_mixinInternalMethod();
    console.log('内部方法结果:', result);
  }
}
</script>

🚀 最佳实践和建议

1. 合理使用前缀

<script>
export default {
  data() {
    return {
      // ✅ 公共接口 - 驼峰命名
      userName: '',
      isLoading: false,

      // ✅ 内部状态 - _ 前缀
      _pendingRequests: new Set(),
      _updateTimer: null,

      // ✅ 框架相关 - $ 前缀  
      $validationRules: {},
      $pluginConfig: {}
    }
  }
}
</script>

2. 提供受控的访问接口

<script>
export default {
  data() {
    return {
      _sensitiveData: '机密信息',
      $internalState: { debug: false }
    }
  },
  methods: {
    // 提供安全的访问方法
    getSensitiveData() {
      // 添加访问控制逻辑
      if (this.hasPermission()) {
        return this.$data._sensitiveData;
      }
      return null;
    },

    setInternalDebug(enable) {
      this.$data.$internalState.debug = enable;
    },

    hasPermission() {
      // 权限检查逻辑
      return true;
    }
  }
}
</script>

3. 在 Vue 3 Composition API 中

<template>
  <div>
    <p>私有状态: {{ internalState }}</p>
    <p>配置信息: {{ config }}</p>
  </div>
</template>

<script>
import { ref, reactive, computed } from 'vue'

export default {
  setup() {
    // Vue 3 中没有这种限制,但可以遵循相同约定
    const publicState = ref('公共状态');

    // 使用 _ 前缀表示私有(约定)
    const _privateState = ref('私有状态');

    // 使用 $ 前缀表示内部(约定)  
    const $internalConfig = reactive({
      debug: true,
      version: '1.0.0'
    });

    // 通过计算属性暴露
    const internalState = computed(() => _privateState.value);
    const config = computed(() => $internalConfig);

    return {
      publicState,
      internalState,
      config
    }
  }
}
</script>

📝 总结

  • 问题:Vue 自动排除 _$ 开头的属性,不进行响应式代理
  • 访问方式:通过 this.$data._propertyName 访问
  • 使用场景
    • _ 前缀:表示私有数据、内部状态
    • $ 前缀:表示框架相关、插件内部属性
  • 最佳实践
    • 使用前缀区分公共接口和内部实现
    • 通过计算属性或方法提供受控访问
    • 在文档中说明这些约定的含义

这种设计有助于代码的组织和维护,明确区分了公共API和内部实现细节。

THE END
喜欢就支持一下吧
点赞8 分享