写点什么

前端面试 5 家公司,被经常问到的 vue 面试题

作者:bb_xiaxia1998
  • 2022 年 9 月 22 日
    浙江
  • 本文字数:19737 字

    阅读完需:约 65 分钟

说说你对 slot 的理解?slot 使用场景有哪些

一、slot 是什么

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符


该占位符可以在后期使用自己的标记语言填充


举个栗子


<template id="element-details-template">  <slot name="element-name">Slot template</slot></template><element-details>  <span slot="element-name">1</span></element-details><element-details>  <span slot="element-name">2</span></element-details>
复制代码


template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,


customElements.define('element-details',  class extends HTMLElement {    constructor() {      super();      const template = document        .getElementById('element-details-template')        .content;      const shadowRoot = this.attachShadow({mode: 'open'})        .appendChild(template.cloneNode(true));  }})
复制代码


Vue中的概念也是如此


Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口

二、使用场景

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理


如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情


通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用


比如布局组件、表格列、下拉选、弹框显示内容等

Watch 中的 deep:true 是如何实现的

当用户指定了 watch 中的 deep 属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新


源码相关


get () {     pushTarget(this) // 先将当前依赖放到 Dep.target上     let value     const vm = this.vm     try {         value = this.getter.call(vm, vm)     } catch (e) {         if (this.user) {             handleError(e, vm, `getter for watcher "${this.expression}"`)         } else {             throw e         }     } finally {         if (this.deep) { // 如果需要深度监控         traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法     }popTarget() }
复制代码


前端vue面试题详细解答

Vue 组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信


组件传参的各种方式



组件通信常用方式有以下几种


  • props / $emit 适用 父子组件通信

  • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的

  • ref$parent / $children(vue3废弃) 适用 父子组件通信

  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法

  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信

  • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件

  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

  • provide / inject 适用于 隔代组件通信

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

  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用

  • Vuex 适用于 父子、隔代、兄弟组件通信

  • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。


根据组件之间关系讨论组件通信最为清晰有效


  • 父子组件:props/$emit/$parent/ref

  • 兄弟组件:$parent/eventbus/vuex

  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root


下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯


1. 父子组件通信


使用props,父组件可以使用props向子组件传递数据。


父组件vue模板father.vue:


<template>  <child :msg="message"></child></template>
<script>import child from './child.vue';export default { components: { child }, data () { return { message: 'father message'; } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <div>{{msg}}</div></template>
<script>export default { props: { msg: { type: String, required: true } }}</script>
复制代码


回调函数(callBack)


父传子:将父组件里定义的method作为props传入子组件


// 父组件Parent.vue:<Child :changeMsgFn="changeMessage">methods: {    changeMessage(){        this.message = 'test'    }}
复制代码


// 子组件Child.vue:<button @click="changeMsgFn">props:['changeMsgFn']
复制代码


子组件向父组件通信


父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件


父组件vue模板father.vue:


<template>    <child @msgFunc="func"></child></template>
<script>import child from './child.vue';export default { components: { child }, methods: { func (msg) { console.log(msg); } }}</script>
复制代码


子组件vue模板child.vue:


<template>    <button @click="handleClick">点我</button></template>
<script>export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } }}</script>
复制代码


2. provide / inject 跨级访问祖先组件的数据


父组件通过使用provide(){return{}}提供需要传递的数据


export default {  data() {    return {      title: '我是父组件',      name: 'poetry'    }  },  methods: {    say() {      alert(1)    }  },  // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法  provide() {    return {      message: '我是祖先组件提供的数据',      name: this.name, // 传递属性      say: this.say    }  }}
复制代码


子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数


<template>  <p>曾孙组件</p>  <p>{{message}}</p></template><script>export default {  // inject 注入/接收祖先组件传递的所需要的数据即可   //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}  inject: [ "message","say"],  mounted() {    this.say();  },};</script>
复制代码


3. children 获取父组件实例和子组件实例的集合


  • this.$parent 可以直接访问该组件的父实例或组件

  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的


<!-- parent.vue --><template><div>  <child1></child1>     <child2></child2>   <button @click="clickChild">$children方式获取子组件值</button></div></template><script>import child1 from './child1'import child2 from './child2'export default {  data(){    return {      total: 108    }  },  components: {    child1,    child2    },  methods: {    funa(e){      console.log("index",e)    },    clickChild(){      console.log(this.$children[0].msg);      console.log(this.$children[1].msg);    }  }}</script>
复制代码


<!-- child1.vue --><template>  <div>    <button @click="parentClick">点击访问父组件</button>  </div></template><script>export default {  data(){    return {      msg:"child1"    }  },  methods: {    // 访问父组件数据    parentClick(){      this.$parent.funa("xx")      console.log(this.$parent.total);    }  }}</script>
复制代码


<!-- child2.vue --><template>  <div>    child2  </div></template><script>export default {  data(){    return {     msg: 'child2'    }  }}</script>
复制代码


4. listeners 多级组件通信


$attrs 包含了从父组件传过来的所有props属性


// 父组件Parent.vue:<Child :name="name" :age="age"/>
// 子组件Child.vue:<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild<p>姓名:{{$attrs.name}}</p><p>年龄:{{$attrs.age}}</p>
复制代码


$listeners包含了父组件监听的所有事件


// 父组件Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:<button @click="$listeners.changeNameFn"></button>
复制代码


5. ref 父子组件通信


// 父组件Parent.vue:<Child ref="childComp"/><button @click="changeName"></button>changeName(){    console.log(this.$refs.childComp.age);    this.$refs.childComp.changeAge()}
// 子组件Child.vue:data(){ return{ age:20 }},methods(){ changeAge(){ this.age=15 }}
复制代码


6. 非父子, 兄弟组件之间通信


vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js可以是这样:


// Bus.js
// 创建一个中央时间总线类 class Bus { constructor() { this.callbacks = {}; // 存放事件的名字 } $on(name, fn) { this.callbacks[name] = this.callbacks[name] || []; this.callbacks[name].push(fn); } $emit(name, args) { if (this.callbacks[name]) { this.callbacks[name].forEach((cb) => cb(args)); } } }
// main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
复制代码


<template>    <button @click="toBus">子组件传给兄弟组件</button></template>
<script>export default{ methods: { toBus () { this.$bus.$emit('foo', '来自兄弟组件') } }}</script>
复制代码


另一个组件也在钩子函数中监听on事件


export default {  data() {    return {      message: ''    }  },  mounted() {    this.$bus.$on('foo', (msg) => {      this.message = msg    })  }}
复制代码


7. $root 访问根组件中的属性或方法


  • 作用:访问根组件中的属性或方法

  • 注意:是根组件,不是父组件。$root只对根组件有用


var vm = new Vue({  el: "#app",  data() {    return {      rootInfo:"我是根元素的属性"    }  },  methods: {    alerts() {      alert(111)    }  },  components: {    com1: {      data() {        return {          info: "组件1"        }      },      template: "<p>{{ info }} <com2></com2></p>",      components: {        com2: {          template: "<p>我是组件1的子组件</p>",          created() {            this.$root.alerts()// 根组件方法            console.log(this.$root.rootInfo)// 我是根元素的属性          }        }      }    }  }});
复制代码


8. vuex


  • 适用场景: 复杂关系的组件数据传递

  • Vuex 作用相当于一个用来存储共享变量的容器



  • state用来存放共享变量的地方

  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值

  • mutations用来存放修改state的方法。

  • actions也是用来存放修改 state 的方法,不过action是在mutations的基础上进行。常用来做一些异步操作


小结


  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref

  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递

  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject

  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

请说明 Vue 中 key 的作用和原理,谈谈你对它的理解


  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速

  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异

  • 尽量不要采用索引作为key

  • 如果不加key,那么vue会选择复用节点(Vue 的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

  • 更准确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。

  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。


源码如下:


function createKeyToOldIdx (children, beginIdx, endIdx) {  let i, key  const map = {}  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key    if (isDef(key)) map[key] = i  }  return map}
复制代码


回答范例


分析


这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次


思路分析:


  • 给出结论,key的作用是用于优化patch性能

  • key的必要性

  • 实际使用方式

  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同


回答范例:


  1. key的作用主要是为了更高效的更新虚拟DOM

  2. vuepatch过程中 判断两个节点是否是相同节点是key是一个必要条件 ,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能

  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果

  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的


如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速



diff 程可以概括为:oldChnewCh各有两个头尾的变量 StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾


相关代码如下


// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用function isSameVnode(oldVnode, newVnode) {  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map;}// 生成的映射表let map = makeIndexByKey(oldCh);
复制代码

怎么缓存当前的组件?缓存后怎么更新

缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3keep-alive有比较大的更新,能说的点比较多


思路


  • 缓存用keep-alive,它的作用与用法

  • 使用细节,例如缓存指定/排除、结合routertransition

  • 组件缓存后更新可以利用activated或者beforeRouteEnter

  • 原理阐述


回答范例


  1. 开发中缓存组件使用keep-alive组件,keep-alivevue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM


<keep-alive>  <component :is="view"></component></keep-alive>
复制代码


  1. 结合属性includeexclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive


<router-view v-slot="{ Component }">  <keep-alive>    <component :is="Component"></component>  </keep-alive></router-view>
复制代码


  1. 缓存后如果要获取数据,解决方案可以有以下两种


  • beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter


beforeRouteEnter(to, from, next){  next(vm=>{    console.log(vm)    // 每次进入路由执行    vm.getData()  // 获取数据  })},
复制代码


  • actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子


activated(){    this.getData() // 获取数据},
复制代码


  1. keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于componentis属性是个响应式数据,因此只要它变化,keep-aliverender函数就会重新执行

组件中写 name 属性的好处

可以标识组件的具体名称方便调试和查找对应属性


// 源码位置 src/core/global-api/extend.js
// enable recursive self-lookupif (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx}
复制代码

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。


其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并


  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。

  • Vue.component你可以创建 ,也可以取组件。


相关代码如下


export default function initExtend(Vue) {  let cid = 0; //组件的唯一标识  // 创建子类继承Vue父类 便于属性扩展  Vue.extend = function (extendOptions) {    // 创建子类的构造函数 并且调用初始化方法    const Sub = function VueComponent(options) {      this._init(options); //调用Vue初始化方法    };    Sub.cid = cid++;    Sub.prototype = Object.create(this.prototype); // 子类原型指向父类    Sub.prototype.constructor = Sub; //constructor指向自己    Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options    return Sub;  };}
复制代码

请说出 vue cli 项目中 src 目录每个文件夹和文件的用法

  • assets文件夹是放静态资源;

  • components是放组件;

  • router是定义路由相关的配置;

  • view视图;

  • app.vue是一个应用主组件;

  • main.js是入口文件

vue-router 守卫

导航守卫 router.beforeEach 全局前置守卫


  • to: Route: 即将要进入的目标(路由对象)

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

  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)

  • 执行效果依赖 next 方法的调用参数。

  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

  • next(false):取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)


// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => {   next();});router.beforeResolve((to, from, next) => {  next();});router.afterEach((to, from) => {  console.log('afterEach 全局后置钩子');});
复制代码


路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫


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`    // 比如还未保存草稿,或者在用户离开前,    将setInterval销毁,防止离开之后,定时器还在调用。  }}
复制代码

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块

  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名

三、分类

slot可以分来以下三种:


  • 默认插槽

  • 具名插槽

  • 作用域插槽


1. 默认插槽


子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面


父组件在使用的时候,直接在子组件的标签内写入内容即可


子组件Child.vue


<template>    <slot>      <p>插槽后备的内容</p>    </slot></template>
复制代码


父组件


<Child>  <div>默认插槽</div>  </Child>
复制代码


2. 具名插槽


子组件用name属性来表示插槽的名字,不传为默认插槽


父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值


子组件Child.vue


<template>    <slot>插槽后备的内容</slot>  <slot name="content">插槽后备的内容</slot></template>
复制代码


父组件


<child>    <template v-slot:default>具名插槽</template>    <!-- 具名插槽⽤插槽名做参数 -->    <template v-slot:content>内容...</template></child>
复制代码


3. 作用域插槽


子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上


父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用


子组件Child.vue


<template>   <slot name="footer" testProps="子组件的值">          <h3>没传footer插槽</h3>    </slot></template>
复制代码


父组件


<child>     <!-- 把v-slot的值指定为作⽤域上下⽂对象 -->    <template v-slot:default="slotProps">      来⾃⼦组件数据:{{slotProps.testProps}}    </template>    <template #default="slotProps">      来⾃⼦组件数据:{{slotProps.testProps}}    </template></child>
复制代码


小结:


  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使用

  • 默认插槽名为default,可以省略 default 直接写v-slot

  • 缩写为#时不能不写参数,写成#default

  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认值'}"

四、原理分析

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM 过程,这里看看slot如何实现:


编写一个buttonCounter组件,使用匿名插槽


Vue.component('button-counter', {  template: '<div> <slot>我是默认内容</slot></div>'})
复制代码


使用该组件


new Vue({    el: '#app',    template: '<button-counter><span>我是slot传入内容</span></button-counter>',    components:{buttonCounter}})
复制代码


获取buttonCounter组件渲染函数


(function anonymous() {with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}})
复制代码


_v表示穿件普通文本节点,_t表示渲染插槽的函数


渲染插槽函数renderSlot(做了简化)


function renderSlot (  name,  fallback,  props,  bindObject) {  // 得到渲染插槽内容的函数      var scopedSlotFn = this.$scopedSlots[name];  var nodes;  // 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回  // 否则使用默认值  nodes = scopedSlotFn(props) || fallback;  return nodes;}
复制代码


name属性表示定义插槽的名字,默认值为defaultfallback表示子组件中的slot节点的默认值


关于this.$scopredSlots是什么,我们可以先看看vm.slot


function initRender (vm) {  ...  vm.$slots = resolveSlots(options._renderChildren, renderContext);  ...}
复制代码


resolveSlots函数会对children节点做归类和过滤处理,返回slots


function resolveSlots (    children,    context  ) {    if (!children || !children.length) {      return {}    }    var slots = {};    for (var i = 0, l = children.length; i < l; i++) {      var child = children[i];      var data = child.data;      // remove slot attribute if the node is resolved as a Vue slot node      if (data && data.attrs && data.attrs.slot) {        delete data.attrs.slot;      }      // named slots should only be respected if the vnode was rendered in the      // same context.      if ((child.context === context || child.fnContext === context) &&        data && data.slot != null      ) {        // 如果slot存在(slot="header") 则拿对应的值作为key        var name = data.slot;        var slot = (slots[name] || (slots[name] = []));        // 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面        if (child.tag === 'template') {          slot.push.apply(slot, child.children || []);        } else {          slot.push(child);        }      } else {        // 如果没有就默认是default        (slots.default || (slots.default = [])).push(child);      }    }    // ignore slots that contains only whitespace    for (var name$1 in slots) {      if (slots[name$1].every(isWhitespace)) {        delete slots[name$1];      }    }    return slots}
复制代码


_render渲染函数通过normalizeScopedSlots得到vm.$scopedSlots


vm.$scopedSlots = normalizeScopedSlots(  _parentVnode.data.scopedSlots,  vm.$slots,  vm.$scopedSlots);
复制代码


作用域插槽中父组件能够得到子组件的值是因为在renderSlot的时候执行会传入props,也就是上述_t第三个参数,父组件则能够得到子组件传递过来的值

二、如何解决

解决跨域的方法有很多,下面列举了三种:


  • JSONP

  • CORS

  • Proxy


而在vue项目中,我们主要针对CORSProxy这两种方案进行展开


CORS


CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应


CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源


只要后端实现了 CORS,就实现了跨域


!



koa框架举例


添加中间件,直接设置Access-Control-Allow-Origin响应头


app.use(async (ctx, next)=> {  ctx.set('Access-Control-Allow-Origin', '*');  ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');  ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');  if (ctx.method == 'OPTIONS') {    ctx.body = 200;   } else {    await next();  }})
复制代码


ps: Access-Control-Allow-Origin 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host


Proxy


代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击


方案一


如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象


通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域


vue.config.js文件,新增以下代码


amodule.exports = {    devServer: {        host: '127.0.0.1',        port: 8084,        open: true,// vue项目启动时自动打开浏览器        proxy: {            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址                changeOrigin: true, //是否跨域                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替                    '^/api': ""                 }            }        }    }}
复制代码


通过axios发送请求中,配置请求的根路径


axios.defaults.baseURL = '/api'
复制代码


方案二


此外,还可通过服务端实现代理请求转发


express框架为例


var express = require('express');const proxy = require('http-proxy-middleware')const app = express()app.use(express.static(__dirname + '/'))app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false                      }));module.exports = app
复制代码


方案三


通过配置nginx实现代理


server {    listen    80;       location / {        root  /var/www/html;        index  index.html index.htm;        try_files $uri $uri/ /index.html;    }    location /api {        proxy_pass  http://127.0.0.1:3000;        proxy_redirect   off;        proxy_set_header  Host       $host;        proxy_set_header  X-Real-IP     $remote_addr;        proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;    }}
复制代码

vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫路由守卫组件守卫


  1. 导航被触发。

  2. 在失活的组件里调用 beforeRouteLeave 守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

  5. 在路由配置里调用 beforeEnter

  6. 解析异步路由组件。

  7. 在被激活的组件里调用 beforeRouteEnter

  8. 调用全局的 beforeResolve 守卫 (2.5+)。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

Vue Ref 的作用

  • 获取dom元素this.$refs.box

  • 获取子组件中的datathis.$refs.box.msg

  • 调用子组件中的方法this.$refs.box.open()

说下你的 vue 项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢

一、为什么要划分

使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更高


在划分项目结构的时候,需要遵循一些基本的原则:


  • 文件夹和文件夹内部文件的语义一致性

  • 单一入口/出口

  • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引用

  • 公共的文件应该以绝对路径的方式从根目录引用

  • /src 外的文件不应该被引入


文件夹和文件夹内部文件的语义一致性


我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件夹


这样做的好处在于一眼就从 pages文件夹看出这个项目的路由有哪些


单一入口/出口


举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口


// 错误用法import sellerReducer from 'src/pages/seller/reducer'
// 正确用法import { reducer as sellerReducer } from 'src/pages/seller'
复制代码


这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点


就近原则,紧耦合的文件应该放到一起,且应以相对路径引用


使用相对路径可以保证模块内部的独立性


// 正确用法import styles from './index.module.scss'// 错误用法import styles from 'src/pages/seller/index.module.scss'
复制代码


举个例子


假设我们现在的 seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller


如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更。


但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改


公共的文件应该以绝对路径的方式从根目录引用


公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components


在使用到的页面中,采用绝对路径的形式引用


// 错误用法import Input from '../../components/input'// 正确用法import Input from 'src/components/input'
复制代码


同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换


再加上绝对路径有全局的语义,相对路径有独立模块的语义


src 外的文件不应该被引入


vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js,css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件


这样的好处是方便划分项目代码文件和配置文件

二、目录结构

单页面目录结构


project│  .browserslistrc│  .env.production│  .eslintrc.js│  .gitignore│  babel.config.js│  package-lock.json│  package.json│  README.md│  vue.config.js│  yarn-error.log│  yarn.lock├─public│      favicon.ico│      index.html|-- src    |-- components        |-- input            |-- index.js            |-- index.module.scss    |-- pages        |-- seller            |-- components                |-- input                    |-- index.js                    |-- index.module.scss            |-- reducer.js            |-- saga.js            |-- index.js            |-- index.module.scss        |-- buyer            |-- index.js        |-- index.js
复制代码


多页面目录结构


my-vue-test:.│  .browserslistrc│  .env.production│  .eslintrc.js│  .gitignore│  babel.config.js│  package-lock.json│  package.json│  README.md│  vue.config.js│  yarn-error.log│  yarn.lock├─public│      favicon.ico│      index.html└─src    ├─apis //接口文件根据页面或实例模块化    │      index.js    │      login.js    ├─components //全局公共组件    │  └─header    │          index.less    │          index.vue    ├─config //配置(环境变量配置不同passid等)    │      env.js    │      index.js    ├─contant //常量    │      index.js    ├─images //图片    │      logo.png    ├─pages //多页面vue项目,不同的实例    │  ├─index //主实例    │  │  │  index.js    │  │  │  index.vue    │  │  │  main.js    │  │  │  router.js    │  │  │  store.js    │  │  │    │  │  ├─components //业务组件    │  │  └─pages //此实例中的各个路由    │  │      ├─amenu    │  │      │      index.vue    │  │      │    │  │      └─bmenu    │  │              index.vue    │  │    │  └─login //另一个实例    │          index.js    │          index.vue    │          main.js    ├─scripts //包含各种常用配置,工具函数    │  │  map.js    │  │    │  └─utils    │          helper.js    ├─store //vuex仓库    │  │  index.js    │  │    │  ├─index    │  │      actions.js    │  │      getters.js    │  │      index.js    │  │      mutation-types.js    │  │      mutations.js    │  │      state.js    │  │    │  └─user    │          actions.js    │          getters.js    │          index.js    │          mutation-types.js    │          mutations.js    │          state.js    └─styles //样式统一配置        │  components.less        ├─animation        │      index.less        │      slide.less        ├─base        │      index.less        │      style.less        │      var.less        │      widget.less        └─common                index.less                reset.less                style.less                transition.less
复制代码


小结


项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

vue 中使用了哪些设计模式

  • 工厂模式 传入参数即可创建实例:虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode

  • 单例模式 整个程序有且仅有一个实例:vuexvue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉

  • 发布-订阅模式 (vue 事件机制)

  • 观察者模式 (响应式数据原理)

  • 装饰模式: (@装饰器的用法)

  • 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

scoped 样式穿透

scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性


  1. 使用/deep/


<!-- Parent --><template><div class="wrap">    <Child /></div></template>
<style lang="scss" scoped>.wrap /deep/ .box{ background: red;}</style>
<!-- Child --><template> <div class="box"></div></template>
复制代码


  1. 使用两个style标签


<!-- Parent --><template><div class="wrap">    <Child /></div></template>
<style lang="scss" scoped>/* 其他样式 */</style><style lang="scss">.wrap .box{ background: red;}</style>
<!-- Child --><template> <div class="box"></div></template>
复制代码

你知道哪些 Vue3 新特性?

官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)



  • Composition API

  • SFC Composition API语法糖

  • Teleport传送门

  • Fragments片段

  • Emits选项

  • 自定义渲染器

  • SFC CSS变量

  • Suspense


以上这些是 api 相关,另外还有很多框架特性也不能落掉


回答范例


  1. api层面Vue3新特性主要包括:Composition APISFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense

  2. 另外,Vue3.0在框架层面也有很多亮眼的改进:


  • 更快

  • 虚拟DOM重写,diff算法优化

  • 编译器优化:静态提升、patchFlags(静态标记)、事件监听缓存

  • 基于Proxy的响应式系统

  • SSR优化

  • 更小 :更好的摇树优化 tree shakingVue3移除一些不常用的 API

  • 更友好vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力

  • 更容易维护TypeScript + 模块化

  • 更容易扩展

  • 独立的响应化模块

  • 自定义渲染器

Vue.observable 你有了解过吗?说说看

一、Observable 是什么

Observable 翻译过来我们可以理解成可观察的


我们先来看一下其在Vue中的定义


Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对象


返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器


Vue.observable({ count : 1})
复制代码


其作用等同于


new vue({ count : 1})
复制代码


Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对象


Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的

二、使用场景

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择


创建一个js文件


// 引入vueimport Vue from 'vue// 创建state对象,使用observable让state对象可响应export let state = Vue.observable({  name: '张三',  'age': 38})// 创建对应的方法export let mutations = {  changeName(name) {    state.name = name  },  setAge(age) {    state.age = age  }}
复制代码


.vue文件中直接使用即可


<template>  <div>    姓名:{{ name }}    年龄:{{ age }}    <button @click="changeName('李四')">改变姓名</button>    <button @click="setAge(18)">改变年龄</button>  </div></template>import { state, mutations } from '@/storeexport default {  // 在计算属性中拿到值  computed: {    name() {      return state.name    },    age() {      return state.age    }  },  // 调用mutations里面的方法,更新数据  methods: {    changeName: mutations.changeName,    setAge: mutations.setAge  }}
复制代码

三、原理分析

源码位置:src\core\observer\index.js


export function observe (value: any, asRootData: ?boolean): Observer | void {  if (!isObject(value) || value instanceof VNode) {    return  }  let ob: Observer | void  // 判断是否存在__ob__响应式属性  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {    ob = value.__ob__  } else if (    shouldObserve &&    !isServerRendering() &&    (Array.isArray(value) || isPlainObject(value)) &&    Object.isExtensible(value) &&    !value._isVue  ) {    // 实例化Observer响应式对象    ob = new Observer(value)  }  if (asRootData && ob) {    ob.vmCount++  }  return ob}
复制代码


Observer


export class Observer {    value: any;    dep: Dep;    vmCount: number; // number of vms that have this object as root $data
constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 实例化对象是一个对象,进入walk方法 this.walk(value) }}
复制代码


walk函数


walk (obj: Object) {    const keys = Object.keys(obj)    // 遍历key,通过defineReactive创建响应式对象    for (let i = 0; i < keys.length; i++) {        defineReactive(obj, keys[i])    }}
复制代码


defineReactive方法


export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {  const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return }
// cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] }
let childOb = !shallow && observe(val) // 接下来调用Object.defineProperty()给对象定义响应式属性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 对观察者watchers进行通知,state就成了全局响应式对象 dep.notify() } })}
复制代码

Vue-router 基本使用

mode


  • hash

  • history


跳转


  • 编程式(js 跳转)this.$router.push('/')

  • 声明式(标签跳转) <router-link to=""></router-link>


vue 路由传参数


  • 使用query方法传入的参数使用this.$route.query接受

  • 使用params方式传入的参数使用this.$route.params接受


占位


<router-view></router-view>
复制代码


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022.09.01 加入

还未添加个人简介

评论

发布
暂无评论
前端面试5家公司,被经常问到的vue面试题_Vue_bb_xiaxia1998_InfoQ写作社区