2020前端面试 - Vue.js篇

作者: 西巴撸 | 来源:发表于2020-06-14 01:49 被阅读0次

    前言:

    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实质是一种语法糖,换成模板写法如下:

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

    • 路由模式
      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的资源
    <body>
        <script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
        <script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
        <script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
        <script src="https://unpkg.com/vue-awesome-swiper@2.6.7/dist/vue-awesome-swiper.js"></script>
    </body>
    

    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 compression-webpack-plugin@1.1.11
    

    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篇

        本文链接:https://www.haomeiwen.com/subject/dzutxktx.html