面试题整理

97 阅读17分钟

1 谈谈对vue生命周期的理解?

每个Vue实例在创建时都会经过一系列的初始化过程,vue的生命周期钩子,就是说在达到某一阶段或条件时去触发的函数,目的就是为了完成一些动作或者事件

  • create阶段:vue实例被创建

beforeCreate: 创建前,此时data和methods中的数据都还没有初始化 created: 创建完毕,data中有值,未挂载

  • mount阶段: vue实例被挂载到真实DOM节点 beforeMount:可以发起服务端请求,去数据 mounted: 此时可以操作Dom
  • update阶段:当vue实例里面的data数据变化时,触发组件的重新渲染beforeUpdate updated
  • destroy阶段:vue实例被销毁 beforeDestroy:实例被销毁前,此时可以手动销毁一些方法 destroyed

第一次页面加载会触发哪几个钩子

答:beforeCreate, created, beforeMount, mounted

2 watch和计算属性的比较

特点和区别

computed主要用于同步数据的处理,而 watch选项主要用于事件的派发,可异步,这两者都能达到同样的效果,但是基于它们各自的特点,使用场景会有区别

  • computed 拥有缓存属性,只有当依赖数据发生变化时,关联的数据才会发生变化,适用于计算或者格式化数据场景,(同步处理数据)
  • watch监听数据,有关联,但是没有依赖,只要某个数据发生变化,就可以处理一些数据或者派发事件,并同步/异步执行

应用场景

  • computed计算属性: 购物车价格计算,贷款利息计算
  • watch侦听器: 1、主要适用于事件和交互有关的场景,数据变化为条件,适用于一个数据同时出发多个事件,不同条件下,有不同的处理方式,满足一定条件才出发,而不是实时触发 抽象概念

弹窗提示等事件交互的,适用于 watch ,数据计算和字符串处理的适用于 computed

3 你怎么理解 vue 中的 diff 算法,结合源码聊聊

diff算法的一个大致过程

WechatIMG868.jpeg

WechatIMG870.jpeg

WechatIMG872.jpeg

WechatIMG873.jpeg

总结:

  • diff 算法是虚拟DOM技术的必然产物:通过新旧虚拟DOM做对比(即 diff),将变化的地方更新在真实的DOM上;另外,也需要 diff高效的执行对比过程,从而降低时间复杂度,
  • vue 2.x中,为了降低 watcher 粒度,每个组件只有一个 watcher 与之对应,只有引入 diff,才能精确找到发生变化的地方
  • vue 中 diff 执行的时刻是组件实例执行其更新函数时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch
  • diff过程整体遵循深度优先,同层比较的策略;两个节点之间比较,会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是算法的重点,首先假设头尾节点可能相同做4次比对尝试,如果没有找到相同节点,才按照通用方式遍历查找,查找结束再按情况处理剩下的节点,借助 key,通常可以非常精确找到相同节点,因此整个 patch 过程非常高效

4 谈谈对 vue组件化的理解

总结:

  • 组件是独立和可复用的代码组件单元,组件系统是vue核心特性之一,它使开发者使用小型,独立和通常可复用的组件构建大型应用;
  • 组件化开发能大幅提高应用开发效率,测试性,复用性等
  • 组件使用按照分类有:页面组件、业务组件(登录,购物车)、通用组件(button, 表单,列表)
  • vue 组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,他们基于VueComponent, 扩展Vue
  • vue中常见组件技术有: 属性props,自定义事件(完成子传父),插槽等,它们主要用于组件通信,扩展等
  • 合理的划分组件,有利于提升应用性能
  • 组件应该是高内聚,低耦合的
  • 遵循单项数据流原则

5 谈一谈对 vue 设计原则的理解

  • 渐进式 JavaScript 框架
  • 易用、灵活和高效

渐进式 JavaScript 框架 与其他大型框架不同的是,Vue被设计为可以自底向上逐层应用,vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合,另一方面,当与现代化的工具链以及各种支持类库结合使用时,vue 也完全能为复杂单页应用提供驱动

易用性 vue 提供数据响应式、声明式模板语法和基于配置的组件系统等核心特性,这些使得我们只需要关注应用的核心业务即可,只要会写 html js css就能轻松编写 vue应用

灵活性 渐进式框架最大的优点就是灵活性,如果应用足够小,我们可能仅需要 vue的核心特性即可完成功能,随着应用功能不断扩大,我们才可能逐渐引入路由,状态管理,vue-cli等库和工具,不管是应用体积,还是学习难度都是一个逐渐增加的平和曲线

高效性 超快的虚拟DOM和 diff 算法使我们的应用拥有最佳的性能表现。 追求高效的过程还在继续,vue3中引入 proxy 对数据响应式改进以及编译器中对于静态内容编译的改进都会让 vue 更加高效

6 vue组件的通信方式: 父子组件通信,兄弟组件通信,跨层组件通信

1、props

2、emit/emit/on 发布订阅模式,先订阅 vm.on,然后vm.on,然后vm.emit 发布

3、vuex

4、parent/parent/children

5、attrs/attrs/listeners

6、provide/inject (官方不推荐使用,但是写组件库时很常用)

兄弟组件通信

Event Bus 实现跨组件通信 Vue.prototype.$bus = new Vue()

跨级组件通信

$attrs、$listeners Provide、inject

如何选用: 简单数据传递,可以选用 props ,如果项目中需要保存状态的时候可以选用 vuex

怎样理解 Vue 的单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改

Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题 ?

受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

但是 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?

我们查看对应的 Vue 源码:vue/src/core/instance/index.js

我们阅读以上源码可知,vm.$set 的实现原理是:

如果目标是数组,直接使用数组的 splice 方法触发相应式;

如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)

父组件可以监听到子组件的生命周期吗?

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:子组件通过$emit触发父组件的事件,更简单的方式是在父组件引用子组件时通过@hook 来监听

7 vue.nextTick 的原理

由于js引擎线程跟Gpu渲染线程是交替运行的,js引擎执行一个宏任务过程中,把遇到的微任务挂起,在执行完本次宏任务后,立即执行本次微任务队列, 然后执行渲染,再然后开启下一次的宏任务 nexttick 把代码加入到了本次的微任务队列末尾执行,既保证了js操作dom完成,又能在渲染之前开始执行,性能较快,但是如果本浏览器不支持微任务,就用 setTimeout(fn, 0) 来代替

flushBatcherQueue 更新dom的操作,会去重对一个数据的操作,等所有数据操作执行结束之后再进行ui渲染,节省性能

8 能不能说一说浏览器的本地存储?各自优劣如何?

浏览器的本地存储主要分为Cookie、WebStorage和IndexDB, 其中WebStorage又可以分为localStorage和sessionStorage

共同点: 都是保存在浏览器端、且同源的

不同点:

  1. cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下sessionStoragelocalStorage不会自动把数据发送给服务器,仅在本地保存。
  2. 存储大小限制也不同,
  • cookie数据不能超过4K,sessionStorage和localStorage可以达到5M
  • sessionStorage:仅在当前浏览器窗口关闭之前有效;
  • localStorage:始终有效,窗口或浏览器关闭也一直保存,本地存储,因此用作持久数据;
  • cookie:只在设置的cookie过期时间之前有效,即使窗口关闭或浏览器关闭
  1. 作用域不同
  • sessionStorage:不在不同的浏览器窗口中共享,即使是同一个页面;
  • localstorage:在所有同源窗口中都是共享的;也就是说只要浏览器不关闭,数据仍然存在
  • cookie: 也是在所有同源窗口中都是共享的.也就是说只要浏览器不关闭,数据仍然存在

9 http如何实现缓存

  1. 强缓存==>Expires(过期时间)/Cache-Control(no-cache)(优先级高) 协商缓存 ==>Last-Modified/Etag(优先级高)Etag适用于经常改变的小文件 Last-Modefied适用于不怎么经常改变的大文件
  2. 强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。

10 跨域通信的几种方式

解决方案:

  1. jsonp(利用script标签没有跨域限制的漏洞实现。缺点:只支持GET请求)
  2. CORS(设置Access-Control-Allow-Origin:指定可访问资源的域名)
  3. postMessage(message, targetOrigin, [transfer])(HTML5新增API 用于多窗口消息、页面内嵌iframe消息传递),通过onmessage监听 传递过来的数据
  4. Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。
  5. Node中间件代理
  6. Nginx反向代理
  7. 各种嵌套iframe的方式,不常用。
  8. 日常工作中用的最对的跨域方案是CORS和Nginx反向代理

11 你有对 Vue 项目进行哪些优化?

如果没有对 Vue 项目没有进行过优化总结的同学,可以参考本文作者的另一篇文章《 Vue 项目性能优化 — 实践指南 》,文章主要介绍从 3 个大方面,22 个小方面详细讲解如何进行 Vue 项目的优化。

(1)代码层面的优化

  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片资源懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 优化无限列表性能
  • 服务端渲染 SSR or 预渲染

(2)Webpack 层面的优化

  • Webpack 对图片进行压缩
  • 减少 ES6 转为 ES5 的冗余代码
  • 提取公共代码
  • 模板预编译
  • 提取组件的 CSS
  • 优化 SourceMap
  • 构建结果输出分析
  • Vue 项目的编译优化

(3)基础的 Web 技术的优化

  • 开启 gzip 压缩
  • 浏览器缓存
  • CDN 的使用
  • 使用 Chrome Performance 查找性能瓶颈

原生数组方法

栈方法

push方法 push方法可以接收一个或多个参数, 把它们追加到数组末尾, 并返回修改后数组的长度.

pop 方法 pop方法是将数组的最后一项移除, 将数组长度减1, 并返回移除的项.

队列方法

shift方法 shift方法将移除数组的第一项, 将数组长度减1, 并返回移除的项.

unshift方法 unshift也可以在接收一个或多个参数, 把它们依次添加到数组的前端, 并返回修改后数组的长度

重排序方法

reverse方法是用来反转数组的.

sort方法 默认情况下, 它是对数组的每一项进行升序排列, 即最小的值在前面. 但sort方法会调用toString方法将每一项转成字符串进行比较(字符串通过Unicode位点进行排序), 那么这种比较方案在多数情况下并不是最佳方案. 例如:

因此, sort方法可以接收一个比较函数作为参数, 由我们来决定排序的规则. 比较函数接收两个参数, 如果第一个参数小于第二个参数 (即第一个参数应在第二个参数之前) 则返回一个负数, 如果两个参数相等则返回0, 如果第一个参数大于第二个参数则返回一个正数

操作方法

concat方法concat方法可以将多个数组合并成一个新的数组. concat可以接收的参数可以是数组, 也可以是非数组值.concat方法并不操作原数组, 而是新创建一个数组, 然后将调用它的对象中的每一项以及参数中的每一项或非数组参数依次放入新数组中, 并且返回这个新数组

slice方法slice方法可以基于源数组中的部分元素, 对其进行浅拷贝, 返回包括从开始到结束(不包括结束位置)位置的元素的新数组.slice方法同样不操作调用它的数组本身, 而是将原数组的每个元素拷贝一份放到新创建的数组中. 而拷贝的过程, 也于concat方法相同.

splice 方法splice方法可以用途删除或修改数组元素.接收三个参数

  • 要删除的第一个元素的位置
  • 要删除的项数
  • 要插入的元素, 如果要插入多个元素可以添加更多的参数

位置方法

indexOf() lastIndexOf() 两个方法都接收两个参数:要查找的项和(可选的)表示查找起点位置的索引,只indexOf() 方法从数组的开头(位置 0)开始向后查找, lastIndexOf() 方法则从数组的末尾开始向前查找,返回要查找项在数组中的位置,没找到会返回-1。采用全等操作符比较查找。

迭代方法

every() :对数组中的每一项运行给定函数,如果该函数对每一项都返回 true ,则返回 true 。 filter() :对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组。 forEach() :对数组中的每一项运行给定函数。这个方法没有返回值。 map() :对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。 some() :对数组中的每一项运行给定函数,如果该函数对任一项返回 true ,则返回 true 。 以上方法都不会修改数组中的包含的值。 每个方法都接收两个参数:要在每一项上运行的函数和(可选的)运行该函数的作用域对象——影响 this 的值。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。 9.归并方法

reduce() reduceRight() 这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。其中, reduce() 方法从数组的第一项开始,逐个遍历到最后。而 reduceRight() 则从数组的最后一项开始,向前遍历到第一项。

ECMAScript中,闭包指的是:

  1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

  2. 从实践角度:以下函数才算是闭包:

    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了自由变量
  • 闭包用途:

    1. 能够访问函数定义时所在的词法作用域(阻止其被回收)
    2. 私有化变量
    3. 模拟块级作用域
    4. 创建模块
  • 闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏

深拷贝 浅拷贝

赋值 深拷贝 浅拷贝区别 基础类型,=赋值,值的复制 引用类型,=赋值, 内存地址复制

拷贝:

  • 准备一个新的对象/数组
  • 把旧的值复制到新对象/数组

浅拷贝

  • 什么是浅拷贝 把对象/数组第一项的值,复制到新的数组/对象中,第二层还是复制的指针,互相引用

  • 浅拷贝使用场景 修改数组/对象,影响另一个数组/对象,砍断它们的联系

  • 如何实现浅拷贝

    1、Object.assign()

    2、lodash 里面的_.clone

    3、...展开运算符

    4、Array.prototype.concat

    5、Array.prototype.slice

深拷贝

  • 什么是深拷贝 把对象/数组所有层的值,复制到新的对象/数组中
  • 如何实现深拷贝 1、创建新对象/数组 2、判断是基础类型,直接赋值 3、判断对象类型,新建对象,递归调用函数,继续判断

事件循环机制

process.nextTick(() => { console.log(1) })

Promise.resolve().then(() => { console.log(2) })

setTimeout(() => { console.log(3)

Promise.resolve().then(() => { console.log(4) }) }, 300)

console.log(5)

51234

防抖

function debounce (fn, wait) {
  let timeout
  return function () {
    const cxt = this
    const args = arguments
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.call(cxt, args)
    }, wait)
  }
}

节流

// 时间戳
function throttle (fn, wait) {
  let preTime = 0
  return function () {
    const cxt = this
    const args = arguments
    const nowTime = new Date()
    if (nowTime - preTime > wait) {
      fn.apply(cxt, args)
      preTime = nowTime
    }
  }
}

// 定时器
function throttle (fn, wait) {
  let timeOut = null
  return function () {
    if (!timeOut) {
      const cxt = this
      const args = arguments
      timeOut = setTimeout(() => {
        fn.apply(cxt, args)
        timeOut = null
      }, wait)
    }
  }
}

大数运算

JS 在存放整数的时候是有一个安全范围的,一旦数字超过这个范围便会损失精度。

我们不能拿精度损失的数字进行运行,因为运算结果一样是会损失精度的。

所以,我们要用字符串来表示数据!(不会丢失精度)

parseInt() 函数可解析一个字符串,并返回一个整数。

Math.floor() 方法可对一个数进行下舍入。

let a = "9007199254740991";
let b = "1234567899999999999";
function add (a, b) {
  let maxLength = Math.max(a.length, b.length)
  console.log(maxLength)
  a = a.padStart(maxLength, 0)
  b = b.padStart(maxLength, 0)
  let t = 0 // 余数
  let f = 0 // 进位
  let sum = ''
  for (let i = maxLength - 1; i >= 0; i--) {
    t = parseInt(a[i]) + parseInt(b[i]) + f
    f = Math.floor(t / 10)
    sum = t % 10 + sum
  }
  if (f === 1) {
    sum = '1' + sum
  }
}

add(a, b)

遍历数组的方案

  • for 循环
  • for...of
  • for...in
  • forEach()
  • entries()
  • keys()
  • values()
  • reduce()
  • map()
  • every
  • some
  • filter

判断是否是数组的方法

1、instanceof 操作符判断是否是Array 是否是它的实例

let arr = [];
console.log(arr instanceof Array); // true

2、arr.constructor === Array

let arr = [];
console.log(arr.constructor === Array); // true

3、用法:Array.prototype.isPrototypeOf(arr)
Array.prototype  属性表示 Array 构造函数的原型
其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。

let arr = [];
console.log(Array.prototype.isPrototypeOf(arr)); // true

4、用法:Object.getPrototypeOf(arr) === Array.prototype
Object.getPrototypeOf() 方法返回指定对象的原型

所以只要跟Array的原型比较即可

let arr = [];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true

5、用法:Object.prototype.toString.call(arr) === '[object Array]'

虽然Array也继承自Object,但js在Array.prototype上重写了toString,而我们通过toString.call(arr)实际上是通过原型链调用了

let arr = [];
console.log(Object.prototype.toString.call(arr) === '[object Array]'); // true

6、用法:Array.isArray(arr)
ES5中新增了Array.isArray方法,IE8及以下不支持

Array.isArray ( arg )
isArray 函数需要一个参数 arg,如果参数是个对象并且 class 内部属性是 "Array", 返回布尔值 true;否则它返回 false。采用如下步骤:
如果 Type(arg) 不是 Object, 返回 false。
如果 arg 的 [[Class]] 内部属性值是 "Array", 则返回 true。
返回 false.

let arr = [];
console.log(Array.isArray(arr)); // true