vue相关

141 阅读13分钟

VUE相关

!1.vue响应式原理

响应式原理即数据更新,视图也跟着更新。

React是依靠虚拟DOM和diff算法实现的;

而VUE是利用Object.definePropertysettergetter方法对数据进行劫持;

利用观察者模式来观察数据;

利用发布订阅者模式,使用dep收集watcher,通过notify方法通知dep里的watcher去进行相应的更新

这个过程定义了三个类:Dep、Watcher、Observer

//Dep类
//Dep对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅
//每个响应式对象和子对象(每一个数据)都拥有一个Dep实例,subs是Watcher实例数组,数据变化时,通过notify通知给各个watcher
//Dep类实例用于管理-->依赖数据的watcher类实例
class Dep {
    constructor() {
        //用来存放Watcher对象的数组
        this.subs = []
    }
    //在subs中添加一个Watcher对象--收集依赖--收集所有使用到这个data的Watcher对象
    addSub(sub) {
        this.subs.push(sub)
    }
    removeSub(sub) {
        this.subs.$remove(sub);
    }
    //get调用depend
    depend() {
        if (Dep.target) {
          // 搜集依赖,最终会调用下面的 addSub 方法,target是一个Watcher对象
        Dep.target.addDep(this);
    }
    //通知所有的Watcher对象更新视图---向依赖发送消息(通知所有的Watcher对象更新
    //set调用notify
    notify() {
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
          // 调用对应的 Watcher,更新视图
          subs[i].update() //该update方法即Wacther类中的开启一个异步队列的方法
    }
}
//订阅者Dep类用于收集依赖、删除依赖和向依赖发送消息等,说白了就是存放创建的一些Watcher观察者对象。
//可以把Watcher理解成一个中介,当数据发生改变时通知Dep类,然后再通知所有的Watcher对象更新视图

image-20220310112302873

// Watcher类
// Watcher 实现了渲染方法 _render 和 Dep 的关联, 初始化 Watcher 的时候,打上 Dep.target 标识,然后调用 get 方法进行页面渲染。
// watcher实例对象分为三种,一是(渲染)render watcher,指编译过程中通过addsub收集依赖;二是user watcher,指在响应化时,对每个对象创建dep实例,也叫做收集依赖;三是计算属性computed watcher,指对compued属性初始化时-没见过
function Watcher() {
    constructor(vm, expOrFn) {
        this.getter = expOrFn //将vm._render的方法赋值给getter  --expOrFn就是vm._render--用来生成虚拟dom、执行diff算法、更新真实dom
        this.value = this.get()
    },
    get() {
        Dep.target = this //Dep.target赋值为当前的Watcher对象
        const value = this.getter.call(this.vm, this.vm)
        return value
    },
    addDep(dep) {
        // 将当前的 Watcher 添加到 Dep 收集池中
        dep.addSub(this);
    }
    update() {
        // 开启异步队列,批量更新 Watcher
        queueWatcher(this);
    }
    run() {
        // 数据更新之后重新走get方法,和初始化一样,会调用 get 方法,更新视图
        const value = this.get();
    }
}
​

image-20220303202139642

image-20220310111558234

//Observer类
//作用是:
//1、把Obeserver类的实例对象挂载在__ob__属性上,以给后续观测数据使用.
//2、避免被重复实例化。
//3、对每一条数据进行实例化Dep类实例,并将数据(对象/数组)保存下来,如果数据是对象,就执行walk函数,对其响应化处理,即调用defineReactive方法;如果数据是数组,就执行observeArray方法,递归地对数组的每个元素调用observe()
class Observer {
    constructor(value) {
        this.value = value
        this.dep = new Dep()
        def(value, '__ob__', this)
        if (isArray(value)) {
            //如果数据是数组,并检查原型链上有没有对数组进行改造,如果没有需要对数组方法进行重写,以便能对数组进行响应式处理
            var augment = hasProto
              ? protoAugment
              : copyAugment
            augment(value, arrayMethods, arrayKeys)
            //完成后调用数组处理方法
            this.observeArray(value)
        } else {
            //不是数组就是对象
            this.walk(value)
        }
    }
    walk(obj) {
        var keys = Object.keys(obj)
        for (var i = 0, l = keys.length; i < l; i++) {
            //遍历调用convert方法
            this.convert方法(keys[i], obj[keys[i]])
        }
    }
    observeArray(items) {
        // 对数组每个元素进行处理
        // 主要是处理数组元素中还有数组的情况
        for (var i = 0, l = items.length; i < l; i++) {
            observe(items[i])
        }
    }
    convert(key, val) {
        //响应化处理,对每条数据添加getter和setter
        defineReactive(this.value, key, val)
    }
    addVm(vm) {
        (this.vms || (this.vms = [])).push(vm)
    }
    removeVm(vm) {
        this.vms.$remove(vm)
    }
}

基于观察者模式,涉及三个类、三个阶段,

Observe类用于给每条数据添加getter、setter;

Watcher类起到一个中介作用,数据更新时通知dep类,然后通知所有订阅的Watcher对象重新渲染,更新视图;

Dep类用于收集依赖、删除依赖、向依赖发送消息,也就是管理订阅的Watcher对象。

  1. 组件初始化阶段,给每一个Data属性通过Object.defineProperty都注册getter、setter,也就是data响应化reactive化,完成数据代理;同时对每条数据创建Dep实例----用来对数据进行user watcher依赖收集,即每个数据的Watcher对象。
  2. 编译模板阶段,new一个Watcher对象,此时watcher会立即调用组件的render函数去生成虚拟DOM,并将Dep.target标识为当前Watcher。
  3. 编译模板过程中,当调用data属性时,即触发get请求,会通过Dep.addSub将new的Watcher对象加入到依赖收集池Dep的subs中。--render watcher依赖收集
  4. 数据更新阶段,data属性改变,触发set方法,通过Dep.notify遍历subs中的所有的watcher对象,通知他们调用update方法,去重新渲染组件。--向依赖发送消息

总之就是给数据注册一个getter、setter,当数据发生改变时,vue监听到了改变,于是重新渲染组件。

但vue无法监听对象的属性新增和删除,直接通过obj.xxx=xxx是检测不到的。只能对对象的属性进行监听,无法对数组的变化监听。

Vue出于性能考虑没有对数组下标进行劫持,而是通过改写数组原型方法,如果想监听数组变化就需要对数组的常用方法进行重写。解决方法:

  • splice:arr.splice(index, 1, value)
  • Vue.set(target, index, value)
  • 想实现对象的属性的新增和删除使网页能更新视图,即view可监听到,解决方法:
  • 新增属性:Vue.set(target, key, value)
  • 删除属性:Vue.delete(target, key)

还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题。

!?2.VUE双向绑定原理

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
  • 观察者(Watcher):组件编译阶段时,会new一个watcher对象,也就是订阅数据,绑定更新函数的过程,以供监听器监听到数据变化通知到Dep对象,再通知到Watcher对象,再调用update函数来更新视图

image-20220310162430802

也是涉及了三个类,三个过程。

Observer类用于给每一条属性注册setter、getter,完成数据代理;

Watcher类起到一个中介作用,在初始化时new一个Watcher对象,添加到Desp中参与订阅,更新时通知dep中所有参与订阅的对象更新视图,重新渲染;

Dep类用于收集依赖、删除依赖、向依赖发送消息,管理所有的依赖。

  1. 组件初始化阶段,给每一个Data属性通过Object.defineProperty都注册getter、setter,也就是data响应化reactive化,完成数据代理,这个过程在Observer中完成。
  2. 编译阶段,找到v-model双向绑定的数据,从data中获取初始化视图,这个过程在Compile中完成。(model->view)
  3. 数据更新阶段,data变化,触发了set方法,通过Dep中的notify方法遍历所有订阅者subs(Wacther对象)执行update函数,重新渲染组件,更新视图。(view->model)
<input id='text' />
<span id='sp'></span>
<script>
    let obj = {}
    let input = document.getElementById('text')
    let span = document.getElementById('sp')
​
    function defineProperty(obj, 'msg', {
        configurable: true,
        enumerable: true,
        get() {
            console.log('获取数据了')
        },
        set(newVal) {
            console.log('数据更新了')
            text.value = newVals
            sp.innerHTML = newVal
        }
     })
​
     //输入监听--当文本框输入内容时,改变obj.msg的值
     text.addEventListener('keyup', function(event) {
         obj.msg = event.target.value
     })
</script>// 以上就是一个简单的vue双向绑定

3.vue中的key有什么作用?

作用:

用于管理可复用的元素,vue为了高效渲染元素,会首先考虑复用,而不是从头渲染。这样做虽然有时候挺快,但是有时候不符合实际的需求。如需要进行不同的input切换时(两种登陆方式),切换后需要清除input中的内容,如果直接复用就不能实现,这就需要给每个input添加一个具有唯一值的key属性即可。或者数据项的顺序发生变化了,不可以简单对元素进行复用,也要给每个元素添加唯一的key属性,保证高效地更新虚拟DOM。

或者可以解释为当数据发生变化时,vue内部要进行节点也就是虚拟DOM的对比,key值是进行判断新旧节点是否是同一个节点的依据,也可以说是判断是否发生变化的依据。

如v-for中,如果不加key,当数据发生改变时,并不会重新渲染,也就是不会重新创建新的dom结构,仅仅是替换了元素,对原结构进行复用,这样的话对新传入的数据做一些操作就不会触发?

4.vue改变数组中的值vue怎么监听到的?

通过Object.defineProperty()劫持数组为其设置getter和setter后,调用的数组的push、splice、pop等方法改变数组元素时并不会触发数组的setter,这就会造成使用上述方法改变数组后,页面上并不能及时体现这些变化,也就是数组数据变化不是响应式的。

但是实际开发时,页面能实现数组的数据响应式变化。

这是因为vue重写了数组的push等一系列方法。

 3 // 获取数组的原型Array.prototype,上面有我们常用的数组方法
 4 const arrayProto = Array.prototype
 5 // 创建一个空对象arrayMethods,并将arrayMethods的原型指向Array.prototype
 6 export const arrayMethods = Object.create(arrayProto)
 7 
 8 // 列出需要重写的数组方法名
 9 const methodsToPatch = [
10   'push',
11   'pop',
12   'shift',
13   'unshift',
14   'splice',
15   'sort',
16   'reverse'
17 ]
18 // 遍历上述数组方法名,依次将上述重写后的数组方法添加到arrayMethods对象上
19 methodsToPatch.forEach(function (method) {
20   // 保存一份当前的方法名对应的数组原始方法
21   const original = arrayProto[method]
22   // 将重写后的方法定义到arrayMethods对象上,function mutator() {}就是重写后的方法
23   def(arrayMethods, method, function mutator (...args) {
24     // 调用数组原始方法,并传入参数args,并将执行结果赋给result
25     const result = original.apply(this, args)
26     // 当数组调用重写后的方法时,this指向该数组,当该数组为响应式时,就可以获取到其__ob__属性
27     const ob = this.__ob__
28     let inserted
29     switch (method) {
30       case 'push':
31       case 'unshift':
32         inserted = args
33         break
34       case 'splice':
35         inserted = args.slice(2)
36         break
37     }
38     if (inserted) ob.observeArray(inserted)
39     // 将当前数组的变更通知给其订阅者
40     ob.dep.notify()
41     // 最后返回执行结果result
42     return result
43   })
44 })

5.vue的传值方式

一、父子之间传值

1.通过props使父组件向子组件传值

  <template>
    <div class="parent">
      <h2>{{ msg }}</h2>
      <son psMsg="父传子的内容:叫爸爸"></son> <!-- 子组件绑定psMsg变量-->
  </div>
  </template>
  <script>
  import son from './Son' //引入子组件
  export default {
    name: 'HelloWorld',
    data () {
      return {
        msg: '父组件',
      }
    },
    components:{son},
  }
  </script>

父组件中通过在子组件上添加值,然后子组件通过props接收一下,子组件就可以用了

  <template>
    <div class="son">
      <p>{{ sonMsg }}</p>
      <p>子组件接收到内容:{{ psMsg }}</p>
    </div>
  </template>
  <script>
  export default {
    name: "son",
    data(){
      return {
        sonMsg:'子组件',
      }
    },
    props:['psMsg'],//接收psMsg值
  }
</script>

2.通过emit事件使子组件向父组件传值

  <template>
    <div class="parent">
      <h2>{{ msg }}</h2>
      <p>父组件接收到的内容:{{ username }}</p>
      <son psMsg="父传子的内容:叫爸爸" @transfer="getUser"></son>  <!--绑定自定义事件transfer-->
    </div>
  </template>
  <script>
  import son from './Son'
  export default {
    name: 'HelloWorld',
    data () {
      return {
        msg: '父组件',
        username:'',
      }
    },
    components:{son},
    methods:{
      getUser(msg){
        this.username= msg
      }
    }
  }
  </script>

emit事件包含两个参数,第二个值是要传给父组件的值,第一个值是父组件的点击事件,该点击事件触发父组件的方法去获取传来的值。

子组件点击按钮,触发setUser方法,通过emit事件触发了父组件中的transfer自定义事件,父组件触发getUser方法,将子组件通过emit事件一起携带过来的user通过msg的形式赋给了username

  <template>
    <div class="son">
      <p>{{ sonMsg }}</p>
      <p>子组件接收到内容:{{ psMsg }}</p>
      <!--<input type="text" v-model="user" @change="setUser">-->
      <button @click="setUser">传值</button>
    </div>
  </template>
  <script>
  export default {
    name: "son",
    data(){
      return {
        sonMsg:'子组件',
        user:'子传父的内容'
      }
    },
    props:['psMsg'],
    methods:{
      setUser:function(){
        this.$emit('transfer',this.user)//将值绑定到transfer上传递过去
      }
    }
  }
  </script>

3.parentparent和children

在组件内部可以直接通过子组件$parent对父组件进行操作,父组件通过$children对子组件进行操作.

定义一个方法:

this.$parent.message = this.mymessage

4.使用v-model实现父子双向动态传值

v-model实现数据双向绑定的原理等同于单向绑定并触发了input事件

<input v-model="something">

等同于

<input
  v-bind:value="something"
  v-on:input="something = $event.target.value">   // 这里的 $event.target.value 是指的js对象的property
​
<input
  :value="something"
  @input="something = $event.target.value"> 

根据这个原理我们可以通过子组件的$emit方法来触发input事件,父组件监听input事件中传递的value值,并储存在父组件的data中;

然后父组件通过props的形式传给子组件value值,在子组件的input中绑定value属性即实现了双向的绑定及传输。

具体实现过程:

子组件中给input标签绑定input事件,使用$emit触发父组件的input事件,通过这种方法实现子组件给父组件传值

<input type="text" @input="$emit('input', $event.target.value)" :value='value'>
​
//...
export default {
  name: 'childClass',
  props: ['value']
}

父组件监听input事件,然后将事件携带的input输入的值传入data

<child-class @input="value = $event" :value='value'></child-class>
​
export default {
  // ...
  data () {
    return {
      value: 'initial value' // 维护一个 value 状态
    }
  }
}

最后给父组件中的child-class增加:value="value"再通过propsvalue给子组件,子组件的input:value="value"双向绑定

如果不是input而是checkbox则触发的是change事件,value就是checked

二、不同组件之间传值。

1.通过vuex实现不同组件之间传值

2.通过eventBus传值(小项目适用-跨多级和兄弟组件传)

第一步,新建一个bus.vue文件

第二步在发送消息的组件中,导入bus.vue,再用bus.$emit来传入两个参数,第一个为消息名或者事件名,第二个为通信的值。

第三步在接收消息的组件中,导入bus.vue,再用bus.$on来接收两个参数,第一个为消息名或者事件名,第二个为一个回调函数,函数的形参为通信的值。

或者不在每个组件中引入bus.vue文件,只引入到main.js中,用Vue.prototype.$bus=Bus挂载到全局,这样直接在组件中使用this.$bus.$emit()this.$bus.$on()即可。

3.$attrs$listeners(跨多级传值-爷给孙用attrs--孙给爷用listeners)

$attrs就像是一个容器,存放着父组件通过v-bind绑定的传给子组件的数据,但子组件没有用props声明接收的数据。

<template>
  <div id="app">
    我是爷组件
    <fu v-bind:msg1="msg1" :msg2="msg2" @fromSun="fromSun"></fu> <!-- msg就是要传给父组件和子组件的数据 -->
  </div>
</template><script>
import fu from "./views/fu.vue";
export default {
  components: {
    fu,
  },
  data() {
    return {
      msg1: "孙悟空",
      msg2: "猪八戒",
    }
  },
  methods: {
    fromSun(payload) {
        console.log("孙传祖", payload);
        this.fromSunData = payload;
    }
  }
}
</script>

父组件中只用props来接收msg1的数据,msg2不做接收,但是由于给了v-bind,所以自动存在了父组件的$attrs中了,可以通过$attrs.msg2来接收。

$listeners的用法:

第一步,在父组件中加上v-on="$listeners"

第二步,爷组件中定义事件方法fromSun

第三步,孙组件中用点击事件sendToZu去触发父组件的事件方法,把孙中的数据传给爷。

<template>
  <div class="fatherClass">
    我是父组件
    <h2>{{ msg1 }}</h2>
    <h2>{{ $attrs.msg2}}</h2>
    <sun-class v-bind="$attrs" v-on="$listeners"></sun-class>
  </div>
</template><script>
export default {
  name: "DemoFather",
  props: {
    msg1: {
      type: String,
      default: "",
    },
  },
};
</script>

再通过在父组件中v-bind="$attrs"把数据传给孙组件,这样孙组件就可以用props来接收数据。

<template>
  <div class="sunClass">
    我是孙子组件
    <h2>接收爷组件数据:-->{{ msg2 }}</h2>
      <button @click='sendToZu'>孙传祖</button>
  </div>
</template><script>
export default {
  // $attrs一般搭配interitAttrs 一块使用
  inheritAttrs: false, // 默认会继承在html标签上传递过来的数据,类似href属性的继承
  /*
    孙子组件通过props,就能接收到父组件传递过来的$attrs了,就能拿到里面的数据了,也就是:
    爷传父、父传子。即:祖孙之间的数据传递。
  */ 
  props: {
    msg2: {
      type: String,
      default: "",
    }
  },
  data() {
      return {
          data: "来自孙组件的数据",
      };
  },
  methods: {
      sendToZu() {
          // 孙组件能够触发爷组件的fromSun方法的原因还是因为父组件中有一个$listeners作为中间人,去转发这个事件的触发
          this.$emit("fromSun", this.data);
      },
  },
  name: "DemoSun",
};
</script>

4.provide和inject (用于爷向子孙传值)

作用:用于父组件向子孙组件传递数据

使用方法:provide在父组件中返回要传给下级的数据,inject在需要使用这个数据的子辈组件或者孙辈等下级组件中注入数据。

使用场景:由于vue有$parent属性可以让子组件访问父组件。但孙组件想要访问祖先组件就比较困难。通过provide/inject可以轻松实现跨级访问父组件的数据

在爷组件中使用provide来发送

<template>
  <div class="yeClass">
      <child></child>
  </div>
</template><script>
export default {
 name: "yeClass",
 data() {
      return {
          data: "来自爷组件的数据",
      };
 },
 components: {
    child
 },
 provide: {
     for: 'demo'
 }
  
};
</script>

在子组件或者孙组件中使用inject来接收

<template>
  <div class="sunClass">
  </div>
</template><script>
export default {
 name: "sunClass",
 inject: ['for'],
 data() {
      return {
          demo:'this.for'
      };
};
</script>

总结:

8种vue组件间通信方式:

父子之间:props$emit$parent$childrenv-model

祖先与后代之间:provideinject$attrs$listeners

任意组件之间:vuex、事件总线BUS

补充:使用$refs获取组件实例,进而获取数据

6.vue的传值方式之一—vuex

  • state:定义初始状态
  • getter:从store从取数据
  • mutation:更改store中状态,只能同步操作
  • action:用于提交mutation,而不直接更改状态,可异步操作
  • module:store的模块拆分

7.vue的优缺点?和react的区别?

优点:

  1. 渐进式框架

    意思是框架的使用者的要求很弱,需要什么功能就用什么。不像Angular,使用它就必须使用全套它的东西;也不像React,但仍有一些强制性要求,即自己的主张,如要求函数式编程等。vue有组件化开发、大规模状态管理vuex、构建工具vite、vue-resource不用推荐用axios,这些功能想用就用,不想要就不要,这也是vue的设计理念-渐进式的框架。

  2. 虚拟dom

    组件刚被创建和数据更新时会重新调用render函数渲染视图,如果直接使用真实DOM,会浪费极大的性能,降低渲染效率;使用虚拟DOM和数据的双向绑定,进行真实和虚拟DOM的对比,具体的是内部的diff算法中的patch函数来进行节点的对比。(vue的diff算法:传统的diff算法时间复杂度为o(n3),而它只需要进行同层对比就行,复杂度为o(n)

  3. 组件化开发

  4. 响应式数据

  5. 单页面开发

    不需要跳转页面和重新渲染就可以切换不同的页面,用户体验好,服务器压力也小;当然也有缺点:首屏加载慢、搜索引擎优化SEO难(所有页面都在一个页面中动态显示)

  6. 数据和视图分开

缺点:

  1. 单页面应用很难去做搜索引擎优化SEO
  2. 首屏加载太慢
  3. 不兼容IE

和react作比较

相同点:

  1. 都是单向数据流
  2. 都使用虚拟dom的技术
  3. 都支持服务端优化ssr
  4. 都是组件化开发

不同点:

数据的变化,vue是响应式的,react用的是手动setState

vue实现的是双向绑定,react是单向

vue状态管理工具vuex,react是Redux、Modx

8.SPA的实现

单页面应用:不需要重新加载整个页面来实现页面之间的切换

原理:

1.监听地址栏中hash变化来驱动界面变化

2.用pushsate记录浏览器的历史,驱动界面发生变化

实现路由跳转从而实现SPA的两种模式:

1.hash模式

核心是通过监听url中的hash来进行路由跳转

2.history模式

核心借用了HTML5 history 的api,如:

  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popStatehistory 发生变化时触发

如何给SPA做SEO---vue2缺点一

1.SSR服务端渲染

将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js

2.静态化

目前主流的静态化主要有两种:

(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中

(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果

3.使用phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。

SPA首屏加载优化方案---vue2缺点二

常见的几种SPA首屏优化方式

  • 减小入口文件积--懒加载
  • 静态资源本地缓存--合理运用localstorage
  • UI框架按需加载
  • 图片资源的压缩
  • 组件重复打包
  • 开启GZip压缩
  • 使用SSR 服务端优化

9.vue路由守卫的作用和应用场景

路由守卫:路由跳转前做一些验证,如登陆验证

全局路由守卫: 指路由实例上直接操作的钩子函数,特点是所有的路由配置都会触发这些钩子函数。包括:

beforeEach:在路由跳转前触发,传入三个参数,包括to、from、next,这个钩子的作用是在路由跳转前进行验证,用的最多的路由守卫钩子。

const router = new VueRouter({ ... })
​
router.beforeEach((to, from, next) => {
  // ...
})

beforResolve:和上面的一样,区别为在 beforeEach 和 组件内beforeRouteEnter 之后,afterEach之前调用。基本不用。

afterEach:和beforeEach相反,路由跳转完成后触发,传入两个参数,包括to、from。发生在beforeEach和beforeResolve之后,beforeRouteEnter(组件内守卫)之前。

路由独享守卫: 单个路由配置的时候也可以设置的钩子函数,

beforeEnter:和beforeEach完全相同,如果都设置则在beforeEach之后紧随执行,参数to、from、next

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件内路由守卫: 是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。

钩子函数按执行顺序包括beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave三个,执行位置如下

export default{
  data(){
    //...
  },
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

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

当我们想要复用组件时,但是想对组件中少量的地方进行更改,不进行重写,而是通过slot插槽的方式向组件内部想改的位置传递内容,完成这个复用组件在不同场景的应用。

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

插槽包括:

  • 默认插槽
  • 具名插槽
  • 作用域插槽

默认插槽就是普通的插槽。

具名插槽:

在子组件中用name属性来表示插槽的名字,不传的时候就是默认插槽。

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

作用域插槽:

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件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 = '默认值'}"

?11.虚拟dom和diff算法?

虚拟dom是对数据通过Object.defineProperty进行数据劫持的同时新建一个Wacther对象,该对象会调用render函数来渲染生成虚拟dom,也就是组件刚被创建时,数据更新时会对新旧虚拟dom对比生成真实dom,而diff算法就是在这个过程中进行优化的。

而虚拟dom存在的作用就是避免多次dom操作而对页面的重排,虚拟dom不会进行重排操作,只会通过新旧dom对比从而生成真实dom,一次性地进行重排。

diff 算法是一种通过同层的树节点进行比较的高效算法,组件创建或更新时会执行vue内部的update函数,该函数运行render函数得到虚拟dom树,将他和旧的虚拟dom树对比,完成真实dom的更新,渲染页面。

两棵树进行对比更新差异就是diff算法,因为diff算法是通过patch函数完成的,所以也叫patch算法。

特点:

  • 比较只会在同层级进行, 不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间比较(这一点相比react更高效)
  • 对比过程中发现新节点不存在时,就直接删掉旧节点;新节点存在,旧节点不存在,就直接新建
  • 这样相比传统的diff算法(新旧节点互相对比o(n^2)、对比完进行dom操作还需要遍历一次,这样就o(n^3))时间复杂度变为o(n)只需要所有节点同级比较,只会遍历一次。

diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

具体步骤:

  • 第一步:调用patch方法,传入新旧虚拟dom,开始同层对比

  • 第二步:调用isSameNode方法,对比新旧节点是否未同类型节点

  • 第三步:如果不同,新节点直接替换旧节点

  • 第四步,如果相同,调用patchNode进行节点对比

    • 如果旧节点和新节点都是文本节点,则新文本代替旧文本
    • 如果旧节点有子节点,新节点没,则删除旧节点的子节点
    • 如果旧节点没有子节点,新节点有,则把子节点新增上去
    • 如果都有子节点,则调用updateChildren方法进行新旧子节点的对比
    • 子节点对比为首尾对比法

?12.vue中的axios封装

axios 是一个轻量的 HTTP客户端,基于ajax和promise的

基于 XMLHttpRequest 服务来执行 HTTP 请求,支持丰富的配置,支持 Promise,支持浏览器端和 Node.js 端。自Vue2.0起,尤大宣布取消对 vue-resource 的官方推荐,转而推荐 axios

特性

  • 从浏览器中创建 XMLHttpRequests
  • node.js 创建 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换JSON 数据
  • 客户端支持防御XSRF

13.说说对nextTick的理解

vue中对于dom元素的修改是异步的,不能马上得到最新的数据变化,在nextTick中的回调函数可以得到最新的数据变化,

new Vue({
  el: '.app',
  data: {
    msg: 'Hello Vue.',
    msg1: '',
    msg2: '',
    msg3: ''
  },
  templete: {
    <div class="app">
      <div ref="msgDiv">{{msg}}</div>
          <div v-if="msg1">Message got outside $nextTick: {{msg1}}</div>
          <div v-if="msg2">Message got inside $nextTick: {{msg2}}</div>
          <div v-if="msg3">Message got outside $nextTick: {{msg3}}</div>
          <button @click="changeMsg">
            Change the Message
          </button>
    </div>
  },
  methods: {
    changeMsg() {
      this.msg = "Hello world."
      this.msg1 = this.$refs.msgDiv.innerHTML
      this.$nextTick(() => {
        this.msg2 = this.$refs.msgDiv.innerHTML //结果只有msg2变成了hello world
      })
      this.msg3 = this.$refs.msgDiv.innerHTML
    }
  }
})

因为vue中dom元素的更新是异步的,当检查到数据变化,vue将会开启一个队列(queueWatcher(this)),并将多个数据改变放到同一个事件循环中?批量更新watcher。而且同一个watcher如果被触发了多次,只会被推入到队列中一次,这样的话可以去除重复数据对于避免不必要的计算和dom操作非常重要。

应用场景:

  1. 在Vue生命周期的created()钩子函数进行的dom操作一定要放在vue.nextTcik()的回调函数中,因为此时还未对dom结构进行渲染。
  2. 当数据变化后想要执行某些操作,而这个操作需要随着数据改变而立即改变dom结构的时候,这个操作需要放到vue.nextTcik()的回调函数中。

14.MVVM模型的理解

MVVMModel-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对ViewViewModel的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给View,即所谓的数据双向绑定。

MVVMMVC的升级版,MVCModel-View-Controller 的缩写。

View :视图-把数据以某种方式呈现给用户

Model:模型-管理数据

Controller :响应用户操作,并将Model 更新到View 上。

随着技术的更新,大量调用相同API,操作复杂、冗余,影响页面性能、用户体验,Model和View之间的数据同步及更新越来越多、复杂。

MVVM出现解决了以上的问题,

Model-数据模型

View-视图

ViewModel-模型视图

MVVM架构下,ViewModel之间并没有直接的联系,而是通过ViewModel进行交互, 在Model更新时,ViewModel通过绑定器将数据更新到View,在View触发指令时,会通过ViewModel传递消息到Model

ViewModel 通过双向数据绑定把 View层和 Model层连接了起来,实现数据的同步,不需要手动更新。

优点:

1、主要目的是为了分离视图和模型,各自独立开发。

2、降低代码耦合,提高代码和逻辑的可读性。--视图view可以独立于Model的变化和修改,ViewModel之间并没有直接的联系

3、方便测试,ViewModel可以方便代码的测试。

4、具有可复用性,可以把一些试图逻辑放在一个ViewModel中,让很多的View复用这段视图逻辑。

15.vue的生命周期

  1. beforeCreate:未初始化组件和响应式数据(未添加setter、getter
  2. created:初始化和响应式数据,可以访问数据
  3. beforeMount:调用render函数,生成虚拟dom
  4. mounted:真实dom挂载完成
  5. beforeUpdate:数据更新,生成新的虚拟dom
  6. updated:新旧虚拟dom比较,打补丁,进行真实dom的更新
  7. beforedestory:实例销毁前,仍可以访问数据
  8. destory:实例销毁、子实例销毁,解绑指令,解绑实例的事件
  9. activated:keep-alive所缓存的组件被激活时调用
  10. deactivated:keep-alive所缓存的组件被停用时调用
  11. errorCaptured:子孙组件的错误捕获,此函数可返回false阻止继续向上传播

16.vue3与vue2的区别?vue3有哪些亮点?

  • 更小:移除了一些不常用的API,通过webpack的tree-shaking,选择性打包,使文件体积更小了。

  • 更快:

    • diff算法优化

      • vue3在diff算法中添加了flag标记,进一步提高性能
    • 静态提升(静态标记)

      • 不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接使用,虚拟DOM按需更新。
    • 事件监听缓存

      • 绑定事件不会每次都去监听它的变化
    • SSR优化

      • 如果存在很多静态属性时,会自动生成一个静态node,不需要创建对象,直接根据对象渲染。
  • 更友好:

    • Vue3是基于TS编写的,类型概念更强,更严谨。
    • 兼顾options API的基础上推出了composition API,提升了代码的逻辑性和复用性。即实现一个功能的所有API会放到一起。适合项目大、逻辑复杂,实现高内聚、低耦合、高复用、逻辑性更强、更易维护。

亮点:

  • 源码管理?
  • 基于TS编写
  • 体积优化
  • 数据劫持优化:Object.defineProperty不能监测对象属性的增加和删除,虽然有set和delete可以用,但是Vue3利用Proxy监听整个对象,同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在getter 中去递归响应式?
  • composition API提升了代码的逻辑性和复用性。

17.为什么要用 Proxy API 替代 defineProperty API ?

vue2:

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

vue3:

Object.defineProperty只能遍历对象属性进行劫持

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的

对象属性的添加和删除、数组方法都能正常监听到

只有在getter时才对深层对象进行监听,优化了性能

function reactive(obj) {
    if (typeof obj !== 'object' && obj != null) {
        return obj
    }
    // Proxy相当于在对象外层加拦截
    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            console.log(`获取${key}:${res}`)
            return res
        },
        set(target, key, value, receiver) {
            const res = Reflect.set(target, key, value, receiver)
            console.log(`设置${key}:${value}`)
            return res
        },
        deleteProperty(target, key) {
            const res = Reflect.deleteProperty(target, key)
            console.log(`删除${key}:${res}`)
            return res
        }
    })
    return observed
}

\