2020前端面试 - Vue.js篇

前言:

2020年是多灾多难的一年,疫情持续至今,到目前,全世界的经济都受到不同程序的影响,各大公司裁员,在这样一片严峻的形式下,找工作更是难上加难。

企业的门槛提高,第一,对于学历的要求,必须学信网可查的统招本科;第二,对于技术的掌握程序,更多的是底层原理,项目经验,等等。

下面是面试几周以来,总结的一些面试中常被问到的题目,还有吸取的一些前辈们分享的贴子,全部系统的罗列出来,希望能够帮到正在面试的人。

1. Vue原理
  • Vue是采用数据劫持配合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的gettersetter

  • 在数据发生变化的时候,发布消息给依赖收集器,去通知观察者,做出对应的回调函数去更新视图。

  • 具体执行流程:
    1.MVVM作为绑定入口,整合Observe,CompilWatcher三者,通过Observe来监听model的变化。
    2.通过Compil来解析编译模版指令,最终利用Watcher搭起ObserveCompil之前的通信桥梁。
    3.从而达到数据变化 => 更新视图,视图交互变化(input) => 数据model变更的双向绑定效果。

2. Vue的生命周期
  • 单个组件的生命周期
    1.beforeCreated
    2.created
    3.beforeMounted
    4.mounted
    5.activated
    6.beforeUpdated
    7.updated
    8.deactivated
    9.beforeDestory
    10.destoryed

  • 父子组件的生命周期顺序
    1.父组件先执行(beforeCreated -> created -> beforeMounted)函数
    2.父组件挂载前,子组件再执行(beforeCreated -> created -> beforeMounted -> mounted )。
    3.子组件挂载完成之后,最后执行父组件挂载函数mounted
    4.接着是下面的三种情况:
    (1)更新
    只更新父或子: 局部更新,父或子beforeUpdate -> updated
    同时更新父和子: 父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
    (2)销毁父组件
    父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
    (3)激活父组件
    子activated -> 父activated -> 停止 -> 子deactivated -> 父deactivated

3. Vue响应式原理
  • 使用 defineReactive 函数将深度遍历一个对象(或循环遍历数组),将对象构建成响应式式对象。明显的标志就是 ob 属性 实质是通过 Object.defineProperty对属性(深度遍历)进行 settergetter 拦截。

  • get中主要做依赖收集dep.depend() 【子属性也收集该依赖】

  • set中主要做派发更新 (新的值才更新)dep.notify()调用dep数组中每个渲染watcherupdate方法更新DOM

  • 响应式对象使用应该注意哪些点
    1.对象的新增属性,数组的新增元素,因为不是响应式的,所以不会触发视图渲染。此时应该使用 $set
    2.改变某一下标的元素,因为Vue未实现监听,所以不会触发视图渲染。此时应该使用 $set
    3.删除对象的属性,数组下标的某一元素,确保删除属性能触发视图渲染。此时应该使用 $delete

4. data为什么必须是函数而不是对象?
  • 首先举个栗子
var option = {
  data: {
    a: 1
  }
}

class component {
  constructor(opt) {
    this.data = opt.data;
    Object.defineProperty(this.data, `a`, {
      get: function () {
        console.log('get val');
        return this._a;
      },
      set: function (newVal) {
        console.log('set val:' + newVal);
        this._a = newVal;
      }
    });
  }
}

var c1 = new component(option);
var c2 = new component(option);
c1.data.a = 3;
c2.data.a = 5;
console.log(`c1 : ` + c1.data.a);//c1 : 5
console.log(`c2 : ` + c2.data.a);//c2 : 5

示例代码中 Object.defineProperty 把传进来组件中的dataa 属性转化为 gettersetter,可以实现 data.a的数据监控。这个转化是Vue.js 响应式的基石。

这样就不难理解data为什么不能是对象了,如果传进来是对象,new出来的两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。

总结:
1.对象是对于内存地址的引用。直接定义个对象的话,组件之间都会使用这个对象,这样会造成组件之间数据相互影响。
2.组件就是为了抽离开来提高复用的, 如果组件之间数据默认存在关系,就违背了组件的意义。
3.函数 return 一个新的对象,其实还是为 data 定义了一个对象, 只不过这个对象是内存地址的单独引用了,这样组件之间就不会存在数据干扰的问题。

5. v-model基本原理
  • 首先在编译阶段,v-model被当成普通指令解析到el.directives,然后在解析v-model的时候,会根据节点的不同请求去执行不同的逻辑。
    1.如果节点是selectcheckbox, radio,则监听的是change事件
    2.如果节点是inputtextarea,则监听一般是input事件,在.lazy下的情况下是change事件。
    3.如果节点是组件,则是使用自定义的回调函数

  • 在运行的时候,通过相应事件的监听函数去更改数据
    v-model实质是一种语法糖,换成模板写法如下:


  • 组件中使用v-model
// 自定义属性名和事件名需要一致
export default { 
  model: { 
    prop: 'num', // 自定义属性名 
    event: 'addNum' // 自定义事件名 
  }, 
  props: { 
    num: Number, 
  }, 
 
  methods: { 
    add() { 
      this.$emit('addNum', this.num + 1) 
    }
  }
}
6. vue2.0响应式的缺陷
  • Object.defineProperty无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应

  • Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,弃用了这个特性。

  • Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历

7. Vue3.0为什么使用Proxy实现响应式
  • Proxy可以劫持整个对象,并返回一个新的对象。
  • Proxy不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。
8. Vue的通信方式
  • props$emit
  • $parent$children
  • vueBus: 中央事务总线,一个发布订阅中心
  • vuex:状态树管理(单一的)
  • refrefs
  • $attr$listener: v-bind="$attrs" v-on="$listeners"
  • provideinject: 实质就是递归父组件帮你寻找对应的provider
  • provideinject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
9. Vue.nextTick的原理
  • Vue.nextTick是在执行render渲染后运行的,即在render渲染后的下一次tickevent loop最开始的时候执行)

  • Vue.nextTikc的降级顺序(优先使用) Promise.then(microtask) , MutationObserver(microtask) , setImmediate(task) , setTimeout(fn, 0)(task)

  • Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

  • 应用场景
    1.在Vue生命周期的created()钩子函数进行DOM操作一定要放到Vue.nextTick()的回调函数中。
    2.在数据变化后要执行的某个操作,而这个操作需要使用随数据改变而改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中。

  • 10. new Vue会做什么操作

Vue.prototype._init = function (options) { 
    const vm = this 
    // ...忽略,从第45行看起 
    if (process.env.NODE_ENV !== 'production') { 
      initProxy(vm) // 作用域代理,拦截组件内访问其它组件的数据
    } else {
      vm._renderProxy = vm 
    } 
    // expose real self 
    vm._self = vm 
    initLifecycle(vm) // 建立父子组件关系,在当前实例上添加一些属性和生命周期标识。
    initEvents(vm) // 用来存放除 @hook:生命周期钩子名称="绑定的函数"事件的对象。如:$on、 $emit等 
    initRender(vm) // 用于初始化 $slots、 $attrs、 $listeners 
    callHook(vm, 'beforeCreate') 
    initInjections(vm) // resolve injections before data/props  // 初始化 inject,一般用于更深层次的组件通信,相当于加强版子组件的 props。用于组件库开发较多 
    initState(vm) // 是很多选项初始化的汇总,包括:props、methods、data、computed和watch 等。
    initProvide(vm) // resolve provide after data/props   // 初始化 provide 
    callHook(vm, 'created') 
    // ...忽略
    if (vm.$options.el) { 
      vm.$mount(vm.$options.el)  // 挂载实例 
    }
  }
11. Vue的diff原理
  • 主要执行的是patch函数。主要流程如下:
function patch (oldVnode, vnode, hydrating, removeOnly)

1.如果oldVnode不存在,即是新添加的节点,则创建vnode的DOM
2.如果不是真实的节点且是相同类型的节点,则进入结点diff,即patchVnode函数。否则会用新的节点替换老的。这里的相同类型指的是具有相同的key值和一些其他条件,例如标签相同等等。
3.如果oldVnode === vnode,则认为没有变化, 如果oldVnodeisAsyncPlaceholder属性为true时,跳过检查异步组件,return
4.如果oldVnodevnode都是静态节点(实例不会发生变化),且具有相同的key,并且当vnode是克隆节点或是v-once指令控制的节点时,则把oldVnode.elmoldVnode.child都复制到vnode上;
5.如果vnode不是文本节点或注释节点
(1)如果vnodeoldVnode都有子节点并且两者的子节点不一致时,就调用updateChildren更新子节点
(2)如果只有vnode有子节点,则调用addVnodes创建子节点
(3)如果只有oldVnode有子节点,则调用removeVnodes把这些子节点都删除
(4)如果vnode文本为undefined,则清空vnode.elm文本;
6.如果vnode是文本节点但是vnode.text != oldVnode.text时只需要更新vnode.elm的文本内容就可以。
7.在updateChildren主要是子节点数组对比,思路是通过首尾两端对比,如果是相同类型的节点也会使用patchVnode函数。

  • 在做对比中key 的作用 主要是
    1.决定节点是否可以复用
    2.建立key-index的索引,主要是替代遍历,提升性能
12. computed 和 watcher
  • computed是计算属性,依赖其他属性计算,并且computed的值有缓存,只有当计算值发生变化才会返回内容。所以,对于任何复杂逻辑,你都应当使用计算属性。

  • watch主要用于监控vue实例的变化,它监控的变量当然必须在data里面声明才可以,它可以监控一个变量,也可以是一个对象。比较适合的场景是一个数据影响多个数据。

  • watch支持异步。

  • watcher的分类
    1.内部-watcher vue组件上的每一条数据绑定指令(例如{{myData}})和computed属性,通过compile最后都会生成一个对应的 watcher 对象。
    2.user--watcherwatch属性中,由用户自己定义的,都属于这种类型,即只要监听的属性改变了,都会触发定义好的回调函数
    3.render-watcher每一个组件都会有一个 render-watcher, function () {vm._update(vm._render(), hydrating);}, 当 data / computed中的属性改变的时候,会调用该 render-watcher 来更新组件的视图

watcher 也有固定的执行顺序,分别是:内部-watcher -> user-watcher -> render-watcher

13. Vue指令
// 全局
Vue.directive('my-click', config) 

// 局部
new Vue({ 
    directives:{ 
        focus: config // v-focus 
    }
}})
  • 配置参数
    1.一个指令定义对象可以提供如下几个钩子函数 (均为可选):

(1)bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
(2)inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
(3)update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
(4)componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
(5)unbind:只调用一次,指令与元素解绑时调用。

每个钩子函数都有四个参数el、binding、vnode 和 oldVnode

14. 混入 (mixin)
  • 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。
  • 全局和局部mixin
var mixin = { 
  data: function () { 
    return { 
      message: 'hello', 
      foo: 'abc' 
    } 
  } 
} 
 
Vue.mixin(mixin) 

 
new Vue({ 
  mixins: [mixin],
})
  • 合并策略
    1.钩子函数将合并成数组,且混入的函数先执行
    2.其他的值为对象的将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
    3.默认的合并策略可以使用下面的方面更改
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // 返回合并后的值 
}
15. vue-router
  • Vue RouterVue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌

  • 路由模式
    1.HashHistory模式:实质是监听onhashchange事件 (window.location API - location.hash
    2.HTML5History模式:实质是使用h5的 window.history API, 监听popstate事件(pushState, replaceState)。使用该模式,服务器和前端需要做好页面404的处理
    3.AbstractHistory模式:在不支持上面两种方式的环境下使用,如node环境,实际是使用数组模拟路由历史栈

  • 导航守卫

// 全局守卫
// 在项目中,一般在beforeEach这个钩子函数中进行路由跳转的一些信息判断。
判断是否登录,是否拿到对应的路由权限等等。
router.beforeEach((to, from, next) => {
  to: Route:  // 即将要进入的目标 路由对象

  from: Route: // 当前导航正要离开的路由

  next: Function: // 一定要调用该方法来 resolve 这个钩子。
})
router.afterEach((to, from) => {})
router.beforeResolve((to, from) => {})  
// 与afterEach类似, 区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用

// 路由独享守卫
const router = new VueRouter({ 
  routes: [ 
    { 
      path: '/foo', 
      component: Foo,
      beforeEnter: (to, from, next) => {}, 
      ...
    }
  ]
})

// 组件内守卫
const Foo = { 
  template: `...`, 
  beforeRouteEnter (to, from, next) { 
    // 在渲染该组件的对应路由被 confirm 前调用 
    // 不!能!获取组件实例 `this` 
    // 因为当守卫执行前,组件实例还没被创建 
  },
  beforeRouteUpdate (to, from, next) {
     // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, 
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this` 
  },
  beforeRouteLeave (to, from, next) { 
    // 导航离开该组件的对应路由时调用 
    // 可以访问组件实例 `this`
  }
16. VueRouter
  • 基于vue的插件机制,全局混入beforeCreateddestroyed的生命钩子
  • 查找根实例上的route,注入到每个组件上,监听current变化
Vue.util.defineReactive(this, '_route', this._router.history.current)
  • vue原型上添加两个属性$router$route, 拦截get操作,限制set操作
Object.defineProperty(Vue.prototype, '$router', { 
    get () { return this._routerRoot._router } 
})
  • 注册全局组件RouterView 和 RouterLink
17. Vuex
  • Vue.js 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • 核心概念
    1.state:使用单一状态树,用一个对象就包含了全部的应用层级状态。
    2.getter:可看成数据的computed计算属性
    3.mutation:唯一更改数据的方法 通过 store.commit 使用相应的 mutation方法
    4.Action:支持异步的提交mutation 通过 store.dispatch 使用相应的Action方法
    5.module:数据分模块。如moduleA.xx

  • 如何注入
    在使用 Vue.use(vuex)的时候会执行install 方法在(vue插件机制)。这个方法会混入一个minxin

Vue.mixin({ 
    beforeCreate() { 
        const options = this.$options 
        // store injection 
        // 非根组件指向其父组件的$store,使得所有组件的实例,都指向根的store对象 
        if (options.store) { 
          this.$store = typeof options.store === 'function' 
            ? options.store() 
            : options.store 
        } else if (options.parent && options.parent.$store) { 
          this.$store = options.parent.$store 
        }
    }
})
  • 如何实现响应式
    通过添加到data中实现响应式
store._vm = new Vue({ 
  data: { 
    $$state: state 
  }, 
  computed // 这里是store的getter 
})
18. 首屏加载慢的优化方案
  • webpack来打包Vue项目,vendor.js过大问题解决
    1.造成过大的原因是因为在main.js导入第三库太多时,webpack合并js时生成了vendor.js(我们习惯把第三方库放在vendor里面)造成的,js文件过多,拖慢加载速度,所以:首先在index.html中,使用CDN的资源

    
    
    
    

2.在bulid/webpack.base.conf.js文件中,添加externals,导入index.html下所需的资源模块:

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: ['babel-polyfill', 'lib-flexible', './src/main.js']
  },
  externals: { // <-添加
    vue: 'Vue',
    vuex: 'Vuex',
    'vue-router': 'VueRouter',
    VueAwesomeSwiper: 'VueAwesomeSwiper'
  },

3.在main.js里将以下 import 注释 替换 require 引入模块

// import Vue from 'vue'
// import VueAwesomeSwiper from 'vue-awesome-swiper'

const Vue = require('vue')
const VueAwesomeSwiper = require('VueAwesomeSwiper')

Vue.use(VueAwesomeSwiper)

4.当然可以在生产环境当中删除掉不必要的console.log,打开build/webpack.prod.conf.jsplugins里添加以下代码

plugins: [
    new webpack.optimize.UglifyJsPlugin({ //添加-删除console.log
      compress: {
        warnings: false,
        drop_debugger: true,
        drop_console: true
      },
      sourceMap: true
    }),

5.执行npm run build之后,会发现文件的体积明显小了很多,如果把一些Ui库也替换成CDN的方式,可能体积会更小,渲染解析更快。

  • Vue-cli开启打包压缩 和后台配合 gzip访问开启打包压缩 和后台配合 gzip访问
    1.首先打开 config/index.js,找到 build 对象中的productionGzip ,改成 true
    2.打开 productionGzip: true 之前,先要安装依赖 compression-webpack-plugin ,官方推荐的命令是:
npm install --save-dev compression-webpack-plugin 
//(此处有坑) 如果打包报错,应该是版本问题 ,先卸载之前安装的此插件 ,然后安装低版本 
 npm install --save-dev [email protected]

3.等安装好了,重新打包 npm run build,此时打包的文件会 新增 .gz文件。是不是比原来的js文件小很多呢,之后项目访问的文件就是这个.gz文件
4.后台nginx开启gzip模式访问,浏览器访问项目,自动会找到 .gz的文件。加载速度明显提高。

http {  //在 http中配置如下代码,
   gzip on;
   gzip_disable "msie6"; 
   gzip_vary on; 
   gzip_proxied any;
   gzip_comp_level 8; #压缩级别
   gzip_buffers 16 8k;
   #gzip_http_version 1.1;
   gzip_min_length 100; #不压缩临界值
   gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
 }
19. Vue核心之虚拟DOM
  • 真实DOM和其解析流程,浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树-->创建StyleRules-->创建Render树-->布局Layout-->绘制Painting
    1.用HTML分析器,创建DOM树。
    2.用CSS分析器,生成样式规则表。
    3.关联DOM树和规则表,生成渲染树。
    4.通过渲染树计算节点属性。
    5.通过计算好的节点属性,渲染页面

DOM树的构建是文档加载完成开始的?构建DOM数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render数和布局。

Render树是DOM树和CSSOM树构建完毕才开始构建的吗?这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。

CSS的解析是从右往左逆向解析的(从DOM树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。

  • JS操作真实DOM的代价
    用我们传统的开发模式,原生JSJQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。
  • 虚拟DOM有什么好处?虚拟DOM,其实是一个大对象
    1.Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化。
    2.虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attchDOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

你可能感兴趣的:(2020前端面试 - Vue.js篇)