前端面试题详解整理93|发布订阅,hash,history,生命周期,diff算法,vue2和vue3的区别,ke的作用,vuex,组件通信,事件循环,rem

76 阅读18分钟

美团SaaS技术部-前端开发-一面(60min-已过)

一、八股:

1.路由的 hash 模式和 history 模式的区别?

2.vue2的生命周期?

3.diff 算法?

4.Vue2 和 Vue3 的区别?

1. 路由的 hash 模式和 history 模式的区别?

  • Hash 模式: 在 URL 中以 # 开头的部分称为 hash,hash 模式下,路由器通过监听 window 对象的 hashchange 事件来实现路由切换,例如 http://example.com/#/home。hash 模式下,路由不会导致页面重新加载,因为 hash 不会被发送到服务器,所以不会触发页面刷新,但这样的 URL 对 SEO 不友好。

  • History 模式: History 模式利用 HTML5 History API,在不刷新页面的情况下修改 URL,使得 URL 更加美观,例如 http://example.com/home。History 模式通过监听 popstate 事件来实现路由切换,与 hash 模式相比,它更加符合 RESTful 风格,但需要服务器端配置支持,以防止页面刷新时出现 404 错误。

2. Vue2 的生命周期?

Vue2 的生命周期钩子函数包括以下阶段:

  • beforeCreate: 实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  • created: 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。
  • beforeMount: 在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted: el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子函数。在这一步,实例已完成以下的配置:用上面编译得到的 HTML 字符串替换掉了 el 属性指向的 DOM 对象。注意此时不能进行 DOM 操作,只能在这里初始化事件等操作。
  • beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发额外的重新渲染过程。
  • updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁后调用。调用时,组件 DOM 已更新,可执行依赖于 DOM 的操作。然而,在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。
  • beforeDestroy: 实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed: 实例销毁之后调用。在这一步,实例已经解除了全部绑定,所有的事件监听器被移除,所有的子实例也都被销毁。

3. diff 算法?

Vue 中 Virtual DOM 的 diff 算法主要用于比较新旧虚拟 DOM 树的差异,然后更新真实 DOM。Vue 使用的是一种基于 O(n) 时间复杂度的算法。

  • Diff 算法的核心思想是:

    1. 新旧节点同层级比较,若节点类型不同,直接替换;
    2. 若节点类型相同,继续比较子节点;
    3. 更新子节点时,Vue 会给子节点设置 key 属性,以便更高效地进行比较。
  • Diff 算法的执行过程:

    1. 根据新旧节点的 key 进行对比,找出相同的节点;
    2. 递归比较子节点;
    3. 标记需要更新的节点;
    4. 对比完毕后,统一进行更新操作。
  • Diff 算法的优化:

    • 为列表中的每个子元素设置唯一的 key,避免全列表重新渲染;
    • 在递归子节点时,采用双端队列,降低时间复杂度。

4. Vue2 和 Vue3 的区别?

  • 性能提升: Vue3 在底层的重写和优化使得性能得到大幅提升,包括渲染性能、初始化速度和打包体积等方面。
  • Composition API: Vue3 引入了 Composition API,可以更好地组织和重用代码逻辑,让组件更加清晰和可维护。
  • Proxy 替代 Object.defineProperty: Vue3 使用 Proxy 替代了 Vue2 中的 Object.defineProperty,提供了更多的拦截和反射能力,使得响应式系统更加高效和灵活。
  • Teleport 和 Suspense 组件: Vue3 新增了 Teleport 和 Suspense 组件,分别用于更方便地进行 Portal 和 Suspense 功能的开发。
  • TypeScript 支持: Vue3 对 TypeScript 的支持更加友好,内置了完整的 TypeScript 类型定义文件,并优化了类型推导能力。
  • Fragments 和片段模式: Vue3 支持了 Fragments 和片段模式,使得组件更加灵活,可以返回多个根节点。
  • 静态提升和按需编译: Vue3 引入了静态提升和按需编译等优化措施,进一步提升了渲染性能和打包体积。

以上是对四道问题的详细解答,每个问题都给出了完整的解释和相关细节,希望对你有帮助。

5.Vue 中 key 的作用

6.Vuex?

7.组件通信?

8.Event Loop 的执行顺序

5. Vue 中 key 的作用

在 Vue 中,key 是用于帮助 Vue 识别节点的唯一标识符。当 Vue 用于维护可重用的元素时(如在 v-for 中),它会基于 key 的变化重新排序或重用元素,而不是销毁和重新创建。key 的作用主要体现在以下几个方面:

  • 重用节点: 当 Vue 在更新 DOM 时,有了 key,它会尽可能地复用相同 key 的元素,而不是销毁后重新创建。这样可以提高性能,减少不必要的 DOM 操作。

  • 防止状态丢失: 在某些场景下,如果没有 key,Vue 可能会错误地重用相同类型的元素,导致状态丢失或混乱。通过给元素设置 key,可以确保每个元素都有唯一的标识符,避免这种问题。

  • 列表过渡动画: 在使用 Vue 的过渡动画时,key 也扮演了重要角色。Vue 会根据元素的 key 来确定它们的进入和离开状态,以便正确地触发过渡效果。

  • 组件缓存: 在 Vue 的动态组件中,key 也可以用来控制组件的缓存。通过动态改变 key 的值,可以强制 Vue 销毁旧组件并创建新组件,实现组件的动态切换和重新加载。

综上所述,key 在 Vue 中具有非常重要的作用,能够优化性能、保持状态稳定以及实现动态组件的缓存和切换。

6. Vuex

Vuex 是 Vue.js 的官方状态管理库,用于管理 Vue 应用中的共享状态。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 的核心概念包括:

  • State(状态): 即存储数据的地方,类似于组件中的 data 属性,但是它是全局的,所有组件都可以直接访问。状态是响应式的,当状态发生变化时,与之相关的组件会自动更新。

  • Getter(获取器): 用于从状态中派生出新的状态,类似于计算属性。Getter 可以接受其他 Getter 作为第二个参数,允许在 Getter 中访问其他 Getter 的返回值。

  • Mutation(突变): 用于修改状态的唯一途径。每个 Mutation 都是一个函数,接受状态作为第一个参数,可以在函数内部修改状态。Mutation 必须是同步函数。

  • Action(动作): 用于提交 Mutation,而不是直接变更状态。Action 可以包含任意异步操作,最终通过提交 Mutation 来变更状态。

  • Module(模块): 用于将 Vuex 分割成多个模块,每个模块可以拥有自己的 State、Getter、Mutation、Action。

Vuex 的设计目标是简化 Vue 应用中状态的管理,并提供一种清晰可预测的方式来处理状态的变化。通过集中式的状态管理,Vuex 能够更好地管理大型复杂应用的状态,使得应用更易于维护和调试。

7. 组件通信

在 Vue 中,组件通信是指不同组件之间进行数据传递和交互的过程。常见的组件通信方式包括:

  • Props / Emit: 父子组件通信的方式。通过 Props 将数据从父组件传递给子组件,子组件可以通过 Emit 事件来向父组件发送消息。

  • EventBus: 通过 EventBus 实现非父子组件之间的通信。可以创建一个空的 Vue 实例作为事件中心,其他组件通过 emitemit 和 on 方法来发送和接收事件。

  • Vuex: 用于管理应用中的共享状态,可以在任何组件中直接访问和修改状态。

  • **refs可以通过refs:** 可以通过 refs 直接访问子组件的属性和方法,用于简单的父子组件通信。

  • Provide / Inject: 父组件通过 Provide 提供数据,子组件通过 Inject 接收数据,用于

祖先和后代之间的通信。

  • attrs/attrs / listeners: 用于传递属性和监听器,适用于动态组件和高阶组件。

选择合适的通信方式取决于应用的架构和需求,需要根据具体情况进行选择。

8. Event Loop 的执行顺序

JavaScript 是单线程执行的语言,但是在浏览器中,它同时具有同步执行和异步执行的特点。Event Loop 是 JavaScript 实现异步编程的核心机制,它负责处理消息队列中的任务,确保任务按照正确的顺序执行。

Event Loop 的执行顺序可以分为以下几个阶段:

  1. 执行栈(Execution Stack): 代码在执行过程中会进入执行栈中,形成执行上下文,并按照先入后出的顺序执行。当执行栈为空时,JavaScript 引擎会检查任务队列中是否有待执行的任务。

  2. 消息队列(Message Queue): JavaScript 运行时环境(如浏览器)维护了一个消息队列,用于存放待执行的异步任务,包括定时器回调、事件处理函数等。每个任务都包含一个回调函数和相应的事件。

  3. 事件循环(Event Loop): 当执行栈为空时,JavaScript 引擎会从消息队列中取出一个任务,放入执行栈中执行。这个过程称为事件循环。事件循环是一个不断重复的过程,确保消息队列中的任务按照正确的顺序执行。

  4. 微任务队列(Microtask Queue): 在执行宏任务(如定时器回调)时,如果任务队列中产生了新的微任务(如 Promise 的回调),会优先执行微任务队列中的任务,直到微任务队列为空,再继续执行宏任务。

  5. 任务分类: 事件循环中的任务分为宏任务(Macrotask)和微任务(Microtask)。宏任务包括 script 代码块、setTimeout、setInterval、I/O 操作等,而微任务包括 Promise 的回调、MutationObserver 的回调等。

综上所述,Event Loop 的执行顺序是由执行栈、消息队列、微任务队列和事件循环共同组成的,确保 JavaScript 代码按照正确的顺序执行,保证了 JavaScript 单线程的异步编程模型。

9.如何使用 rem 或 viewport 进行移动端适配?

移动端适配是指确保网站或应用在不同移动设备上显示效果一致,并且能够适应不同屏幕尺寸的过程。使用 rem(root em)或 viewport 单位是常见的移动端适配方案之一。

使用 rem 进行移动端适配:

  1. 设置基准字体大小: 在 CSS 中,使用 rem 单位时,其大小是相对于根元素(html 元素)的字体大小来计算的。因此,首先需要设置根元素的字体大小,通常将其设置为设备宽度的一部分,比如 1/10 或者 1/100。

    html {
        font-size: calc(100vw / 10); /* 设备宽度的 1/10 */
    }
    
  2. 使用 rem 单位: 一旦根元素的字体大小设置好了,就可以在其他元素中使用 rem 单位。例如:

    .container {
        width: 20rem; /* 相当于设备宽度的 20/10 = 2rem */
    }
    
  3. 动态调整字体大小: 为了保证在不同设备上显示效果一致,可以使用媒体查询或 JavaScript 动态调整根元素的字体大小。

使用 viewport 进行移动端适配:

  1. 设置 viewport: 在 HTML 页面的 <head> 标签中,通过设置 viewport 元标签来控制网页在移动设备上的显示:

    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    

    width=device-width 表示页面宽度等于设备宽度,initial-scale=1.0 表示初始缩放比例为 1。

  2. 使用 viewport 单位: 在 CSS 中,可以使用 viewport 单位(vw、vh、vmin、vmax)来相对于视口尺寸设置元素的大小。

    .container {
        width: 50vw; /* 相当于视口宽度的 50% */
        height: 80vh; /* 相当于视口高度的 80% */
    }
    
  3. 动态调整视口大小: 可以使用媒体查询或 JavaScript 动态调整视口大小,以适应不同的设备尺寸和方向。

选择 rem 或 viewport 单位进行移动端适配取决于具体的项目需求和个人偏好。通常情况下,rem 更适合于需要适配较复杂布局和字体大小的场景,而 viewport 则更适合于简单的宽度和高度适配。

10.实现元素水平垂直居中

要实现元素水平垂直居中,可以使用以下几种方法:

方法一:Flexbox(弹性盒子布局)

.container {
    display: flex;
    justify-content: center; /* 水平居中 */
    align-items: center; /* 垂直居中 */
}

方法二:Grid(网格布局)

.container {
    display: grid;
    place-items: center; /* 水平垂直居中 */
}

方法三:绝对定位和负边距

.container {
    position: relative;
}

.item {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

方法四:绝对定位和 calc()

.container {
    position: relative;
}

.item {
    position: absolute;
    top: calc(50% - 想要元素高度的一半);
    left: calc(50% - 想要元素宽度的一半);
}

方法五:Flexbox + margin:auto

.container {
    display: flex;
}

.item {
    margin: auto; /* 水平垂直居中 */
}

方法六:CSS Grid + justify-self 和 align-self

.container {
    display: grid;
}

.item {
    justify-self: center; /* 水平居中 */
    align-self: center; /* 垂直居中 */
}

以上方法中,Flexbox 和 Grid 是最常用的布局方法,因为它们具有灵活性和易用性。在选择时,可以根据具体的布局需求和兼容性要求来决定。

11.写代码:发布订阅(js)

发布订阅模式(也称为观察者模式)是一种设计模式,用于处理对象之间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。下面是一个用JavaScript实现发布订阅模式的示例代码,包含详细的注释。

// 创建一个发布者(或主题)对象
class Publisher {
    constructor() {
        this.subscribers = []; // 存储订阅者的数组
    }

    // 添加订阅者
    subscribe(subscriber) {
        this.subscribers.push(subscriber);
    }

    // 移除订阅者
    unsubscribe(subscriber) {
        this.subscribers = this.subscribers.filter(sub => sub !== subscriber);
    }

    // 发布消息给所有订阅者
    publish(message) {
        console.log(`Publisher 发布消息: ${message}`);
        this.subscribers.forEach(subscriber => subscriber.notify(message));
    }
}

// 创建一个订阅者(或观察者)对象
class Subscriber {
    constructor(name) {
        this.name = name;
    }

    // 接收到发布者的通知
    notify(message) {
        console.log(`${this.name} 收到消息: ${message}`);
    }
}

// 创建发布者对象
const publisher = new Publisher();

// 创建订阅者对象
const subscriber1 = new Subscriber('Subscriber 1');
const subscriber2 = new Subscriber('Subscriber 2');

// 订阅者订阅发布者
publisher.subscribe(subscriber1);
publisher.subscribe(subscriber2);

// 发布者发布消息
publisher.publish('Hello World!');

// 订阅者取消订阅发布者
publisher.unsubscribe(subscriber1);

// 再次发布消息
publisher.publish('How are you?');

这段代码实现了一个简单的发布订阅模式。Publisher对象负责管理订阅者,并在状态改变时发布消息给所有订阅者;Subscriber对象接收到发布者的消息并作出响应。通过调用subscribe方法订阅发布者,调用unsubscribe方法取消订阅,以及调用publish方法发布消息。

此外,这段代码还展示了如何创建发布者对象、订阅者对象,以及如何进行订阅和取消订阅的操作。

12.vue3当中响应的不一样的地方?

13.vue2怎么设计的响应式

14.vue3的优势?

12. Vue3中响应式的不同之处:

在Vue 3中,响应式系统进行了重大改进,主要体现在以下方面:

  1. Proxy替代Object.defineProperty: Vue 3使用ES6的Proxy对象来实现响应式,而不再使用Vue 2中的Object.defineProperty。这样做带来了性能上的提升,同时也解决了一些Object.defineProperty存在的限制和问题。

  2. 支持Map、Set等数据结构: Vue 3的响应式系统不仅支持对象和数组的响应式,还支持Map、Set等ES6新增的数据结构的响应式。这使得开发者能够更灵活地处理不同类型的数据。

  3. 更好的类型推断和支持: Vue 3使用TypeScript编写,并提供了更好的类型推断和支持,使得在开发过程中能够更早地发现错误并提供更好的开发体验。

  4. 逻辑解耦和更好的组合性: Vue 3中的响应式系统更加模块化和解耦,可以更灵活地组合和重用。例如,Vue 3中的Composition API就是基于这种思想设计的,能够更好地组织和管理组件逻辑。

13. Vue2中的响应式设计:

在Vue 2中,响应式是通过Object.defineProperty来实现的。当数据被添加到Vue实例中时,Vue会递归地将对象的属性转换为getter/setter,并且当属性被访问时,会收集依赖以便之后的重新渲染。

具体步骤如下:

  1. Vue在初始化阶段对data中的每个属性进行递归遍历,并使用Object.defineProperty()将其转换为getter/setter。
  2. 当访问一个被代理的属性时,会触发其getter函数,Vue会将Watcher对象添加到依赖列表中。
  3. 当属性的值发生变化时,触发其setter函数,Vue会通知相关的Watcher对象,从而触发重新渲染。

Vue 2的响应式系统有一些局限性,例如无法检测对象属性的添加或删除,需要通过Vue.set()或this.$set()来手动触发响应式更新。此外,Vue 2的响应式系统对数组的变化监测也相对复杂。

14. Vue3的优势:

Vue 3相较于Vue 2带来了许多改进和优势,主要包括以下几点:

  1. 性能提升: Vue 3通过使用Proxy对象、优化虚拟DOM算法等方式提升了性能,使得应用的渲染速度更快,响应更加及时。

  2. 更小的体积: Vue 3的代码体积更小,打包后的文件更加精简,加载速度更快。

  3. Composition API: Vue 3引入了Composition API,提供了一种更灵活、更直观的组织组件逻辑的方式,能够更好地组织和复用代码。

  4. Typescript支持: Vue 3是使用TypeScript编写的,并且提供了更好的类型支持和推断,使得开发过程更加安全和高效。

  5. Teleport和Suspense组件: Vue 3引入了Teleport和Suspense等新的内置组件,提供了更好的组件复用和异步组件加载的支持。

  6. 更好的支持Tree-shaking: Vue 3的模块更容易进行Tree-shaking,能够更好地优化应用的打包体积。

  7. 更好的编译器优化: Vue 3的编译器进行了优化,生成的渲染函数更加高效,减少了运行时的开销。

总的来说,Vue 3在性能、体积、开发体验等方面都有很大的提升,是一个更加现代化和强大的前端框架。

15.用flex实现居中分布 使用Flexbox布局可以轻松实现元素的居中分布。以下是一种常见的方法,通过设置父容器的display: flex;以及justify-contentalign-items属性来实现水平和垂直居中:

HTML结构:

<div class="container">
  <div class="item">内容</div>
</div>

CSS样式:

.container {
  display: flex; /* 使用Flexbox布局 */
  justify-content: center; /* 水平居中 */
  align-items: center; /* 垂直居中 */
  width: 100%; /* 设置父容器宽度 */
  height: 100vh; /* 设置父容器高度,这里使用视口高度作为示例 */
}

.item {
  /* 可选样式,为了示例增加了一些装饰 */
  width: 200px;
  height: 100px;
  background-color: #ccc;
  text-align: center;
  line-height: 100px;
}

在这个示例中,.container是父容器,.item是子元素。通过设置.containerdisplay: flex;以及justify-content: center;align-items: center;属性,可以使.item元素在父容器中水平和垂直居中。

16.Cookie、sessionStorage、localStorage 的区别?

17.闭包?

18.实现深拷贝的思路?

16. Cookie、sessionStorage 和 localStorage 的区别:

  1. Cookie:

    • 存储:最大可存储4KB的数据,通过HTTP请求发送给服务器。
    • 生命周期:可设置过期时间,关闭浏览器后也可以保存(如果设置了过期时间)。
    • 作用域:可以设置跨域访问,通过设置domainpath属性。
  2. sessionStorage:

    • 存储:最大可存储5MB的数据,仅在当前会话期间有效,即当页面关闭时数据被清除。
    • 生命周期:仅在当前会话期间有效,关闭页面或浏览器后数据被清除。
    • 作用域:仅在当前页面的标签页有效,不会跨页面共享数据。
  3. localStorage:

    • 存储:最大可存储5MB的数据,除非被清除,否则永久有效。
    • 生命周期:永久有效,除非主动清除。
    • 作用域:同源的页面之间共享,跨标签页和窗口也是有效的。

17. 闭包:

闭包是指函数和其相关的引用环境的组合。闭包允许函数访问其外部作用域中的变量,即使在函数本身被返回之后仍然可以访问这些变量。

常见用途包括:

  • 创建私有变量和函数。
  • 实现函数柯里化。
  • 延迟执行。

例如:

function outerFunction() {
  let outerVar = 'I am from outer function';
  
  function innerFunction() {
    console.log(outerVar); // 内部函数可以访问外部函数的变量
  }
  
  return innerFunction;
}

let inner = outerFunction();
inner(); // 输出 "I am from outer function"

18. 实现深拷贝的思路:

实现深拷贝的主要思路是遍历对象及其属性,并对每个属性进行递归复制,以保证拷贝后的对象与原对象没有任何引用关系。

常见的深拷贝方法有:

  1. 递归复制: 遍历对象及其属性,递归复制每个属性的值,直到属性的值不再是对象为止。

  2. JSON 序列化与反序列化: 利用 JSON.stringify() 将对象序列化为字符串,再利用 JSON.parse() 将字符串反序列化为对象,这样可以实现完全的深拷贝。但是这种方法无法处理对象中含有函数、undefined等特殊值的情况。

  3. 使用第三方库: 如 lodash 的 _.cloneDeep() 方法可以实现深拷贝,它会递归复制对象的每个属性值,包括对象的嵌套对象。

示例代码:

// 递归复制实现深拷贝
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  let clone = Array.isArray(obj) ? [] : {};
  
  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clone[key] = deepClone(obj[key]);
    }
  }
  
  return clone;
}

19.async?

20.script 标签放在 header 里和放在 body 底部里有什么区别?

19. async:

async 是 JavaScript 的关键字,用于声明一个函数是异步的。在函数内部,可以使用 await 关键字来等待异步操作的完成。异步函数会返回一个 Promise 对象,该 Promise 对象的结果是函数的返回值。

async function fetchData() {
  let response = await fetch('https://api.example.com/data');
  let data = await response.json();
  return data;
}

fetchData().then(data => {
  console.log(data);
});

20. <script> 标签放在 <head> 和放在 <body> 底部的区别:

  1. 加载顺序:

    • <script> 放在 <head> 中会在 HTML 解析到该标签时立即加载并执行,可能会阻塞 HTML 的渲染。
    • <script> 放在 <body> 底部会等到 HTML 解析完毕后加载,不会阻塞 HTML 的渲染,但会延迟脚本的执行。
  2. 性能优化:

    • <script> 放在 <body> 底部可以使页面更快地显示,因为 HTML 和 CSS 可以更快地加载和解析,用户能够更快地看到页面内容。
    • 如果脚本需要在页面加载前执行,必须将其放在 <head> 中,以确保在页面渲染之前加载和执行脚本。
  3. 优先级:

    • 在某些情况下,可能需要在页面加载时立即执行的脚本,此时应将脚本放在 <head> 中。
    • 在其他情况下,可以将脚本放在 <body> 底部,以提高页面加载性能。

综上所述,根据页面需求和性能优化考虑,可以选择将 <script> 标签放在 <head><body> 底部。

二、其他问题:

1.多久开始学习前端?

2.项目当中遇到了什么困难,如何解决?

3.了解现在的主流框架有哪些?

4.怎么学习前端?

三、反问面试官

#美团春招##美团面经##前端#

作者:想被叫靓仔的大卫在许愿
链接:www.nowcoder.com/discuss/596…
来源:牛客网