Vue

476 阅读15分钟

1、Vue基本原理

Vue 使用 数据劫持 + 订阅、发布的方式。
使用 Object.defineProperty 劫持数据的 setter、getter,
数据改变时,发布消息给订阅者,触发相应的回调,完成视图的跟新。
具体实现如下:

  1. Observer:将需要Observe 的数据进行遍历,包括子属性对象的数据,通过 Object.defineProperty() 劫持数据的 setter、getter。
  2. Compile:解析模版指令,将模版的变量替换成数据,然后初始化页面渲染视图,且在指令节点中绑定更新函数,添加监听数据的订阅者,当数据变化时,会触发图的跟新。
  3. Watcher:订阅者,主要做了如下两件事
    • 自身实列化时将自身添加到 属性订阅器中,
    • 数据变更,调用本身的 update()方法,并触发 compile的回调
  4. MVVM 作为数据绑定的入口,整合 Observer、Compile、Watcher,通过
  • Observer 监听 model 数据变化;
  • Compile 解析模块指令,完成视图跟新;
  • Watcher 作为两者的通信桥梁
    完成数据到视图、视图-数据的动态跟新

2、v-model 双向数据绑定原理

vue 的双向绑定通过 v-model 实现, 实际上是 input 输入时 通过 change 事件将 value 赋给当前组件数据的语法糖。

<input v-model="data" />

<input :value="data" @change="data = $event.targent.value" />

自定义组件实现数据的双向绑定 当前组件的 fvalue 通过 props 传递给 子组件 用于给 input 赋值; 子组件 input 的 change 事件 通过 $emit 将数据传递给当前组件; 当前组件获取跟新的值符给 fvalue

F:
<f-model :f-value="fValue" @fclick="fclick" />
C:
<input :value="fValue" @input="$emit('fclick', $event.target.value)" />

3、Object.defineProperty 的局限性

Vue 使用 Object.defineProperty 无法劫持到 某些属性的操作,从而不会使视图进行跟新。
比如: Array 的 length 方法。
因为 Array - length 的属性定义中 configurable:false.

Vue3 通过 Proxy 代理的方式进行属性的拦截监听,可以完美的监听属性的变化

4、Vue 封装数组的方法,如何实现页面刷新

Vue 封装的数组方法有:

push()、pop()、split()、shift()、unshift()、sort()、reserve()

不会自动更新的原因:

  • 因为vue2 通过 Object.defineProperty 方法实现对数据跟新拦截,其方法不能监听到数组内部的变化。
  • 所以需要对这个方法进行重写,从而能让vue 可以监听到数组的变化,从而进行视图的更新。

实现页面刷新主要方法 - 重写数组的原生方法

  • 获取该数组的监听对象,对值进行监听,有数据改变时
  • 手动调用方法(notify),通知渲染 watcher 执行响应的回调函数,继而进行页面的视图跟新。

5、MVVM、MVP、MVC

MVVM:

M - Model: 数据层,数据和业务逻辑都在 model 层定义
V - View : 视图层,UI视图,负责数据的展示
VM - ViewModel:桥梁,负责监听 Model 数据的改变,控制视图的跟新,处理用户交互操作

mvvm.png

View 和 Model 之间没有直接关联,
通过 viewModel 进行交互,viewModel 与 View 是双向绑定的。
所以 Model 数据变化可以通过 viewModel 触发 View 达到视图跟新
View 由于用户操作改变 也会 通过 ViewModel 给 Model 返回进行数据同步

这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。

MVC:

M - Model: 数据层,数据的相应操作及业务逻辑处理
V - View:视图层,数据的展示,UI界面的展示
C - Controller:两者的纽带,主要负责用户与应用的响应操作,

mvc.png

Model 数据变化:可以通知 View 的视图变更
View 用户进行交互操作:会触发 Controller 的事件触发器,进行Model层的调用,完成对Model 的修改,Model 再通知 View 的视图跟新。

MVC用户交互时,通过触发Controller 事件触发器,调用 Model 进行数据处理,在通知View 跟新视图。 View 和 Model 是耦合在一起的,复杂功能时,会照成代码混乱。

MVP :

p - Presenter : 桥梁,View 层的接口暴露给了 Presenter。
所以在 Presenter 中 将 View、Model 绑定在一起,实现 View、Model 的同步跟新,进而实现 View、Model 的解藕。

mvp.png

6、Computed 和 Watch 的区别

对于Computed:
Conputed 是计算属性,当需要对数据进行计算再展示时使用,可以利用 Compouted 的缓存,只有当它依赖的属性值变化才会触发重新计算。

  • 支持缓存,只有依赖的属性值发生变化才会触发重新计算
  • 默认走缓存,及依赖的属性为data/props数据
  • 不支持异步,不可在此方法中进行副作用操作
  • 有两种写法
computed(){
    resData:()=> return this.money + '$',
    
    fullName:{
        get(){
            return this.firstName + ' ' + this.lastName // 返回的值
        },
        set(newValue){
            // 一些计算
            [this.firstName, this.lastName] = newValue.split(' ')
        }
    }
}

对于Watch:
Watch监听器,监听数据改变时,触发 Watch - data;
当数据变化需要执行一些函数、开销大的操作、异步请求、副作用操作时;
限制执行该操作的频率,结果返回前,设置中间状态。

  • 不支持缓存,数据变化会触发相应操作
  • 支持异步监听
  • 可在此方法中进行副作用操作
  • 监听的属性为 data/props 数据
  • V3 支持多种写法
watch:{
    someData(n,o){
        ...
    },
    otherData:{
        hander(n,o){...},
        immediate:true,  // 监听器在第一次创建时,立马触发回调【第一次的 o: undefined】
        deep:true        // 深度监听
    },
    // V3 ============================
    c.d:function(n,o){...},
    // 可以传入回调数组,它们将会被逐一调用
    f:[ 
        'someMethod', // 字符串方法名
        functon handle2(n,o){...},
        {
            hander(n,o){... },
            immediate: true
        }
    ]
}

7、Computed 和 Methods 的区别

Methods: 用于声明要混入到组件实例中的方法,一般用于方法的定义,组件实列/模板语法可以进行访问。 所有方法会将this 上下文自动绑为组件实列,因此避免使用箭头函数。 Computed:是一个计算属性,不可进行副操作。

8、过滤器的作用,如何实现一个过滤器

1、过滤器作用

  • filters 在 Vue2 中用来过滤数据,不修改数据,只是针对数据的过滤,改变用户看到的输出;
  • computed、methods 都是修改数据处理显示的。
  • filter,过滤器名称,第一个参数是变量,之后的参数按顺序为输入的参数;
  • filter,局部调用是 filters,全局调用是 filter;
  • 全局 filter名称 和 局部 filter名称 重复的话,采用就近原则,即局部的 filter 优先级更高。

Vue3 删除了 filter,因为 Vue3 希望精简代码,filter 的功能重复,可以使用 compontend 、methods 实现 filter 可以实现的功能;有利于代码的维护。 2、使用

  • filters 方法的定义有两种写法:
  1. 组件内局部定义 filters:{ 过滤器名: fn }
  2. 全局方法定义 Vue.filter('过滤器名',fn)
  • filter 的调用,有两种方式:
  1. 使用 双括号 {{变量 | 过滤器名称(参数)}} 的形式;
  2. 在 v-bind 中使用 - 使用 双引号,v-bind:money="msg | msgFormat('$')"
<template>
  <div>
    <div>当前价格 {{msg | msgFormat('$')}}</div>
    <div v-bind:money="msg | msgFormat('$')"></div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        msg: 200
      }
    },
    filters:{
      msgFormat(msg,unit){
        console.log('msg',msg)
        if(!msg) return '--'
        return msg + unit
      }
    },
  }
</script>
/* 如果全局使用:*/
Vue.filter('msgFormat', function (msg, unit) {
  if (!msg) return '--'
  return msg + unit
})

9、如何保存页面的当前状态:

1、组件会被卸载

1、存储在 localStorage、sessionStorage 中;

  • 在组件卸载前,即 beforeDestory() 中【 Vue3 在 beforeUnMount() 】 将状态存储在 localstroage 中;
  • 在组件创建后,created() 中判断 localStorage.getItem('xx') 不为 null,则赋给 data 进行页面渲染。

优点:兼容性好,简单快捷;

缺点:

  • 使用了 JSON.parse、JSON.stringify 属于深拷贝,有些特殊数据需要进行特殊处理,如 date 对象、正则 等返回的就直接是字符串;
  • 数据暴露在控制台内,不要存放一些敏感信息。

2、通过路由传参的形式: 通过 标签 <router-link> - to 设置:

<router-link to="/跳转到的路径/传入的参数"></router-link> 

router需要设置动态字段,:id
{
  path: "/:id",
  name: "login",
  component: HomeView,
}

使用 this.$route.id 获取参数

或者使用 this.$router.push 的 query 传参

this.$router.push({
  path: 'xx',
  quer:{
    xxx:xxx
  }
})

this.$route.query.xx 获取参数

在Vue3中 router、route 使用:

<script lang="ts" setup>
   import { useRoute, useRouter} from "vue-router";
   const route = useRoute();
   const router = useRouter();
   // ...
   router.push({
     path: 'xx',
     quer:{
        xxx:xxx
     }
   })
   // ...
   route.query.xxx
</script>

书写简单便捷,数据会直接在url 中进行体现,一般用作为 id、name 等标识性的少量参数。

2、组件不会被卸载

  • 使用 keep-alive 进行页面缓存,切换时 activated、deactivated 这两个生命周期会随着组件的调用、关闭而执行。
  • keep-alive 默认是组件都走缓存的;
  • 标签内使用 include、exclude 定制哪些组件使用、不使用缓存,参数传递的是 组件对应显示申明的 name;
  • Vue3 不需要显示声明,<script setup> 的单文件组件会自动根据文件名生成对应的 name 选项,无需再手动声明。

使用:

<KeepAlive>
  <component :is="current"></component>
</KeepAlive>

结合路由跳转:

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

...
router文件需要在对应的路由参数添加:meta: { keepAlive: false }
{
  path: "/about",
  name: "About",
  component: () => import("../views/AboutView.vue"),
  meta: { keepAlive: false },
},

10、常见事件修饰符。

  • stop:阻止事件冒泡;
  • prevent:取消默认行为
  • self:只触发本身事件
  • capture:添加捕获事件
  • once:只执行一次

11、v-if、v-show 原理 以及有什么区别

原理

  • vif 会调用 addifCondition 方法,生成 vdom 树就会忽略对应的节点,render时就不会渲染;
  • vshow 会生成 vdom,渲染时不管是否为 fasle,都会生成对应的真实节点,只是在 render 过程中 通过 display 进行css 上的显示隐藏。

区别

编译过程:

  • vshow 是通过 css:display:none 进行 dom 的显示、隐藏;只是简单的css切换 dom 实际是一直存在的。
  • vif 会有个行局部编译、卸载的过程,在切换过程中会销毁、重建内部的事件监听和子组件。

性能消耗:

  • vshow 有更高的初始渲染消耗,vif有更高的切换消耗;所以如频繁切换,使用vshow;如在运行条件很少改变,使用vif。

vif会触发局部的钩子:

  • vif 由 false -> true 会触发 beforeCreate、create、beforeMount、mounted 钩子,
  • 由 true -> false 会触发 beforeDestroy、destroy【V3:beforeUnMount、unmount】钩子;

12、data 为什么是一个函数而不是一个对象

  • JavaScript 的对象是一个引用类型的数据,当多个实列引用同一个对象时,实际上是对指针指向的引用,是一个浅拷贝;其中一个实列对象的修改,也会影响其他实列对象的变化。
  • 组件是一个函数,内部参数以函数返回值的形式定义,这样复用组件时,就会return 一个新的data,每个组件都有自己私有的数据空间,不会干扰其他组件的正常运行。

13、keep-alive 理解,如何实现的,具体缓存的是什么?

理解

  • 组件来回切换的时候,对一些组件的状态进行缓存,可以使用 keep-alive 进行包裹, keep-alive 声一个抽象组件,本身不会渲染出一个dom元素。
  • 默认会缓存内部的所有组件实列,有三个参数;
  • keep-alive 可以通过属性来定制组件的缓存:
  • include - 表示需要被缓存的组件,使用 字符串 或者正则的形式进行传参
  • ecclude - 表示不需要被缓存的组件
  • nax - 表示最多缓存的组件实列
<KeepAlive include="a,b" >
  <component :is = "view" />
</KeepAlive>

<KeepAlive :include="/a|b/">
  <component :is="view" />
</KeepAlive>

作用:在组件切换过程中,会把切换过去的组件保留在内存中,防止重复渲染 dom,减少加载时间及性能消耗,提高用户体验。

原理:在 Create 钩子中,将需要缓存的 Vnode 节点保存在 this.cache 中,在 render 时 如果 Vnode 的 name 符合缓存条件【include、exclude 定制组件的缓存】,就从 this.cache 中取出缓存的 Vnode 实列进行渲染。

生命周期
1、activated:

  • keep-alive 组件被激活时调用;
  • 钩子函数在服务器渲染期间不被调用
  • 如果需要在每次进入页面获取部分最新数据,则在 activated 钩子中 获取数据进行更新。

2、deactivated

  • keep-akeep-alive 组件停用时调用;
  • 钩子函数在服务器渲染期间不被调用;

使用

  • 动态组件中使用,按 keep-alive 包裹 动态组件就行;
  • 结合路由使用,需要在 router.ts 内设置 meta:{keepAlive: true}

14、$nextTick 原理及作用

$nextTick:在下次 DOM 更新循环结束之后执行延迟回调。在数据修改之后立即使用这个方法,获取更新后的 DOM

原理

  • Vue 数据发生变化时,不会立马进行 DOM 的更新,而是将其缓存在一个执行队列中,等到同一事件循环的所有数据变化完成后,再将队列中的事件数据进行处理,进行视图的更新。这样为了确保每个组件无论发生多少次状态改变,都只执行一次更新。
  • Vue 的 nextTick 本质是对 JavaScript 执行原理 EventLoop(事件队列) 的一种应用。
  • nextTick 的核心是利用了 Promise、MutationObserver、setTimeout 的原生 JavaScript 方法模拟对应的 微/宏任务的实现,
  • 本质是利用JavaScript 的异步回调任务队列实现 Vue框架中自己的异步回调队列。

作用

  1. 如果是同步更新,则对此对一个或多个属性赋值,会频繁触发 UI/DOM 渲染,可以减少一些无用的渲染
  2. 同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用 Vnode 进行计算得出需要更新的具体 DOM 节点,然后对 DOM进行更新操作,如果每次更新状态后对渲染过程需 要更多的计算,会浪费很多的性能,所以异步渲染变得更加重要

Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM:

  • 在数据变化后执行的某个操作,而这个操作需要使用数据变化而变化的DOM结构时,这个操作就需要方法在 nextTick() 的回调函数中;
  • 在Vue 生命周期中,如果 created() 钩子进行 DOM 操作,也需要放在 nextTick 的回调函数中。

Vue3 中使用:使用 async await nextTick() 等待异步返回,即等待 DOM 更新后的操作

<script setup>
  import { ref, nextTick } from 'vue'

  const count = ref(0)

  async function increment() {
    count.value++

    // DOM 还未更新
    console.log(document.getElementById('counter').textContent) // 0

    await nextTick()
    // DOM 此时已经更新
    console.log(document.getElementById('counter').textContent) // 1
  }
</script>

<template>
  <button id="counter" @click="increment" > {{ count }}</button>
</template>

15、Vue 给 data 中的对象添一个新的属性时会发生什么,如何解决

Vue2 中直接给对象添加一个新的属性,视图不会更新。

  • 因为在Vue 实列创建时,新增的属性并没有声明,所以没有被Vue 转换为响应式的属性,就不会触发视图更新。
  • 需要使用 $set 全局API 方法把 需要添加的属性处理成一个响应式属性,这样视图就会跟着变化了。
<div v-for="v in obj" :key="v">{{v}}</div>
<button @click="btnClick">点击</button>

<script>
  export default {
    data(){
      obj:{
        a:'obj.a'
      }
    }
  },
  methods:{
    btnClick(){
      this.$set(this.obj,'b','obj.b')
    },
  }
</script>

Vue3 中会更新,因为 Vue3 使用 nginx 代理的方式进行属性的拦截监听,可以完美检测属性的变化,从而更新视图。

16、Vue 单页面、多页面应用的区别,以及他们的优缺点

什么是SPA:

SPA 即单页面应用,也称为 CSR,即客户端渲染。它所需要的资源 如 HTML、CSS 和 JS 等,在一次请求中就加载完成,也就是不需要 刷新的动态加载。浏览器渲染就是所有页面渲染、逻辑处理、页面路由、接口请求均是在浏览器中发生。对SPA 来说,页面的切换就是组件 或视图的切换。

SPA应用程序避免了由于在服务器上呈现而导致的中断。这消除了 web 开发世界在提供无缝用户体验方面通常面临的最大问题。

SPA原理:

js 会感知 url 变化,通过这一点可以用js监听url中 hash 值的变化,通过 onhashchange 事件,由于 hash 值的变化不会引发页面的刷新和跳转,当监听到 hash 的变化,就可以动态切换组件,也就是实现无刷新切换页面。

SPA优点:

  1. 页面切换快

页面每次切换跳转时,都不需要做 html 文件请求,这样节约了很多 http 发送延迟,在切换页面时速度会变快

  1. 用户体验好

页面片段间的切换快,包括移动设备。尤其在网络环境差的时候,因为组件已经预先加载好了,并不需要发送网络请求,所以用户体验好。

SPA缺点:

  1. 首屏加载慢

首屏需要请求一次 html,同时还需要发送一次 js 请求,两次请求回来了,首屏才会展示出来。向对于多页面应用首屏时间会慢很多。

  1. 不易于 SEO

SEO 效果差,因为 搜索引擎只识别 html 里的内容,不认识 js 内容,而单页面应用的内容都是靠 js 渲染生成出来的,搜索引擎不 识别这部分内容,也就不会给一个好的排名,会导致 SPA 应用做出来的网页排名相对较低。

什么是MPA

MAP 多页面应用,指有多个独立页面的应用,每个页面必须重复加载 js、html、css 等相关资源。多页面应用跳转,需要整页资源刷新。

与 SPA 对比最大的不同即是页面路由切换由原生浏览器文档跳转控制,页面跳转是返回 HTML 的。

MPA 优点:

  1. 首屏加载速度快

访问页面时,服务器返回一个 html,页面就会展示出来,这个过程经历了一个 http 请求,所以页面展示速度非常快。

  1. SEO 效果好

搜索引擎在做网页排名的时候,需根据网页内容才能给网页权重,来进行网页的排名。搜索引擎识别 html 内容,每个页面的所有内容都放 在 html 中,有利于搜索引擎的排名。

MPA 缺点: 页面切换慢,影响用户体验

页面的每次跳转都需要发送一次 HTTP 请求,如果网络状态不好,页面来回切换的时候,会发生明显的卡顿,用户体验会变差。

24、对SSR理解

SSR

将页面渲染成完整的 HTML 工作在服务端完成后,再将完整的 HTML 返回给客户端显示。

使用SSR目的: SPA单页面的局限性:首次加载需要加载所以页面的JS库,导致首次加载页面缓慢,以及搜索引擎的劣势。

SSR 优势:

1、首屏加载更快

  1. SPA 需要等所有的JS文件下载后,才开始页面的渲染。
  2. SSR 是通过服务端将渲染好的页面直接返回到浏览器展示,不需等待 js 的下载,所以首屏加载会更快。

2、更好的SEO

  1. SPA 页面是靠js 渲染生成的,不被搜索引擎识别。
  2. SSR 是服务端直接返回给浏览器处理好的 HTML 文件,SEO 可以进行爬取。

17、Vue template 到 render 的过程(Vue 模板编译原理)

Vue 中的模板 template 无法被浏览器解析并渲染,因为它不属于浏览器标准,不是正确的 HTML语法。

Vue 模板编译就是 将 template 转换为 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素。

  • Vue模板编译过程主要如下:template 解析、优化静态节点、生成 render 函数
  • Vue 在模板编译中,会执行 compileToFunction 将template 转化为render函数;

compileToFunctions 主要逻辑如下:

  1. 调用 parse 方法将 template 转换成 AST 抽象语法树。
  • 其中 AST 表示 用 JavaScriipt 对象的形式描述整个模板
  • 解析过程 - 利用正则表达式顺序将模板进行解析,遇到 开始标签、结束标签、文本标签,会执行对应的回调函数,以达到生成 AST 抽象语法树的目的。
  1. 再优化静态节点
  • 对静态节点的优化,需要知道哪些是静态节点,对其做标签;以便于在再次渲染 进行 diff 比较时,会直接跳过这些静态节点,达到优化性能的目的。
  • 深度遍历 AST,以查找出 AST 节点或根节点的静态节点/静态节点根,它们生成的 dom 永远不会改变,对运行时模板的更新有很大的优化作用。
  1. 生成代码
  • 调用 generate 方法将 AST 树转换为 render 字符串;
  • 并将静态部分放到 staticRenderFns 中,最后通过 new Function 转换为 Render函数

18. Vue data 中某一个属性值发生变化后,视图会立即同步执行重新渲染吗

  • Vue的响应是按照一定的策略进行更新的,DOM 的更新是异步的。
  • 数据更新时,会生成一个队列,并缓冲在一个事件循环中发生的所有数据变化;
  • 当一个 watcher 多次变更, 也只会在队列中加入一次,这样的缓存去重数据可以避免不必要的计算和dom操作。
  • 然后在下一个事件循环 tick 中,Vue 刷新队列,并执行去重的数据对应的dom更新。

19、简述mixin、extends 覆盖逻辑

mixins

  • 用于可重复功能的封装,这样其中 mixins 内部调用的生命周期,会和 组件内部的生命周期进行合并,
  • 如果组件和 minxins重名 则组件 覆盖 mixins 内部的。
  • 如果多个 minxins 在同一组件内调用,且存在钩子内部方法、参数重名,则后面覆盖前面

其缺点就是:

  1. 隐式引入,不利于阅读,代码也不利于维护:多个 minxins 的引入,其变量、方法会容易扰乱不知是调用的哪个。
  2. 多个 minxins 生命周期会融合在一块,重复的变量、方法会照成覆盖、冲突。
  3. minxins 可能会出现多对多关系,即一个组件引入多个 minxins,一个 minxin 被多个组件引入多现象。

使用方法:

/* minxins/common.js */
export default {
  data(){
    return {}
  },
  methods:{},
  computed:{},
  filters:{},
  created(){},
  mounted(){
    console.log("我是mixins");
  }
}

a.png

Vue3 保留了对 minxins 的支持,但是更推荐使用 hooks 的方式进行公共方法的封装,可以很好的优化 minxins 的缺点。

  1. 可以使用对象的解构显示引入变量、方法
  2. 可以用解构赋值内部命名方式,解决命名冲突问题
  3. 便于代码阅读

b.png

extends

extends:用于继承组件的方式,可以用于扩展第三方组件,或者自定义基本组件逻辑,然后通过extends 继承,再此基础上进行开发。

基本用法:

APP.vue

<template>
  <div>
    <Son></Son>
  </div>
</template>

<script>
import Son from "./components/Son";

export default {
  components: {
    Son,
  },
};
</script>

<style scoped></style>

Son.vue

<script>
import HelloWorld from "./HelloWorld.vue";

export default {
  extends: HelloWorld,
  data() {
    return {
      aa: 10,
    };
  },
};
</script>

HelloWorld.vue

<template>
  <div>
    <h1>{{ aa }}</h1>
    <h1>{{ bb }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      aa: 0,
      bb: 123,
    };
  },
  mounted() {
    this.init();
  },
  methods: {
    init() {
      this.aa += 10;
    },
  },
};
</script>

<style scoped></style>
  • minxins、extends 都是用于合并、拓展组件的,两者都通过 mergeOptions 进行合并。
  • minxins 公共方法的封装,其内部的钩子方法、属性会被合并到组件中。
  • extends 主要用于拓展单文件组件,用于第三方组件、或基本组件 的拓展
  • mergeOptions 根据一个通用Vue 实列所包含的选项进行逐一判断合并,如 props、data、methods、生命周期等。将合并结果存储在新定义的 options 对象里。并将合并的结果进行返回。

20、描述 Vue 自定义指令

除了 Vue 的内置指令 v-model、v-show,也可以根据需求注册自定义指令。 在有些情况下需要对 DOM 元素进行底层操作,就会用到自定义指令。

列:页面加载时,元素获取焦点

  1. 全局注册
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
  1. 局部注册
export default {
  directives:{
    focus: {
        inserted: function (el) {
        el.focus()
      }
    }
  }
}

使用:

<input v-focus>

封装自定义指令

main.ts

import directives from './directives/index'
const app = createApp(App);
directives.install(app)

directives/index.ts

...
import debounce from './debounce'
const directives = {
  ...,
  debounce
}

export default {
  install(Vue) {
    Object.keys(directives).forEach(key => {
      console.log('key =====', directives[key])
      Vue.directive(key, directives[key])
    })
  }
}

debounce.ts // 详细的某个自定义指令【防抖】

const debounce = {
  mounted: function (el, binding) {
    let timer
    el.addEventListener('click', () => {
      console.log('>>>')
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(() => {
        binding.value()
      }, 1000)
    })
  },
}
export default debounce

21、子组件可以直接改变父组件的数据吗

子组件不可直接修改父组件的数据。
Vue 是单向数据流的,子组件修改父组件可以通过 $emit 调用父组件方法,
将需要改变的数据通过参数的形式返回给父组件,父组件的对应方法拿到参数,做数据处理。

22、assets 和 static 区别

相同: 都是存放静态资源文件,项目中需要的资源文件如图片、字体图标、样式 等都可以放在这两个文件下。

不同:

  • assets 中存放的静态资源文件在 build 时,会将 assets 内的静态资源进行打包上传【压缩体积、代码格式化】。 压缩后的静态资源文件最终也会放置在static文件中跟着 idnex.html 一同上传至服务器。

  • static 内部的文件不会做处理,就直接进入打包好的目录,直接上传至服务器。 避免了压缩操作,其打包速度会提升,但其打包后的体积会相对比较大点。

建议:

  • 可将一些样式文件、js、图片等放置在 assets 中,在打包时再进行一次压缩、编译处理,减少打包体积。 iconfont 放在 static 中,第三方文件已经处理过,放在这里可提升打包速度。

23、Vue 如何监听对象/数组某个属性的变化

Vue2 中直接设置数组的某一项的值,或者直接设置对象的某个属性值,页面不会更新,因为 Object.defineProperty() 的限制。

解决方式: this.$set(要改变的对象,key,value)

vm.$set 原理:

  • 如果目标是数组,就触发 splice 方法;splice 会改变原有数组,所以会触发视图的更新。
  • 如果目标是对象,会先判断属性是否存在、对象是否是响应式,如最终需要对对象进行响应式处理, 通过调用 defineReactive 方法进行响应式处理
  • defineReactive:Vue 在初始化对象时,对对象采用 Object.defineProperty 动态添加 getter、setter 功能调用的方法。

关于 splice()、push()、pop()、shift()、unshift()、sort()、reverse()

  • Vue2 源码里缓存了 array 的原型链,然后重写了这几个方法,触发这些方法时触发 数据观察, 使用这些方法不用再进行额外操作,视图会自动进行更新。

25、template 和 jsx 有什么分别

**template **

  • 是模板语法 html 的扩展。使用 双大括号 进行数据绑定: <span> hello: {{message}} </span>
  • 上手简单,有完善指令,有自己 css 作用域;缺点是不够灵活。

jsx

  • 是 javascript 语法扩展,数据绑定使用 单大括号 进行数据绑定:<span> hello: {message}</span>
  • jsx 特别灵活,可以任意在 {} 里写入逻辑

1. template 和 jsx 都是 render 的一种表现形式,jsx 有更高的灵活性,在复杂组件中更具有优势, 2. template 在代码结构上更符合视图与逻辑分离的习惯,更加简单、直观、好维护。

26、Vue 初始化页面闪动问题

Vue 在初始化之前,template 还未解析时 可能会出现一种叫做 “未编译模板闪现” 的情况。 一般这种情况比较短暂,一闪而过,优化如下:

  1. 可以在 el 挂载的标签下添加指令 v-cloak 配合 display: none 可以在组件编译完成前对原始模板进行隐藏。
  2. v-cloak 用于隐藏尚未完成编译的 DOM 模板。会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。
<p class="#app" v-cloak>
  <p>{{value.name}}</p>
</p>

css 中:

[v-cloak] {
  display: none;
}

27、MVVM 优缺点

优点:

  1. 分离 view 和 model,降低代码耦合,提高视图或逻辑的重用性: 比如 view 可 独立于 model 变化和修改,一个 viewModel 可绑定在不同的 view 上, 可以把一些逻辑放在一个 viewModel 中,让多个 view 复用这段视图逻辑 【vue2 使用 minxs,vue3 使用 hooks】。
  2. 提高可测试性:viewModel 的存在可以更好的编写测试代码
  3. 数据双向绑定,视图自动更新

缺点:

  1. 难调试

因为是双向绑定,界面异常可能是 view 也可能 是 model 照成的, 数据绑定使得 一个位置的 BUG 可能会被传递到别的位置,照成定位比较难, 且数据绑定是通过指令写入 view 模板中,无法进行 断点 debug。

  1. model 长期持有不被销毁时,其内存的不释放,就照成了内存的消耗。
  2. 大型图层应用程序,视图状态较多的情况,其构建和维护成本就会比较高。

28、VUE 生命周期

Vue有一个完整的生命周期,从开始的实列化,到初始化数据,再进行DOM挂载,挂载完成后进行视图渲染;数据的更新到再次的更新、渲染,到最后将组件进行卸载,这一系列的过程就加Vue 的生命周期。

  1. beforeCreate 进行Vue 实列化

此时只是实列化开始阶段,data 数据、methods方法、DOM 都获取不到

  1. created 实列化完成
  • 完成实列化,实列上配置的 data、methods 方法、Watcher、props 都配置完成,可进行数据操作和方法的调用。
  • 但是还未进行DOM的挂载,无法执行DOM操作
  1. beforeMount 挂载前阶段
  • 此时模板已经在内存中编译完成,只是还未进行页面的更新渲染。也是无法进行DOM操作
  1. mounted 挂载完成

完成DOM挂载,页面和数据都是最新的,可进行 DOM 操作。

  1. beforeUpdate 更新前

响应式数据更新时调用,此时数据已经更新,只是页面还未更新渲染

  1. updated 更新完成
  • 响应式数据更新导致的 vir-dom 重渲染和打补丁之后调用。
  • 已经完成了视图的更新,数据和UI都是最新的,可在此进行依赖dom的一些操作
  • 不赞成在此钩子进行状态更改,以避免导致无限循环
  1. beforeDestory【Vue3:beforeUnMount】卸载前

即将进行组件的卸载,此钩子还可以获取实列参数、方法。一般在此进行定时器清除、localStorage 的清除等

  1. Destoryed【Vue3:unMounted】卸载完成

组件被销毁,实列不可操作

  1. keep-alive 包裹的组件缓存,引发的生命周期
  • activated:keep-alive 缓存组件,组件显示时的钩子
  • deactivated:keep-alive 缓存组件,组件移除时的钩子
  1. errorCaptured(err,instance,info){} 捕获后代组件传递的错误

  2. Vue3 新增

  • renderTracked(event){ }, 响应依赖组件渲染后调用,只在开发模式可用
  • renderTriggered(){}, 响应依赖组件重新触发时调用,只在开发模式可用

29、Vue 子组件和父组件执行顺序

加载渲染阶段

父组件先进行实列化,在挂载前,完成 子组件的 实列化、挂载阶段,再进行父组件的挂载。

f - beforeCreate、
f - created、
f - beforeMount
c - beforeCreate
c - created
c - beforeMount
c - mounted
f - mounted

更新阶段

父组件先进行数据更新,在UI 更新前先将子组件渲染完毕,再进行父组件渲染

f - beforeUpdate
c - beforeUpdate
c - updated
f - updated

销毁过程

销毁前,先对子组件进行销毁,再销毁本身

f - beforeDestory 【Vue3: beforeUnMount】
c - beforeDestory
c - destoryed  【Vu3: Unmount 】
f - destoryed

30、created和mounted的区别

  1. created 完成实列化,在模板渲染成 html 前调用,即对数据进行初始化。
  2. mouted 完成挂载,在模板渲染成 html 后调用,即一般在初始化完成后,在对 dom 节点进行一些操作。

31、一般在哪个生命周期请求异步数据

  1. 可以在 created、beforeMount、mounted 中进行调用异步请求
  • 因为这些钩子中实列已经生成,data 可以获取到,可对数据进行更新。
  1. 一般推荐在 create 中进行异步调用。
  • 因为此时实列完成、还未进行挂载,可以更快速获取到服务端数据进行ui展示;
  • 且 SSR 不支持 beforeMount、mounted 钩子,有利于保持一致性。

32、keep-alive 的生命周期有哪些

  • 组件被 keep-alive 进行包裹,进行缓存,会走 activateddeactivated 这两个钩子分别表示页面从缓存中展示、将页面缓存在内存中。
  • 而之前的 beforeDestroy、destroy 不会再触发,因为 缓存组件被缓存在内存中不会进行实际销毁。

33、父子组件间通信

1、props、$emit

父向子传递数据

  • 通过 props 传参数:在父组件中调用子组件,在模板标签中 通小写的 xxxsx-xxx 的形式进行数据的传参,子组件中通过 props 进行获取,且 数组、对象 的默认值通过 return 返回的形式
  • 且 props 是单向向下绑定的,子组件会随着父组件数据不断更新。
<script>
export default{
  props:{
    dataObj:{
      type: Object,
      default: ()=> []
    }
  }
}
</script>

子向父传递数据 在父组件中标签模板中,调用子组件标签上添加 v-on:自定义父事件, 子组件中点击事件触发 $emit('自定义父事件',传递的参数)。被触发时会触发父组件的自定父义事件然后获取参数。

父组件

<template>
  <child @fclick="changefClick" />
</template>

<script>
export default {
  methods:{
    changefClick(data){
      console.log('获取参数 ==',data)
    }
  }
}
</script>

子组件

<template>
  <button @click="clickC">点击</button>
</template>

<script>
export default {
  methods:{
    clickC(){
      this.$emit('fclick',1)
    }
  }
}
</script>

2、eventBus 事件总栈

使用事件总栈(EventBus) 作为沟通桥梁,所有组件间,都可以通过这个方法进行通信。

通过一个中间件 event-bus.js,进行一些实列化方法的封装,利用Vue实列的 $emit$out 进行消息的发送、接收。 在页面销毁时,通过 $('off') 移除对应的 eventBtns 事件监听。

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
// a.vue 通过 $emit 进行 eventbus 消息发送,并在销毁前 $off 移除对应的事件监听
<template>
  <button @click="sendMsg">点击</button>
</template>

<script>
  import { EventBus } from "@/utils/event-bus";
  export default {
    methods:{
      sendMsg(){
        EventBus.$emit('aMsg','来自A页面的消息');
      },
      beforeDestory(){
        EventBus.$off('aMsg',{})
      }
    }
}
</script>
// b.vue 通过 $on 进行消息的接收
<template>
  <p>{{msg}}</p>
</template>

<script>
export default {
  data(){
    return {
      msg: '暂无消息'
    }
  },
  mounted(){
      EventBus.$on('aMsg', data => {
      msg.value = data;
    })
  }
}
</script>

Vue3是不支持事件总栈 EventBus 取消了 $on、$off、$off 的实列,$emit 未被取消。因为虽然 Eventbus 简单,但是长时间内还是很难维护的。比较推荐使用 props、event、插槽、Provide/inject、全局状态管理等方式进行组件间数据通信。

3、Provide/inject

  • 针对父子组件的通信(可能是子孙组件),只要在这个父组件的链路下,就可使用 provide / inject 方式进行深层级的数据嵌套交互。
  • Vue2 通过 provide/inject 进行多层级嵌套的参数传递,是非响应的。在Vue3 中优化了这个功能,可在父组件中直接对参数进行更新,或者在子孙组件通过 inject 传递过来的方法进行父组件方法的触发,从而实现数据的更新。

Vue3 写法:

// 父组件:通过 provide(注入名称, 参数) 进行注入值、方法
<script setup lang="ts"> 
  import { provide ,ref } from 'vue'
  import EventB from './b.vue'
  const location = ref('hello')
  function updateLocation(){
    location.value = location.value + 'he'
  }
  provide('location',{
    location,
    updateLocation
  }); // 参数:注入名、参数(任意类型)
</script>

<template>
<div>父组件</div>
<button @click="updateLocation">点击我呀 {{location}}</button>
<hr />
<!-- 调用子组件 -->
<event-b />
</template>
// 子孙组件通过 inject 获取父传入的参数值,且通过传递过来的方法进行数据的更新
<script setup lang="ts"> 
import { inject } from 'vue'

// 一般放在公用的类型定义文件中
type InjectType = {
  location: String;
  updateLocation:()=> void;
}
const {location, updateLocation} = inject<InjectType>('location',{location:'默认值...',updateLocation: ()=>{
  console.log('默认事件')
} }) 

</script>

<template>
 <button @click="updateLocation">{{location}}</button>
</template>

34、Vue-router 懒加载如何实现

  • Vue 页面 一般会打包成一个总的 javaScript 文件,一次性的从服务器请求这个页面,会花费一定的时间,用户体验不好。尤其是SPA页面,白屏时间会较长。
  • 通过懒加载,将路由对应的组件打包成一个个单独对应的 js 文件,当路由被访问的时候才去加载对应的组件。这样可以有效分担首屏加载的能力。
  1. 使用动态导入的方式:函数rturn 出的url [箭头函数 + import]
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [{ path: '/users/:id', component: UserDetails }],
})
  1. 组件按组分块:webpack-chunk,使用一个特殊的注释语法来提供 chunk name,相同的 chunk name 会被打包成一个 js 文件
const UserDetails = () =>
  import(/* webpackChunkName: "details-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "dashboard-user" */ './UserDashboard.vue')
  
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
  1. 组件按组分块:vite 在vite.config.js 中进行 rollupOptions 配置
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
    },
  },
})

slot:是什么、作用、原理。

1、是什么:元素是一个插槽出口,标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

2、作用:

  • 可以为子组件添加一些模板片段,让子组件可在他们的组件中固定位置渲染这些片段的作用;
  • 插槽是在父组件模板中定义的,所以访问的是父组件的数据作用域,无法访问子组件的数据。
  • v-slot 只能添加在 <template> 上,除非使用默认插槽,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上

匿名插槽:一个组件只能有一个匿名插槽

<div>
    <Childer>
        <template v-slot:default>需要插入的内容...</template>
    </Childer>
</div>

Childer:
<div>
    ...
    <slot>Submit</slot>
</div>

V3: ==============================
f:
<div>
    <Childer>
       需要插入的内容...
    </Childer>
</div>

Childer:
<div>
    ...
   <slot/>
</div>

具名插槽: v-slot:name 可以简写成 #name

<div>
    <Childer>
       <template v-slot:name>需要插入的内容...</template>
    </Childer>
</div>

Childer:
<div>
    ...
   <slot name="name"></slot>
</div>

作用域插槽:某些场景需要使用子组件内的数据,需要子组件渲染时将一部分数据提供给插槽。

// 匿名插槽中,传递数据:
<div>
    <Childer v-slot="slotProps">
       {{ slotProps.text }} {{ slotProps.count }}
    </Childer>
</div>

或者使用解构优化代码:
<Childer v-slot="{ text, count }">
  {{ text }} {{ count }}
</Childer>


Childer:
<div>
    ...
   <slot :text="greetingMessage" :count="1"></slot>
</div>



// 具名插槽中传递数据:
<div>
    <Childer>
       <template #name="nameProps"> {{ nameProps }} 需要插入的内容...</template>
       // nameProps:{ message: 'hello' }
    </Childer>
</div>

Childer:
<div>
    ...
   <slot name="name" message="hello"></slot>
</div>

3、原理:

1、子组件vm实列化时,获取父组件传入 slot 标签内容,存放在 vm.$slot 中 【默认插槽为 vm.$slot.default, 具名插槽为 vm.$slot.xx 】,
2、当组件执行渲染函数,遇到 slot 标签,使用 $slot 中的内容进行替换,
3、此时可以作为插槽传递数据,若存在数据,则为作用域插槽。

35、vue-router

1、懒加载实现

1、动态倒入代替静态倒入:箭头函数 + import

  {
      path: "/about",
      name: "About",
      component: () => import("../views/AboutView.vue"),
    },

2、组件按组分块
webpack 使用注释语法提供 chunk name

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')

vite:rollupOption 下定义分块

export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
    },
  },
})

2、路由的hash和history模式的区别

vue-router 有两种: hash 和 history。默认 hash 模式

1、hash:

  • 又称之为前端路由
  • 通过 createHashWebHistory 实现,是SPA[单页面]的标配,
  • hash值会出现在 url 里,不会出现在 http 请求中,对后端没有影响。
  • 改变 hash 值 不会导致页面的重新加载
  • 且这种模式的浏览器支持比较友好,低版本的IE浏览器也支持这个模式。

原理:

利用 javascript 可以通过 onhashchange 事件监听到 hash 值的变化,从而进行页面(组件)切换

window.onhashchange = function(event){
    console.log(event.oldURL, event.newURL);
    let hash = location.hash.slice(1);
}

优点:

  • 在页面的hash值发生变化时,无需向后端发起请求,直接通过 hash 值变化进行页面(组件)的切换,切换比较顺滑
  • 且hash 值的变化,会被浏览器进行记录下来,可以在浏览器中进行前进、后退操作。

缺点:

  • 因为是SPA单页面,首屏加载时需要将资源全都加载完毕,会导致首屏加载过长的现象。
  • 且是通过 js 渲染成的 html,不会 SEO 识别,不利于排名。

2、history

  • history 是用传统的路由分发模式,即url切换时,服务器接收到这个 url 的请求,并对其进行解析,作出相应的处理。
  • 需要服务器进行相应配置,在 url 请求时,如果没有相应的路由/ 资源,会返回404.
  • history 模式 可以在首屏加载时只访问当前的资源,但是页面切换的时候,需要再次访问当前需要切换的资源。页面的切换需要时间;
  • 但是页面是 html 文件,所以有利于 SEO 排名

3、如何获取页面 hash 值的变化

1、vue 通过 watch 对 $route 进行监听

watch: {
  $route: {
    handler: function(n, o) {
      console.log(n)
    },
    deep: true
  }
}

2、通过window.location.hash 进行读写操作 写入时,可通过不重载网页的形式添加一条url历史访问记录

4、$route$router 区别

  • $route 是路由信息对象,获取路由的信息参数:如 path、params、query 等
  • $router 是路由实列例,对象包括了路由的跳转方法,钩子函数等。

5、如何定义动态路由,以及获取传过来的动态参数

1、通过 params 的方法进行动态路由设置

  • 其配置后的路由形式为:/router/:id
  • URL 形式为:.../user/id , 在 URL 中 id 的不同也会被映射到相同的路由

实现步骤:

1、配置路由 router/index.ts 中,设置动态路由

{
  path: '/user/:id',
  component:()=> import('../... /user/index.vue')
}

2、路由跳转传参的方式

在 template 中:

<router-link to="/user/333">Move</router-link>
// 或:
<router-link :to="{name:'user', params: { id: 333 }}">Move</router-link>

在js 中

this.$router.push({name: 'user', params: { id: 333 }})
// 或者
this.$router.push('/user/' + '333')

3、获取参数的方式:

this.$route.params.id

2、通过 query 方法

  • 通过路由跳转 $router 的 query 作为参数传递,
  • url 的形式:.../user?id=xxx 的形式

路由跳转方式

1、template 中

<router-link :to="{name:'user', query: { id: 333 }}"">跳转<router-link>
<router-link :to="{path:'/user', query: { id: 333 }}"">跳转<router-link>
<router-link :to="{path:'/user?id=xxx'}"">跳转<router-link>

2、在js 中

this.$router.push({
  name: 'user',
  // path: '/user',
  query: {
    id: xxx
  }
})

3、参数的获取

ths.$route.query.id

36、路由导航守卫

  • vue-router 提供的路由导航守卫,主要是以跳转或者取消的方式进行导航守卫。
  • 植入导航守卫有多种方式:全局导航守卫、局部路由导航守卫、组件导航守卫。

导航守卫的列子:

1、通过全局前置守卫(beforeEach)实现登录权限验证:

  • 已登录才可进入导航,否则让其跳转到登录界面。
  • 避免了需要在每个组件 created 中重复添加此逻辑。

2、通过全局后置钩子(afterEach)实现进入路由后,滑动条回到顶部

2、通过组件内路由导航守卫(beforeRouteLeave)实现:

  • 离开页面前,清除一些数据(定时器等)
  • 用户误操作点击退出当前导航时,提示确定是否退出,或者阻止退出当前导航

3、通过组件内路由导航守卫(beforeRouteUpdate)实现:

  • 连续进入一个动态路由时,产生了组件的复用,vue 为了节省性能,不会再次创建执行钩子函数。
  • beforeRouteUpdate 可以被触发,也可以获取到 this, 因为是之前创建过的组件

4、跳转路由、取消路由

  • vue2 通过 next('/router') 或者 next(false)
  • vue3 可直接进入 return {name: 'routername'} 或者 return false

导航守卫介绍

1、全局导航守卫

全局守卫名称参数执行条件使用场景
全局前置守卫beforeEachto,from, next(可选)进入路由前执行登录授权功能
全局解析守卫beforeResolveto导航被确认之前调用
组件内的守卫和异步路由组件被解析后
\
全局后置钩子afterEachto, from进入路由之后执行\

2、路由独享守卫

路由独享守卫参数执行条件
路由独享守卫beforeEnterto,from, next(可选)只在进入路由时触发

3、组件内守卫

组件内守卫参数执行条件使用场景
beforeRouteEnterto,from, next(可选)进入组件前触发,不可以访问 this
beforeRouteUpdateto,from, next(可选)组件复用时触发,可以访问 this复用组件时,不会触发钩子的创建执行,通过此方法进行一些必要的操作
beforeRouteLeaveto,from, next(可选)离开组件时触发,可以访问 this1、误触离离开当前页面时,跳出提示框,可以选择是否确定离开页面
2、离开页面前,进行一些内存清除操作:定时器、storage 等

其中参数表示

参数意义
to代表将要跳到哪个路由
from代表之前是来自哪个路由
next()进行正常跳转
next(false)取消跳转
next(“路径”)或者next({ path: ‘路径’ })路由重定向,跳转到其它页面

使用场景

1、全局守卫

在 main.ts 中,添加全局前置守卫,用于登录授权

router.beforeEach(async (to, from) => {
  console.log(to.name, !localStorage.getItem('token'))

  if (!localStorage.getItem('token') && to.name !== 'login') {
    return { name: 'login' }
  }
})

2、路由独享守卫

直接在路由配置上进行设置:

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation return false }, 
      if (!localStorage.getItem('token') && to.name !== 'login') {
        return { name: 'login' }
      }
    }, 
]

3、组件内守卫

误触离离开当前页面时,跳出提示框,可以选择是否确定离开页面

<template>
</template>

<script>
export default {
  // 进入组件之前调用,beforeRouteEnter,这个比较少用
  beforeRouteEnter(to, from, next) {
    console.log(to, from, next);
    next();
  },
  
  beforeRouteLeave(to, from, next) {
    const r = window.confirm("那你走?输入信息将不会保留");
    if (r) {
      next();
    } else {
      next(false);
    }
  },
  methods: {},
  created() {},
};
</script>

37、前端路由的理解

1、传统页面:

  • 前端早期,必须在刷新页面的情况下才可以重新请求数据。

2、ajax 出现:

  • 后期ajax 的出现,实现数据的请求不再需要进行页面的刷新。
  • 随之而来有了新的需求:页面之间的切换也希望实现不进行页面的刷新。

3、SPA:

  • SPA 解决了这个问题,只有一个 html 文件,页面的切换实际就是 content 的切换。
  • 但是 SPA 开始也遇到了两个问题:
  • 1、SPA 无法记录用户的操作,比如:刷新、前进、后退。
  • 2、SPA 仅有一个 url 作为页面的展示,对 SEO 不友好。

4、前端路由就是为了解决上述 SPA 的 问题:

  • 1、什么是前端路由:
  • 保证只有一个 html 页面,
  • 且与用户交互时不会刷新、跳转页面的同时,为SPA 每个视图展示时 匹配一个特殊的 url。
  • 在刷新、前进、后退,以及 SEO 均可通过这个 特殊的 url 去实现。

需要做到以下两点:

  • 1> 改变 url 时,不让浏览器向服务器发送请求
  • 2> 可以监听到 url 的变化

2、hash 和 history 模式实现了上面的功能

2.1 可以通过 hash 实现上面的功能:

  • hash 值在 改变时,不会引起浏览器向服务器发送请求;
  • hash 的改变 会触发 hashchange 事件,且在浏览器前进后退也可通过此方法控制。

2.2 通过 history 实现上面的功能:

  • 传统的 history 只能通过多页面跳转
  • HTML5 新增了 history.pushState()、history.replaceState() 方法,在url 改变时不会刷新页面,所以也具备了前端路由的能力
history.pushState() 会保留现有历史记录的同时,将 url 存入 历史记录中
history.replaceState() 将历史记录的当前页面历史替换为 url 
  • history 的改变不会触发任何事件,所以需要将 可能改变 history url 的情况进行拦截,从而间接监听到 url 的变化。
浏览器中的前进、后退;
点击 a 标签;
触发 history.pushState 或者 history.replaceState 

38、Vuex

1、Vuex 的原理

Vuex 是一个专门为 Vue.js 应用程序开发的状态管理模式(state) + 库(store)。它集中式管理所有组件的状态,并以相应的规则保证状态的改变。

  • Vuex 状态的存储是响应式的。 当从组件读取 store 中的 state,当 state 发生变化时,相应的组件也会进行高效更新。
  • 改变 store 状态的唯一方式就是 commit 一个 mutation。这样就可以方便的跟踪每一个状态的变化。

Vuex 为 vue 组件对状态的改变建立了一个完整的生态圈:

Vuex.png

  • 1、Vue Components (组件) - Action

组件会触发(dispatch) 一些动作(action)

  • 2、Action - Mutations

在组件发生动作,希望去获取或者改变数据,
但是在 Vuex 中 数据是集中管理的,不能直接修改数据,所以会将这个动作提交到(Commit)到 Mutations 中

  • 3、Mutations - State

然后通过 Mutations 中的方法 对 state 中的数据进行更改

  • 4、State - Vue Components

state 数据改变后,会重新渲染(render)到 组件(Vue Components)中,从而完成组件展示更新后的数据。

2、Vuex 中 Action 和 Mutation 的区别

Action

  • 用于提交 Mutation ,不可以直接变更状态;

  • Action 可以包含任意的异步操作

  • 参数:context,具有 store 相同的属性 和方法,所以

    可以通过 context.commit 去提交 Mutation
    可以通过 context.state 去获取 state
    
  • 通过 store.dispach('actionName') 触发(dispatch) action。

  • 在组件中触发 action:

    this.$store.dispach('actionName') 
    或者使用 mapActions 将 methods 方法映射为 store.dispatch 调用
    

使用试列:

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷的方式进行传参
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
  }
}

acton 支持异步方法,以及组合 Action:



actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData()) 
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

Mutation

  • Vuex 中唯一修改 store 中 state 的方式。

  • 只能进行同步操作

  • 参数:接收一个 state

  • 在组件中提及 Mutation:

    可以通过 this.$store.commit('mutationName') 提交 Mutation 
    或者使用 mapMutations 将组件中的 methods 映射为 store.commit 调用
    

试例:

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷的方式进行传参
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ])
  }
}

3、Vuex 和 localStorage 区别

1、存储方式

  • Vuex 存的是状态,存储在内存中
  • localStorage 是浏览器提供的接口,调用此接口以文件的形式存储在本地, 只能存储字符串的数据,以 JSON.pares 和 JSON.stringify 进行数据切换的使用

2、页面刷新时的保留

  • 刷新页面时, Vuex 的数据会被清除
  • 页面刷新时,localStorage 的数据不会丢失

3、应用场景

  • Vuex 目的是状态管理机制,集中式管理所有组件的状态,可以实现组件的视图随着 Vuex 的 stata 数据改变而响应式更新。
  • localStorage 是本地存储,是数据存储浏览器的方法,一般在跨页面传递数据时候使用。比如用户的登录状态。

4、Redux 和 Vuex 的区别,他们有什么共同的思想

区别:

1、Vuex 数据流的顺序:

Vue Components -> Action -> Mutations -> store - view

Vue Components 去 dispatch(触发) 一个可以异步的 Action(动作),
再由 Action 去 commit(提交) 到 Mutation 函数,
在Mutaions 中改变 state 数据,
Vue 检测到数据变化去 Render(渲染) Vue Components(组件)

2、Redux 数据流的顺序:

view——>action——>store——>reducer(返回)——>store—— view

在视图上 dispatch 一个 Action
这时 Redux 就会自动调用 Reducer,其中 Reducer 是一个纯函数,接收触发的 Action 和 当前的 state,
进行处理后返回新的 state ,进而触发视图的更新

Vuex 改进了Redux 中 Actoin 和 Reducer 函数:

reducer 不能直接修改 state,而是通过计算返回新的 state 进行数据更新;
Vuex 改进了 此方法,通过 commit Action 到 Mutations 中,去更新 state。
使得框架更加简易。

共同思想:

都是状态管理库,本着单一数据源,变化可预测的思想。

本质上:Redux 和 Vuex 都是 mvvm 思想,将数据从视图中抽离的一种方案;
形式上:Vuex 借鉴了 Redux,将 store 作为全局数据中心,进行 mode管理

5、为什么要使用 Vuex 或 Redux

  • 在实际使用当中,多层嵌套组件间的参数传递特别繁琐,
  • 使用全局状态管理,将组件中的数据进行统一管理,这样直接从 store 的 state 进行数据的展示,更新。
  • 且统一的 store,在状态管理时会执行一定的规则,代码变得更结构化和容易维护。

6、Vuex 有几种属性

state Getter Action Mutation Module

state  =>  基本数据,用来存放统一管理的数据
getters =>  对 state 派生出来的数据(对 state 的数据进行加工处理)
Action => 是组件触发的动作,可以 commit 对应的 Mutaion,Action 可以进行异步操作
Mutations => 提交更改数据的方法,是同步操作
Module => 模块化 Vuex

7、Vuex 和单纯的全局对象有什么区别

Vuex 是响应式状态存储:

  • 当 Vue 组件从 store 中读取状态,若 store 中 状态发生更新,相应的组件也会得到高效的 render
  • Vuex 不可以直接进行 store 的数据改。改变的唯一途径就是 commit 到 Mutaion。

8、为什么 Vuex 中的 Mutaions 中不能做异步操作

  • 每个 mutaion 执行完成都会对应一个新的状态变更,这样在 devtools【开发工具】 就可以打个快照存下来,

  • 然后就可以实现 time-travel【时间旅行,直白点就是回到某一次 state 时的状态】 了。如果 mutaion 支持异步操作,就无法进行状态追踪,调试会变得困难。

9、Vuex 的严格模式是什么,有什么作用,如何开启

  • 在严格模式下,无论何时发生了状态的变更,如果不是 由于 mutations函数引起的,就会抛出错误。
  • 这样可以保证所有的状态变更都能被调试工具跟踪到。
  • 确保在发布环境下,需要关闭严格模式。因为严格模式会深度监听状态树以检测数据的变更是否合规,照成性能损失。
new Vuex.Store({
  strict: true
})

10、如何在组件中批量使用 Vuex的 state、getter 属性,mutations 方法

  • 在 computed 对象中,使用 mapState、mapGetters 在 computed 注入 getter 对应的属性。

  • 在 methods 对象中,使用 mapMutaions 在 methods 注入 mutations 方法。

    • 相当于使用 this.$store.commit('changeDeviceData',data)
  computed: {
    ...mapState(['deviceData']),
     ...mapGetters(['getterObj'])
  },

  methods:{
    ...mapMutations(['changeDeviceData']),
  }