阅读 2690

答对这些vue面试题,我是一个合格的中级前端开发工程师吗?

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

面试题只是药引子,如何通过面试题总结出自己的由的知识库才是最重要的。面试题由简入难,自己查漏补缺,作为面试官其实不希望只是听到这一道题的点,更是考察这一道题的面。

1.什么是MVVM?

MVVM是Model-View-ViewModel的缩写。MVVM是一种设计思想。Model 层代表数据模型,也可以在 Model中定义数据修改和操作的业务逻辑;View 代表UI 组件,它负责将数据模型转化成UI 展现出来, ViewModel 是一个同步View 和 Model的对象。 在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会 立即反应到View 上。 ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完 全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状 态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。

2. mvvm和mvc区别?它和其它框架(jquery)的区别是什么?哪些场景适合?

mvc和mvvm其实区别并不大。都是一种设计思想。主要就是mvc中Controller演变成mvvm中的 viewModel。mvvm主要解决了mvc中大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户 体验。 区别:vue数据驱动,通过数据来显示视图层而不是节点操作。 场景:数据操作比较多的场景,更加便捷

3. 组件之间的传值?

父组件与子组件传值 父组件通过标签上面定义传值 子组件通过props方法接受数据 子组件向父组件传递数据 子组件通过$emit方法传递参数

4. Vue 双向绑定原理

mvvm 双向绑定,采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来 劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

微信图片_20210703144438.png

注意要点

(1) 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

(2) 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

(3) 实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

(4) mvvm 入口函数,整合以上三者

具体步骤:
  • 需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
  • compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
  • Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:在自身实例化时往属性订阅器(dep)里面添加自己自身必须有一个 update() 方法待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。
  • MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过Observer来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起Observer和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

5. 描述下 vue 从初始化页面--修改数据--刷新页面 UI 的过程?

当 Vue 进入初始化阶段时,一方面 Vue 会遍历 data 中的属性,并用 Object.defineProperty 将它转化成 getter/setter 的形式,实现数据劫持(暂不谈 Vue3.0 的 Proxy);另一方面,Vue 的指令编译器Compiler 对元素节点的各个指令进行解析,初始化视图,并订阅 Watcher 来更新试图,此时 Watcher会将自己添加到消息订阅器 Dep 中,此时初始化完毕。当数据发生变化时,触发 Observer 中 setter 方法,立即调用 Dep.notify(),Dep 这个数组开始遍历所有的订阅者,并调用其 update 方法,Vue 内部再通过 diff 算法,patch 相应的更新完成对订阅者视图的改变。

6. 你是如何理解 Vue 的响应式系统的?

  • 任何一个 Vue Component 都有一个与之对应的 Watcher 实例
  • Vue 的 data 上的属性会被添加 getter 和 setter 属性
  • 当 Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被读, getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这一过程被称为依赖收集)
  • data 被改动时(主要是用户操作), 即被写, setter 方法会被调用, 此时 Vue 会去通知所有依赖于此data 的组件去调用他们的 render 函数进行更新

7. 虚拟 DOM 实现原理

  • 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
  • 状态变更时,记录新树和旧树的差异
  • 最后把差异更新到真正的dom

8.Vue 中 key 值的作用?

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key 的作用主要是为了高效的更新虚拟DOM。

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

  • props/$emit
  • emit/emit/on
  • vuex
  • attrs/attrs/listeners
  • provide/inject
  • parent/parent/children 与 ref

10.vue 中怎么重置data?

使用Object.assign(),vm.data可以获取当前状态下的datavm.data可以获取当前状态下的data,vm.options.data(this)可以获取到组件初始化状态下的data。

Object.assign(this.$data, this.$options.data(this)) // 注意加this,不然取不到

data(){ a: this.methodA } 中的this.methodA。

复制代码

11.组件中写name有什么作用?

  • 项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
  • DOM 做递归组件时需要调用自身 name
  • vue-devtools 调试工具里显示的组见名称是由vue中组件name决定的

12.vue-router 有哪些钩子函数?

  • 全局前置守卫 router.beforeEach
  • 全局解析守卫 router.beforeResolve
  • 全局后置钩子 router.afterEach
  • 路由独享的守卫 beforeEnter
  • 组件内的守卫 beforeRouteEnter 、 beforeRouteUpdate 、 beforeRouteLeave

13.route 和 router 的区别是什么?

route 是“路由信息对象”,包括 path , params , hash , query , fullPath , matched , name 等路由信息参数。 router 是“路由实例对象”,包括了路由的跳转方法( push 、 replace ),钩子函数等。

14.说一下 Vue 和 React 的认识,做一个简单的对比

(1)监听数据变化的实现原理不同
  • Vue 通过 getter/setter 以及一些函数的劫持,能精确快速的计算出 Virtual DOM 的差异。这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
  • React 默认是通过比较引用的方式进行的,如果不优化,每当应用的状态被改变时,全部子组件都会重新渲染,可能导致大量不必要的 VDOM 的重新渲染。
  • Vue 不需要特别的优化就能达到很好的性能,而对于 React 而言,需要通过PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制。如果你的应用中,交互复杂,需要处理大量的 UI 变化,那么使用 Virtual DOM 是一个好主意。如果你更新元素并不频繁,那么 Virtual DOM 并不一定适用,性能很可能还不如直接操控 DOM。

为什么 React 不精确监听数据变化呢?这是因为 Vue 和 React 设计理念上的区别,Vue 使用的是可变数据,而 React 更强调数据的不可变。

(2)数据流不同
  • Vue 中默认支持双向绑定,组件与 DOM 之间可以通过 v-model 双向绑定。但是,父子组件之间,props 在 2.x 版本是单向数据流
  • React 一直提倡的是单向数据流,他称之为 onChange/setState()模式。不过由于我们一般都会用Vuex 以及 Redux 等单向数据流的状态管理框架,因此很多时候我们感受不到这一点的区别了。
(3) 模板渲染方式的不同

在表层上,模板的语法不同

  • React 是通过 JSX 渲染模板
  • 而 Vue 是通过一种拓展的 HTML 语法进行渲染

在深层上,模板的原理不同,这才是他们的本质区别:

  • React 是在组件 JS 代码中,通过原生 JS 实现模板中的常见语法,比如插值,条件,循环等,都是通过 JS 语法实现的
  • Vue 是在和组件 JS 代码分离的单独的模板中,通过指令来实现的,比如条件语句就需要 v-if 来实现对这一点,我个人比较喜欢 React 的做法,因为他更加纯粹更加原生,而 Vue 的做法显得有些独特,会把 HTML 弄得很乱。举个例子,说明 React 的好处:react 中 render 函数是支持闭包特性的,所以我们 import 的组件在 render 中可以直接调用。但是在 Vue 中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们 import 一个组件完了之后,还需要在 components中再声明下,这样显然是很奇怪但又不得不这样的做法。

15.Vue 的 nextTick 的原理是什么?

(1)为什么需要 nextTick Vue 是异步修改 DOM 的并且不鼓励开发者直接接触 DOM,但有时候业务需要必须对数据更改--刷新后的 DOM 做相应的处理,这时候就可以使用 Vue.nextTick(callback)这个 api 了。

(2)理解原理前的准备 首先需要知道事件循环中宏任务和微任务这两个概念(这其实也是面试常考点)。 常见的宏任务有 script, setTimeout, setInterval, setImmediate, I/O, UI rendering 常见的微任务有 process.nextTick(Nodejs),Promise.then(), MutationObserver;

(3)理解 nextTick而 nextTick 的原理正是 vue 通过异步队列控制 DOM 更新和 nextTick 回调函数先后执行的方式。如果大家看过这部分的源码,会发现其中做了很多 isNative()的判断,因为这里还存在兼容性优雅降级的问题。可见 Vue 开发团队的深思熟虑,对性能的良苦用心。

16. Vuex 有哪几种属性?

有五种

  • State
  • Getter
  • Mutation
  • Action
  • Modul

17.Vue2.x和Vue3.x渲染器的diff算法分别说一下

简单来说,diff算法有以下过程

  • 同级比较,再比较子节点
  • 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
  • 比较都有子节点的情况(核心diff)
  • 递归比较子节点
正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

Vue3.x借鉴了ivi算法和 inferno算法

在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。(实际的实现可以结合Vue3.x源码看。)

18. vue源码看过吗,学到了什么?

  • 实现只执行一次的函数(vue/src/shared/util.js)

很多时候我们需要一个函数只被执行一次,就算它被调用多次,也只有第一次调用时会被执行,所以我们可以写出如下代码:

function once (fn) {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}
复制代码
  • 确定对象的类型(vue/src/shared/util.js)

我们可以利用Object.prototype.toString把一个对象转换成一个字符串,如果是我们用{}创建的 对象,这个方法总是返回[object Object]。

function isPlainObject (obj){
  return Object.prototype.toString.call(obj) === '[object Object]'
}
复制代码

我们注意到,Object.prototype.toString()的返回值总是以[object tag]的形式出现,如果我们 只想要这个tag,我们可以把其他东西剔除掉,这边比较简单用正则或者String.prototype.slice()都可以。

function toRawType (value) {
    const _toString = Object.prototype.toString
    return _toString.call(value).slice(8, -1)
}
toRawType(null) // "Null"
toRawType(/sdfsd/) //"RegExp"
复制代码

19.vue 首屏加载优化

  • 把不常改变的库放到 index.html 中,通过 cdn 引入,然后找到build/webpack.base.conf.js 文件,在 module.exports = { } 中添加以下代码
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT',
},
复制代码
  • vue 路由的懒加载

import 或者 require 懒加载。

  • 不生成 map 文件

找到 config/index.js,修改为 productionSourceMap: false

  • vue 组件尽量不要全局引入
  • 使用更轻量级的工具库

20.webpack 知道吗?

publicPath 类型:String 默认:'/'

部署应用包时的基本 URL。默认情况下,Vue CLI会假设你的应用是被部署在一个域名的根路径上,例如https://www.my-app.com/。 如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在https://www.my-app.com/my-app/,则设置publicPath为/my-app/

这个值也可以被设置为空字符串 ('') 或是相对路径 ('./'),这样所有的资源都会被链接为相对路径,这样打出来的包可以被部署在任意路径,也可以用在类似 Cordova hybrid 应用的文件系统中。

productionSourceMap

类型:boolean

moren:true 不允许打包时生成项目来源映射文件,在生产环境下可以显著的减少包的体积

注 Source map的作用:针对打包后的代码进行的处理,就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便

assetsDir

放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录,默认是'',

indexPath

指定生成的 index.html 的输出路径(相对于outputDir)。也可以是一个绝对路径。默认是'index.html'

lintOnSave

是否在每次保存时使用eslint检查,这个对语法的要求比较严格,对自己有要求的同学可以使用 css

    //是否启用css分离插件,默认是true,如果不启用css样式分离插件,打包出来的css是通过内
    联样式的方式注入至dom中的,
    extract: true,
    sourceMap: false,//效果同上
    modules: false,// 为所有的 CSS 及其预处理文件开启 CSS Modules。
    // 这个选项不会影响 `*.vue` 文件。
  },
复制代码

devServer

本地开发服务器配置,此处直接贴上我常用的配置,以注释的方式介绍

    //配置开发服务器
    host: "0.0.0.0",
    //是否启用热加载,就是每次更新代码,是否需要重新刷新浏览器才能看到新代码效果
    hot: true,
    //服务启动端口
    port: "8080",
    //是否自动打开浏览器默认为false
    open: false,
    //配置http代理
    proxy: { 
      "/api": { //如果ajax请求的地址是http://192.168.0.118:9999/api1那么你就可以
      jajx中使用/api/api1路径,其请求路径会解析
      // http://192.168.0.118:9999/api1,
      当然你在浏览器上开到的还http://localhost:8080/api/api1;
        target: "http://192.168.0.118:9999",
        //是否允许跨域,这里是在开发环境会起作用,但在生产环境下,还是由后台去处理 所以不必太在意
        changeOrigin: true,
        pathRewrite: {
            //把多余的路径置为''
          "api": ""
        }
      },
      "/api2": {//可以配置多个代理,匹配上那个就使用哪种解析方式
        target: "http://api2",
        // ...
      }
    }
},
复制代码

pluginOptions

这是一个不进行任何 schema 验证的对象,因此它可以用来传递任何第三方插件选项,例如:

{
    //定义一个全局的less文件,把公共样式变量放入其中,这样每次使用的时候就不用重新引用了
    'style-resources-loader': {
      preProcessor: 'less',
      patterns: [
        './src/assets/public.less'
      ]
    }
}
复制代码

chainWebpack

是一个函数,会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改。例如:

    //定义一个全局的less文件,把公共样式变量放入其中,这样每次使用的时候就不用重新引用了
    'style-resources-loader': {
      preProcessor: 'less',
      patterns: [
        './src/assets/public.less'
      ]
    }
}
复制代码

往期面试专题热门文章

文章分类
前端
文章标签