阅读 137

高频前端面试题汇总之Vue篇

Vue共识:
在 Vue 中我们习惯把虚拟DOM称为 VNode,它既可以代表一个 VNode 节点,也可以代表一颗 VNode 树。
组件的核心是它能够产出一堆VNode。
对于 Vue 来说一个组件的核心就是它的渲染函数,组件的挂载本质就是执行渲染函数并得到要渲染的VNode,至于data/props/computed 这都是为渲染函数产出 VNode 过程中提供数据来源服务的,最关键的就是组件最终产出的VNode,因为这个才是要渲染的内容。


vue.png

一、Vue基础

1. Vue的基本原理

当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter 并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。


12cd66d0cdf5233131cc966ff9e70a37.png

2. 双向数据绑定的原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

1、需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

2、compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

3、Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
①在自身实例化时往属性订阅器(dep)里面添加自己
②自身必须有一个update()方法
③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

4、MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

3. 使用 Object.defineProperty() 来进行数据劫持有什么缺点?

有一些对属性的操作,使用这种方法无法拦截,比如说通过下标方式修改数组数据或者给对象新增属性,vue 内部通过重写函数解决了这个问题。在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为这是 ES6 的语法。

4. MVVM和MVC的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化我们的开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,当时一旦项目变得复杂,那么整个文件就会变得冗长,混乱,这样对我们的项目开发和后期的项目维护是非常不利的。

(1)MVC

MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。


dbcd93678d7afbc145082ca7c41d7841.png

(2)MVVM

MVVM 分为 Model、View、ViewModel 三者。

Model代表数据模型,数据和业务逻辑都在Model层中定义;
View代表UI视图,负责数据的展示;
ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用 户交互操作;

Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中 的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的 数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注 对数据的维护操作即可,而不需要自己操作DOM。


2003aeb436b48322038d4b21891f1758.png

(2)MVP

MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中我们使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此我们可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。

5. Computed和Watch的区别

对于Computed:

它支持缓存,只有依赖的数据发生了变化,才会重新计算
不支持异步,当Computed中有异步操作时,无法监听数据的变化
computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。

对于Watch:

它不支持缓存,数据变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
当一个属性发生变化时,就需要执行相应的操作
监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会出大其他操作,函数有两个的参数:
    immediate:组件加载立即触发回调函数
    deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

总结:

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的 属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每 当监听的数据变化时都会执行回调进行后续操作。

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利 用 computed 的缓存特性,避免每次获取值时,都要重新计算。
当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率, 并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

6. Computed 和 Methods 的区别

https://segmentfault.com/a/1190000014478664
methods与computed之间的差别:
(1)methods和computed里的方法在初始化执行过后,只要任何值有更新,那么所有在computed计算属性里和其相关的值都会更新。

methods只有在调用的时候才会执行对应的方法,不会自动同步数据。
(2) computed是属性访问,而methods是函数调用
computed带有缓存功能,而methods不是
computed其实是就是属性,之所以与data区分开,只不过为了防止文本插值中逻辑过重,会导致不易维护

(3) computed定义的方法我们是以属性的形式访问的,和data里的属性访问形式一样,{{function}}
但是methods定义的方法,我们必须要加上()来调用,如{{function()}},否则,视图会出现function (){[native code]}的情况

7. slot是什么?有什么作用?原理是什么?

slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。

(1) 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
(2) 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
(3)作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。

实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm. slot.default,具名插槽为vm. slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。

slot的意思是插槽,想想你的电脑主板上的各种插槽,有插CPU的,有插显卡的,有插内存的,有插硬盘的,所以假设有个组件是computer,组件computer:

<template>
<div>
  <slot name="CPU">这儿插你的CPU</slot>
  <slot name="GPU">这儿插你的显卡</slot>
<slot></slot>
  <slot name="Memory">这儿插你的内存</slot>
  <slot name="Hard-drive">这儿插你的硬盘</slot>
</div>
</template>


那么组装一个电脑,就可以在调用组件的页面这么写:

<template>
<computer>
  <div slot="CPU">Intel Core i7</div>
  <div slot="GPU">GTX980Ti</div>
<div>想加内容就加内容</div>
  <div slot="Memory">Kingston 32G</div>
  <div slot="Hard-drive">Samsung SSD 1T</divt>
</computer>
</template>
<script>
import computerfrom "./computer";
export default {
  name: "page",
  components: {
    computer
  },
  data() {
    return {
      
    };
  },
  computed: {},
  methods: {
    
  }
};
</script>

页面显示:Intel Core i7 GTX980Ti 想加内容就加内容 Kingston 32G Samsung SSD 1T


二、生命周期

vue生命周期.png
vue生命周期desc.png

使用建议:
1. beforeCreate:加载loading事件
2. created:结束loading、初始化、请求数据、实现函数自执行
3. mounted:拿回数据,配合路由钩子做一些事
4. beforeDestory:destoryed:当前组件已被删除,清空相关内容

1. created和mounted的区别

(1)created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
(2)mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。

2. 接口请求一般放在哪个生命周期中?

可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
    能更快获取到服务端数据,减少页面loading 时间;
    ssr不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

三、组件通信

组件通信1.png

如图所示:
A和B、B和C、B和D都是父子关系,C和D是兄弟关系,A和C是隔代关系(可能隔多代)。
eg:
props、 on、vuex、 children、 listeners和provide/inject

方法一、props/$emit

父组件A通过props的方向子组件B传递,B到A通过在B组件中$emit,A组件中v-on的方式实现。
eg:
子组件

<template>
  <div class="navBar">
    <div class="navBarItem" v-for="item in dataList" :key="item.id" :index="item.id">
      <span v-if="item.isNow" class="nowTitle" @click="navClick(item)">{{item.title}}</span>
      <span v-else class="title" @click="navClick(item)">{{item.title}}</span>
      <span class="separator">></span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'navBar',
  data () {
    return {}
  },
  props: {
    dataList: Array
  },
  methods: {
    navClick (item) {
      this.$emit('navClick', item)
    }
  }
}
</script>

父组件:

<template>
  <NavBar :dataList="navBarData" @navClick="navClick"></NavBar>
</template>
<script>
import NavBar from '@/components/NavBar'
export default {
  name: 'detail',
  data () {
    return {
 navBarData: [
        { title: '鲸选资源', url: '/whaleselect', id: 1, isNow: false },
        { title: '文件详情', url: '/jx', id: 2, isNow: true }
      ]}
  },
  props: {
    dataList: Array
  },
  methods: {
    navClick ({ url, id }) {
      this.$utils.link(`${url}/` + id)
    }
  },
 components: {
    NavBar,
   
  }
}
</script>

1.父组件向子组件传值

父组件通过props向下传递数据给子组件

2.子组件向父组件传值(通过事件形式)

子组件通过this.$emit('方法名',传递的值),父组件定义同名方法即可

EventBus

EventBus事件总线适用于父子组件、非父子组件等之间的通信;
全局或者公共组件注册一个vue实例,利用里面的注册喝监听事件。

(1)创建事件中心管理组件之间的通信

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件

假设我们有两个兄弟组件C和D:

<template>
  <div>
    <C></C>
    <D></D>
  </div>
</template>

<script>
import Cfrom './C.vue'
import D from './D.vue'
export default {
  components: { C, D}
}
</script>

在C组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

(3)接收事件

在D组件中发送事件:

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', param => {
      this.count = this.count + param.num;
    })
  }
}
</script>

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

方法三 Vuex

vuex.png

1.原理

Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但是Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。

(1)vuex下的store.js文件

import Vuex from 'vuex'
export default new Vuex.Store({
  // 定义状态 值 方法
  state: {
 // 获取文件夹
    getfolderscallback: function () {},
    // 验证用户的Id的token
    token: '',
    // 用户id
    uid: '',
    // 是否登录
    isLogin: false,
    // 用户名
    username: '',
    // 用户头像
    logo: ''
  },
  mutations: {
   // 获取文件夹
    getfolders (state) {
      state.getfolderscallback()
    },
    // 设置上面的全局变量
    setAttr (state, data) {
      state[data.name] = data.val
    }
  }
})

(2)页面改变值

  mounted () {
    // 设置全局变量-方法-获取文件夹
    this.$store.commit('setAttr', {
      name: 'getfolderscallback',
      val: this.getfolders
    })
  },}
  methods: {
    getuser () {
      // 本地判断Cookie,判断用户是否登录
      if (this.$utils.getCookie(this.$glb.fmCookieName) !== null) {
        this.$api.post('/center/getuser', {}, res => {
          if (!res.status) {
            return
          }
          this.$store.commit('setAttr', {name: 'isLogin', val: true})
          this.$store.commit('setAttr', {name: 'logo', val: res.data.logo})
          this.$store.commit('setAttr', {name: 'uid', val: res.data.userid})
          this.$store.commit('setAttr', {name: 'token', val: res.data.token})
          this.$store.commit('setAttr', {name: 'username', val: res.data.username})
        })
      }
    },
  // 上传文件框选择文件夹
    getfolders () {
      this.$api.post('/file/foldertreelist', {}, (res) => {
        this.folderList = res.data
      })
    },
}

(3)页面调用vuex State中的值

<template>
  <!-- 登录或未登录 --S-->
  <div class="isLoginBox">
    <!-- 登录 -->
    <div class="loginBox" v-if="this.$store.state.isLogin">
退出
    </div>
    <!-- 未登录 -->
    <div class="unLoginBox" v-else>
      <div class="loginOrRegister">
        <span class="login" >登录</span>
        <span class="line"></span>
        <span class="register">注册</span>
      </div>
    </div>
  </div>
  <!-- 登录或未登录 --E-->
</template>
<script>
export default {
  name: 'SignUpBar',
  data () {
    return {
    }
  },
  methods: {
    
}
</script>

2.各模块在核心流程中的主要功能:

1.Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。

  1. dispatch∶操作行为触发方法,是唯一能执行action的方法。
    3.actions∶ 操作行为处理模块,由组件中的$store.dispatch('action 名称',data1)来触发。然后由commit()来触发mutation的调用,间接更新state。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  2. commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  3. mutations∶状态改变操作方法,由actions中的commit('mutation 名称')来触发。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  4. state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
    7。 getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。

3.Vuex(状态:数组)与localStorage(字符串)

vuex是vue的状态管理器,存储的数据是响应式的。但是并不会保存起来,刷新之后就回到初始状态,具体做饭应该在vuex里数据改变的时候拷贝一份保存到localStorge里面,刷新之后,如果localStorge里有保存的数据,取出来再替换store里面的state

方法四、 listeners

方法五、依赖注入 provide/inject

祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。provide/inject API主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

project / inject是Vue提供的两个钩子,和data、methods是同级的。并且project的书写形式和data一样。

project 钩子用来发送数据或方法
inject钩子用来接收数据或方法

eg:两个组件:A.vue和B.vue,B是A的子组件

// A.vue
export default {
  provide: {
    name: '测试张三'
  }
}
// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // 测试张三
  }
}

核心用法:在A.vue里,设置了一个provide:name,值是测试张三,它的作用就是将name这个变量提供给它的所有子组件。
在B.vue中,通过inject注入了从A组件中提供的name变量,在B组件中,就可以直接通过this.name 访问这个变量,值就是测试张三
注意:**provide和inject绑定并不是可响应的。这是可以为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。A.vue的name如果改变了,B.vue的this.name是不改变的,仍然是测试张三

provide与inject实现数据响应式

两个办法:
(1)provide祖先组件的实例,然后在子孙组件中注入依赖,这样就可以在子孙组件中直接修改祖先组件的实例的属性,不过这种方法有个缺点就是这个实例上挂载很多没有必要的东西,eg:props,methods
(2)使用2.6最新API Vue.observable 优化响应式 provide

provide和inject.png
// A 组件 
<div>
      <h1>A 组件</h1>
      <button @click="() => changeColor()">改变color</button>
      <ChildrenB />
      <ChildrenC />
</div>
......
  data() {
    return {
      color: "blue"
    };
  },
  // provide() {
  //   return {
  //     theme: {
  //       color: this.color //这种方式绑定的数据并不是可响应的
  //     } // 即A组件的color变化后,组件D、E、F不会跟着变
  //   };
  // },
  provide() {
    return {
      theme: this//方法一:提供祖先组件的实例
    };
  },
  methods: {
    changeColor(color) {
      if (color) {
        this.color = color;
      } else {
        this.color = this.color === "blue" ? "red" : "blue";
      }
    }
  }
  // 方法二:使用2.6最新API Vue.observable 优化响应式 provide
  // provide() {
  //   this.theme = Vue.observable({
  //     color: "blue"
  //   });
  //   return {
  //     theme: this.theme
  //   };
  // },
  // methods: {
  //   changeColor(color) {
  //     if (color) {
  //       this.theme.color = color;
  //     } else {
  //       this.theme.color = this.theme.color === "blue" ? "red" : "blue";
  //     }
  //   }
  // }

// F 组件 
<template functional>
  <div class="border2">
    <h3 :style="{ color: injections.theme.color }">F 组件</h3>
  </div>
</template>
<script>
export default {
  inject: {
    theme: {
      //函数式组件取值不一样
      default: () => ({})
    }
  }
};
</script>

方法五、 children 与ref

ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例;
children:访问父/子实例
注意:这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据。
(1)用ref来访问组件:

// component-a 子组件
export default {
  data () {
    return {
      title: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      window.alert('Hello');
    }
  }
}

// 父组件(页面)
<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.title);  // Vue.js
      comA.sayHello();  // 弹窗
    }
  }
</script>

不过,这两种方法的弊端是:无法在跨级或者兄弟间通信

// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>

总结

常用使用场景可以分为三类:

(1)父子通信:
父to子传递数据是通过props,子to父是通过 parent/ attrs/$listeners;

(2)兄弟通信:
EventBus;
Vuex;

(3跨级通信:
Event Bus;
Vuex;
provide/inject API;
listeners;

四、

作者:逸笛

原文链接:https://www.jianshu.com/p/018e7ffa23e6

文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐