Vue 框架

整理不易,小的快yue过去了,家人们不想开通 VIP ,可以私我,(四元购 PDF版)有更新都会踢。
如果觉得还不错,可以订阅专栏。
【狗头保命,小的认真总结的,大部分都是我面试遇到的】

更新日志

2024年8月30日更新 —— Vue 的内置指令有哪些?(增加了 v-slot)
2024年8月31日更新 ——

  • Vue中,computed和watch一般会在什么情况下使用?它们有什么区别?
  • Vue 和 React 有什么不同?
  • 增加 子组件向子孙组件传值

2024年9月15日更新 ——

  • Vue 子组件和父组件执行顺序
  • Vue-Router 的懒加载如何实现?

前端八股文

1.【HTML + HTML5】前端八股文面试题
2.【CSS + CSS3】前端八股文面试题
3.【JavaScript + ES6】前端八股文面试题
4.【jQuery】前端八股文面试题
5.【Vue2 + Vue3】前端八股文面试题
6.【Uniapp】前端面试题
7.【HTTP、Web常用协议等等】前端八股文面试题
8.【实例总结】前端八股文面试

1.Vue的基本原理?

Vue.js 是一个渐进式 JavaScript 框架,它的核心原理主要包括以下几个方面:

1)数据驱动视图
  • 响应式系统
    • Vue 通过数据劫持结合发布者-订阅者模式,实现了数据的响应式更新。当数据发生变化时,Vue 能够自动更新与之相关的视图部分,而无需手动操作 DOM。
    • Vue 使用 Object.defineProperty()(在 Vue 2.x 中)或 Proxy(在 Vue 3.x 中)来对数据进行劫持。当数据被读取或修改时,Vue 可以追踪到这些操作,并触发相应的更新。
  • 虚拟 DOM
    • Vue 在更新视图时,不是直接操作真实 DOM,而是先创建一个虚拟 DOM(Virtual DOM)树。虚拟 DOM 是一个轻量级的 JavaScript 对象树,它与真实 DOM 结构相对应。
    • 当数据发生变化时,Vue 会比较新旧虚拟 DOM 树的差异,然后只对发生变化的部分进行实际的 DOM 操作,从而提高性能。
2)组件化开发
  • 组件定义
    • Vue 允许将用户界面拆分成独立的、可复用的组件。每个组件都有自己的模板、逻辑和样式,可以独立开发、测试和维护。
    • 组件可以接收输入属性(props),并通过自定义事件($emit)向父组件传递数据。
  • 组件生命周期
    • Vue 组件有一系列的生命周期钩子函数,在组件的不同阶段被调用。例如,created钩子在组件实例被创建后调用,mounted 钩子在组件被挂载到 DOM 后调用。
    • 开发者可以在这些钩子函数中执行特定的逻辑,如数据初始化、DOM 操作、订阅事件等。
3)模板语法
  • 插值表达式
    • Vue 使用双大括号({{}})进行插值表达式,在模板中显示数据。例如,<p>{{ message }}</p>会将数据对象中的 message 属性的值显示在 <p> 标签中。
  • 指令
    • Vue 提供了一系列指令来操作 DOM。例如,v-if 和 v-for 指令可以根据条件显示或循环渲染 DOM 元素。
    • v-model 指令实现了表单元素和数据的双向绑定,当表单元素的值发生变化时,数据也会相应更新;反之,当数据发生变化时,表单元素的值也会更新。
4)单文件组件(SFC)
  • 结构
    • Vue 支持单文件组件格式,将一个组件的模板、逻辑和样式写在一个文件中,以 .vue 扩展名结尾。
    • 单文件组件使得代码结构更加清晰,易于维护和复用。
  • 编译过程
    • 在开发过程中,需要使用构建工具(如 Webpack、Vite 等)对单文件组件进行编译。构建工具会将 .vue 文件中的模板、逻辑和样式分别编译成 JavaScript 代码,然后打包成最终的应用程序。
5)路由和状态管理
  • 路由(Vue Router)
    • Vue Router 是 Vue 的官方路由管理器,用于实现单页应用(SPA)的页面导航。它允许开发者定义不同的路由路径,并将对应的组件渲染到页面中。
    • Vue Router 支持动态路由、嵌套路由等功能,可以满足复杂的应用需求。
  • 状态管理(Vuex)
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它集中管理应用的所有状态,并提供了一种可预测的方式来更新状态。
    • Vuex 包含 state(状态)、mutations(更改状态的方法)、actions(异步操作)、getters(计算属性)、module(模块)等概念,帮助开发者更好地组织和管理应用的状态。

2.说一说 Vue 双向数据绑定的原理

Vue 的双向数据绑定主要是通过以下几个核心技术实现的:

1)数据劫持

在 Vue 2.x 中,利用 Object.defineProperty() 方法来实现数据劫持。这个方法可以为对象的属性添加 getter 和 setter,当属性被读取或修改时,可以触发相应的操作。

例如:

const obj = {};
Object.defineProperty(obj, 'property', {
  enumerable: true,
  configurable: true,
  get: function () {
    console.log('Getting property');
    return value;
  },
  set: function (newValue) {
    console.log(`Setting property to ${newValue}`);
    value = newValue;
  }
});

Vue 遍历数据对象的所有属性,为其添加 getter 和 setter,这样当数据发生变化时,Vue 可以追踪到这个变化。

2)发布者-订阅者模式
  • Dep(依赖收集器)和 Watcher(观察者):
    • Vue 在数据劫持的过程中,会为每个属性创建一个 Dep 对象,Dep 负责收集依赖于该属性的 Watcher。
    • Watcher 则代表一个具体的依赖,比如一个计算属性或者一个模板中的插值表达式。
  • 数据变化触发更新:
    • 当数据发生变化时,会触发属性的 setter,在 setter 中通知 Dep 去通知所有的 Watcher 进行更新。
    • Watcher 收到更新通知后,会触发自身的回调函数,从而更新相关的视图。
3)虚拟 DOM 和 diff 算法
  • 虚拟 DOM:
    • Vue 在内存中维护了一份虚拟 DOM 树,它是真实 DOM 的轻量级描述。
    • 当数据发生变化时,Vue 会先更新虚拟 DOM,而不是直接操作真实 DOM。
  • diff 算法:
    • Vue 会使用 diff 算法比较新旧虚拟 DOM 的差异,只对发生变化的部分进行实际的 DOM 操作,从而提高性能。

例如:

<div id="app">
  <p>{{ message }}</p>
</div>
const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});

当 message 的值发生变化时,Vue 会通过数据劫持检测到这个变化,然后通知相关的 Watcher 更新虚拟 DOM。接着,通过 diff 算法找到需要更新的真实 DOM 节点,并进行更新操作。

在 Vue 3.x 中,使用了 Proxy 来替代 Object.defineProperty() 进行数据劫持,它能够更全面地监听对象的变化,并且性能更好。原理基本类似,但实现方式有所不同。

3. v-if 和 v-show 的区别?

1)共同点:

通过条件控制元素的显示和隐藏。

2)区别:
  • v-if 是一个“惰性”指令
    • 在编译时刻,Vue会根据条件决定是否编译或挂载元素到 DOM 中,为false 时,元素不会被挂载到 DOM;
    • 在条件切换时,会涉及到 DOM 的删除和重新插入,可能会有一定开销;
    • 在初始渲染时,若条件为false ,元素不会渲染到 DOM 中,有一定性能优势。
  • v-show是一个“非惰性”指令
    • 在编译时刻,元素总会被渲染到 DOM 中。会通过 CSS 控制元素的显示和隐藏,不会从 DOM 中移除;
    • 在条件切换时,只是通过 CSS ,不会销毁和重新创建元素,因此开销较小;
    • 在初始渲染时,总是会被渲染到 DOM 中,会有一些额外的开销。

4. 为什么 Vue 组件实例对象中的 data 属性是一个函数,而不是一个对象?

  • 根实例对象(它是一个单例) data 可以是函数 () ,也可以是对象{},不会产生数据污染。
  • 组件实例对象 data必须为函数,
    • 目的是为了防止多个组件实例对象之间,共用一个 data,产生污染数据;
    • 采用函数形式,initData时会将其作为工厂函数,返回全新的 data 对象。

5.简述一下 Vue 实例挂载的过程

Vue 实例挂载的过程主要包括以下几个步骤:

1)创建 Vue 实例

当使用 new Vue() 创建一个 Vue 实例时,会进行一系列的初始化操作:

  • 选项合并:
    • 将传入的选项(如 data、methods、computed、watch 等)与 Vue 的默认选项进行合并。
  • 数据响应式处理:
    • 对 data 选项中的数据进行数据劫持,通过 Object.defineProperty()(在 Vue 2.x 中)或 Proxy(在 Vue 3.x 中)使其成为响应式数据。
    • 这意味着当这些数据发生变化时,Vue 能够自动更新相关的视图。
2)编译模板
  • 如果在创建 Vue 实例时指定了 el 选项,Vue 会开始查找对应的 DOM 元素,并进行模板编译。
  • 如果使用单文件组件(.vue 文件)或通过字符串模板的方式,Vue 会在构建阶段进行模板编译,并将编译结果存储起来,以便在实例挂载时使用。
3)挂载前钩子函数调用
  • 在挂载实例之前,会调用 beforeMount 生命周期钩子函数。
  • 这个钩子函数可以在此时进行一些准备工作,例如加载数据、初始化插件等。
4)创建真实 DOM 并替换占位元素
  • Vue 会根据编译后的模板创建真实的 DOM 元素,并将其插入到页面中。
  • 如果在创建 Vue 实例时指定了 el 选项,并且该选项对应的 DOM 元素中已经存在内容,Vue 会将其替换为新创建的 DOM 元素。
  • 如果使用了路由或动态组件,Vue 会根据当前的路由或组件状态来决定要挂载的组件,并进行相应的 DOM 操作。
5)挂载完成
  • 当 DOM 操作完成后,Vue 会调用 mounted 生命周期钩子函数。
  • 此时,实例已经被挂载到 DOM 上,可以在这个钩子函数中进行一些与 DOM 操作相关的初始化工作,例如获取 DOM 元素、初始化第三方库等。
  • Vue 会建立起数据与视图之间的双向绑定关系,当数据发生变化时,会自动更新视图;
  • 同时,当用户在视图上进行交互操作时,也会自动更新数据。

例如:

<div id="app">
  <p>{{ message }}</p>
</div>
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  beforeMount() {
    console.log('Before mount');
  },
  mounted() {
    console.log('Mounted');
  }
});

在这个示例中,Vue 实例的挂载过程会依次输出 Before mount 和 Mounted,表示在挂载前和挂载完成时调用了相应的生命周期钩子函数。

6.说说你对Vue生命周期的理解?

Vue 的生命周期是指一个 Vue 实例从创建到销毁的整个过程中所经历的一系列阶段,每个阶段都有特定的钩子函数可以让开发者在相应的时刻执行自定义的逻辑。

1)主要的生命周期阶段和钩子函数
  • 创建阶段:
    • beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。此时,实例的选项对象还未创建完成,data、methods、computed等属性还不可用。
    • created:在实例创建完成后被立即调用。此时,实例的数据观测、事件和方法已经配置完成,可以访问到data、methods等属性,但 DOM 还未挂载,无法访问到真实的 DOM 元素。
  • 挂载阶段:
    • beforeMount:在挂载开始之前被调用。此时,模板已经编译完成,但真实的 DOM 还未渲染。
    • mounted:在挂载完成后被调用。此时,真实的 DOM 已经渲染完成,可以访问到真实的 DOM 元素,可以进行 DOM 操作和初始化第三方库等。
  • 更新阶段:
    • beforeUpdate:在数据更新时,但在虚拟 DOM 重新渲染和打补丁之前被调用。可以在这个钩子中获取更新前的状态。
    • updated:在数据更新导致的虚拟 DOM 重新渲染和打补丁之后被调用。可以在这个钩子中获取更新后的状态,但要注意避免在这个钩子中进行数据的更改,以免陷入无限循环。
  • 销毁阶段:
    • beforeDestroy:在实例销毁之前被调用。可以在这个钩子中进行一些清理操作,如解绑事件、清除定时器等。
    • destroyed:在实例销毁后被调用。此时,实例的所有指令、监听器和子实例都已经被销毁。
2)生命周期的应用场景
  • 数据初始化:在created钩子中可以进行数据的初始化操作,如从服务器获取数据、初始化计算属性等。
    created() {
      // 从服务器获取数据
      this.fetchDataFromServer();
    },
    
  • DOM 操作:在mounted钩子中可以进行 DOM 操作,如获取 DOM 元素、初始化第三方库等。
    mounted() {
      const element = document.getElementById('myElement');
      // 对 element 进行操作
    },
    
  • 数据更新处理:在updated钩子中可以处理数据更新后的逻辑,如重新计算某些值、更新图表等。但要注意避免在这个钩子中再次更改数据,以免引起无限循环。
    updated() {
      // 处理数据更新后的逻辑
    },
    
  • 清理操作:在beforeDestroy钩子中可以进行一些清理操作,如解绑事件、清除定时器、取消订阅等。
    beforeDestroy() {
      // 解绑事件
      window.removeEventListener('resize', this.handleResize);
      // 清除定时器
      clearInterval(this.timer);
    },
    

7. Vue组件间通信的方式有哪些?

1)父组件向子组件通信
  • props 属性传递:
    • 子组件通过在props选项中声明需要接收的属性,
    • 父组件在使用子组件时通过属性绑定的方式将数据传递给子组件。

例如:

 <!-- 父组件 -->
 <template>
   <child-component :message="parentMessage"></child-component>
 </template>

 <script>
 import ChildComponent from './ChildComponent.vue';

 export default {
   components: {
     ChildComponent,
   },
   data() {
     return {
       parentMessage: '这是父组件的数据',
     };
   },
 };
 </script>

 <!-- 子组件 -->
 <template>
   <p>{{ message }}</p>
 </template>

 <script>
 export default {
   props: ['message'],
 };
 </script>
2)子组件向父组件通信
  • 自定义事件:
    • 子组件通过$emit方法触发自定义事件,并传递数据给父组件。
    • 父组件在使用子组件时通过监听子组件的自定义事件来接收数据。

例如:

     <!-- 父组件 -->
     <template>
       <child-component @custom-event="handleChildEvent"></child-component>
     </template>

     <script>
     import ChildComponent from './ChildComponent.vue';

     export default {
       components: {
         ChildComponent,
       },
       methods: {
         handleChildEvent(data) {
           console.log(data); // 这是子组件传递过来的数据
         },
       },
     };
     </script>

     <!-- 子组件 -->
     <template>
       <button @click="emitEvent">触发事件</button>
     </template>

     <script>
     export default {
       methods: {
         emitEvent() {
           this.$emit('custom-event', '这是子组件的数据');
         },
       },
     };
     </script>
3)兄弟组件之间通信
  • 事件总线(Event Bus):
    • 创建一个全局的事件总线对象,可以使用 Vue 实例或一个单独的空 Vue 实例。
    • 兄弟组件通过在事件总线上触发和监听事件来传递数据。

例如:

 // 创建事件总线
 const eventBus = new Vue();

 // 组件 A
 export default {
   methods: {
     sendDataToBrother() {
       eventBus.$emit('custom-event', '这是组件 A 的数据');
     },
   },
 };

 // 组件 B
 export default {
   created() {
     eventBus.$on('custom-event', (data) => {
       console.log(data); // 这是组件 A 传递过来的数据
     });
   },
 };
  • Vuex 状态管理:
    • 使用 Vuex 集中管理应用的状态。
    • 兄弟组件可以通过在 Vuex 的 actions、mutations 和 getters 中进行操作来实现数据传递。

例如:

 // store.js
 import Vue from 'vue';
 import Vuex from 'vuex';

 Vue.use(Vuex);

 export default new Vuex.Store({
   state: {
     sharedData: '',
   },
   mutations: {
     setSharedData(state, data) {
       state.sharedData = data;
     },
   },
   actions: {
     updateSharedData(context, data) {
       context.commit('setSharedData', data);
     },
   },
   getters: {
     getSharedData: (state) => state.sharedData,
   },
 });

 // 组件 A
 export default {
   methods: {
     sendDataToBrother() {
       this.$store.dispatch('updateSharedData', '这是组件 A 的数据');
     },
   },
 };

 // 组件 B
 export default {
   computed: {
     sharedData() {
       return this.$store.getters.getSharedData;
     },
   },
 };
4)祖孙组件之间通信
  • 结合父组件向子组件通信和子组件向父组件通信的方式:
    • 可以通过父组件作为中间层,先将数据从祖组件传递给父组件,再由父组件传递给子组件;
    • 反之,子组件向父组件传递数据,父组件再将数据传递给祖组件。

例如:

 <!-- 祖组件 -->
 <template>
   <parent-component :grandparentData="grandparentMessage"></parent-component>
 </template>

 <script>
 import ParentComponent from './ParentComponent.vue';

 export default {
   components: {
     ParentComponent,
   },
   data() {
     return {
       grandparentMessage: '这是祖组件的数据',
     };
   },
 };
 </script>

 <!-- 父组件 -->
 <template>
   <child-component :parentData="parentMessage"></child-component>
 </template>

 <script>
 import ChildComponent from './ChildComponent.vue';

 export default {
   components: {
     ChildComponent,
   },
   props: ['grandparentData'],
   data() {
     return {
       parentMessage: this.grandparentData,
     };
   },
 };
 </script>

 <!-- 子组件 -->
 <template>
   <p>{{ parentData }}</p>
 </template>

 <script>
 export default {
   props: ['parentData'],
 };
 </script>
5)子组件向子孙组件传值
  • 使用provide和inject:
    • 在子组件中使用provide选项提供一个值,子孙组件可以使用inject选项来注入这个值。
  • 例如:
    • 子组件:
      <template>
        <div>
          <slot></slot>
        </div>
      </template>
      
      <script>
      export default {
        provide() {
          return {
            childMessage: '这是子组件传递给子孙组件的值'
          };
        }
      };
      </script>
      
    • 子孙组件:
      <template>
        <div>{{ grandchildMessage }}</div>
      </template>
      
      <script>
      export default {
        inject: ['childMessage'],
        computed: {
          grandchildMessage() {
            return this.childMessage;
          }
        }
      };
      </script>
      

通过这种方式,可以实现从父组件向子组件再向子孙组件的传值。
【注意】使用provide和inject时,值的传递是单向的,并且子孙组件不能直接修改从父组件传递下来的值。
如果需要修改,可以通过派发事件等方式让父组件进行修改。

8. Vue中的$nextTick有什么作用?

在 Vue 中,$nextTick是一个非常有用的方法,它的主要作用是在下次 DOM 更新循环结束之后执行延迟回调。这意味着可以在数据变化导致的 DOM 更新完成后执行特定的操作。

以下是一些主要的应用场景:

1)操作更新后的 DOM
  • 当数据变化后,立即访问 DOM 元素可能无法获取到最新的状态,因为 Vue 的异步更新队列会在适当的时候批量更新 DOM。
  • 使用$nextTick可以确保在 DOM 更新完成后再进行操作。
    • 例如:
      <div id="app">
        <p>{{ message }}</p>
        <button @click="updateMessage">更新消息</button>
      </div>
      
      const app = new Vue({
        el: '#app',
        data: {
          message: '初始消息',
        },
        methods: {
          updateMessage() {
            this.message = '新的消息';
            // 这里立即访问 DOM 可能获取不到更新后的内容
            console.log(document.querySelector('p').textContent); 
            this.$nextTick(() => {
              // 在 DOM 更新后执行
              console.log(document.querySelector('p').textContent); 
            });
          },
        },
      });
      
2)在组件更新后执行某些操作
  • 当组件的状态发生变化导致重新渲染时,可以使用$nextTick在组件更新后执行一些逻辑,比如更新第三方库的状态、执行依赖于更新后 DOM 结构的操作等。
    • 例如,在使用一些基于 DOM 的图表库时,当数据变化导致图表需要更新,可以在$nextTick中调用图表的更新方法。
    • 假设使用了一个名为myChart的图表库:
      this.$nextTick(() => {
        myChart.updateData(this.chartData);
      });
      
3)解决异步操作与 DOM 操作的顺序问题
  • 在某些情况下,可能会有异步操作(如 Ajax 请求)和 DOM 操作混合在一起的情况。
  • 使用$nextTick可以确保在异步操作完成并且 DOM 更新后再执行特定的 DOM 操作。
    • 例如:
      this.getDataFromServer().then(() => {
        this.updateData();
        this.$nextTick(() => {
          // 在数据更新和 DOM 更新后执行
          console.log('DOM 更新后的操作');
        });
      });
      

9.为什么 Vue中 v-if 和 v-for 不建议一起用?

1)性能问题
  • 当 v-for 和 v-if 处于同一节点时,每次循环都会执行 v-if 的判断,这会带来不必要的性能开销。
  • 尤其是当列表数据量较大时,这种开销会更加明显。
    • 例如:
      <ul>
        <li v-for="item in items" v-if="item.isVisible">{{ item.name }}</li>
      </ul>
      
    • 在这个例子中,对于每一个 item,Vue 都需要先进行 v-for 的循环,然后再进行 v-if 的判断,这会导致很多额外的计算。
2)优先级问题与可维护性
  • 在 Vue 的模板解析中,v-for 的优先级比 v-if 高。这意味着 v-for 会先于 v-if 执行,可能会导致一些意外的结果。
    • 例如,如果希望根据某个条件过滤列表数据,使用 v-for 和 v-if 一起可能会先循环出所有的数据,然后再进行过滤,而不是直接只循环符合条件的数据。
    • 这种行为可能会让开发者感到困惑,降低代码的可维护性。
3)更好的替代方案
  • 对于过滤列表数据的情况,可以使用计算属性或者方法来预先处理数据,然后在模板中只使用 v-for 进行循环。
    • 使用计算属性:
      <ul>
        <li v-for="item in filteredItems">{{ item.name }}</li>
      </ul>
      
      data() {
        return {
          items: [/* 原始数据列表 */],
        };
      },
      computed: {
        filteredItems() {
          return this.items.filter(item => item.isVisible);
        },
      },
      
    • 使用方法:
      <ul>
        <li v-for="item in getFilteredItems()">{{ item.name }}</li>
      </ul>
      
      data() {
        return {
          items: [/* 原始数据列表 */],
        };
      },
      methods: {
        getFilteredItems() {
          return this.items.filter(item => item.isVisible);
        },
      },
      

这样可以将数据过滤的逻辑从模板中分离出来,提高代码的可读性和可维护性,同时也避免了性能问题。

10. 对Vue中 keep-alive的理解

在 Vue 中,keep-alive是一个内置的组件,它的主要作用是缓存组件实例,以提高性能和用户体验。

1)主要功能
  • 缓存组件实例:
    • 当一个组件被包裹在keep-alive中时,Vue 会在组件切换时缓存该组件的实例,而不是销毁并重新创建它。这对于那些需要保持状态或进行复杂初始化的组件非常有用。
    • 例如,当用户在一个包含多个选项卡的页面中切换选项卡时,如果每个选项卡对应的组件都被keep-alive缓存,那么在切换回之前访问过的选项卡时,组件的状态将被保留,用户不需要重新等待组件的初始化和数据加载。
  • 控制缓存的组件:
    • keep-alive可以通过include和exclude属性来指定哪些组件需要被缓存,哪些组件不需要被缓存。
    • include属性可以是一个字符串、正则表达式或数组,用于指定需要被缓存的组件名称。exclude属性同理,用于指定不需要被缓存的组件名称。
    • 例如:
      <keep-alive include="cachedComponent">
        <component :is="currentComponent"></component>
      </keep-alive>
      
    • 在这个例子中,只有名为cachedComponent的组件会被缓存,其他组件不会被缓存。
2)生命周期钩子函数的变化
  • 被缓存的组件在激活和停用时会触发特定的生命周期钩子函数:
    • activated:在组件被激活时调用。当一个被缓存的组件再次显示时,会触发这个钩子函数。
    • deactivated:在组件被停用时调用。当一个被缓存的组件被切换到其他组件时,会触发这个钩子函数。
    • 例如:
      export default {
        data() {
          return {
            //...
          };
        },
        activated() {
          console.log('组件被激活');
        },
        deactivated() {
          console.log('组件被停用');
        },
      };
      
3)应用场景
  • 选项卡切换:
    • 如前面提到的选项卡场景,每个选项卡对应的组件可以被keep-alive缓存,以提高切换的速度和用户体验。
  • 复杂表单:
    • 对于包含大量输入项的复杂表单,如果用户在填写表单的过程中切换到其他页面,然后再返回,使用keep-alive可以保留表单的状态,避免用户重新输入。
  • 路由切换:
    • 在某些单页应用中,不同的路由可能对应不同的组件,对于那些不经常变化的组件,可以使用keep-alive进行缓存,以减少组件的创建和销毁次数,提高性能。

11.Vue中给对象添加新属性时,界面不刷新怎么办?

1)使用 Vue.set() 方法
  • Vue.set()可以向响应式对象中添加一个新属性,并确保这个新属性也是响应式的,触发视图更新。
    • 例如:
      const vm = new Vue({
        data: {
          obj: {
            existingProperty: 'initial value'
          }
        }
      });
      
      Vue.set(vm.obj, 'newProperty', 'new value');
      
2)使用 $set 实例方法
  • 在 Vue 实例中,可以使用$set方法来添加新属性并触发视图更新。
    • 例如:
      const vm = new Vue({
        data: {
          obj: {
            existingProperty: 'initial value'
          }
        }
      });
      
      vm.$set(vm.obj, 'newProperty', 'new value');
      
3)使用扩展运算符创建新对象
  • 可以先将原对象扩展为一个新对象,然后再添加新属性,这样新对象就是响应式的。
    • 例如:
      const vm = new Vue({
        data: {
          obj: {
            existingProperty: 'initial value'
          }
        }
      });
      
      vm.obj = {...vm.obj, newProperty: 'new value' };
      

12.对 mixin的理解?有哪些应用场景?

在 Vue 中,mixin(混入)是一种灵活的代码复用机制。

1)理解
  • 定义:
    • mixin是一个包含一组选项(如data、methods、computed、created生命周期钩子等)的对象。
    • 可以将可复用的功能封装到一个mixin中,然后在多个组件中引入这个mixin,以实现代码复用。
  • 合并规则:
    • 当组件和mixin具有相同的选项时,会进行合并。具体的合并规则如下:
      • data:如果是函数形式的data,会分别调用组件和mixin中的函数,并将返回的结果进行合并。如果是对象形式的data,会进行浅层合并,即同名属性会被组件中的属性覆盖。
      • methods、computed、components等:直接合并,同名方法或组件会被组件中的覆盖。
      • 生命周期钩子:会依次调用,先调用mixin中的钩子,再调用组件中的钩子。
2)应用场景
  • 提取公共逻辑:
    • 当多个组件具有相同的功能逻辑时,可以将这些公共逻辑提取到一个mixin中。
    • 例如,多个页面都需要进行数据请求并处理响应结果,可以创建一个包含数据请求方法和响应处理逻辑的mixin。
    • 示例代码:
      // mixin.js
      export const dataFetchMixin = {
        data() {
          return {
            loading: false,
            data: null,
            error: null
          };
        },
        methods: {
          fetchData() {
            this.loading = true;
            // 模拟数据请求
            setTimeout(() => {
              this.data = 'Some data';
              this.loading = false;
            }, 1000);
          }
        },
        created() {
          this.fetchData();
        }
      };
      
    • 在组件中使用:
      <template>
        <div>
          <p v-if="loading">Loading...</p>
          <p v-if="data">{{ data }}</p>
          <p v-if="error">{{ error }}</p>
        </div>
      </template>
      
      <script>
      import { dataFetchMixin } from './mixin.js';
      
      export default {
        mixins: [dataFetchMixin]
      };
      </script>
      
  • 全局状态管理的补充:
    • 在一些简单的应用中,可以使用mixin来实现类似全局状态管理的功能。
    • 例如,创建一个包含全局状态和操作方法的mixin,在多个组件中引入这个mixin,以实现对全局状态的共享和操作。
    • 示例代码:
      // mixin.js
      export const globalStateMixin = {
        data() {
          return {
            globalCounter: 0
          };
        },
        methods: {
          incrementCounter() {
            this.globalCounter++;
          },
          decrementCounter() {
            this.globalCounter--;
          }
        }
      };
      
    • 在组件中使用:
      <template>
        <div>
          <p>Global Counter: {{ globalCounter }}</p>
          <button @click="incrementCounter">Increment</button>
          <button @click="decrementCounter">Decrement</button>
        </div>
      </template>
      
      <script>
      import { globalStateMixin } from './mixin.js';
      
      export default {
        mixins: [globalStateMixin]
      };
      </script>
      
  • 第三方库集成:
  • 当需要在多个组件中集成第三方库时,可以创建一个mixin来封装与第三方库的交互逻辑。
  • 例如,集成一个图表库,可以创建一个包含图表初始化、更新和销毁逻辑的mixin,在需要使用图表的组件中引入这个mixin。
  • 示例代码:
    // mixin.js
    import Chart from 'chart.js';
    
    export const chartMixin = {
      data() {
        return {
          chart: null
        };
      },
      mounted() {
        this.chart = new Chart(this.$refs.canvas, {
          type: 'bar',
          data: {
            labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
            datasets: [{
              label: '# of Votes',
              data: [12, 19, 3, 5, 2, 3],
              backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
              ],
              borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
              ],
              borderWidth: 1
            }]
          },
          options: {
            scales: {
              y: {
                beginAtZero: true
              }
            }
          }
        });
      },
      beforeDestroy() {
        if (this.chart) {
          this.chart.destroy();
        }
      }
    };
    
  • 在组件中使用:
    <template>
      <div>
        <canvas ref="canvas"></canvas>
      </div>
    </template>
    
    <script>
    import { chartMixin } from './mixin.js';
    
    export default {
      mixins: [chartMixin]
    };
    </script>
    

使用mixin可以提高代码的可维护性和可复用性,但也需要注意一些问题,如命名冲突、难以调试等。在大型项目中,可以考虑使用更强大的状态管理方案(如 Vuex)来替代部分mixin的功能。

13.对 slot 的理解,它的使用场景有哪些?

在 Vue 中,slot(插槽)是一种用于组件内容分发的机制。它允许父组件向子组件传递内容,并在子组件的模板中指定这些内容的插入位置。

1)理解
  • 作用:
    • slot使得组件更加灵活和可复用。
    • 通过使用slot,子组件可以定义一个或多个插入点,父组件可以在使用子组件时将内容插入到这些插入点中。
    • 这样,子组件可以在不知道具体内容的情况下,为父组件提供一个框架,让父组件决定在特定位置显示什么内容。
  • 分类:
    • 匿名插槽:
      • 没有命名的插槽,也称为默认插槽。
      • 当子组件中只有一个匿名插槽时,父组件传递的内容会自动填充到这个插槽中。
    • 具名插槽:
      • 有命名的插槽。
      • 父组件可以通过在模板中使用<template>标签并指定slot属性来将内容传递给具名插槽。
    • 作用域插槽:
      • 带有数据传递功能的插槽。
      • 子组件可以将数据传递给作用域插槽,父组件在使用作用域插槽时可以访问这些数据。
2)使用场景
  • 组件布局定制:
    • 当创建一个可复用的组件时,可能无法确定组件的具体内容,但可以确定组件的布局结构。使用slot可以让父组件根据自己的需求定制组件的内容,而不影响组件的布局。
    • 例如,创建一个卡片组件,卡片的头部、内容和底部可能因使用场景的不同而有所变化。可以在卡片组件中使用slot来让父组件决定这些部分的内容。
    • 示例代码:
      <!-- 子组件:Card.vue -->
      <template>
        <div class="card">
          <div class="card-header">
            <slot name="header"></slot>
          </div>
          <div class="card-body">
            <slot></slot>
          </div>
          <div class="card-footer">
            <slot name="footer"></slot>
          </div>
        </div>
      </template>
      
      <!-- 父组件 -->
      <template>
        <div>
          <Card>
            <template v-slot:header>
              <h3>Card Header</h3>
            </template>
            <p>Card Content</p>
            <template v-slot:footer>
              <p>Card Footer</p>
            </template>
          </Card>
        </div>
      </template>
      
  • 动态内容传递:
    • 在某些情况下,需要根据不同的条件传递不同的内容给子组件。使用slot可以方便地实现这种动态内容传递。
    • 例如,创建一个列表组件,根据列表项的类型显示不同的内容。可以在列表组件中使用slot来让父组件根据列表项的类型传递不同的模板。
    • 示例代码:
      <!-- 子组件:List.vue -->
      <template>
        <ul>
          <li v-for="item in items" :key="item.id">
            <slot v-bind:item="item"></slot>
          </li>
        </ul>
      </template>
      
      <!-- 父组件 -->
      <template>
        <div>
          <List :items="listItems">
            <template v-slot="{ item }">
              <span v-if="item.type === 'text'">{{ item.value }}</span>
              <img v-if="item.type === 'image'" :src="item.value" alt="">
            </template>
          </List>
        </div>
      </template>
      
  • 组件扩展:
    • 当需要扩展一个现有组件的功能时,可以使用slot来在不修改原有组件代码的情况下,添加新的内容或功能。
    • 例如,有一个现有的表单组件,需要在表单的某个位置添加一个额外的按钮。可以使用slot在表单组件中预留一个插入点,然后在扩展组件中传递按钮的内容到这个插槽中。
    • 示例代码:
      <!-- 子组件:Form.vue -->
      <template>
        <form>
          <label for="input">Input:</label>
          <input id="input" type="text">
          <slot name="extra"></slot>
          <button type="submit">Submit</button>
        </form>
      </template>
      
      <!-- 扩展组件 -->
      <template>
        <div>
          <Form>
            <template v-slot:extra>
              <button>Extra Button</button>
            </template>
          </Form>
        </div>
      </template>
      

14. Vue中组件和插件有什么区别?

1)定义和功能
  • 组件:
    • 组件是可复用的 Vue 实例,封装了特定的功能和界面。它通常专注于实现一个局部的、特定的功能模块,比如一个按钮、一个表单、一个导航栏等。
    • 组件有自己的模板、逻辑和样式,可以接受输入属性(props)并通过自定义事件($emit)与父组件进行通信。
  • 插件:
    • 插件是一种用于增强 Vue 功能的扩展机制。它可以为 Vue 添加全局的功能、方法、指令、过滤器等,或者对 Vue 的内部机制进行扩展和修改。
    • 插件通常具有更广泛的作用范围,可以影响整个应用程序的行为和功能。
2)使用方式
  • 组件:
    • 在 Vue 应用中,组件通过在模板中使用标签的方式进行引用,或者通过路由配置、动态组件等方式进行加载。
    • 例如:
      <template>
        <div>
          <my-component :prop="value"></my-component>
        </div>
      </template>
      
      <script>
      import MyComponent from './MyComponent.vue';
      
      export default {
        components: {
          MyComponent
        },
        data() {
          return {
            value: 'some value'
          };
        }
      };
      </script>
      
  • 插件:
    • 插件通过调用Vue.use()方法进行安装,将插件的功能注册到 Vue 的全局实例中。
    • 例如,安装一个名为myPlugin的插件:
      import Vue from 'vue';
      import MyPlugin from './myPlugin';
      
      Vue.use(MyPlugin);
      
3)开发和封装
  • 组件:
    • 组件通常是为了实现特定的界面和功能而开发的,具有明确的业务逻辑和界面展示。
    • 组件的开发重点在于实现具体的功能需求,并与其他组件进行良好的交互和组合。
  • 插件:
    • 插件的开发更侧重于为 Vue 提供通用的功能扩展,通常具有较高的抽象层次和通用性。
    • 插件的封装通常需要考虑如何与 Vue 的核心机制进行集成,以及如何提供灵活的配置选项和接口。
4)作用范围
  • 组件:
    • 组件的作用范围通常是局部的,只在引用它的父组件或子组件的范围内生效。
    • 组件之间通过父子关系、兄弟关系等进行通信和交互。
  • 插件:
    • 插件的作用范围是全局的,一旦安装,就可以在整个 Vue 应用程序中使用。
    • 插件提供的功能可以被任何组件、指令、过滤器等访问和使用。
5)示例场景
  • 组件:
    • 当需要实现一个特定的界面元素,如一个登录表单、一个商品列表、一个用户头像等时,使用组件是合适的选择。
    • 组件可以根据具体的业务需求进行定制和组合,以构建复杂的用户界面。
  • 插件:
    • 当需要为 Vue 应用程序添加全局的功能,如路由守卫、状态管理(如 Vuex)、全局指令(如v-click-outside)、全局过滤器等时,使用插件是更好的选择。
    • 插件可以提供一些通用的功能和机制,使得应用程序的开发更加高效和便捷。

15. 简要说说,Vue2 和 Vue3的区别?

1)性能优化
  • 编译优化:
    • Vue 3 在编译阶段进行了更多的优化,使得模板的编译速度更快,并且生成的代码更加高效。
    • 静态提升(Static Hoisting):Vue 3 会将静态的节点提升到渲染函数之外,避免在每次渲染时重复创建,从而提高性能。
    • 事件监听缓存:对于频繁触发的事件监听器,Vue 3 会进行缓存,减少不必要的更新。
  • 响应式系统:
    • Vue 3 使用了新的响应式系统Proxy替代了 Vue 2 中的Object.defineProperty。
    • Proxy可以直接代理整个对象,而不是对对象的每个属性进行劫持,因此在处理复杂对象和嵌套对象时性能更好。
2)语法和 API 变化
  • 组合式 API(Composition API):
    • Vue 3 引入了组合式 API,它允许开发者将相关的逻辑组合在一起,提高代码的可维护性和复用性。
    • 使用组合式 API,可以将组件的逻辑拆分为多个函数,每个函数负责一个特定的功能,然后在setup函数中组合这些函数。
  • 生命周期钩子函数名称变化:
    • Vue 3 中的生命周期钩子函数名称有些变化。
    • 例如,beforeCreate和created变为setup函数,beforeMount变为onBeforeMount,mounted变为onMounted等。
  • Teleport 组件:
    • Vue 3 新增了Teleport组件,它可以将一个组件的模板内容渲染到指定的 DOM 节点中,而不是在组件所在的位置渲染。这在处理模态框、弹出窗口等场景时非常有用。
  • Emits选项的变化:
    • 在 Vue 3 中,emits选项用于声明组件触发的自定义事件,并且可以进行类型检查。
3)其他变化
  • 更好的 TypeScript 支持:
    • Vue 3 对 TypeScript 的支持更好,提供了更完善的类型定义和类型推断,使得在使用 TypeScript 开发 Vue 应用时更加方便。
  • 自定义渲染器:
    • Vue 3 允许开发者创建自定义渲染器,以便在不同的平台上进行渲染,如 WebGL、Canvas 等。
  • 更小的包体积:
    • Vue 3 通过优化代码结构和去除一些不常用的功能,使得包体积更小,加载速度更快。

16.Vue 的内置指令有哪些?

  • v-model :实现双向数据绑定。
  • v-if :根据条件判断是否渲染元素。
  • v-for :循环渲染列表。
  • v-show :通过控制元素的显示和隐藏来切换显示状态。
  • v-on :用于绑定事件监听器。
  • v-bind :动态绑定属性。
  • v-text :更新元素的文本内容。
  • v-html :更新元素的innerHTML。
  • v-slot :指令用于定义插槽的模板内容。它用于在父组件中传递内容到子组件中的插槽。可以用于标签或组件标签上,以便在子组件中使用插槽。

17.Vue 虚拟 DOM 中 Diff 算法比较新旧 vnode 的过程

  • 首先,比较新旧 vnode 的根节点,如果根节点类型不同,则直接替换整个根节点。
  • 如果根节点类型相同,则比较根节点的属性,包括标签名、属性列表等。如果属性有变化,则更新相应的属性。
  • 接着,比较根节点的子节点。Diff 算法会对新旧子节点列表进行同步遍历,通过 key 属性来识别和复用节点。
  • 如果新子节点列表中存在旧子节点列表中没有的节点,则创建这些新节点并插入到 DOM 中。
  • 如果旧子节点列表中存在新子节点列表中没有的节点,则删除这些旧节点。
  • 对于类型相同且 key 相同的节点,进行进一步的深度比较,包括子节点的属性和子节点列表。

18.Diff 算法的原理

在 Vue 和 React 等前端框架中,Diff 算法用于比较两个虚拟 DOM 树的差异,并最小化对真实 DOM 的操作以提高性能。以下是 Diff 算法的基本原理:

1)整体流程
  • 比较新旧虚拟 DOM 树的根节点:
    • 如果根节点的类型不同,直接替换整个旧的根节点对应的真实 DOM。
    • 如果根节点类型相同,继续比较它们的属性和子节点。
  • 属性比较:
    • 比较新旧根节点的属性,若有不同则更新对应的真实 DOM 属性。
  • 子节点比较:
    • 对新旧根节点的子节点列表进行比较,这是 Diff 算法中最复杂的部分。
2)子节点比较策略
  • 新旧子节点均为多个的情况:
    • 首先通过唯一的 key 属性进行快速匹配,减少不必要的 DOM 操作。
    • 从两端向中间进行比较,以提高效率。如果两端的节点相同,则继续向中间比较;如果不同,则判断是否需要移动、插入或删除节点。
  • 新子节点列表中有新增节点的情况:
    • 如果在新子节点列表中发现了旧子节点列表中没有的节点,则在真实 DOM 中插入这些新节点。
  • 旧子节点列表中有节点在新子节点列表中不存在的情况:
    • 如果旧子节点列表中的节点在新子节点列表中不存在,则在真实 DOM 中删除这些节点。
  • 节点移动的判断:
    • 如果在比较过程中发现某个旧节点在新子节点列表中的位置发生了变化,则在真实 DOM 中移动该节点到新的位置,而不是删除后再插入。
3)示例说明

假设旧的虚拟 DOM 树如下:

<ul>
  <li key="A">Item A</li>
  <li key="B">Item B</li>
  <li key="C">Item C</li>
</ul>

新的虚拟 DOM 树变为:

<ul>
  <li key="B">Item B</li>
  <li key="A">Item A</li>
  <li key="D">Item D</li>
</ul>

Diff 算法的执行过程如下:

  • 首先比较根节点<ul>,类型相同,继续比较属性和子节点。
  • 比较子节点列表:
    • 从两端开始比较,发现两端的节点<li key="C">Item C</li>和<li key="D">Item D</li>不同。
    • 然后通过 key 属性进行匹配,发现<li key="A">Item A</li>和<li key="B">Item B</li>在新列表中的位置发生了变化。
    • 对于新增的<li key="D">Item D</li>,在真实 DOM 中插入该节点。
    • 对于旧列表中不存在于新列表的<li key="C">Item C</li>,在真实 DOM 中删除该节点。
    • 对于移动位置的<li key="A">Item A</li>和<li key="B">Item B</li>,在真实 DOM 中移动它们到新的位置。

19. VueX 状态管理器

1) State(状态):
  • 存储应用的状态数据,类似于组件中的data。它应该是唯一的数据源,所有组件都可以从这里读取状态。
  • 例如:
    const store = new Vuex.Store({
      state: {
        count: 0
      }
    });
    
2)Getters(获取器):
  • 可以理解为计算属性,用于从 state 中派生出一些状态。Getters 可以对 state 进行计算和过滤,并且可以依赖其他 Getters。
  • 例如:
    const store = new Vuex.Store({
      state: {
        todos: [
          { id: 1, text: 'Learn Vuex', done: false },
          { id: 2, text: 'Learn Vue', done: true }
        ]
      },
      getters: {
        doneTodos: state => {
          return state.todos.filter(todo => todo.done);
        }
      }
    });
    
3)Mutations(变更):
  • 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Mutations 是同步函数,用于直接修改 state。
  • 例如:
    const store = new Vuex.Store({
      state: {
        count: 0
      },
      mutations: {
        increment(state) {
          state.count++;
        }
      }
    });
    
  • 在组件中调用 mutation:
    this.$store.commit('increment');
    
4)Actions(动作):
  • Action 类似于 mutation,但是 Action 可以包含异步操作。Action 提交 mutation,而不是直接修改状态。
  • 例如:
    const store = new Vuex.Store({
      state: {
        count: 0
      },
      mutations: {
        increment(state) {
          state.count++;
        }
      },
      actions: {
        incrementAsync({ commit }) {
          setTimeout(() => {
            commit('increment');
          }, 1000);
        }
      }
    });
    
  • 在组件中调用 action:
    this.$store.dispatch('incrementAsync');
    
5)Modules(模块):
  • 当应用变得非常复杂时,可以将 store 分割成模块(module)。每个模块拥有自己的 state、mutations、actions 和 getters。
  • 例如:
    const moduleA = {
      state: {... },
      mutations: {... },
      actions: {... },
      getters: {... }
    };
    
    const moduleB = {
      state: {... },
      mutations: {... },
      actions: {... },
      getters: {... }
    };
    
    const store = new Vuex.Store({
      modules: {
        a: moduleA,
        b: moduleB
      }
    });
    

20.如何在vue中设置一个全局方法?

1)方法一:使用 Vue.prototype

这个方法适用于Vue 2.x版本。可以通过在 main.js 文件中将函数挂载到 Vue.prototype 上,这样就可以在任何组件中通过 this.$方法名 来调用。

示例代码:

// main.js
import Vue from 'vue';

Vue.prototype.$myGlobalMethod = function() {
    console.log('这是一个全局方法');
};

// 创建Vue实例
new Vue({
    el: '#app',
    render: h => h(App),
});

在组件中使用这个全局方法:
export default {
    mounted() {
        this.$myGlobalMethod(); // 调用全局方法
    }
};
2)方法二:创建一个全局工具函数模块

单独创建一个 JavaScript 文件,定义全局方法,并通过 import 引入。在这个模块中,可以使用 export 导出函数,这样在其他组件中需要时可以直接引入。

示例代码:

// utils.js
export function myGlobalFunction() {
    console.log('这是一个全球工具函数');
}

在 main.js 中引入并挂载到全局:
import Vue from 'vue';
import { myGlobalFunction } from './utils';

Vue.prototype.$myGlobalFunction = myGlobalFunction;

在组件中调用:
export default {
    mounted() {
        this.$myGlobalFunction(); // 调用全局函数
    }
};
3)方法三:使用提供者插件(Webpack的ProvidePlugin)

通过Webpack的ProvidePlugin将工具函数全局引入,这样不需要在每个组件中单独导入。

示例代码:

首先在插件或库中定义函数,然后配置Webpack:
// webpack.config.js
const webpack = require('webpack');

module.exports = {
    // 其他配置...
    plugins: [
        new webpack.ProvidePlugin({
            myGlobalUtils: path.resolve(__dirname, 'path/to/your/utils.js')
        })
    ]
};

在组件中使用:
export default {
    mounted() {
        myGlobalUtils.someFunction(); // 调用全局方法
    }
};

21.vueUse()为什么能够将方法注册到全局?

vueUse能够将方法注册到全局主要是通过以下方式实现的:

1)利用 Vue 的插件机制
  • Vue 提供了一种插件机制,可以通过Vue.use()方法来安装插件。插件可以向 Vue 实例添加全局方法、属性、指令等。
  • vueUse通常会被设计为一个函数或者一个包含install方法的对象,以符合 Vue 插件的规范。
2)在插件安装过程中进行全局注册
  • 当调用Vue.use(vueUse)时,vueUse的install方法(如果是一个对象形式的插件)或者函数本身(如果是一个函数形式的插件)会被执行。
  • 在这个过程中,可以通过以下方式将方法注册到全局:
    • 添加全局方法:可以将特定的方法直接添加到Vue.prototype上,这样所有的 Vue 组件实例都可以通过this访问到这些方法。例如:
      function vueUse(Vue) {
        Vue.prototype.globalMethod = function () {
          // 全局方法的实现
        };
      }
      
    • 注册全局指令:可以使用Vue.directive()方法注册全局指令,这些指令可以在模板中使用。例如:
      function vueUse(Vue) {
        Vue.directive('globalDirective', {
          // 指令的定义
        });
      }
      
    • 提供全局混入:可以创建一个全局混入对象,并使用Vue.mixin()方法将其混入到所有的组件中。例如:
      function vueUse(Vue) {
        Vue.mixin({
          // 混入的选项
        });
      }
      
3)实现原理总结

vueUse利用 Vue 的插件机制,在插件安装时将特定的方法、指令或混入等注册到全局,使得在整个 Vue 应用中都可以方便地访问和使用这些功能。这样可以提高代码的可复用性和开发效率,同时也有助于保持代码的一致性和可维护性。

22. Vue中,computed和watch一般会在什么情况下使用?它们有什么区别?

在 Vue 中,computed和watch都用于处理数据的变化。

1)computed的使用情况
  • 依赖多个数据进行计算:
    • 当需要根据多个数据属性进行计算并返回一个新的值时,可以使用computed。
    • 例如,根据商品的原价和折扣率计算出折后价格。
     <template>
       <div>
         原价:{{ originalPrice }}<br>
         折扣率:{{ discountRate }}<br>
         折后价格:{{ discountedPrice }}
       </div>
     </template>
    
     <script>
     export default {
       data() {
         return {
           originalPrice: 100,
           discountRate: 0.8
         };
       },
       computed: {
         discountedPrice() {
           return this.originalPrice * this.discountRate;
         }
       }
     };
     </script>
    
  • 缓存计算结果:
    • computed属性会缓存计算结果,只有当它的依赖数据发生变化时才会重新计算。
    • 如果依赖的数据没有变化,直接使用缓存的结果,提高性能。
    • 例如,在一个复杂的计算场景中,如果某些数据没有变化,不需要重复进行计算。
  • 格式化数据:
    • 用于对数据进行格式化显示,例如将日期格式化为特定的字符串格式。
      <template>
        <div>
          原始日期:{{ rawDate }}<br>
          格式化日期:{{ formattedDate }}
        </div>
      </template>
    
      <script>
      export default {
        data() {
          return {
            rawDate: new Date()
          };
        },
        computed: {
          formattedDate() {
            return this.rawDate.toLocaleDateString();
          }
        }
      };
      </script>
    
2)watch的使用情况
  • 执行异步操作:
    • 当数据变化时需要执行异步操作,如发送网络请求、读取文件等,可以使用watch。
    • 例如,当用户输入搜索关键词时,发送网络请求获取搜索结果。
     <template>
       <div>
         <input v-model="searchKeyword" />
         <ul v-if="searchResults.length > 0">
           <li v-for="result in searchResults">{{ result }}</li>
         </ul>
       </div>
     </template>
    
     <script>
     export default {
       data() {
         return {
           searchKeyword: '',
           searchResults: []
         };
       },
       watch: {
         searchKeyword(newValue, oldValue) {
           if (newValue.length > 2) {
             // 发送网络请求获取搜索结果
             axios.get(`/api/search?keyword=${newValue}`)
              .then(response => {
                 this.searchResults = response.data;
               })
              .catch(error => {
                 console.error(error);
               });
           }
         }
       }
     };
     </script>
    
  • 深度监听对象变化:
    • watch可以配置为深度监听对象的属性变化。
    • 当需要监听一个复杂对象内部的属性变化时,可以使用deep选项。
     <template>
       <div>
         <input v-model="user.name" />
         <input v-model="user.age" />
         <p>用户信息:{{ userInfo }}</p>
       </div>
     </template>
    
     <script>
     export default {
       data() {
         return {
           user: {
             name: 'John',
             age: 30
           }
         };
       },
       computed: {
         userInfo() {
           return `姓名:${this.user.name},年龄:${this.user.age}`;
         }
       },
       watch: {
         user: {
           handler(newValue, oldValue) {
             // 当用户对象的任何属性变化时触发
             this.userInfo = `姓名:${newValue.name},年龄:${newValue.age}`;
           },
           deep: true
         }
       }
     };
     </script>
    
3)区别
  • 功能重点:
    • computed主要用于计算属性,根据依赖的数据自动计算并返回结果。
    • watch主要用于监听数据的变化,执行特定的操作。
  • 缓存与执行时机:
    • computed具有缓存特性,只有当依赖的数据发生变化时才会重新计算。
    • watch在数据变化时立即执行,无论变化是否会影响最终的结果。
  • 用法复杂性:
    • computed通常用于简单的计算和格式化数据,使用起来比较简洁。
    • watch可以处理更复杂的逻辑,如异步操作和深度监听,但配置相对复杂一些。

总结
如果需要根据多个数据进行计算并缓存结果,可以使用computed;
如果需要在数据变化时执行异步操作或深度监听对象的变化,可以使用watch。

23.Vue 和 React 有什么不同?

1)设计理念
  • 渐进式框架与声明式编程:
    • Vue 被设计为一个渐进式框架,可以逐步引入到项目中,允许开发者根据项目需求灵活选择使用的功能。它提供了简洁的 API 和语法,易于上手,适合小型项目和快速开发。
    • React 则强调声明式编程,通过构建可复用的组件来描述用户界面。它更注重组件的可组合性和灵活性,适合大型复杂的应用程序开发。
  • 响应式原理:
    • Vue 使用基于数据劫持的响应式系统,通过Object.defineProperty()或Proxy来实现数据的自动追踪和更新。当数据发生变化时,Vue 能够自动更新相关的视图。
    • React 使用虚拟 DOM 和单向数据流的方式来实现界面的更新。当状态发生变化时,React 会重新渲染整个组件树,并通过对比虚拟 DOM 来确定实际需要更新的部分,从而提高性能。
2)语法和模板
  • 模板语法:
    • Vue 使用基于 HTML 的模板语法,允许开发者在模板中使用指令和插值表达式来绑定数据和处理事件。模板语法相对简洁直观,类似于传统的 HTML 开发。
    • React 使用 JSX(JavaScript XML)语法,将 HTML 结构嵌入到 JavaScript 代码中。JSX 语法更加灵活,可以在 JavaScript 中直接编写和操作 DOM 结构,但对于一些开发者来说可能需要一定的学习成本。
  • 组件定义方式:
    • Vue 的组件可以使用选项式 API 或组合式 API 来定义。选项式 API 将组件的各个部分(如数据、方法、生命周期钩子等)分别定义在一个对象的不同属性中,易于理解和使用。组合式 API 则更加灵活和可组合,适合大型项目的开发。
    • React 的组件使用函数式组件或类组件来定义。函数式组件是一种无状态的纯函数,接收输入属性并返回 JSX 结构。类组件则使用 ES6 类的语法,包含状态和生命周期方法。
3)状态管理
  • 内置状态管理:
    • Vue 提供了内置的状态管理解决方案 Vuex,用于集中管理应用的状态。Vuex 采用单向数据流的模式,通过 mutations 和 actions 来修改状态,使得状态的变化可追踪和可预测。
    • React 本身没有内置的状态管理解决方案,但有一些流行的状态管理库,如 Redux 和 MobX。Redux 是一个严格的单向数据流状态管理库,强调不可变性和纯函数。MobX 则采用响应式编程的方式,使得状态的变化更加自动和高效。
  • 状态更新方式:
    • 在 Vue 中,数据的变化会自动触发视图的更新,开发者不需要手动操作 DOM。Vue 的响应式系统会跟踪数据的变化,并自动更新相关的视图。
    • 在 React 中,状态的变化需要通过调用setState()方法或使用useState()钩子来触发重新渲染。开发者需要手动管理状态的更新,并确保在状态变化时正确地更新组件的视图。
4)生态系统和社区支持
  • 生态系统丰富度:
    • Vue 和 React 都有庞大的生态系统和活跃的社区。Vue 的生态系统相对较小,但发展迅速,提供了许多实用的插件和工具,如 Vue Router、Vue CLI 等。
    • React 的生态系统非常丰富,有大量的第三方库和工具可供选择,涵盖了从状态管理到 UI 组件库的各个方面。React 的社区也非常活跃,不断推出新的技术和最佳实践。
  • 学习资源和社区支持:
    • Vue 和 React 都有丰富的学习资源和文档。Vue 的文档相对简洁明了,易于理解,适合初学者。React 的文档也很详细,但对于一些概念的解释可能需要更多的阅读和理解。
    • 两个框架的社区都非常活跃,开发者可以在社区中寻求帮助、分享经验和参与讨论。React 的社区更大,问题的解决速度可能会更快,但 Vue 的社区也在不断壮大,提供了很多有用的解决方案。

总结
Vue 适合小型项目和快速开发,具有简洁的语法和内置的状态管理;
React 适合大型复杂的应用程序,具有强大的可组合性和丰富的生态系统。

24.Vue 子组件和父组件执行顺序

1) 创建阶段:
  • 父组件的beforeCreate生命周期钩子函数被调用。
  • 父组件的created生命周期钩子函数被调用。
  • 父组件开始创建子组件,子组件的beforeCreate生命周期钩子函数被调用。
  • 子组件的created生命周期钩子函数被调用。
2) 挂载阶段:
  • 父组件的beforeMount生命周期钩子函数被调用。
  • 子组件的beforeMount生命周期钩子函数被调用。
  • 子组件被挂载,子组件的mounted生命周期钩子函数被调用。
  • 父组件被挂载,父组件的mounted生命周期钩子函数被调用。
3) 更新阶段:
  • 当父组件的数据发生变化时,会触发父组件的更新流程。
  • 父组件的beforeUpdate生命周期钩子函数被调用。
  • 如果子组件依赖于父组件传递的数据,子组件也会进入更新流程,子组件的beforeUpdate生命周期钩子函数被调用。
  • 父组件更新完成,父组件的updated生命周期钩子函数被调用。
  • 子组件更新完成,子组件的updated生命周期钩子函数被调用。
4) 销毁阶段:
  • 当父组件被销毁时,会触发父组件的销毁流程。
  • 父组件的beforeDestroy生命周期钩子函数被调用。
  • 父组件开始销毁子组件,子组件的beforeDestroy生命周期钩子函数被调用。
  • 子组件被销毁,子组件的destroyed生命周期钩子函数被调用。
  • 父组件被销毁,父组件的destroyed生命周期钩子函数被调用。

总结

  • 在创建和挂载阶段,先创建和挂载父组件,然后再创建和挂载子组件;
  • 在更新和销毁阶段,父组件的变化会影响子组件,并且父组件的更新和销毁流程会先触发,然后再触发子组件的相应流程。

25.Vue-Router 的懒加载如何实现?

1)使用动态导入(import())
  • 传统的路由配置中,组件是直接引入的:

    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import Home from './views/Home.vue';
    
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home,
      },
      // 其他路由
    ];
    
    const router = new VueRouter({
      routes,
    });
    
    Vue.use(VueRouter);
    
    export default router;
    
  • 使用懒加载时,将组件的引入改为使用动态导入的方式:

    import Vue from 'vue';
    import VueRouter from 'vue-router';
    
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: () => import('./views/Home.vue'),
      },
      // 其他路由
    ];
    
    const router = new VueRouter({
      routes,
    });
    
    Vue.use(VueRouter);
    
    export default router;
    

这里的component: () => import('./views/Home.vue')表示当该路由被访问时,才会动态地加载Home.vue组件。

2)好处
  • 优化性能:
    • 懒加载可以减少初始加载时需要加载的资源量。
    • 在大型应用中,如果所有的组件都在初始时加载,可能会导致较长的加载时间,影响用户体验。
    • 而懒加载只在需要的时候加载特定的组件,从而提高了应用的初始加载速度。
    • 例如,一个包含多个复杂页面的管理系统,用户可能在初始时只访问其中一个页面。如果采用懒加载,那么只有当用户导航到特定页面时,该页面的组件才会被加载,而不是在一开始就加载所有页面的组件。
  • 提高可维护性:
    • 懒加载使得代码结构更加清晰。
    • 每个路由对应的组件都独立地进行加载,这使得代码的逻辑更加清晰,易于维护和扩展。
    • 例如,当需要修改某个特定页面的组件时,可以很容易地找到对应的代码文件进行修改,而不会影响其他未被访问的组件。
  • 按需加载资源:
    • 根据用户的实际访问情况加载资源,可以更好地利用网络带宽和设备资源。
    • 对于移动设备或网络速度较慢的环境,这一点尤为重要。
    • 例如,在移动网络环境下,用户可能只需要访问应用的一部分功能。通过懒加载,可以避免不必要的资源下载,节省流量和提高加载速度。

更多推荐