Vue汇总

212

vue基础

谈一下对vue数据双向绑定的理解

Vue 核心之数据双向绑定

image.png

view=>data的实现的原理

给input框绑定oninput事件,在方法中去修改data中对应属性的值,就会立刻调用底层对象对应的set方法,进而实现data中数据的更新,再将数据渲染到view中

data => view

通过实现以下 4 个步骤,来实现数据的双向绑定:

image.png

1 、实现一个监听器 Observer ,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者;

Observer.prototype = {
    walk: function(data) {
        var self = this;
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },
    defineReactive: function(data, key, val) {
        var dep = new Dep();
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function getter () {
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return val;
            },
            set: function setter (newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                dep.notify();
            }
        });
    }
};
  • 递归遍历data中的属性,通过Object.defineProperty(obj, prop, descriptor),为每一项属性添加get(获取值触发)和set(修改值触发)方法,实现数据的可观测性

2 、实现一个订阅器 Dep,用来收集订阅者,对监听器 Observer 和 订阅者 Watcher 进行统一管理,即当数据变化的时候后执行对应订阅者的更新函数;

  • dep的属性subs:存的是watcher对象this.subs = []

    • 每次为data的属性defineProperty时,就会生成对应属性的dep实例
  • dep的方法addSub:实现将watcher添加的subs数组中。

    • 判断Dep在什么时候添加wather,是通过在get中会判断Dep.target(初始化为null),如果为null,就不会为dep添加watcher,即初始化时,dep数组时不会有值的
  • dep的方法notify:遍历通知每个watcher,触发watcher的update回调函数

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

3 、实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图;

function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全局变量 订阅者 赋值
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null; // 全局变量 订阅者 释放
        return value;
    }
};
  • Watcher类的属性

    • vm:一个 Vue 的实例对象
    • exp:是 node 节点的 v-model 等指令的属性值 或者插值符号中的属性。如 v-model="name"exp 就是name;
    • cb:是 Watcher 绑定的更新函数;
  • watcher的方法get:当去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,会执行

    get: function() {
        Dep.target = this;  // 缓存自己
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
    
    • 利用触发defineProperty中的get方法,同时将当前watcher实例赋值给Dep.target,就实现了 为dep中添加watcher同时当前watcher也绑定了vm.data中的数据
  • watcher的方法update:当数据发生变化时调用 Watcher 自身的更新函数进行更新的操作。

    • 先通过 let value = this.vm.data[this.exp]; 获取到最新的数据,
    • 然后将其与之前 get() 获得的旧数据进行比较,如果不一样,则调用更新函数 cb 进行更新。

提出问题:应该在什么时候创建watcher实例呢?

4 、实现一个解析器 Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化,将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器。

  • 首先通过递归去遍历复制template模版的整个dom节点树,拿到fragment

  • 再挨个遍历根结点的每个孩子节点,进行判断再做相应的操作

    • 如果是文本节点node.nodeType == 3,且满足了{{exp}}差值表达式,就创建一个watcher实例,并传入data中对应的属性exp,就添加对this.vm[exp]的订阅者,实现了model->view的数据绑定

    • 如果是标签元素node.nodeType == 1,则拿到当前节点的属性,

      • 如果是v-model='name',则需要绑定数据。通过modelUpdater方法,为视图中的值初始化绑定vm中的数据,同时创建watcher实例,并将modelUpdater以更新数据的回调函数传递过去
      modelUpdater: function (node, value, oldValue) {
          node.value = typeof value == 'undefined' ? '' : value
      }
      
      • 如果是v-on:click='handel',则需要绑定事件。通过attr.name拿到click,attr.value拿到handel,并为当前节点注册对应的事件函数
var Watcher = require('./watcher')
function Compile(el, vm) {
  this.vm = vm
  this.el = document.querySelector(el)
  this.fragment = null
  this.init()
}

Compile.prototype = {
  init: function () {
    if (this.el) {
      // 复制整个dom节点树
      this.fragment = this.nodeToFragment(this.el)
      //   挨个遍历根节点的所有孩子
      this.compileElement(this.fragment)
      // 进行模版替换后,将真实的dom添加到页面上
      this.el.appendChild(this.fragment)
    } else {
      console.log('Dom元素不存在')
    }
  },
  nodeToFragment: function (el) {
    var fragment = document.createDocumentFragment()
    // 获取第一个 节点,包含文本节点
    var child = el.firstChild
    while (child) {
      // 将Dom元素移入fragment中
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  },
  compileElement: function (el) {
    //   el.childNodes按代码顺序获取所有的节点,包括文本换行节点等
    //   el.children 只获取标签元素,不包括文本换行节点等
    var childNodes = el.childNodes
    var self = this
    ;[].slice.call(childNodes).forEach(function (node) {
      var reg = /\{\{(.*)\}\}/
      //   拿到当前节点的文本
      var text = node.textContent
      // 判断是不是标签
      if (self.isElementNode(node)) {
        self.compile(node)
      } else if (self.isTextNode(node) && reg.test(text)) {
        //    RegExp.$1------reg.exec(text)[1]
        self.compileText(node, reg.exec(text)[1])
      }

      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node)
      }
    })
  },
  //   编译 标签
  compile: function (node) {
    var nodeAttrs = node.attributes
    var self = this
    Array.prototype.forEach.call(nodeAttrs, function (attr) {
      // attr.name---获取 key
      var attrName = attr.name
      //   如果是以v-开头的指定
      if (self.isDirective(attrName)) {
        //  attr.value--- 获取value
        var exp = attr.value
        // 去掉v-,比如为on:click
        var dir = attrName.substring(2)
        // 判断是否为事件指令
        if (self.isEventDirective(dir)) {
          self.compileEvent(node, self.vm, exp, dir)
        } else {
          // v-model 指令
          self.compileModel(node, self.vm, exp, dir)
        }
        node.removeAttribute(attrName)
      }
    })
  },
  //   编译文本 {{exp}}
  compileText: function (node, exp) {
    var self = this
    var initText = this.vm[exp]
    this.updateText(node, initText)
    new Watcher(this.vm, exp, function (value) {
      self.updateText(node, value)
    })
  },
  //   编译 绑定的事件 比如 v-on:click='handel',exp是绑定的值handel,dir就是on:click,
  compileEvent: function (node, vm, exp, dir) {
    //   拿到click
    var eventType = dir.split(':')[1]
    // 拿到 handel方法
    var cb = vm.methods && vm.methods[exp]

    if (eventType && cb) {
      // 为节点注册事件
      node.addEventListener(eventType, cb.bind(vm), false)
    }
  },
  compileModel: function (node, vm, exp, dir) {
    var self = this
    // 拿到data里面exp对应的值
    var val = this.vm[exp]
    // 判断 view的value===model里面的data
    this.modelUpdater(node, val)

    // 生成watcher实例,data更新时,就会触发cb回调函数(即实现 data--》view的同步)
    new Watcher(this.vm, exp, function (value) {
      self.modelUpdater(node, value)
    })

    node.addEventListener('input', function (e) {
      var newValue = e.target.value
      if (val === newValue) {
        return
      }
      self.vm[exp] = newValue
      val = newValue
    })
  },
  updateText: function (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value
  },
  modelUpdater: function (node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value
  },
  isDirective: function (attr) {
    return attr.indexOf('v-') == 0
  },
  isEventDirective: function (dir) {
    return dir.indexOf('on:') === 0
  },
  isElementNode: function (node) {
    return node.nodeType == 1
  },
  isTextNode: function (node) {
    return node.nodeType == 3
  }
}
module.exports = Compile

vue-router的实现原理

谈一下对Vue是渐进式框架的理解

面试官:聊聊对Vue.js框架的理解

组件中data为什么必须是函数

当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。 为了保证组件的独立性和可复用性,数据不被污染

data是一个函数,组件实例化的时候这个函数将会被调用,返回一个对象,计算机会给这个对象分配一个内存地址,你实例化几次,就分配几个内存地址,他们的地址都不一样,所以每个组件中的数据不会相互干扰,改变其中一个组件的状态,其它组件不变。

虚拟dom和diff算法

vue数据更新

采用虚拟DOM+diff算法提高更新性能

  • 生成新的虚拟DOM结构
  • 和旧的虚拟DOM结构对比
  • 利用diff算法, 找不同, 只更新变化的部分(重绘/回流)到页面 - 也叫打补丁

虚拟DOM

真实的DOM属性好几百个, 没办法快速的知道哪个属性改变了

虚拟DOM保存在内存中,只记录dom关键信息, 配合diff算法提高DOM更新的性能

virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。 比如dom是这样的:

<div>
    <p>123</p>
</div>

对应的virtual DOM(伪代码):

var Vnode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};

注意:VNodeoldVNode都是对象

diff算法

在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。

diff流程图

当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。

diff算法如何比较新旧虚拟DOM?

  • 根元素改变 – 删除当前DOM树重新建
  • 根元素未变, 属性改变 – 更新属性
  • 根元素未变, 子元素/内容改变
  • 无key – 就地更新 / 有key – 按key比较

image-20210927102354872.png

v-for中key的作用

key 的特殊属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes。

  • 如果不使用 key,Vue会采用就地复地原则:使用一种最大限度减少移动元素并且尽可能的尝试修复/再利用相同类型元素的算法,做patch或者reuse。
  • 使用key,Vue会根据keys的顺序记录element,它会基于key的变化重新排列元素顺序,并且会移除 key 不存在的元素。

渲染函数 & JSX(待补充)


组件通信(待补充)

Vue 组件间通信六种方式(完整版)

1. 父子组件

a. 通过发送和接受消息

父组件

例如

通过v-bind或:实现把父组件的数据传给子组件
通过v-on或@,实现监听子组件事件和接收数据
<son :uname="uname" @update-uname="uname = $event"></son>

子组件

例如

export default {
  //通过props接收父组件传过来的数据
  props: ['uname'],
  methods: {
    handle(){
      //通过$emit触发事件,发送数据给父组件
      this.$emit('update-uname','ls')
    }
  }
}

语法糖

v-model

父组件

<son v-model="uname"></son>

子组件

export default {
  props: ['value'],
  methods: {
    handle(){
      this.$emit('input','ls')
    }
  }
}
.sync

父组件

<son :uname.sync="uname"></son>

子组件

export default {
  props: ['uname'],
  methods: {
    handle(){
      this.$emit('update:uname','ls')
    }
  }
}

b.通过组件实例直接通信

父->子

通过ref和$refs

<template>
  <son ref="sonRef"></son>
</template>
<script>
export default = {
  methods: {
    handle(){
      this.$refs.sonRef.uname //访问数据
      this.$refs.sonRef.updateUname('ls')//访问方法
    }
  }
}
</script>

子->父

通过$parent

export default {
  methods: {
    handle(){
      this.$parent.uname //访问数据
      this.$parent.upateUname('ls') //访问方法
    }
  }
}

2. 兄弟组件

a通过事件中心

事件中心

例如

import Vue from 'vue'
const eventBus = new Vue()
export funtion on(eventName, fn){
  eventBus.$on(eventName, fn)
}
export funtion emit(eventName, data){
  eventBus.$emit(eventName, data)
}

兄弟A

例如

import {emit} from '@/utils/eventBus.js'
export default {
  methods: {
     handle(){
        //发消息
        emit('add-success', 4)
     }
  }
}

兄弟B

例如

import {on} from '@/utils/eventBus.js'
export default {
  data(){
    return {
        list: [1,2,3]
    }  
  },
  mounted(){
    //接受消息
    on('add-success', function(data){
      this.list.push(data)
    })
  }
}

b.通过父组件通信(通过消息和直接通信)

父组件

例如

<template>
    <son-a @add-success="handle"><son-a>
    <son-b ref="sonbRef"><son-b>
</template>
<script>
export defalut {
  methods: {
    handle(data){
      //父组件之间操作子组件实例实现通信
      this.$refs.sonbRef.list.push(data)
    }
  }
}
</script>

兄弟A

例如

export default {
  methods: {
     handle(){
        //发消息
        this.$emit('add-success', 4)
     }
  }
}

兄弟B

例如

export default {
  data(){
    return {
        list: [1,2,3]
    }  
  }
}

c.通过父组件通信(通过消息)

父组件

例如

<template>
    <son-a @add-success="handle"><son-a>
    <son-b :list="list"><son-b>
</template>
<script>
export defalut {
    data(){
      list: []
    },
  methods: {
    handle(data){
      //父子组件通过数据绑定实现通信
      this.list.push(data)
    }
  }
}
</script>

兄弟A

例如

export default {
  methods: {
     handle(){
        //发消息
        this.$emit('add-success', 4)
     }
  }
}

兄弟B

例如

export default {
  props: {
    //通过props接收父组件传过来的数据
    list: {
      type: Array,
      default: ()=>[]
    }
  }
}

d.通过插槽

父组件

<template>
    <son-a><button @click="handle(4)">add</button><son-a>
    <son-b :list="list"><son-b>
</template>
<script>
export defalut {
    data(){
      return {
          list: [1,2,3]
      }  
    },
  methods: {
    handle(data){
      //父组件之间操作子组件实例实现通信
      this.list.push(data)
    }
  }
}
</script>

兄弟A

例如

<template>
  <div class="son-a">
    <slot></slot>
  </div>
</template>

兄弟B

例如

export default {
  props: {
    //通过props接收父组件传过来的数据
    list: {
      type: Array,
      default: ()=>[]
    }
  }
}

3. 爷孙组件

a.通过父子通信

<body>
    <div id="app">
        <grand-father></grand-father>
    </div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
    data() {
        return {
            money: 1000
        }
    },
    template: `
        <div>
            <div>爷爷</div>
            <father :money="money" @pay="money-=$event"/>
        </div>
    `
})
// 父亲组件
Vue.component('father', {
    props: ['money'],
    template: `
        <div>
            <div>父亲</div>
            <son :money="money" @pay="$emit('pay',$event)"/>
        </div>
    `
})
// 孙子组件
Vue.component('son', {
    props: ['money'],
    template: `
        <div>
            <div>孙子-{{money}}</div>
            <button @click="$emit('pay', 100)">-100</button>
        </div>
    `
})
var vm = new Vue({
    el: '#app',
    data: {
    },
    methods: {
    },
    mounted() {
    },
})

b.通过$parent

<body>
    <div id="app">
        <grand-father></grand-father>
    </div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
    data() {
        return {
            money: 1000
        }
    },
    template: `
        <div>
            <div>爷爷</div>
            <father ref="father"/>
        </div>
    `,
    methods: {
        pay(num){
            if(this.money>num){
                this.money -= num
            }
        }
    }
})
// 父亲组件
Vue.component('father', {
    template: `
        <div>
            <div>父亲</div>
            <son/>
        </div>
    `
})
// 孙子组件
Vue.component('son', {
    template: `
        <div>
            <div>孙子-{{$parent.$parent.money}}</div>
            <!--可以,但是不要做-->
            <!--<button @click="$parent.$parent.money-=100">-100</button>-->
            <button @click="$parent.$parent.pay(100)">-100</button>
        </div>
    `
})
var vm = new Vue({
    el: '#app',
    data: {
    },
    methods: {
    },
    mounted() {
    },
})
</script>

c.通过provide和inject

<body>
    <div id="app">
        <grand-father></grand-father>
    </div>
</body>
<script>
// 爷爷组件
Vue.component('grand-father', {
    data() {
        return {
            money: 1000
        }
    },
    provide(){
        return {
            grandFather: this
        }
    },
    template: `
        <div>
            <div>爷爷</div>
            <father/>
        </div>
    `,
    methods: {
        pay(num){
            if(this.money>num){
                this.money -= num
            }
        }
    }
})
// 父亲组件
Vue.component('father', {
    template: `
        <div>
            <div>父亲</div>
            <son/>
        </div>
    `
})
// 孙子组件
Vue.component('son', {
    inject: ['grandFather'],
    mounted() {
    },
    template: `
        <div>
            <div>孙子-{{grandFather.money}}</div>
            <button @click="grandFather.pay(100)">-100</button>
        </div>
    `
})
var vm = new Vue({
    el: '#app',
    data: {
    },
    methods: {
    },
    mounted() {
    },
})
</script>

4. 没关系组件-Vuex

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  // 定义数据
  state: {
    count: 0
  },
  // 同步代码
  mutations: {
    addCount (state) {
      state.count++
    },
    //接受参数
    addCountN (state, payload) {
      state.count += payload
    }
  },
  // 异步代码
  actions: {
    addCountSync (context) {
      setTimeout(() => {
        context.commit('addCount')
      }, 1000)
    },
    // 接受参数
    addCountNSync (context, payload) {
      setTimeout(() => {
        context.commit('addCountN', payload)
      }, 1000)
    }
  },
  // 计算属性
  getters: {
    showCount (state) {
      return `当前的count值=${state.count}`
    },
    // 简化模块中state的获取
    token (state) {
      return state.user.token
    }
  },
  // 模块化
  modules: {
    user: {
      // 命名空间
      namespaced: true,
      state: {
        token: 'abc'
      },
      mutations: {
        updateToken () {
          console.log('2')
        }
      }
    }
  }
})
// state
<div>{{$store.state.count}}</div>
// mutations
<button @click="$store.commit('addCount')">+1</button>
// actions
<button @click="$store.dispatch('addCountSync')">+1 sync</button>
// getters
<div>{{$store.getters.showCount}}</div>


<script>
import {mapState,mapMutations,mapActions,mapGetters} from 'vuex'
export default {
  computed: {
    ...mapState(['count']),
    ...mapGetters(['showCount'])
  },  
  methods: {
    ...mapMutations(['addCount', 'addCountN']),
    ...mapActions(['addCountSync', 'addCountNSync']),
  }
}
</script>

自定义事件

官方文档

v-model自定义

自定义 v-model 的使用方法

.native 修饰符

想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on 的 .native 修饰符:

核心:通过冒泡的语法糖

      <!-- 通过冒泡 -->
      <!-- <div @click="handleClick">
            <el-button></el-button>
        </div> -->
      <!-- 语法糖 -->
      <el-button @click.native="handleClick"></el-button>
      <!-- 注意:.stop,.keyup,.prevent只能作用于dom事件对象,不能作用于组件通过$emit通信!!! -->
      <el-input @keyup.native.enter="handleEnter"></el-input>
    </div>
  </body>
  <script>
    Vue.component('el-button', {
      template: `
        <div><button>按钮</button></div>
        `
    })
    Vue.component('el-input', {
      template: `
        <input type="text"/>
        `
    })
    new Vue({
      el: '#app',
      methods: {
        handleClick() {
          console.log('click')
        },
        handleEnter() {
          console.log('enter')
        }
      }
    })
  </script>

注意:.stop,.keyup,.prevent只能作用于dom事件对象,不能作用于组件通过$emit通信!!!

.sync 修饰符

使用场景:父子组件相互传参

注意:带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model父组件 传给子组件值visible,并接收子组件update:visible的事件

<!-- :visible="showDialog" @update:visible="showDialog = $event" -->

<!-- 语法糖 -->
:visible.sync="showDialog"

子组件 接收父组件传的值visible,通过@update:visible触发父组件的事件

    Vue.component('el-dialog', {
      props: ['visible'],
      template: `
            <div v-if="visible">这是一个弹层, <a @click="$emit('update:visible', false)">关闭</a></div>
        `
    })

v-bind.sync(多个prop)

注意:将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。 父组件

<el-dialog v-bind.sync="info"></el-dialog>

子组件

    Vue.component('el-dialog', {
      props: ['visible', 'name'],
      template: `
            <div v-if="visible">这是{{name}}的一个弹层, <a @click="$emit('update:visible', false)">关闭</a></div>
        `
    })

组件注册的方式

官方文档

1.全局注册

注意:全局注册的行为必须在根 Vue 实例 (通过 new Vue) 创建之前发生

Vue.component('component-a', { /* ... */ }) 
Vue.component('component-b', { /* ... */ }) 
Vue.component('component-c', { /* ... */ }) 

new Vue({ el: '#app' })

2.局部注册

var ComponentA = { /* ... */ } 
var ComponentB = { /* ... */ } 
var ComponentC = { /* ... */ }

new Vue({ 
    el: '#app', 
    components: { 
        'component-a': ComponentA, 
        'component-b': ComponentB 
    } 
})

3.注册路由组件

src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export const routes = [
  {
    path: '/login',
    component: () => import('@/views/login/index'),
  }
]

const router = new Router({
    routes
})

export default router

4.以插件模式注册组件

开发插件Vue组件之全局注册

src/components/index.js

该文件负责所有的公共的组件的全局注册,并默认导出

// 该文件负责所有的公共的组件的全局注册   Vue.use
import PageTools from './PageTools'
import UploadExcel from './UploadExcel/index'
import ImageUpload from './ImageUpload'

export default {
  install(Vue) {
    //  注册全局的通用栏组件对象
    Vue.component('PageTools', PageTools)
    // 注册导入excel数据的文件
    Vue.component('UploadExcel', UploadExcel)
    // 注册导入上传组件
    Vue.component('ImageUpload', ImageUpload)
  }
}

在main.js中引入,并全局注册自己的插件

import Component from '@/components'
Vue.use(Component) // 注册自己的插件

搭建vue开发环境

自己搭建过vue开发环境吗?

路由

路由传参

组件传参

hash和history

hash

优点

  • 只需要前端配置路由表, 不需要后端的参与
  • 兼容性好, 浏览器都能支持
  • hash值改变不会向后端发送请求, 完全属于前端路由
  • 同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置

缺点

  • hash值前面需要加#, 不符合url规范,也不美观

history

优点

  • 符合url地址规范, 不需要#, 使用起来比较美观

缺点

  • 在用户手动输入地址或刷新页面时会发起url请求, 后端需要配置index.html页面用户匹配不到静态资源的情况, 否则会出现404错误
  • 兼容性比较差, 是利用了 HTML5 History对象中新增的 pushState() 和 replaceState() 方法,需要特定浏览器的支持.

路由守卫

官方文档

参考文章

调用顺序:全局守卫--》路由独享守卫--》路由组件内守卫

1.全局守卫

vue-router全局有三个守卫:

  1. router.beforeEach 全局前置守卫 进入路由之前
  2. router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  3. router.afterEach 全局后置钩子 进入路由之后

使用方法:

 // 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 全局后置钩子');
    });

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

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

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

  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

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

    • next(false) : 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

    • next('/') 或者 next({ path: '/' }) : 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。

    • next(error) : (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

2.路由独享守卫

为某些路由单独配置守卫:

注意:调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖

const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { 
            // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
            // ...
          }
        }
      ]
    })

3.路由组件内守卫

  1. beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建

  2. beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this

  3. beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

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

补充应用

beforeRouteEnter访问this

因为钩子在组件实例还没被创建的时候调用,所以不能获取组件实例 this,可以通过传一个回调给next来访问组件实例 。

但是回调的执行时机在mounted后面,所以在我看来这里对this的访问意义不太大,可以放在created或者mounted里面。

    beforeRouteEnter (to, from, next) {
    console.log('在路由独享守卫后调用');
      next(vm => {
        // 通过 `vm` 访问组件实例`this` 执行回调的时机在mounted后面,
      })
    }
复制代码

beforeRouteLeave

导航离开该组件的对应路由时调用,我们用它来禁止用户离开,比如还未保存草稿,或者在用户离开前,将setInterval销毁,防止离开之后,定时器还在调用。

    beforeRouteLeave (to, from , next) {
      if (文章保存) {
        next(); // 允许离开或者可以跳到别的路由 上面讲过了
      } else {
        next(false); // 取消离开
      }
    }
复制代码

路由钩子函数的错误捕获

如果我们在全局守卫/路由独享守卫/组件路由守卫的钩子函数中有错误,可以这样捕获:

    router.onError(callback => { 
    // 2.4.0新增 并不常用,了解一下就可以了 
      console.log(callback, 'callback');
    });

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

完整的路由导航解析流程(不包括其他生命周期):

  1. 触发进入其他路由。
  2. 调用要离开路由的组件守卫beforeRouteLeave
  3. 调用局前置守卫:beforeEach
  4. 在重用的组件里调用 beforeRouteUpdate
  5. 调用路由独享守卫 beforeEnter
  6. 解析异步路由组件。
  7. 在将要进入的路由组件中调用beforeRouteEnter
  8. 调用全局解析守卫 beforeResolve
  9. 导航被确认。
  10. 调用全局后置钩子的 afterEach 钩子。
  11. 触发DOM更新(mounted)。
  12. 执行beforeRouteEnter 守卫中传给 next 的回调函数

生命周期

image.png

常用钩子

  1. ajax请求最好放在created里面,因为此时已经可以访问this了,请求到数据就可以直接放在data里面。

    这里也碰到过几次,面试官问:ajax请求应该放在哪个生命周期。

  2. 关于dom的操作要放在mounted里面,在mounted前面访问dom会是undefined

  3. 每次进入/离开组件都要做一些事情,用什么钩子:

  • 不缓存:

    进入的时候可以用createdmounted钩子,离开的时候用beforeDestorydestroyed钩子,beforeDestory可以访问thisdestroyed不可以访问this

  • 缓存了组件:

    缓存了组件之后,再次进入组件不会触发beforeCreatecreatedbeforeMountmounted如果你想每次进入组件都做一些事情的话,你可以放在activated进入缓存组件的钩子中

    同理:离开缓存组件的时候,beforeDestroydestroyed并不会触发,可以使用deactivated离开缓存组件的钩子来代替。


vuex

vuex模块化

Vuex 模块化使用

image-20200904155846709.png