Vue面试——高频题之二

1 阅读16分钟

1. 说一下Vue的生命周期

Vue 生命周期钩子

Vue 的生命周期钩子函数分为以下几个阶段:

  1. 创建阶段

    • beforeCreate(创建前)​: 数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 datacomputedwatchmethods 上的方法和数据。
    • created(创建后)​: 实例创建完成,数据观测和事件配置已完成,此时可以访问 datacomputedwatchmethods 上的方法和数据,但 DOM 还未生成。
  2. 挂载阶段

    • beforeMount(挂载前)​: 模板编译完成,但还未将 DOM 挂载到页面,此时无法访问 DOM 元素。
    • mounted(挂载后)​: DOM 挂载完成,可以访问 DOM 元素,此时组件已经渲染到页面中。
  3. 更新阶段

    • beforeUpdate(更新前)​: 数据更新时触发,此时 DOM 还未重新渲染,可以在更新前访问现有的 DOM。
    • updated(更新后)​: 数据更新完成,DOM 重新渲染,此时可以操作更新后的 DOM。
  4. 销毁阶段

    • beforeDestroy(销毁前)​: 实例销毁之前触发,此时实例仍可用,可以清理定时器、解绑事件等。
    • destroyed(销毁后)​: 实例销毁完成,所有事件监听器和子组件已被移除,此时无法再访问实例的属性和方法。

以下是基于您的要求,结合上述内容,对组件通信方式的详细结构化回答,采用 Markdown 格式输出,适合在掘金发布博客。


1.组件通信的方式

1. Props 和 Events

父组件向子组件传值

特点: • props 只能是父组件向子组件进行传值,形成单向下行绑定。 • 子组件的数据会随着父组件不断更新。 • props 可以定义多种数据类型,也可以传递函数。 • 属性名规则:若在 props 中使用驼峰形式,模板中需使用短横线形式。

示例

// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>

// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>

子组件向父组件传值

特点: • 子组件通过 $emit 触发自定义事件,父组件通过 v-on 监听并接收参数。

示例

// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>

// 子组件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}
</script>

2. EventBus 事件总线

特点

• 适用于父子组件、非父子组件等之间的通信。 • 通过一个空的 Vue 实例作为事件中心管理组件之间的通信。

示例

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// 发送事件
<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js'
export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

// 接收事件
<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

3. 依赖注入(Provide / Inject)

特点

• 用于父子组件或祖孙组件之间的通信。 • provide 钩子用来发送数据或方法,inject 钩子用来接收数据或方法。 • 依赖注入提供的属性是非响应式的。

示例

// 父组件
provide() {
 return {
    num: this.num
  };
}

// 子组件
inject: ['num']

4. Ref / $refs

特点

• 通过 ref 属性访问子组件实例,实现父子组件通信。

示例

// 子组件
<template>
  <div>
    <span>{{message}}</span>
  </div>
</template>

<script>
export default {
  data () {
    return {
      message: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}
</script>

// 父组件
<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

5. parent/parent / children

特点

$parent 访问父组件实例,$children 访问子组件实例。 • $children 是无序数组,且数据非响应式。

示例

// 子组件
<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

// 父组件
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

6. attrs/attrs / listeners

特点

• 用于跨代组件通信。 • $attrs 继承父组件属性(除 propsclassstyle)。 • $listeners 继承父组件的事件监听器。

示例

// A组件(APP.vue)
<template>
    <div id="app">
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

// B组件(Child1.vue)
<template>
    <div class="child-1">
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{$attrs}}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
    </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1');
    }
};
</script>

// C组件(Child2.vue)
<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');
    }
};
</script>

总结

Vue 提供了多种组件通信方式,适用于不同场景:

  • Props / Events:父子组件通信。
  • EventBus:跨组件通信。
  • Provide / Inject:祖孙组件通信。
  • Ref / $refs:访问子组件实例。
  • parent/parent / children:访问父组件或子组件实例。
  • attrs/attrs / listeners:跨代组件通信。

根据具体需求选择合适的通信方式,可以提升代码的可维护性和开发效率。

2. 路由的hash和history模式的区别

Vue-Router 提供了两种路由模式:hash 模式history 模式。默认情况下,Vue-Router 使用的是 hash 模式。以下是两种模式的详细介绍和对比。


1. Hash 模式

简介

• Hash 模式的 URL 中会带有一个 #,例如:http://www.abc.com/#/vue,其中 #/vue 就是 hash 值。 • Hash 模式是开发中默认的模式,也是单页面应用(SPA)的标配。

特点

URL 特征:Hash 值会出现在 URL 中,但不会出现在 HTTP 请求中,对后端完全没有影响。 • 页面刷新:改变 hash 值不会重新加载页面。 • 兼容性:支持低版本浏览器,包括 IE8。 • 前端路由:Hash 路由被称为前端路由,完全由前端控制。

原理

Hash 模式的核心原理是 onhashchange 事件:

window.onhashchange = function(event) {
    console.log(event.oldURL, event.newURL);
    let hash = location.hash.slice(1); // 获取当前 hash 值
};

优点: • 无需向后端发起请求,window 可以监听 hash 值的变化,并按规则加载相应的代码。 • 浏览器会记录 hash 值变化对应的 URL,从而实现页面的前进和后退。 • 缺点: • URL 中带有 #,影响美观。


2. History 模式

简介

• History 模式的 URL 中没有 #,例如:http://abc.com/user/id。 • 它使用的是传统的路由分发模式,用户在输入 URL 时,服务器会接收并解析这个 URL,然后做出相应的逻辑处理。

特点

URL 特征:URL 更加美观,没有 #。 • 后端支持:需要后端配置支持,否则刷新页面时会返回 404。 • API:基于 HTML5 的 History API,包括 pushState()replaceState()

原理

History 模式的核心是 History API,分为两部分:

  1. 修改历史状态: • pushState():向历史记录栈中添加一条记录。 • replaceState():替换当前历史记录栈中的记录。 • 这两个方法可以修改 URL,但不会触发页面刷新。
  2. 切换历史状态: • forward():前进。 • back():后退。 • go():跳转到指定页面。

配置

要切换到 history 模式,需要在 Vue-Router 中进行以下配置:

const router = new VueRouter({
  mode: 'history', // 使用 history 模式
  routes: [...]
});

缺点

刷新问题:如果后端没有正确配置,刷新页面时会返回 404。 • 兼容性:不支持 IE9 及以下版本。


3. 两种模式对比

特性Hash 模式History 模式
URL 特征#,例如 http://abc.com/#/vue#,例如 http://abc.com/vue
兼容性支持低版本浏览器(包括 IE8)不支持 IE9 及以下版本
页面刷新不会重新加载页面需要后端支持,否则返回 404
HTTP 请求Hash 值不会出现在请求中URL 会出现在请求中
美观性URL 不美观URL 美观
实现原理基于 onhashchange 事件基于 History API
后端支持无需后端支持需要后端支持

调用 pushState() 的优势

  1. URL 灵活性pushState() 可以设置与当前 URL 同源的任意 URL,而 hash 只能修改 # 后面的部分。
  2. 数据传递pushState() 可以通过 stateObject 参数添加任意类型的数据到记录中,而 hash 只能添加短字符串。
  3. 记录添加pushState() 设置的新 URL 可以与当前 URL 相同,仍会添加记录;而 hash 必须设置不同的值才会触发记录添加。
  4. SEO 友好:History 模式的 URL 更符合传统 URL 格式,对搜索引擎更友好。

总结

  • Hash 模式
    • 优点:兼容性好,无需后端支持,适合简单的单页面应用。
    • 缺点:URL 不美观,SEO 不友好。
  • History 模式
    • 优点:URL 美观,SEO 友好,适合复杂的单页面应用。
    • 缺点:需要后端支持,兼容性较差。

根据实际项目需求选择合适的路由模式:

  • 如果项目需要兼容低版本浏览器且无需考虑 SEO,可以选择 Hash 模式。
  • 如果项目需要美观的 URL 和良好的 SEO,且后端支持 History 模式,可以选择 History 模式。

1. Vuex 的原理

Vuex 的核心概念

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库),它是一个容器,包含了应用中大部分的状态(state)。

主要特点

  1. 响应式状态存储: • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态时,如果 store 中的状态发生变化,相应的组件也会高效更新。
  2. 状态变更的唯一途径: • 改变 store 中状态的唯一途径是显式地提交(commit)mutation。这种方式可以方便地跟踪每一个状态的变化。

Vuex 的核心流程

Vuex 为 Vue Components 建立起了一个完整的生态圈,其核心流程如下:

  1. Vue Components: • Vue 组件负责接收用户操作等交互行为,通过 dispatch 方法触发对应的 action
  2. Actions: • Actions 负责处理 Vue Components 接收到的所有交互行为,包含同步/异步操作。Actions 可以触发其他 action 或提交 mutation
  3. Mutations: • Mutations 是修改 state 的唯一推荐方法,只能进行同步操作。每个 mutation 都有一个字符串的事件类型(type)和一个回调函数(handler),用于实际修改 state。
  4. State: • State 是页面状态管理容器对象,集中存储 Vue Components 中零散的数据。页面显示所需的数据从 state 中读取。
  5. Getters: • Getters 是 state 的读取方法,用于从 state 中派生出计算属性。

流程图

Vue Components -> dispatch -> Actions -> commit -> Mutations -> State -> render -> Vue Components

各模块的功能

Vue Components:接收用户操作,触发 action。 • dispatch:唯一能执行 action 的方法。 • actions:处理交互行为,支持异步操作,提交 mutation。 • commit:唯一能执行 mutation 的方法。 • mutations:修改 state 的唯一途径,同步操作。 • state:集中存储页面状态数据。 • getters:从 state 中读取数据。


2. Vuex 中 action 和 mutation 的区别

Mutation

特点: • 必须是同步函数。 • 直接修改 state。 • 每个 mutation 都有一个字符串的事件类型(type)和一个回调函数(handler)。 • 示例

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment(state) {
      state.count++; // 变更状态
    }
  }
});

// 触发 mutation
store.commit('increment');

Action

特点: • 可以包含任意异步操作。 • 通过提交 mutation 来间接修改 state。 • Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象。 • 示例

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment(context) {
      context.commit('increment'); // 提交 mutation
    }
  }
});

对比

特性MutationAction
同步/异步必须是同步操作可以包含异步操作
修改 state直接修改 state通过提交 mutation 间接修改 state
参数接受 state 作为第一个参数接受 context 对象
触发方式通过 commit 触发通过 dispatch 触发

4. Redux 和 Vuex 的区别,它们的共同思想

区别

  1. API 设计: • Redux:核心是 storereduceraction。 • Vuex:核心是 statemutationsactionsgetters
  2. 异步处理: • Redux:需要中间件(如 redux-thunk)处理异步操作。 • Vuex:内置支持异步操作,通过 actions 实现。
  3. 数据流: • Redux:View -> Action -> Reducer -> State -> View。 • Vuex:View -> Action -> Mutation -> State -> View。
  4. 与框架的集成: • Redux:独立于框架,通常与 React 结合使用。 • Vuex:专为 Vue 设计,深度集成于 Vue 生态。

共同思想

  1. 单一数据源: • 应用的状态集中存储在一个全局的 store 中。
  2. 状态可预测: • 通过严格的规则(如 mutation 或 reducer)确保状态的变化是可预测的。
  3. 服务于 MVVM: • 两者都是对 MVVM 思想的服务,将数据从视图中抽离出来,实现数据与视图的分离。

对比图

特性ReduxVuex
核心概念Store, Reducer, ActionState, Mutations, Actions, Getters
异步处理需要中间件(如 redux-thunk)内置支持异步操作
数据流View -> Action -> Reducer -> State -> ViewView -> Action -> Mutation -> State -> View
框架依赖独立于框架,通常与 React 结合专为 Vue 设计,深度集成

总结

  1. Vuex 的原理: • Vuex 通过集中式状态管理,解决了复杂应用中的数据共享和状态管理问题。其核心是 store,通过 actions、mutations 和 state 实现数据流的闭环。
  2. Action 和 Mutation 的区别: • Mutation 用于同步修改 state,而 Action 可以包含异步操作,通过提交 mutation 间接修改 state。
  3. Redux 和 Vuex 的区别: • Redux 和 Vuex 的核心思想一致,都是单一数据源和状态可预测,但在 API 设计、异步处理和框架集成上有明显区别。

1. Vue3.0 有什么更新

Vue 3.0 是一次重大的版本升级,带来了许多新特性和改进。以下是主要更新内容:


(1)监测机制的改变

基于 Proxy 的响应式系统: • Vue 3.0 使用 Proxy 替代 Vue 2.x 中的 Object.defineProperty,提供了全语言覆盖的反应性跟踪。 • 解决了 Vue 2.x 中 Object.defineProperty 的诸多限制: ◦ 只能监测属性,不能监测整个对象。 ◦ 无法检测属性的添加和删除。 ◦ 无法直接监听数组索引和长度的变化。 • 支持更多数据结构,如 MapSetWeakMapWeakSet


(2)模板优化

作用域插槽的改进: • Vue 2.x 中,作用域插槽的变化会导致父组件重新渲染。 • Vue 3.0 将作用域插槽改为函数的方式,只会影响子组件的重新渲染,提升了性能。 • Render 函数的优化: • 对 render 函数进行了改进,方便开发者直接使用 API 生成虚拟 DOM。


(3)对象式的组件声明方式

类式组件声明: • Vue 2.x 中,组件是通过声明式的方式传入一系列 option,与 TypeScript 结合需要通过装饰器实现,较为麻烦。 • Vue 3.0 改用了类式的写法,使得与 TypeScript 的结合更加自然和简单。


(4)其他方面的改进

自定义渲染器: • 支持自定义渲染器,使得 Weex 等可以通过自定义渲染器扩展,而无需直接修改源码。 • Fragment 和 Portal 组件: • 支持 Fragment(多个根节点)和 Portal(在 DOM 其他部分渲染组件内容),为特殊场景提供了更好的支持。 • 函数式编程(FP): • 从面向对象编程(OOP)切换到函数式编程(FP),基于 Tree Shaking 优化,提供了更多的内置功能。


总结

Vue 3.0 通过基于 Proxy 的响应式系统、模板优化、类式组件声明、自定义渲染器等改进,提升了性能、开发体验和灵活性。


2. defineProperty 和 Proxy 的区别

Vue 2.x 使用 Object.defineProperty 实现响应式系统,而 Vue 3.0 使用 Proxy。以下是两者的主要区别:


(1)Object.defineProperty 的局限性

  1. 只能监听属性: • Object.defineProperty 只能监听对象的属性,无法监听整个对象。
  2. 无法检测新增/删除属性: • 在初始化后,新增或删除的属性无法被监听,需要通过 Vue.setVue.delete 手动处理。
  3. 数组监听问题: • 无法直接监听数组索引和长度的变化。

(2)Proxy 的优势

  1. 监听整个对象: • Proxy 直接代理整个对象,可以监听对象的所有属性变化,包括新增和删除属性。
  2. 支持更多操作: • Proxy 可以监听数组索引和长度的变化,支持更多数据结构(如 Map、Set 等)。
  3. 性能更好: • Proxy 只需一层代理即可监听所有属性变化,性能优于 Object.defineProperty

对比表

特性Object.definePropertyProxy
监听范围只能监听属性监听整个对象
新增/删除属性不支持,需手动处理支持
数组监听不支持索引和长度变化支持
性能较差,需为每个属性单独设置代理更好,只需一层代理
数据结构支持仅支持普通对象支持 Map、Set、WeakMap、WeakSet

总结

Object.defineProperty:在 Vue 2.x 中实现响应式系统,但存在诸多局限性。 • Proxy:在 Vue 3.0 中替代 Object.defineProperty,提供了更强大和灵活的响应式能力,解决了 Vue 2.x 中的许多问题。


以上内容详细描述了 Vue 3.0 的更新内容以及 Object.definePropertyProxy 的区别,适合在掘金发布博客。

1. 对虚拟 DOM 的理解

什么是虚拟 DOM?

虚拟 DOM(Virtual DOM)是一个 JavaScript 对象,用于描述真实 DOM 的结构。它将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。


虚拟 DOM 的核心特点

  1. 轻量级描述: • 虚拟 DOM 是对真实 DOM 的抽象,比直接操作真实 DOM 更轻量。
  2. 跨平台支持: • 虚拟 DOM 的本质是 JS 对象,因此可以在没有 DOM 的环境(如 Node.js)中使用,支持 SSR(服务器端渲染)。
  3. 高效更新: • 通过事务处理机制,将多次 DOM 修改的结果一次性更新到页面上,减少页面渲染的次数,提高性能。
  4. 减少手动操作: • 现代前端框架(如 Vue、React)通过虚拟 DOM 避免了手动操作 DOM,既保证了性能,又提高了开发效率。

虚拟 DOM 的工作流程

  1. 初始化: • 在代码渲染到页面之前,Vue 会将模板转换为虚拟 DOM 对象。 • 虚拟 DOM 对象包含 TagNamepropsChildren 等属性,用于描述真实 DOM 的结构。
  2. 更新: • 当数据发生变化时,Vue 会生成一个新的虚拟 DOM 对象。 • 通过 DIFF 算法,比较新旧虚拟 DOM 的差异。
  3. 渲染: • 将记录的有差异的地方应用到真实 DOM 树中,视图更新。

虚拟 DOM 的优势

  1. 性能优化: • 减少直接操作真实 DOM 的次数,提升渲染效率。
  2. 跨平台支持: • 虚拟 DOM 可以渲染到不同的平台(如 Web、Native)。
  3. 简化开发: • 开发者只需关注数据变化,无需手动操作 DOM。

总结

虚拟 DOM 通过抽象和优化 DOM 操作,提升了渲染性能和开发体验,是现代前端框架的核心技术之一。


5. DIFF 算法的原理

什么是 DIFF 算法?

DIFF 算法是虚拟 DOM 的核心,用于比较新旧虚拟 DOM 的差异,并将差异应用到真实 DOM 中,实现高效更新。


DIFF 算法的核心思想

  1. 同层比较: • 只比较同一层级的节点,不跨层级比较。
  2. Key 值优化: • 通过 key 标识节点,减少不必要的节点操作。
  3. 节点复用: • 如果节点类型相同,则复用节点,只更新属性。

DIFF 算法的工作流程

  1. 初始化虚拟 DOM: • 根据模板生成初始的虚拟 DOM 对象。
  2. 生成新虚拟 DOM: • 当数据变化时,生成新的虚拟 DOM 对象。
  3. 比较差异: • 通过 DIFF 算法,比较新旧虚拟 DOM 的差异。 • 记录需要更新的节点和属性。
  4. 应用差异: • 将差异应用到真实 DOM 中,完成视图更新。

DIFF 算法的优化策略

  1. 深度优先遍历: • 逐层比较节点,减少比较次数。
  2. 最小化操作: • 只更新有变化的节点,减少 DOM 操作。
  3. 节点复用: • 如果节点类型相同,则复用节点,只更新属性。

DIFF 算法的示例

// 旧虚拟 DOM
const oldVNode = {
  tag: 'div',
  props: { id: 'app' },
  children: [
    { tag: 'p', props: {}, children: ['Hello'] }
  ]
};

// 新虚拟 DOM
const newVNode = {
  tag: 'div',
  props: { id: 'app' },
  children: [
    { tag: 'p', props: {}, children: ['World'] }
  ]
};

// DIFF 算法比较
function diff(oldVNode, newVNode) {
  if (oldVNode.tag !== newVNode.tag) {
    // 替换整个节点
    replaceNode(oldVNode, newVNode);
  } else {
    // 更新属性
    updateProps(oldVNode.props, newVNode.props);
    // 比较子节点
    diffChildren(oldVNode.children, newVNode.children);
  }
}

总结

DIFF 算法通过高效比较虚拟 DOM,最小化 DOM 操作,提升了渲染性能,是虚拟 DOM 实现的核心。


以上内容详细描述了虚拟 DOM 的理解和 DIFF 算法的原理,适合在掘金发布博客。