深入前端面试题-Vue

239 阅读10分钟
1. vue2响应式原理 Object.defineProperty

(1) defineProperty: 定义属性

Object.defineProperty 声明一个属性

     function defineProperty() {
        var _obj = {};
        Object.defineProperty(_obj, "a", {
          value: 1,
        });
        return _obj;
      }
      var obj = defineProperty();
      console.log(obj);                    // { a: 1 }

Object.defineProperties 声明多个属性

     function defineProperty() {
        var _obj = {};
        Object.defineProperties(_obj,{
            a: {
                value: 11
            },
            b: {
                value: 22
            }
        })
        return _obj;
      }
      var obj = defineProperty();
      console.log(obj);               // { a: 11, b: 22 }

(2) Object.defineProperties 默认的增加了属性后,所输出的这个对象是不可修改,不可枚举,不可删除

  var _obj = {};
  Object.defineProperties(_obj, {
    a: {
      value: 11,
    },
    b: {
      value: 22,
    },
  });
  console.log(_obj)        // {a: 11, b: 22}
  
  _obj.a = 888
  console.log(_obj)       // {a: 11, b: 22}  属性值不可修改
  
  for(let i in _obj){
    console.log(i + ':' + _obj[i])   // 属性不可枚举 
                                     // 枚举: 把对象的每一项列举出来
                                     // 不可枚举: 不能拿到到key值
  }
  
  delete _obj.a
  console.log(_obj)      // {a: 11, b: 22}  属性不可删除

writable => 写; enumerable => 枚举; configurable => 可配置 , 可操作

  var _obj = {};
  Object.defineProperties(_obj, {
    a: {
      value: 11,
      writable: true,
      enumerable:true,
      configurable:true
    },
    b: {
      value: 22,
    },
  });
  console.log(_obj)       // {a: 11, b: 22}
  
  _obj.a = 888
  console.log(_obj)       // {a: 888, b: 22}
  
  for(let i in _obj){
    console.log(i + ':' + _obj[i])   // a:888
  }
  
  delete _obj.a
  console.log(_obj)      // {b: 22}

(3) 每个属性定义的时候,都会产生 getter 和 setter 的机制。

数据劫持 : 对待一个对象,它的取值或设置值,有一系列的配置和阻止的方法,按getter和setter的机制去进行数据操作的逻辑,数据在javaScript当中往往就是对象。

输出顺序: get => 1111

    const obj = {}
    let a = 1111
    Object.defineProperty(obj,'a',{
      get: function() {
        console.log('get')
        return a
      }
    })
    console.log(obj.a)   

输出顺序: get => 1111 => set => get => bbb

    const obj = {}
    let a = 1111
    Object.defineProperty(obj,'a',{
      get: function() {
        console.log('get')
        return a
      },
      set: function(newValue) {
        console.log('set')
        a = newValue  
        //视图重新渲染
      }
    })
    console.log(obj.a)
    
    obj.a = 'bbb'
    console.log(obj.a)

value 、writable 和 getter 、setter 不能同时一起设置,这两者是互斥的。

(4) 处理不同类型的情况

处理值为字符串的情况

输出: 更新视图层

  const data = {
    name: 'aa',
    age: 18
  }
  
  tyoeFn(data)
  
  function tyoeFn(target) {
    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  
  function defineFn(target,key,value) {
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  
  data.name = 'bb'

处理值为复杂对象的情况

输出: 更新视图层

  const data = {
    name: 'aa',
    age: 18,
    obj: {
      nameObj: 'nameObj'
    }
  }
  
  tyoeFn(data)

  function tyoeFn(target) {
    if(typeof target !== 'object' || target === null) {
      return target
    }

    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  function defineFn(target,key,value) {
  
    tyoeFn(value)
    
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        tyoeFn(newValue)
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  
  data.obj.nameObj = 'bb'

Object.defineProperty:需要进行深度监听,才能够在数据改变的时候视图进行更新。

第一个缺点(深度监听):如果说这个数据是个对象,并且层级很深的话,一直不断的深度监听下去,直到是一个普通的值为止。如果数据很复杂的话,页面初次渲染会卡死。

第二个缺点:不能处理删除,新增,所以在vue中 删除用Vue.delete,新增用 Vue.set

(3) 处理值为数组的情况

用原型链给 Object.defineProperty里的 数组 增加push等方法

  const data = {
    arr:[1,2,3,5]
  }

  const oldArr = Array.prototype;
  const newArr = Object.create(oldArr);
  ['push','pop','shift','unshift'].forEach(methodName => {
    newArr[methodName] = function() {
      console.log('更新视图层')
      oldArr[methodName].call(this,...arguments)
    }
  })

  tyoeFn(data)

  function tyoeFn(target) {
    if(Array.isArray(target)) {
      target.__proto__ = newArr      //
    }

    if(typeof target !== 'object' || target === null) {
      return target
    }

    for(let key in target) {
      defineFn(target,key,target[key])
    }
  }
  function defineFn(target,key,value) {
    tyoeFn(value)
    Object.defineProperty(target,key,{
      get: function() {
        return value
      },
      set: function(newValue) {
        if(value != newValue) {
          console.log('更新视图层')
          value = newValue
        }
      }
    })
  }
  data.arr.push(666)
2. vue3响应式原理Proxy

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

proxy是es6新特性,为了对目标的作用主要是通过handler对象中的拦截方法拦截目标对象target的某些行为(如属性查找、赋值、枚举、函数调用等)。

target 目标对象 进行处理的对象; handler 容器 无数操作对象属性的方法

Proxy(target,handler)

target 操作的对象;prop 对象的属性;value: 对象属性值

get: (target, prop) set: (target, prop, value)

  let tar = {
    a:1,
    b:2
  }
  let proxy = new Proxy(tar, {
    get: function(tar, prop) {
      return '属性值' + tar[prop]
    },
    set: function(tar, prop,value) {
      tar[prop] = value
      console.log(tar[prop])
    }
  })
  console.log(proxy.a)   // 属性值1    访问proxy,就会走 get, 就会执行get里的方法
  console.log(tar.a)    // 1
  proxy.b = 3           // 3

总结:

(1) 通过proxy对象来代理target,先去操作proxy,然后通过handel重写proxy里面这些对对象操作的方式,来间接的操作target

(2)defineProperty,原则上是给对象增加属性用的,它在修改数组的长度,用所引去设置元素的值,数组的push,pop这些方法是无法触发defineProperty的setter方法

缺点:vue里使用的对数组的操作全都是vue自己重修写的,不是js的原生,所以导致代码非常重。

proxy没有以上问题,对数组的操作完全可以触发set方法。

(3)vue2没有使用proxy,因为proxy是es6的方法,因为兼容性的问题。vue3.0时,es6广泛推崇,很多浏览器es6兼容,所以使用proxy更加合理。proxy在vue在编译的时候,会进行转换,转换成es5相关执行,就是defineProperty。

(4) vue2.0的data是一个函数,用return返回的原因: 一是,防止引用值重写; 二是,产生的对象,是defineProperty直接操作这个对象。

vue3.0的data:{} 原因是: proxy做一层代理去访问,而这个对象target是必须通过代理proxy才能去完成一系列的data的操作

3. vue2.0 深入响应式原理——为啥数据更改,但页面没有响应更改?

是因为数据没有被依赖收集

数组 => 不会依赖收集,不管有没有值;

对象 => 空值不会依赖收集,有值才会依赖收集。

Vue2.0 不能检测数组和对象的变化。

(1)1. 对于对象

vue初始化实例时执行getter 和 setter 的转化,所以 prototy 必须在 data 上才能 让 vue 把它转换为响应式。

解决方法:Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。

1.1 => 对于已经创建的实例 obj ,Vue 不允许动态添加根级别的响应式proterty; 即不存在的值(vue无法能检测proterty的添加或删除)。

    export default {
      data() {
        return {
          obj: { a: 1 },
        };
      },
      methods: {
        btn() {
          console.log(this.obj.a);    // 1
          console.log(this.obj.b);   // undefined

          //解决方法:Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
          this.$set(this.obj, "b", 2);
          console.log(this.obj.b); // 2
        },
      },
    };

1.2 => 空值不会依赖收集( obj 没有 a ,a 是事后添加的,此时虽然 obj有a ,但 Vue并没有响应式 a ,所以页面上 obj 显示 仍未 {} );

<p>{{obj}}</p>                            // 页面显示 {}
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
      obj: {}
    }
  },
  methods:{
    btn() {
      this.obj.a = 111
      console.log(this.obj.a)    // 111
    }
  }
}

1.3 => 有值才会依赖收集 ( obj 有 b ,b 是 在 data 里事前的,已被 Vue 依赖收集 转换响应式 )。

<p>{{obj}}</p>                           // 页面显示  { 'b': 33 }
<button @click="btn">按钮</button>
 
export default {
  name'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn() {
      this.obj.b = 33
      console.dir(this.obj.b)    // 33
    }
  }
}

1.4 => 重新更新 b 的依赖,vue 会遍历 obj ,在遍历的时候 把 a 依赖收集

<p>{{obj}}</p>                        // 页面显示 {"b": 33,"a": 555}
<button @click="btn">按钮</button>

export default {
  name: 'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn() {
      this.obj.b = 33
      this.obj.a = 555
      console.dir(this.obj)    // {"b": 33,"a": 555}
    }
  }
}

1.5 => 先点击 按钮22 ,再点击 按钮11,页面显示 {"b": 33,"a": 555} ;

这是因为 先点击 按钮22 ,此时 obj 里已经有了 a;

再点击 按钮11,重新更新 b 的依赖,vue 会遍历 obj ,在遍历的时候 把 a 依赖收集。

反过来,先点击 按钮 11,再点击 按钮22,页面显示 {"b": 33} 。

<p>{{obj}}</p>
<button @click="btn11">按钮11</button>
<button @click="btn22">按钮22</button>

export default {
  name'Home',
  data() {
    return {
      obj: {b:22}
    }
  },
  methods:{
    btn11() {
      this.obj.b = 33
    },
    btn22() {
       this.obj.a = 555
    }
  }
}

(2) 对于数组

Vue 不能检测以下数组的变动:

=> 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue

=> 当你修改数组的长度时,例如:vm.items.length = newLength

解决办法 :

Vue.set => vm.$set(vm.items, indexOfItem, newValue)

splice => vm.items.splice(newLength)

<p>{{arr}}</p>                        // 页面显示 [1,2,3,4,5]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3,4,5]
    }
  },
  methods:{
    btn() {
      this.arr[2] = 'aaa'
      console.dir(this.arr)        // arr:[1,2,"aaa",4,5]
    }
  }
}
<p>{{arr}}</p>                            // 页面显示 [1,2,3,4,5]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3,4,5]
    }
  },
  methods:{
    btn() {
      this.arr.length = 2        
      console.dir(this.arr)        //  [1,2]
    }
  }
}

不会依赖收集,不管有没有值。

<p>{{arr}}</p>                        // 页面显示  []
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[]
    }
  },
  methods:{
    btn() {
      this.arr[0] = '111'
      console.dir(this.arr)        // arr['111']
    }
  }
}
<p>{{arr}}</p>                            // 页面显示  [1,2,3]
<button @click="btn">按钮</button>

export default {
  name'Home',
  data() {
    return {
     arr:[1,2,3]
    }
  },
  methods:{
    btn() {
      this.arr[0] = 'aaa'
      console.dir(this.arr)        // arr['aaa',2,3]
    }
  }
}
4.vue 中传值的形式

父子,子父 之间传值:

父 传 子 => propx

子 传 父 => on/emit

父 传 子 => parent/parent/children 仅适用只有一个子组件

父 传 子 => ref 用 this.refs来标记子组件,从而获取子组件实例

父 传 子 ,爷 传 孙子 => provoid / inject

父 传 子 => .sync修饰符

(1) 父 传 子 => propx

image.png

image.png

(2) 子 传 父 => on/emit

父组件 自定义组件使用on监听派发出的事件

子组件 this.$emit('gx') 使用emit派发自定义事件: gx

image.png

(3) parent/parent/children

this.$parent 得到父组件实例;

this.$children 得到子组件实例,可能有多个,且不保证顺序 ;

这种仅适用 一个 子组件。

image.png

image.png

image.png

(4) ref

image.png

(5) 子孙后代组件传值

=> provoid / inject 兄弟俩需要一起使用

  • provoid 对象 | 返回一个对象的函数。
  • inject 字符串数组 | 对象。

image.png

image.png

(6) 使用.sync修饰符

直接在子组件修改 父组件传过来的值,虽然可以, 但是 控制台 会报错;vue 不建议这么做,因为会造成 属性修改来源不明。

image.png

.sync修饰符可以实现 子组件 与 父组件 的双向绑定,并且可以实现子组件同步修改父组件的值。

image.png

同级/兄弟组件传值:

同级/兄弟组件,它们没有直接的隶属关系,对于这类的组件传值,通常会使用vuex、EventBus、observable。

(1) Event Bus

image.png

EventBus,也叫事件总线或者中央事件总线。说到底,无非就是利用Vue本身自带的 onon、emit进行。 on接收数据的那个组件;on 接收数据的那个组件; emit 发送数据的那个组件

image.png

需要注意的是:销毁组件时记得移除$on绑定的事件,避免造成重复监听。

image.png

(2)Vuex 实现组件全局状态(数据)管理机制

频繁、大范围的数据共享 => vuex

使用vuex的优点:1.能够在vuex集中管理共享的数据,易于开发和后期维护;2.能够高效实现组件之间的数据共享;3.存储在vuex中的数据都是响应式的,能够实时保持数据与页面的同步。

vuex的主要核心概念:State、Mutation、Action、Getter

image.png

(2.1) State 提供唯一的 公共数据源,所有的共享数据放在State存储

2.1.1 组件访问State中数据的第一种方式: this.$store.state.全局数据名称

image.png

2.1.2 组件访问State中数据的第二种方式: image.png

(2.2) Mutation 用于变更Store中的数据

2.2.1 只能通过mutation变更Store数据,可集中监控所有数据变化;不可以在组件中直接操作Store中的数据。

image.png

2.2.2 this.$store.commit() 触发 mutation的第一种方式

image.png

2.2.3 mapMutations函数 触发 mutation的第二种方式

image.png

(2.3) Action 用于处理异步任务

this.$store.dispatch() 触发actions 的第一种方式 image.png

image.png

image.png

image.png

(2.4)Getter 不会修改Store里的数据,只包装数据

image.png

image.png

image.png

image.png

(2.5) 解决浏览器刷新后vuex数据消失

image.png

image.png

image.png

(3) observable

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

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

使用场景

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

创建一个js文件

// 引入vue
import 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 '@/store
export default {
  // 在计算属性中拿到值
  computed: {
    name() {
      return state.name
    },
    age() {
      return state.age
    }
  },
  // 调用mutations里面的方法,更新数据
  methods: {
    changeName: mutations.changeName,
    setAge: mutations.setAge
  }
}
5 方法调用、computed、watch的区别

方法:页面数据每次重新渲染都会重新执行。性能消耗大,除非不希望有缓存的时候用。

computed:计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。

watch:监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。

除非不希望缓存,一般用方法; 一般来说需要依赖别的属性来动态获得值的时候可以使用computed; 对于监听到的值变化需要做到异步操作或开销较大的操作用 watch。

6 混入 mixin 抽离组件公共逻辑

组件和 mixin 的 data 和 事件重复时,就用 组件的 替换 mixin 的,没有重复就用 mixin的;

生命周期钩子重复时,两者是并存的, 先执行 mixin ,再执行 组件的 。 image.png

7 keep-alive

Vue会把组件渲染缓存起来,当再次回到已经渲染的组件,这个组件不会再重新执行渲染的过程,会从缓存里的结果最终显示这个组件。

(1) 属性

keep-alivevue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
  • max - 数字。最多可以缓存多少组件实例

(2) 生命周期钩子 activateddeactivated

设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > createdmounted > activated > ... ... > beforeRouteLeave > deactivated

  • 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated

  • activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用

因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子beforeCreate created beforeMount mounted 都不会触发。

  • deactivated:组件被停用(离开路由)时调用

使用了keep-alive就不会调用beforeDestroy(组件销毁前钩子)和destroyed(组件销毁),因为组件没被销毁,被缓存起来了

这个钩子可以看作beforeDestroy的替代,如果你缓存了组件,要在组件销毁的的时候做一些事情,你可以放在这个钩子里。

(3) 那假设切换组件时需要数据请求呢?缓存的组件应该不会重新请求了吧

不会,缓冲的还是之前请求到的数据,可以监听路由的变化再对应的请求接口;

不会重新请求了,因为mounted钩子不会再执行~。

=>所以在使用 keep-alive 的时候,Vue 会给你额外提供两个钩子,activated 和 deactivated;所以要请求数据的话可以放在 activated 中~。

(4) 缓存后如何获取数据? 解决方案可以有以下两种:

  • beforeRouteEnter
  • actived

每次组件渲染的时候,都会执行beforeRouteEnter

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

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

activated(){\
   this.getData() // 获取数据\
}

(5)v-show 与 keep-alive

状态频繁切换,也可以使用 v-show

v-show 是通过css样式 display:none 来显示与否;

整体结构简单,使用v-show;

组件比较复杂,代码比较多,使用专门的组件缓存keep-alive。

8 异步组件

当组件test 的代码提交非常大时,首次渲染时加载不必要的代码时,这样就会很大的影响性能

=> 解决办法:异步组件

image.png

在组件注册时使用异步组件加载,当组件test代码很大的时候,首次渲染时不去下载这么大的代码,而是到需要的时候再去下载,这样就可以极大的提升性能。

image.png

image.png

image.png

9. 动态组件

通过 component 的 is 属性,来切换不同的组件。

  • <component></component> 按名称访问全局和本地组件,可以动态绑定我们的组件,根据数据不同更换不同的组件,在这种情况下,它更像是一个容器。

  • :is 属性:is-bind的缩写,component标签中的is属性决定了当前采用的是那个组件。

image.png

10 插槽

(1) 插槽 <slot> ,可以实现内容的动态发布

有时需要在组件模板中定义大量重复的内容区域,可以使用插槽来避免重复

PS:其中<span>****</span> 我们不希望是静态不变的,而是动态内容。 image.png

image.png

(2) 这时,需要将动态内容的模板部分使用<slot>插槽来实现

image.png

(3)<slot> 插槽会获取到组件元素包含的内容进行渲染;没有插槽,组件元素内部无法被识别。

image.png

(4)如果动态内容有较多一样,只有少部分不同,可以设置插槽默认值。

image.png

11 具名插槽 v-slot:名字

为了实现更多的复杂布局,系统提供了具名插槽来实现调用机制。

image.png

image.png

image.png

具名插槽,除了使用指令 v-slot 来调用,还支持 #号 的简写方式。

image.png

12 作用域插槽

父子组件之间存在作用域,它们的模板内容只能在自己的作用域编辑;

作用域的问题导致使用插槽时,无法通过父组件区域之间访问子组件内容。

解决思路:1.子组件插槽 v-bind 把数据传出;2.父组件 v-slot 指令获取子组件插槽的数据。

image.png

简写方案:

<current-user>
    <template v-slot="slotProps">
        {{slotProps.user.age}}
    </template>
</current-user>

ES6写法,解构

<current-user>
    <template v-slot="{{user}}">
        {{user.age}}
    </template>
</current-user>
13 nextTick

下次DOM 更新循环结束之后执行延迟回调。

在修改数据之后立即使用这个方法,获取更新后的DOM 。

使用场景: 如果想要在修改数据后立刻得到更新后的 DOM 结构,可以使用 VUE.nextTick()

image.png

image.png

image.png

image.png

14 父子组件生命周期执行顺序

image.png

15 Vue2 与 Vue3 生命周期的区别

image.png

16 v-if 和 v-show 的区别

最大的区别在于:对于不显示在页面上的处理

v-if是直接不渲染该元素;

v-show是做了css样式,display:none来控制元素不显示。

=> 什么时候使用v-if,啥时使用v-show呢?

=> 元素在页面上渲染时,它的显示与隐藏是一次性决定的,后面不再改变的,使用v-if会更好,因为使用v-show它会把不需要显示的也渲染在DOM里面。

=> 如果元素是在刚开始渲染后,还会再频繁的切换,一会显示一会隐藏,使用v-show会更好,因为它只需要操作css样式即可;但使用v-if的话,它要去创建dom元素,销毁dom元素,那这样的话性能开销会更加大。

17 v-model

父组件传入一个参数道子组件;

子组件监听这个值的变化;

利用emit。通知父组件更新这个值

image.png

18 对 vue 的理解

Vue.js 是一款流行的JavaScript前端框架;Vue作者是尤雨溪。

Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互。

(1) Vue核心特性: 数据驱动(MVVM)

  • Model:模型层,应用的数据及业务逻辑
  • View:视图层:应用的展示效果,各类UI组件,可以简单的理解为HTML页面
  • ViewModel:视图模型层,框架封装的核心,用来连接Model和View,是Model和View之间的通信桥梁

(2) 组件化 : 在Vue中每一个.vue文件都可以视为一个组件

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现

  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单

  • 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

(3) 指令系统 解释:指令 (Directives) 是带有 v- 前缀的特殊属性

作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

  • 常用的指令
    • 条件渲染指令 v-if
    • 列表渲染指令v-for
    • 属性绑定指令v-bind
    • 事件绑定指令v-on
    • 双向数据绑定指令v-model

没有指令之前我们是怎么做的?是不是先要获取到DOM然后在....干点啥

19 双向绑定的理解

(1) 什么是双向绑定

单向绑定 : 把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新。

双向绑定 : 在单向绑定的基础上,用户更新了ViewModel的数据也自动被更新了,这种情况就是双向绑定。

(2) 双向绑定的原理是什么

Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM

这里的控制层的核心功能便是 “数据双向绑定” 。

理解ViewModel 它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据
20 Vue生命周期的理解

(1) 生命周期有哪些

Vue生命周期总共可以分为8个阶段创建前后, 载入前后, 更新前后, 销毁前销毁后,以及一些特殊场景的生命周期

image.png

(2)Vue生命周期流程图

61a2bebd6a0cf2d90a722ad6ca00eb42.png

具体分析

beforeCreate -> created

  • 初始化vue实例,进行数据观测

created

  • 完成数据观测,属性与方法的运算,watchevent事件回调的配置
  • 可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computedwatch完成数据计算
  • 此时vm.$el 并没有被创建

created -> beforeMount

  • 判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译
  • 优先级:render > template > outerHTML
  • vm.el获取到的是挂载DOM

beforeMount

  • 在此阶段可获取到vm.el
  • 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上

beforeMount -> mounted

  • 此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM

mounted

  • vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM

beforeUpdate

  • 更新的数据必须是被渲染在模板上的(eltemplaterender之一)
  • 此时view层还未更新
  • 若在beforeUpdate中再次修改数据,不会再次触发更新方法

updated

  • 完成view层的更新
  • 若在updated中再次修改数据,会再次触发更新方法(beforeUpdateupdated

beforeDestroy

  • 实例被销毁前调用,此时实例属性与方法仍可访问

destroyed

  • 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
  • 并不能清除DOM,仅仅销毁实例

(3) 使用场景分析

image.png

(4) 数据请求在created和mouted的区别

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成

mounted是在页面dom节点渲染完毕之后就立刻执行的

触发时机上created是比mounted要更早的

两者相同点:都能拿到实例对象的属性和方法

讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况

建议:放在create生命周期当中

21 为什么要在列表组件中写 key,其作用是什么?

key的作用 : key是给每一个vnode的唯一id,也是diff的一种优化策略可以根据key,更准确, 更快的找到对应的vnode节点

如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed

22 Vue常用的修饰符有哪些?有什么应用场景?

(1) 修饰符

vue中修饰符分为以下五种:

  • 表单修饰符
  • 事件修饰符
  • 鼠标按键修饰符
  • 键值修饰符
  • v-bind修饰符

(2) 修饰符的作用

(2.1) 表单修饰符

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步 v-model.lazy="value"

  • trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤 v-model.trim="value"

  • number 自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的值 v-model.number="age"

(2.2) 事件修饰符

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
  • prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
  • self 只当在 event.target 是当前元素自身时触发处理函数; 将事件绑定在自身身上,相当于阻止事件冒泡
  • once 绑定了事件以后只能触发一次,第二次就不会触发
  • capture 使事件触发从包含这个元素的顶层开始往下触发

(2.3) 鼠标按钮修饰符 鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • left 左键点击
  • right 右键点击
  • middle 中键点击

(2.4) 键盘修饰符 键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的

  • 普通键(enter、tab、delete、space、esc、up...)
  • 系统修饰键(ctrl、alt、meta、shift...)
<input @keyup.enter="submit">

(2.5) v-bind修饰符

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async
  • prop
  • camel

async: 能对props进行一个双向绑定

//父组件
<comp :myMessage.sync="bar"></comp> 

//子组件
this.$emit('update:myMessage',params);

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用
  • 将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的

props: 设置自定义标签属性,避免暴露数据,防止污染HTML结

<input id="uid" title="title1" value="1" :index.prop="index">

camel : 将命名变为驼峰命名法,如将view-Box属性名转换为 viewBox

<svg :viewBox="viewBox"></svg>
23 跨域是什么?Vue项目中你是如何解决跨域的呢?

(1) 跨域是什么

跨域本质是浏览器基于同源策略的一种安全手段

同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能

所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol)
  • 主机相同(host)
  • 端口相同(port)

反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域

(2)如何解决 解决跨域的方法有很多,下面列举了三种:

  • JSONP
  • CORS
  • Proxy

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

(3) CORS : 跨域资源共享

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

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

(4) 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'"" 
                }
            }
        }
    }
}
24 Vue2 与 Vue3 的区别

Vue3 简要就是:

  • 利用新的语言特性(es6)
  • 解决架构问题

Vue3的新特性:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用
25 过滤器

过滤器: 就是把一些不必要的东西过滤掉

Vue 允许你自定义过滤器,可被用于一些常见的文本格式化

ps: Vue3中已废弃filter

使用方法:vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 JavaScript表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

image.png