vue前端开发那些事儿面试题库

2020前端面试 - Vue.js篇

2020-06-14  本文已影响0人  西巴撸

前言:

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

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

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

1. Vue原理
2. Vue的生命周期
3. Vue响应式原理
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基本原理
<input :value="sth" @input="sth = $event.target.value" />
// 自定义属性名和事件名需要一致
export default { 
  model: { 
    prop: 'num', // 自定义属性名 
    event: 'addNum' // 自定义事件名 
  }, 
  props: { 
    num: Number, 
  }, 
 
  methods: { 
    add() { 
      this.$emit('addNum', this.num + 1) 
    }
  }
}
6. vue2.0响应式的缺陷
7. Vue3.0为什么使用Proxy实现响应式
8. Vue的通信方式
9. Vue.nextTick的原理

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原理
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函数。

12. computed 和 watcher

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

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

// 局部
new Vue({ 
    directives:{ 
        focus: config // v-focus 
    }
}})

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

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

14. 混入 (mixin)
var mixin = { 
  data: function () { 
    return { 
      message: 'hello', 
      foo: 'abc' 
    } 
  } 
} 
<!-- 全局mixin --> 
Vue.mixin(mixin) 

<!-- 局部mixin --> 
new Vue({ 
  mixins: [mixin],
})
Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) {
  // 返回合并后的值 
}
15. vue-router
// 全局守卫
// 在项目中,一般在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.util.defineReactive(this, '_route', this._router.history.current)
Object.defineProperty(Vue.prototype, '$router', { 
    get () { return this._routerRoot._router } 
})
17. Vuex
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 
        }
    }
})
store._vm = new Vue({ 
  data: { 
    $$state: state 
  }, 
  computed // 这里是store的getter 
})
18. 首屏加载慢的优化方案
<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的方式,可能体积会更小,渲染解析更快。

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

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

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

上一篇下一篇

猜你喜欢

热点阅读