写点什么

前端 vue 面试题

作者:bb_xiaxia1998
  • 2023-02-07
    浙江
  • 本文字数:8434 字

    阅读完需:约 28 分钟

父子组件生命周期调用顺序(简单)

渲染顺序:先父后子,完成顺序:先子后父


更新顺序:父更新导致子更新,子更新完成后父


销毁顺序:先父后子,完成顺序:先子后父

diff 算法

时间复杂度: 个树的完全 diff 算法是一个时间复杂度为 O(n*3) ,vue 进行优化转化成 O(n)


理解:


  • 最小量更新, key 很重要。这个可以是这个节点的唯一标识,告诉 diff 算法,在更改前后它们是同一个 DOM 节点

  • 扩展 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改 DOM),加 key 只会移动减少操作 DOM。

  • 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。

  • 只进行同层比较,不会进行跨层比较。


diff 算法的优化策略:四种命中查找,四个指针


  1. 旧前与新前(先比开头,后插入和删除节点的这种情况)

  2. 旧后与新后(比结尾,前插入或删除的情况)

  3. 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)

  4. 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)

v-show 与 v-if 有什么区别?

v-if真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。


v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。


所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

Vue 性能优化

编码优化


  • 事件代理

  • keep-alive

  • 拆分组件

  • key 保证唯一性

  • 路由懒加载、异步组件

  • 防抖节流


Vue 加载性能优化


  • 第三方模块按需导入( babel-plugin-component

  • 图片懒加载


用户体验


  • app-skeleton 骨架屏

  • shellap p 壳

  • pwa


SEO 优化


  • 预渲染

实际工作中,你总结的 vue 最佳实践有哪些

从编码风格、性能、安全等方面说几条:


编码风格方面:


  • 命名组件时使用“多词”风格避免和HTML元素冲突

  • 使用“细节化”方式定义属性而不是只有一个属性名

  • 属性名声明时使用“驼峰命名”,模板或jsx中使用“肉串命名”

  • 使用v-for时务必加上key,且不要跟v-if写在一起


性能方面:


  • 路由懒加载减少应用尺寸

  • 利用SSR减少首屏加载时间

  • 利用v-once渲染那些不需要更新的内容

  • 一些长列表可以利用虚拟滚动技术避免内存过度占用

  • 对于深层嵌套对象的大数组可以使用shallowRefshallowReactive降低开销

  • 避免不必要的组件抽象


安全:


  • 不使用不可信模板,例如使用用户输入拼接模板:template: <div> + userProvidedString + </div>

  • 避免使用v-html:url:style等,避免htmlurl、样式等注入

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示


switch (mode) {    case 'history':    this.history = new HTML5History(this, options.base)    break    case 'hash':    this.history = new HashHistory(this, options.base, this.fallback)      break    case 'abstract':    this.history = new AbstractHistory(this, options.base)      break  default:    if (process.env.NODE_ENV !== 'production') {      assert(false, `invalid mode: ${mode}`)    }}
复制代码


其中,3 种路由模式的说明如下:


  • hash: 使用 URL hash 值来作路由,支持所有浏览器

  • history : 依赖 HTML5 History API 和服务器配置

  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.


参考 前端进阶面试题详细解答

Vue-router 基本使用

mode


  • hash

  • history


跳转


  • 编程式(js 跳转)this.$router.push('/')

  • 声明式(标签跳转) <router-link to=""></router-link>


vue 路由传参数


  • 使用query方法传入的参数使用this.$route.query接受

  • 使用params方式传入的参数使用this.$route.params接受


占位


<router-view></router-view>
复制代码

Vue 中组件和插件有什么区别

1. 组件是什么


组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在 Vue 中每一个.vue 文件都可以视为一个组件


组件的优势


  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单

  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级


2. 插件是什么


插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:


  • 添加全局方法或者属性。如: vue-custom-element

  • 添加全局资源:指令/过滤器/过渡等。如 vue-touch

  • 通过全局混入来添加一些组件选项。如vue-router

  • 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。

  • 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如vue-router


3. 两者的区别


两者的区别主要表现在以下几个方面:


  • 编写形式

  • 注册形式

  • 使用场景


3.1 编写形式


编写组件


编写一个组件,可以有很多方式,我们最常见的就是 vue 单文件的这种格式,每一个.vue文件我们都可以看成是一个组件


vue 文件标准格式


<template></template><script>export default{     ...}</script><style></style>
复制代码


我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上


<template id="testComponent">     // 组件显示的内容    <div>component!</div>   </template>
Vue.component('componentA',{ template: '#testComponent' template: `<div>component</div>` // 组件内容少可以通过这种形式})
复制代码


编写插件


vue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象


MyPlugin.install = function (Vue, options) {  // 1. 添加全局方法或 property  Vue.myGlobalMethod = function () {    // 逻辑...  }
// 2. 添加全局资源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 逻辑... } ... })
// 3. 注入组件选项 Vue.mixin({ created: function () { // 逻辑... } ... })
// 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑... }}
复制代码


3.2 注册形式


组件注册


vue 组件注册主要分为全局注册局部注册


全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项


Vue.component('my-component-name', { /* ... */ })
复制代码


局部注册只需在用到的地方通过components属性注册一个组件


const component1 = {...} // 定义一个组件
export default { components:{ component1 // 局部注册 }}
复制代码


插件注册


插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项


Vue.use(插件名字,{ /* ... */} )
复制代码


注意的是:


注册插件的时候,需要在调用 new Vue() 启动应用之前完成


Vue.use会自动阻止多次注册相同插件,只会注册一次


4. 使用场景


  • 组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue

  • 插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身


简单来说,插件就是指对Vue的功能的增强或补充

既然 Vue 通过数据劫持可以精准探测数据变化,为什么还需要虚拟 DOM 进行 diff 检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能

  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

Vue.set 的实现原理

  • 给对应和数组本身都增加了dep属性

  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新

  • 当修改数组索引时,我们调用数组本身的splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)


基本使用


以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )


  • 调用方法:Vue.set(target, key, value )

  • target:要更改的数据源(可以是对象或者数组)

  • key:要更改的具体数据

  • value :重新赋的值


<div id="app">{{user.name}} {{user.age}}</div><div id="app"></div><script>    // 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher    // 2. vue会给每个对象也增加一个dep属性    const vm = new Vue({        el: '#app',        data: { // vm._data              user: {name:'poetry'}        }    });    // 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新     // 如果是数组 通过调用 splice方法,触发视图更新    vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher</script>
复制代码


相关源码


// src/core/observer/index.js 44export class Observer { // new Observer(value)  value: any;  dep: Dep;  vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() // 给所有对象类型增加dep属性 }}
复制代码


// src/core/observer/index.js 201export function set (target: Array<any> | Object, key: any, val: any): any {  // 1.是开发环境 target 没定义或者是基础类型则报错  if (process.env.NODE_ENV !== 'production' &&    (isUndef(target) || isPrimitive(target))  ) {    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)  }  // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)  if (Array.isArray(target) && isValidArrayIndex(key)) {    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // 3.如果是对象本身的属性,则直接添加即可  if (key in target && !(key in Object.prototype)) {    target[key] = val // 直接修改属性值      return val  }  // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)  const ob = (target: any).__ob__  if (target._isVue || (ob && ob.vmCount)) {    process.env.NODE_ENV !== 'production' && warn(      'Avoid adding reactive properties to a Vue instance or its root $data ' +      'at runtime - declare it upfront in the data option.'    )    return val  }  // 5.如果不是响应式的也不需要将其定义成响应式属性  if (!ob) {    target[key] = val    return val  }  // 6.将属性定义成响应式的  defineReactive(ob.value, key, val)  // 通知视图更新  ob.dep.notify()  return val}
复制代码


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


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

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

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;

  • 属性改变后,通知计算属性重新计算


实现时,主要如下


  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter

  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。

  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。

  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

vue-cli 工程常用的 npm 命令有哪些

  • 下载 node_modules 资源包的命令:


npm install
复制代码


  • 启动 vue-cli 开发环境的 npm 命令:


npm run dev
复制代码


  • vue-cli 生成 生产环境部署资源 的 npm命令:


npm run build
复制代码


  • 用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:


npm run build --report
复制代码


在浏览器上自动弹出一个 展示 vue-cli 工程打包后 app.jsmanifest.jsvendor.js 文件里面所包含代码的页面。可以具此优化 vue-cli 生产环境部署的静态资源,提升 页面 的加载速度

Vue 3.0 中的 Vue Composition API?

在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React 极为容易上手,同时也造成了几个问题:


  1. 由于 Options API 不够灵活的开发方式,使得 Vue 开发缺乏优雅的方法来在组件间共用代码。

  2. Vue 组件过于依赖this上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods 中的this竟然指向组件实例来不指向methods所在的对象。这也使得 TypeScript 在 Vue2 中很不好用。


于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API 本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。


如下,是一个使用了 Vue Composition API 的 Vue3 组件:


<template>  <button @click="increment">    Count: {{ count }}  </button></template>
<script>// Composition API 将组件属性暴露为函数,因此第一步是导入所需的函数import { ref, computed, onMounted } from 'vue'
export default { setup() {// 使用 ref 函数声明了称为 count 的响应属性,对应于Vue2中的data函数 const count = ref(0) // Vue2中需要在methods option中声明的函数,现在直接声明 function increment() { count.value++ } // 对应于Vue2中的mounted声明周期 onMounted(() => console.log('component mounted!')) return { count, increment } }}</script>
复制代码


显而易见,Vue Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性

如果让你从零开始写一个 vue 路由,说说你的思路

思路分析:


首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。


  • 借助hash或者 history api实现url跳转页面不刷新

  • 同时监听hashchange事件或者popstate事件处理跳转

  • 根据hash值或者state值从routes表中匹配对应component并渲染


回答范例:


一个SPA应用的路由需要解决的问题是 页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:


  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事


  • 保存用户传入的配置项

  • 监听hash或者popstate事件

  • 回调里根据path匹配对应路由


  1. router定义成一个Vue插件,即实现install方法,内部做两件事


  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示

  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例

Vue 项目中你是如何解决跨域的呢

一、跨域是什么

跨域本质是浏览器基于同源策略的一种安全手段


同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能


所谓同源(即指在同一个域)具有以下三个相同点


  • 协议相同(protocol)

  • 主机相同(host)

  • 端口相同(port)


反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域


一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用 postman 请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?

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


  1. Vue 使用了 Object.defineProperty 实现双向数据绑定

  2. 在初始化实例时对属性执行 getter/setter 转化

  3. 属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)


所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)


2)接下来我们看看框架本身是如何实现的呢?


Vue 源码位置:vue/src/core/instance/index.js


export function set (target: Array<any> | Object, key: any, val: any): any {  // target 为数组    if (Array.isArray(target) && isValidArrayIndex(key)) {    // 修改数组的长度, 避免索引>数组长度导致splcie()执行有误    target.length = Math.max(target.length, key)    // 利用数组的splice变异方法触发响应式      target.splice(key, 1, val)    return val  }  // key 已经存在,直接修改属性值    if (key in target && !(key in Object.prototype)) {    target[key] = val    return val  }  const ob = (target: any).__ob__  // target 本身就不是响应式数据, 直接赋值  if (!ob) {    target[key] = val    return val  }  // 对属性进行响应式处理  defineReactive(ob.value, key, val)  ob.dep.notify()  return val}
复制代码


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


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

  2. 如果目标是对象,会先判读属性是否存在、对象是否是响应式,

  3. 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理


defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法

Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:


  • Proxy 可以直接监听对象而非属性;

  • Proxy 可以直接监听数组的变化;

  • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;

  • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;


Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;


Object.defineProperty 的优势如下:


  • 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

v-model 的原理?

我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:


  • text 和 textarea 元素使用 value 属性和 input 事件;

  • checkbox 和 radio 使用 checked 属性和 change 事件;

  • select 字段将 value 作为 prop 并将 change 作为事件。


以 input 表单元素为例:


<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码


如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:


父组件:<ModelChild v-model="message"></ModelChild>
子组件:<div>{{value}}</div>
props:{ value: String},methods: { test1(){ this.$emit('input', '小红') },},
复制代码

Vue 中组件生命周期调用顺序说一下

组件的调用顺序都是先父后子,渲染完成的顺序是先子后父


组件的销毁操作是先父后子,销毁完成的顺序是先子后父


加载渲染过程


父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
复制代码


子组件更新过程


父beforeUpdate->子beforeUpdate->子updated->父updated
复制代码


父组件更新过程


父 beforeUpdate -> 父 updated
复制代码


销毁过程


父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
复制代码

v-if 和 v-show 的区别

v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。


v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
前端vue面试题_Vue_bb_xiaxia1998_InfoQ写作社区