2023前端面试题及答案整理(Vue)

Posted suli77

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2023前端面试题及答案整理(Vue)相关的知识,希望对你有一定的参考价值。

watch 和 computed 区别

  • watch 是监听动作,computed 是计算属性
  • watch 没缓存,只要数据变化就执行。computed 有缓存,只在属性变化的时候才去计算。
  • watch 可以执行异步操作,而 computed 不能
  • watch 常用于一个数据影响多个数据,computed 则常用于多个数据影响一个数据

讲一下 Vue 的生命周期?

创建期间的生命周期函数:

  • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好 data 和 methods 属性
  • created:实例已经在内存中创建OK,此时 data 和 methods 已经创建OK,此时还没有开始 编译模板
  • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中。
    • 换句话说,此时页面中的类似 msg 这样的语法还没有被替换成真正的数据。
  • mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示【可以获取 DOM 节点 | 发起异步请求】
    • 用户已经可以看到渲染好的页面了

运行期间的生命周期函数:

  • beforeUpdate:状态更新之前执行此函数, 此时 data 中的状态值是最新的,但是界面上显示的 数据还是旧的,因为此时还没有开始重新渲染DOM节点
  • updated:实例更新完毕之后调用此函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了!

销毁期间的生命周期函数:

  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

Vue 的父组件和子组件生命周期钩子执行顺序是什么

  1. 加载渲染过程
    父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  2. 子组件更新过程
    父beforeUpdate->子beforeUpdate->子updated->父updated
  3. 父组件更新过程
    父beforeUpdate->父updated
  4. 销毁过程
    父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

总结:从外到内,再从内到外

Vue 中父组件如何监听子组件的生命周期?

v-on / $emit

<child-comp @child-event="handleChildEvent"></child-comp></div>
Vue.component('child-comp', 
  template: '<div></div>',
  data: function () 
    return 
      childMsg: 'Hello, I am Child'
    ;
  ,
  methods: ,
  mounted() 
    this.$emit('child-event');
  
);
const app = new Vue(
  el: '#app',
  data: function () 
    return 
      parentData: 'parent Message'
    ;
  ,
  beforeCreate: function () 
    console.log('before created');
  ,
  methods: 
    handleChildEvent() 
      console.log('child mounted');
    
  
);

在子组件中的 mounted 钩子函数中调用 this.$emit("child-event"); 向父组件发送 child-event 消息。 父组件 @child-event="handleChildEvent" 监听了此消息。

@hook

假如我们这里的子组件是外部的,是不可更改的。那我们父组件监听这个外部子组件中的生命周期钩子函数怎么办呢?

<div id="app">
  <child-comp @hook:mounted="handleChildEvent"></child-comp>
</div>
Vue.component('child-comp', 
  template: '<div></div>',
  data: function () 
    return 
      childMsg: 'Hello, I am Child'
    ;
  ,
  methods: ,
  mounted() 
    //this.$emit("child-event");
  
);
const app = new Vue(
  el: '#app',
  data: function () 
    return 
      parentData: 'parent Message'
    ;
  ,
  beforeCreate: function () 
    console.log('before created');
  ,
  methods: 
    handleChildEvent() 
      console.log('child mounted');
    
  
);

把子组件中的 mounted 钩子函数中的 $emit 方法去掉, 在父组件中使用 @hook:mounted

更多阅读

组件

现在有个父子组件,我希望在父级中给子组件绑定一个原生click事件,这个事件会被触发吗?

<div id='app'>
  <my-button @click='change'></my-button>
</div>
<script>
 export default 
  methods: 
    change() 
      alert(1)
    
  
 
</script>

答:不能,绑定的该click事件会被当做组件上的一个普通属性看待,如果想要使click事件生效,可以使用 @click.native='change' 的方式来实现。

为什么Vue实例对象中的data直接是个对象,而组件内的data是个函数,且返回一个对象?

因为组件中是 data: 的话,这个 是个对象,引用类型。如果多处地方引用同一个组件的话,则共享同一个data对象,这是不合理的。所以需要每次使用组件时,return一个新的对象,这样就不会共享了。

组件间如何通讯?

  • props/$emit+v-on: 通过props将数据自上而下传递,而通过$emit和v-on来向上传递信息。
  • EventBus: 通过EventBus进行信息的发布与订阅
  • vuex: 是全局数据管理库,可以通过vuex管理全局的数据流
  • $attrs/$listeners: Vue2.4中加入的$attrs/$listeners可以进行跨级的组件通信
  • provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础

Vue 组件间通信六种方式

this.$emit 的返回值是什么?如果需要返回值该怎么办?

this.$emit 的返回值就是 this ,即当前子组件 VueComponent 。

如果想要有返回值可以如下操作:

子组件

<template>
  <input :value="name" @change="handleChange" />
</template>
<script>
export default 
  props: ['name'],
  methods: 
    handleChange(e) 
      const res = this.$emit("Echange", e.target.value, val => 
    console.log(val);
      );
      console.log(res, res === this);
    ,
  

</script>

父组件

<template>
  <Child :name='name' @Echange="handleEventChange" />
</template>
<script>
export default 
  data() 
    return 
      name: '',
    
  
  methods: 
    handleEventChange(val, callback) 
      this.name = val
      callback("hello")
      return 'hello'
    
  

</script>

filter 过滤器

filter中的this是什么?

this是undefined,在filter中拿不到vue实例。filter应该是个纯函数,不应该依赖外界或者对外界有所影响。如果需要用到this,可以用 computed 或者 method 代替。

vue 指令

能讲下 v-if 和 v-show 的区别吗?

  • v-if: 是否加载这个元素(一次性的)
  • v-show:控制显示方式block or none(需要切换的,侧边栏)

因此:如果需要频繁切换 v-show 较好,如果在运行时条件不大可能改变 v-if 较好

v-for 你使用过程中,有遇到什么问题或者关注点吗?

  1. 避免将 v-ifv-for 放在同一个元素上,因为 v-for 优先级比 v-if 更高。例如要渲染 todo 列表中未完成的任务,给 li 标签同时写上 v-for 和 v-if 后会导致每次重新渲染都得遍历整个列表。优化方案是把需要遍历的 todoList 更换为在计算属性上遍历过滤。(Vue文档有详细说明)
  2. v-for 设置键绑定键值 key。理由见下。

在列表组件中添加 key 属性的作用?

key的主要作用就是在更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。这样可以更高效的更新虚拟 DOM。

另外如果给列表组件设置了过渡效果,不添加key属性会导致过渡效果无法触发。因为不添加key会导致vue无法区分它们,导致只会替换节点内部属性而不会触发过渡效果。

为什么不建议用 index 作为 key 呢?

更新DOM时会出现性能问题

例如我们在使用index作为key值时想要下面列表进行倒序排列:

<li key='0'>React</li>
<li key='1'>Vue</li>
<li key='2'>Angular</li>
<!-- 倒序后↓↓↓↓↓↓↓↓↓↓ -->
<li key='0'>Angular</li>
<li key='1'>Vue</li>
<li key='2'>React</li>

vue这个时候仅会调换第一和第三项的文本,li元素则不会调换顺序,因为vue发现key值也就是index没变化。而这会导致li里面的文本重新渲染,影响性能。

而如果采用 id 作为 key,则仅仅需要移动下DOM就OK了,并不需要重新渲染DOM:

<li key='react'>React</li>
<li key='vue'>Vue</li>
<li key='angular'>Angular</li>
<!-- 倒序后↓↓↓↓↓↓↓↓↓↓ -->
<li key='angular'>Angular</li>
<li key='vue'>Vue</li>
<li key='react'>React</li>

会发生一些状态bug

还是以上面例子为例,给每个li元素中加一个checkbox:

<li key='react'><input type='checkbox'>React</li>
<li key='vue'><input type='checkbox'>Vue</li>
<li key='angular'><input type='checkbox'>Angular</li>

如果采用 index 作为 key,选中状态会出现bug(左边列表采用index,右边采用id形式):

原因也是 index 没有变化被复用了,导致选中状态永远都是 index=0 的第一项。

阅读更多:

数据响应式(双向绑定)怎么做到的?

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

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

参考:

追问1:那如果我要监听一个对象属性的删除或添加呢?

受 defineProperty 限制,Vue 无法检测对象属性的删除和添加。所以我们可以利用 Vue 提供的 Vue.set 来解决此问题。

例子:

有一个obj:a:1,想要this.obj.b=233,不会触发视图更新

Vue.set(this.obj, 'b', 233) or this.$set(this.obj, 'b', 233)

追问2:为什么对象属性的删除或添加无法触发页面更新

因为 vue 在实例化过程中,深度遍历了 data 下所有属性, 把属性全转为 getter/setter 。这样才能监听属性变化。所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。

当你在对象上新加了一个属性 newProperty ,当前新加的这个属性并没有加入 vue 检测数据更新的机制(因为是在初始化之后添加的), vue.$set 是能让 vue 知道你添加了属性, 它会给你做处理。

js 实现简单的双向绑定

<body>
  <div id="app">
    <input type="text" id="txt">
    <p id="show"></p>
  </div>

  <script>
    window.onload = function () 
      let obj = ;
      window.obj = obj
      Object.defineProperty(obj, "txt", 
        get: function () 
          return obj;
        ,
        set: function (newValue) 
          document.getElementById("txt").value = newValue;
          document.getElementById("show").innerhtml = newValue;
        
      )
      document.addEventListener("keyup", function (e) 
        obj.txt = e.target.value;
      )
    
  </script>
</body>

Vue 的响应式原理中 Object.defineProperty 有什么缺陷?为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

  1. Vue 中使用 Object.defineProperty 进行双向数据绑定时,告知使用者是可以监听数组的,但是只是监听了数组的 push()、pop()、shift()、unshift()、splice()、sort()、reverse() 这八种方法,其他数组的属性检测不到。
  2. Object.defineProperty 无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应;
  3. Object.defineProperty 只能劫持对象的属性,因此对每个对象的属性进行遍历时,如果属性值也是对象需要深度遍历,那么就比较麻烦了,所以在比较 Proxy 能完整劫持对象的对比下,选择 Proxy。
  4. Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

Vue模板渲染的原理是什么?

vue中的模板template无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所有需要将template转化成一个javascript函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。

模板编译又分三个阶段,解析parse,优化optimize,生成generate,最终生成可执行函数render。

  • parse阶段:使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。
  • optimize阶段:遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能。
  • generate阶段:将最终的AST转化为render函数字符串。

来源:https://juejin.im/post/6870374238760894472

Vuex 用过吗?

Vuex 是专为 Vue 应用程序开发的状态管理工具,相当于共享仓库,方便任何组件直接获取和修改。

  • state - 数据【存项目共享状态,是响应式的,store的数据改变,所有依赖此状态的组件会更新】
    • $store.state.count
  • mutations - 方法【同步函数】
    • inc(state, 参数唯一)
    • $store.commit(‘inc’, 2)
  • getters - 包装数据 【store的计算属性,可缓存】
    • show: function(state)
    • this.$store.getters.show
    • 传参,返回函数:show(state) return function(参数) return …【不会缓存数据】
  • actions -【异步操作】【提交的是mutations,不直接修改状态】
    • increment(context, num) context.commit()
    • this.$store.dispatch(‘’,arg)

使用 Vuex 管理数据,与直接在全局window下定义变量相比,有什么区别或者说优势?

全局作用域下定义的数据是静态的,只能通过手动修改,修改后数据变了,但使用这些数据的组件并不会重新渲染,也必须得手动渲染。而且全局作用域下定义太多变量还容易造成变量污染。

Vuex 只要 store 中的数据更新,就会立即渲染所有使用 store 数据的组件。Vuex 使用单向数据流,要想修改 store 数据需要经过 action 层,mutation 层,层次划分明确,便于管理。

Vuex 是通过什么方式提供响应式数据的?

在 Store 构造函数中通过 new Vue() 实现的。利用 Vue 来监听 state 下的数据变化,给状态添加 getter、setter。

Vuex 如何区分 state 是外部直接修改,还是通过 mutation 方法修改的?

Vuex 中修改 state 的唯一渠道就是执行 commit(‘xx’, payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。

Vuex 原理

vuex 仅仅是作为 vue 的一个插件而存在,不像 Redux,MobX 等库可以应用于所有框架,vuex 只能使用在 vue 上,很大的程度是因为其高度依赖于 vue 的 computed 依赖检测系统以及其插件系统,

vuex 整体思想诞生于 flux,可其的实现方式完完全全的使用了 vue 自身的响应式设计,依赖监听、依赖收集都属于 vue 对对象 Property set get 方法的代理劫持。最后一句话结束 vuex 工作原理,vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件。

VueRouter 是什么?你平常是怎么用的?

  • 是什么:Vue-Router 是 Vue 官方的路由管理器

  • 作用:为了页面跳转

  • 原理:监听锚点值改变,渲染指定页面

    <div class="h">我是头部</div>
    <div id="content" class="b"></div>
    <div class="f">我是底部</div>
    <script type="text/javascript">
    //监视锚点值的改变
    window.addEventListener('hashchange', function() 
        var text = '';
        switch (location.hash) 
            case '#/music':
                text = '各种音乐的数据';
                break;
            case '#/movie':
                text = '各种电影的数据';
                break;
        
        document.getElementById('content').innerHTML = text;
    )
    </script>
    

动态路由

查询字符串:

  1. 去哪(列表页传参) xxx
  2. 导航(router中) name:‘detail’ , path:‘/detail’, component: Detail
  3. 去了干嘛(详情页接收参数)this.$route.query.id

path方式:

  1. 去哪里 xxx
  2. 导航 name:‘detail’ , path:‘/detail/:name’, component: Detail
  3. 去了干嘛(获取路由参数)this.$route.params.name

编程式导航

// name配params:
this.$router.push(name: 'Goods', params: goodsId:id)

// path配query:
this.$router.push(path: '/goods', query: goodsId:id)

// 参数接收匹配:
this.goodsId = this.$route.query.goodsId
this.goodsId = this.$route.params.goodsId

路由守卫

  • 全局守卫:beforeEach, beforeResolve, afterEach【没有next】
  • 路由独享守卫:beforeEnter
  • 组件内守卫:beforeRouteEnter【唯一next有回调】, Update, Leave

应用

  • 全局守卫:beforeEach(用户登录以及权限判定)

    router.beforeEach((to, from, next) => 
      const isLogin = localStroage.token
      // 个人中心需要登录
      if (to.name === 'Member') 
        isLogin ? next() : next('/login')
       else 
        next()
      
    )
    
  • 组件内守卫:beforeRouteEnter(根据用户从何而来,修改当前组件标题)

    // 详情页组件
    beforeRouteEnter(to, from, next) 
        let title = ''
        title = from.name === 'news.list' ? '新闻详情' : '商品详情'
        // 一定要调用 next ,否则无法从列表页跳转到详情页
        next(vm => vm.title = title) // 通过 vm 访问组件实例
    
    

讲一下完整的Vue路由生命周期

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter以上是关于2023前端面试题及答案整理(Vue)的主要内容,如果未能解决你的问题,请参考以下文章

    2023前端面试题及答案整理(JavaScript)

    最全Java面试题及答案整理(2023最新版)

    Java面试题及答案整理汇总(2023最新版)

    前端vue面试题分享(附答案)

    前端面试题及答案整理(转)

    前端面试题及答案整理(转)