vue相关

221 阅读16分钟

我对vue的态度就是会用就行,但是在提升自己的道路上不刨根问底是不行的,学习原理的目的就是锻炼自己的计算机思维,站在开发者的角度想问题这样才能在面对问题的时候会更得心应手。

1.聊聊对vue的理解。

1. 框架简介

Vue.js 是一个用于构建用户界面的渐进式框架。与其他单页面应用(SPA)框架相比,Vue.js 设计的初衷是逐步采用,这意味着你可以逐步地引入它的功能,而不需要一下子把整个应用迁移到 Vue 上。你可以从一个简单的视图层框架开始,然后逐步引入路由、状态管理等功能,形成一个完整的单页面应用。

所以,Vue.js 可以用于小型项目,也可以扩展到大型项目。它的设计目标是尽量保持简单,同时通过插件和生态系统满足复杂应用的需求。

2. 核心概念

  • 数据驱动视图: Vue.js 采用响应式数据绑定,数据的变化会自动更新视图。通过双向绑定,使数据与视图的同步变得简单和高效。

  • 组件系统: 组件是 Vue 的核心。组件允许我们将 UI 划分为独立、可复用的部分。每个组件都有自己的逻辑和样式,可以嵌套使用。

  • 模板语法: Vue.js 使用基于 HTML 的模板语法来声明式地绑定 DOM。模板在底层实现上会被编译成虚拟 DOM 渲染函数。

3. 核心特性

  • 指令系统:Vue.js 提供了一组指令(Directives),如 v-bind, v-model, v-if, v-for 等,用来高效地操作 DOM。

  • 计算属性(Computed Properties): 计算属性是基于响应式数据的派生数据。它们会缓存,并且只有在相关响应式依赖发生变化时才会重新计算。

  • 侦听器(Watchers):对于需要异步或开销较大的操作时,可以使用侦听器来观察和响应数据的变化。

4. 状态管理 Vuex

Vuex 是 Vue.js 官方的状态管理库,用于集中式管理应用的所有组件状态。它的核心概念是单一状态树(single state tree),提供可预测的状态管理。

5. 路由管理 Vue Router

Vue Router 是 Vue.js 官方的路由管理库,它与 Vue.js 深度集成,允许开发者用 Vue 组件来构建应用的多页面路由。

6. 生态系统

  • 工具: Vue.js 提供了丰富的工具链支持,如 Vue CLI、Vue Devtools 等,使开发、调试和发布变得方便高效。

  • 第三方库: 丰富的第三方库和插件生态,如 Vuetify、Element UI 等,使得 Vue.js 在各种项目中的应用变得更加灵活和强大。

7. 性能优化

  • 虚拟 DOM: Vue.js 使用虚拟 DOM 来提升性能。每次数据变化,Vue 会创建一个新的虚拟 DOM 树并与上一个进行比较,只更新实际发生变化的部分。

  • 按需加载: 通过代码分割和按需加载,可以减少初始加载时间,提高应用的性能。

8. 单文件组件(SFC)

单文件组件是 Vue.js 的一大特色。一个 .vue 文件可以包含模板(template)、逻辑(script)和样式(style),使得组件开发更加直观和模块化。

9. 开发体验

Vue.js 提供了直观的 API 和详细的文档,使得上手变得简单。其友好的社区和丰富的学习资源也为开发者提供了极大的支持。

10.对spa的理解

SPA单页面应用 SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验。

在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面

  1. SPA将所有的活动局限于一个Web页面中,仅在该Web页面初始化时加载相应的HTML、JavaScript、CSS

  2. 一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转,取而代之的是用路由机制实现htm内容的变换,从而实现UI与用户的交互

我们熟知的JS框架如react,vue,angular,ember都属于SPA

单页应用优缺点

优点:

  • 具有桌面应用的即时性、网站的可移植性和可访问性
  • 用户体验好、快,内容的改变不需要重新加载整个页面
  • 良好的前后端分离,分工更明确

缺点:

  • 不利于搜索引擎的抓取
  • 首次渲染速度相对较慢

MPA多页应用

上面大家已经对单页面有所了解了,下面来讲讲多页应用MPA(MultiPage-page application),翻译过来就是多页应用 在MPA中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载html、css、js文件,公共文件则根据需求按需加载如下图

单页应用与多页应用的区别

image.png

2. mvvm

1. 概念:

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

2. 为什么说vue没有严格遵循mvvm

  • 在 Vue.js 中,模板语法允许在模板中编写一些逻辑,如条件判断和循环。
  • Vue 实例既扮演了 ViewModel 的角色,也承担了部分控制器(Controller)的职责。Vue 实例不仅负责将数据和视图绑定,还管理组件的生命周期、事件处理和与其他组件的交互。
  • 允许用户直接操作 DOM 元素。
  • Vuex 强调单向数据流和状态的集中管理,这与 MVVM 模式中的双向绑定和局部状态管理有所不同。

3.生命周期

beforeCreate:vue实例的挂载元素el和数据对象data都是undefined,还没有初始化。

created:vue实例的数据对象data有了,可以访问里面的数据和方法,未挂载到DOM(Document Object Model),el(element)还没有,但可以通过vm.$nextTick来访问Dom。

beforeMount:vue实例的el和data都初始化了,但是挂载之前为虚拟的dom节点

mounted:vue实例挂载到真实DOM上,就可以通过DOM获取DOM节点,数据完成双向绑定,可以使用$refs属性对Dom进行操作。

beforeUpdate:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器 。

updated:虚拟DOM重新渲染和打补丁之后调用,组成新的DOM已经更新,避免在这个钩子函数中操作数据,防止死循环。

beforeDestroy:实例销毁前调用,实例还可以用,this能获取到实例,常用于销毁定时器,解绑事件。

destroyed:实例销毁后调用,调用后所有事件监听器会被移除,所有的子实例都会被销毁。

4. 响应式原理

Vue 2 中的数据响应式会根据数据类型做不同的处理。

如果是对象,则通过Object.defineProperty(obj,key,descriptor)拦截对象属性访问,当数据被访问或改变时,感知并作出反应;

如果是数组,则通过覆盖数组原型的方法,扩展它的7个变更方法(push、pop、shift、unshift、splice、sort、reverse),使这些方法可以额外的做更新通知,从而做出响应。

缺点:

  • 深度监听,需要递归到底,一次计算量大
  • 无法监听新增属性、删除属性(使用Vue.setVue.delete可以)
  • 无法监听原生数组,需要重写数组原型

基本流程:

1. 数据劫持(Data Hijacking)

Vue 通过递归遍历数据对象的每个属性,并使用 Object.defineProperty 为每个属性设置 getter 和 setter。在访问属性时,getter 会被调用;在修改属性时,setter 会被调用。

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`Getting ${key}: ${val}`);
      return val;
    },
    set(newVal) {
      console.log(`Setting ${key}: ${newVal}`);
      if (val !== newVal) {
        val = newVal;
      }
    }
  });
}

let data = { message: 'Hello, Vue!' };
defineReactive(data, 'message', data.message);
2. 依赖收集(Dependency Collection)

Vue 使用一个名为 Dep 的类来管理依赖关系。每个响应式属性都有一个 Dep 实例,存储所有订阅该属性变化的观察者(Watcher)。

class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach(sub => sub.update());
  }
}
3. 观察者(Watcher)

Watcher 是一个中介,用于连接响应式数据和视图。每个组件实例都有一个对应的 Watcher 实例,当数据变化时,Watcher 会触发视图更新。

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    Dep.target = this;
    let value = this.vm[this.exp];
    Dep.target = null;
    return value;
  }

  update() {
    let value = this.vm[this.exp];
    let oldValue = this.value;
    if (value !== oldValue) {
      this.value = value;
      this.cb.call(this.vm, value, oldValue);
    }
  }
}
4. 示例:响应式系统的工作流程

当我们访问或修改数据对象的属性时,会触发 getter 或 setter。在 getter 中,当前的 Watcher 会被添加到 Dep 的依赖列表中。在 setter 中,Dep 会通知所有的 Watcher 执行更新操作,从而更新视图。

let data = { message: 'Hello, Vue!' };
let dep = new Dep();

Object.defineProperty(data, 'message', {
  get() {
    if (Dep.target) {
      dep.addSub(Dep.target);
    }
    return 'Hello, Vue!';
  },
  set(newVal) {
    dep.notify();
  }
});

new Watcher(data, 'message', (newVal, oldVal) => {
  console.log(`View updated: ${newVal}`);
});

data.message = 'Hello, World!'; // 触发视图更新

5.模板语法的模板编译原理

Vue.js 的模板编译器(compiler)将模板字符串(template)编译成 render渲染函数,这些函数可以生成虚拟 DOM(Virtual DOM)节点。这个过程分为几个阶段:解析、优化和代码生成。以下是详细的解释:

1. 模板解析(Parsing)

在这一阶段,Vue.js 会将模板字符串(template)解析成抽象语法树(AST)。AST 是一种用来描述代码结构的树状表示方式,它捕获了模板的层次结构和语法信息。

解析步骤:
  • 模板字符串:首先,Vue.js 会将模板字符串传递给解析器。
  • 解析器:解析器通过逐字符扫描模板字符串,识别出各种标签、属性、指令、文本等,并生成相应的 AST 节点。
  • AST 节点:最终的输出是一个包含模板结构和内容的 AST。
<!-- 模板字符串 -->
<div id="app">
  <p>{{ message }}</p>
</div>

对应的 AST 结构可能如下:

{
  "type": "Element",
  "tag": "div",
  "attrsList": [{ "name": "id", "value": "app" }],
  "children": [
    {
      "type": "Element",
      "tag": "p",
      "children": [
        { "type": "Expression", "expression": "message" }
      ]
    }
  ]
}

2. 优化(Optimization)

在优化阶段,Vue.js 会遍历 AST,标记出静态节点和静态根节点。静态节点是指在渲染过程中不会改变的节点,这样可以避免在后续的更新中重新计算和生成这些节点,从而提升渲染性能。

优化步骤:
  • 静态节点标记:遍历 AST,判断每个节点是否为静态节点。
  • 静态根节点标记:如果一个节点是静态节点并且包含子节点,则将其标记为静态根节点。

3. 代码生成(Code Generation)

在代码生成阶段,Vue.js 会将优化后的 AST 转换成渲染函数(render function)。渲染函数是用来生成虚拟 DOM 的 JavaScript 函数。

代码生成步骤:
  • AST 转换:将 AST 转换为 JavaScript 渲染函数。
  • 渲染函数:渲染函数在执行时会生成虚拟 DOM 节点。
function render() {
  return _c('div', { attrs: { id: 'app' } }, [
    _c('p', [_v(_s(message))])
  ]);
}

在这个渲染函数中:

  • _c 是一个创建元素的方法。
  • _v 是一个创建文本节点的方法。
  • _s 是一个将数据转换为字符串的方法。

4. 渲染和更新

编译后的渲染函数在组件实例的生命周期内被调用。首次渲染时,渲染函数会生成虚拟 DOM,并通过虚拟 DOM 树与真实 DOM 进行比对,生成最终的 DOM 结构。当数据发生变化时,渲染函数会重新执行,生成新的虚拟 DOM,并与旧的虚拟 DOM 进行比较(diff),只更新发生变化的部分。

5. 组件间的通信方法

  • 父子组件通信:

    父向子传递数据是通过props,子向父是通过$emit触发事件;

    通过父链/子链也可以通信($parent/$children);

    ref也可以访问组件实例;

    provide/inject$attrs/$listeners

  • 兄弟组件通信:

    全局事件总线EventBusVuex

  • 跨层级组件通信:

    全局事件总线EventBusVuexprovide/inject

    juejin.cn/post/735681…

5. vue组件的data为什么必须是函数

一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。

6. vue组件写name的好处

  • 可以通过名字找到对应的组件( 递归组件:组件自身调用自身 )

  • 可以通过 name 属性实现缓存功能(keep-alive

  • 可以通过 name 来识别组件(跨级组件通信时非常重要)

  • 使用 vue-devtools 调试工具里显示的组件名称是由 vue 中组件 name 决定的

7. keep-alive

<keep-alive> 是 Vue.js 提供的一个内置组件,主要用于缓存动态组件。它可以缓存组件实例,从而避免不必要的重新渲染,提升应用的性能。

基本用法:

<keep-alive> 组件通常包裹在动态组件或者路由视图的外层。当一个动态组件在 <keep-alive> 中被切换时,它的状态会被缓存,而不会被销毁。

工作原理:

<keep-alive> 组件在内部维护了一个缓存对象(cache),用于存储被缓存的组件实例,以及一个键值对(keys)来记录组件的访问顺序。每个被缓存的组件实例都有一个唯一的 key,这个 key 是基于组件的 name 或者标签名和一些其他信息生成的。

a. 组件激活

当一个组件实例被缓存后,再次被使用时,Vue 不会重新创建该组件实例,而是从缓存中取出,并触发组件的 activated 钩子函数。

function activateChildComponent(instance, direct) {
  callHook(instance, 'activated', direct);
}
b. 组件停用

当组件实例从活动状态切换到缓存状态时,Vue 不会销毁该实例,而是触发组件的 deactivated 钩子函数。

function deactivateChildComponent(instance, direct) {
  callHook(instance, 'deactivated', direct);
}

8. Vue.$nextTick

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

nextTick 是 Vue 提供的一个全局 API,由于 Vue 的异步更新策略,导致我们对数据修改后不会直接体现在 DOM 上,此时如果想要立即获取更新后的 DOM 状态,就需要借助该方法。

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue 将开启一个异步更新队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入队列一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick方法会在队列中加入一个回调函数,确保该函数在前面的 DOM 操作完成后才调用。

使用场景:

  1. 如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()
  2. created生命周期中进行DOM操作

9. v-show 和 v-if 的区别?

  1. 控制手段不同。v-show是通过给元素添加 css 属性display: none,但元素仍然存在;而v-if控制元素显示或隐藏是将元素整个添加或删除。
  2. 编译过程不同。v-if切换有一个局部编译/卸载的过程,切换过程中合适的销毁和重建内部的事件监听和子组件;v-show只是简单的基于 css 切换。
  3. 编译条件不同。v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,渲染条件为假时,并不做操作,直到为真才渲染。
  4. 触发生命周期不同。v-show由 false 变为 true 的时候不会触发组件的生命周期;v-if由 false 变为 true 的时候,触发组件的beforeCreatecreatedbeforeMountmounted钩子,由 true 变为 false 的时候触发组件的beforeDestorydestoryed钩子。
  5. 性能消耗不同。v-if有更高的切换消耗;v-show有更高的初始渲染消耗。

使用场景:
如果需要非常频繁地切换,则使用v-show较好,如:手风琴菜单,tab 页签等; 如果在运行时条件很少改变,则使用v-if较好,如:用户登录之后,根据权限不同来显示不同的内容。

10. v-if 和 v-for 为什么不建议放在一起使用?

Vue 2 中,v-for的优先级比v-if高,这意味着v-if将分别重复运行于每一个v-for循环中。如果要遍历的数组很大,而真正要展示的数据很少时,将造成很大的性能浪费。

Vue 3 中,则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,会导致异常。

通常有两种情况导致要这样做:

  • 为了过滤列表中的项目,比如:v-for = "user in users" v-if = "user.active"。这种情况,可以定义一个计算属性,让其返回过滤后的列表即可。
  • 为了避免渲染本该被隐藏的列表,比如v-for = "user in users" v-if = "showUsersFlag"。这种情况,可以将v-if移至容器元素上或在外面包一层template即可。

11. 虚拟dom和diff算法(虚拟dom是怎么更新的)


(vue中key的作用) key 的作用主要是为了高效的更新虚拟 DOM 。另外 vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。

要解释 key 的作用,不得不先介绍一下虚拟 DOMDiff 算法了。

我们知道,vue 可以不直接操作 DOM 元素,只操作数据便可以重新渲染页面。而隐藏在背后的原理便是其高效的 Diff 算法。 我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。


虚拟dom概念:虚拟 DOM 是由一系列的 Js对象组成的树状结构,每个对象代表着一个DOM元素,包括元素的标签名、属性、子节点等信息。虚拟 DOM 中的每个节点都是一个 JavaScript 对象,它们可以轻松地被创建、更新和销毁,而不涉及到实际的DOM操作。

虚拟dom的结构: 没有统一的标准,一般包括tagpropschildren三项。
tag:必选。就是标签,也可以是组件,或者函数。
props:非必选。就是这个标签上的属性和方法。
children:非必选。就是这个标签的内容或者子节点。如果是文本节点就是字符串;如果有子节点就是数组。换句话说,如果判断children是字符串的话,就表示一定是文本节点,这个节点肯定没有子元素。

diff的概念diff算法是一种对比算法,通过对比旧的虚拟DOM和新的虚拟DOM,得出是哪个虚拟节点发生了改变,找出这个虚拟节点并只更新这个虚拟节点所对应的真实节点,而不用更新其他未发生改变的节点,实现精准地更新真实DOM,进而提高效率。

diff整体策略为:深度优先,同层比较。也就是说,比较只会在同层级进行, 不会跨层级比较;比较的过程中,循环从两边向中间收拢。

Diff 算法的实现流程可以概括为以下几个步骤:

  1. 比较根节点: 首先,对比新旧虚拟 DOM 树的根节点,判断它们是否相同。

  2. 逐层对比子节点: 如果根节点相同,则逐层对比子节点。

    • 比较子节点类型tag:

      • 如果节点类型不同,则直接替换整个节点。
      • 如果节点类型相同,继续对比节点的属性和事件。
    • 对比子节点列表:

      • 通过双指针法(新旧头尾指针进行比较,循环向中间靠拢)对比新旧节点列表,查找相同位置的节点。
      • 如果节点相同,进行递归对比子节点。
      • 如果节点不同,根据情况执行插入、删除或移动节点的操作。
  3. 处理新增、删除和移动的节点:

    • 如果新节点列表中存在旧节点列表中没有的节点,执行新增操作。
    • 如果旧节点列表中存在新节点列表中没有的节点,执行删除操作。
    • 如果新旧节点列表中都存在相同的节点,但顺序不同,执行移动节点的操作。
  4. 更新节点属性和事件:

    • 如果节点相同但属性或事件发生了变化,更新节点的属性和事件。
  5. 递归对比子节点:

    • 如果节点类型相同且是容器节点(例如 div、ul 等),则递归对比子节点。
//patchVnode是diff发生的地方,下面是patchVnode的源码
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  // 如果新旧节点一致,什么都不做
  if (oldVnode === vnode) {
    return
  }

  // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
  const elm = vnode.elm = oldVnode.elm

  // 异步占位符
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }
  // 如果新旧都是静态节点,并且具有相同的key
  // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
  // 也不用再有其他操作
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children
  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  // 如果vnode不是文本节点或者注释节点
  if (isUndef(vnode.text)) {
    // 并且都有子节点
    if (isDef(oldCh) && isDef(ch)) {
      // 并且子节点不完全一致,则调用updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

      // 如果只有新的vnode有子节点
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      // elm已经引用了老的dom节点,在老的dom节点上添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

      // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)

      // 如果老节点是文本节点
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }

    // 如果新vnode和老vnode是文本节点或注释节点
    // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}
-   **判断是否需要更新:** 首先,函数会比较新旧虚拟 DOM 节点是否相同,如果相同则直接返回,无需进行后续操作。

-   **获取旧节点的真实 DOM 引用:** 通过 `elm = vnode.elm = oldVnode.elm` 将新节点 `vnode` 的真实 DOM 引用指向旧节点的真实 DOM。

-   **处理异步占位符:** 如果旧节点是异步占位符(`asyncPlaceholder`),并且新节点的异步工厂已经解析,则通过 `hydrate` 函数进行同步操作;否则,将新节点标记为异步占位符并返回。

-   **处理静态节点:** 如果新旧节点都是静态节点(`isStatic` 为真),并且具有相同的 key,则将新节点的组件实例引用指向旧节点的组件实例。

-   **触发 prepatch 钩子:** 如果新节点的数据对象中定义了 `hook` 并且 `prepatch` 钩子存在,则执行该钩子函数,用于预处理新旧节点之间的差异。

-   **更新节点的属性和事件:** 如果新节点的数据对象中定义了 `hook` 并且 `update` 钩子存在,则执行该钩子函数,用于更新节点的属性和事件。

-   **处理子节点:** 如果新旧节点都有子节点,则比较它们之间的差异并进行更新,调用 `updateChildren` 函数。如果只有新节点有子节点,则将新节点的子节点添加到旧节点上。如果只有旧节点有子节点,则删除旧节点的子节点。如果旧节点是文本节点,则清空其内容。

-   **更新文本内容:** 如果新旧节点都是文本节点或注释节点,并且它们的文本内容不同,则更新新节点的文本内容。

-   **触发 postpatch 钩子:** 如果新节点的数据对象中定义了 `hook` 并且 `postpatch` 钩子存在,则执行该钩子函数,用于处理节点更新后的操作。
 怎么将虚拟 Dom 转化为真实 Dom?
// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字符串类型直接就是文本节点
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

12. watch和computed

watch 是 Vue 中监视数据变化的一种方法,监听特定数据的变化并执行相应的操作。

watch的工作原理:

  • Watch 对象的定义:在 Vue 组件实例中,我们可以通过在watch选项中定义一个对象来创建 Watch 监听器。这个对象中可以包含多个键值对,其中键是要监听的数据属性的名称,值是处理数据变化的回调函数。

  • Watch 的注册:当 Vue 组件实例创建时,Watch 对象会被注册并与组件实例关联起来。Vue 会遍历 Watch 对象,并为每个键值对创建一个 Watcher 实例。

  • Watcher 实例的创建:Watcher 实例是 Watch 的核心,它负责监听和响应数据变化。Watcher 实例在 Watch 对象的键值对中创建,并与要监听的数据属性进行关联。

  • 数据的变化检测:当被 Watch 监听的数据发生变化时,Vue 会触发数据的变化检测机制。这个机制会比较新旧值,如果发现变化,就会通知相关的 Watcher 实例。

  • Watcher 的回调执行:一旦 Watcher 实例接收到变化通知,它将调用相应的回调函数。这个回调函数可以是用户自定义的,用于实现数据变化后的特定操作。

深度监听deep:在 Vue 中,深度监听数据的变化意味着不仅监听对象或数组本身的变化,还监听它们内部属性或元素的变化。

当将deep选项设置为true时,Vue 会递归遍历对象的所有属性或数组的所有元素,并为每个属性或元素都创建一个深度观察者。这样,无论是对象的某个属性还是数组的某个元素发生变化,都能触发相应的回调函数。

立即执行immediate: 当在 watch 选项中设置immediate: true时,Vue 会在监听开始之初立即执行回调函数,无论数据是否已经发生变化。

computed:

computed:模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以,对于任何复杂逻辑,你都应当使用计算属性。通过计算属性,我们可以将复杂的逻辑封装成一个属性,并在模板中直接使用。计算属性会自动追踪依赖的数据,

computed的工作原理:

  • .每个computed属性都会生成对应的观察者(Watcher 实例)观察者存在 values 属性和 get 方法。computed 属性的 getter 函数会在 get 方法中调用,并将返回值赋值给 value。初始设置 dirty 和 lazy 的值为 true,lazy 为 true 不会立即 get 方法(懒执行),而是会在读取 computed 值时执行。

  • 当计算属性被访问时,会触发计算函数的执行。在计算函数中,可以访问其他响应式数据。

  • 计算函数会根据依赖的响应式数据进行计算,并返回计算结果。

  • 计算属性会将计算结果缓存起来,下次访问时直接返回缓存的值。

  • 当依赖的响应式数据发生变化时,计算属性会被标记为"dirty"(脏),下次访问时会重新计算并更新缓存的值。

注意:  计算属性适用于那些依赖其他响应式数据的场景,而不适用于需要进行异步操作或有副作用的场景。对于这些情况,可以使用侦听器(watcher)或使用methods来处理。

computed vs method

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

computed vs watch

  • computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
  • watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
  • 所以一般来说需要依赖别的属性来动态获得值的时候可以使用 computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用 watch
  • 都是观察数据变化的(相同)

  • 计算属性将会混入到 vue 的实例中,所以需要监听自定义变量;watch 监听 data 、props 里面数据的变化;

  • computed 有缓存,它依赖的值变了才会重新计算,watch 没有;

  • watch 支持异步,computed 不支持;

  • watch 是一对多(监听某一个值变化,执行对应操作);computed 是多对一(监听属性依赖于其他属性)

  • watch 监听函数接收两个参数,第一个是最新值,第二个是输入之前的值;

  • computed 属性是函数时,都有 get 和 set 方法,默认走 get 方法,get 必须有返回值(return)

13. vuex

  • 概念:
    Vuex 是 Vue 专用的状态管理库,它以全局方式集中管理应用的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

  • 解决的问题:
    Vuex 主要解决的问题是多组件之间状态共享。利用各种通信方式,虽然也能够实现状态共享,但是往往需要在多个组件之间保持状态的一致性,会使程序逻辑变得复杂,传参就会很繁琐。Vuex 通过把组件的共享状态抽取出来,,这样任何组件都能用一致的方式获取和修改状态,响应式的数据也能够保证简洁的单向流动,使代码变得更具结构化且易于维护。

  • 什么时候用:
    Vuex 并非是必须的,它能够管理状态,但同时也带来更多的概念和框架。如果我们不打算开发大型单页应用或应用里没有大量全局的状态需要维护,完全没有使用Vuex的必要,一个简单的 store 模式就够了。反之,Vuex将是自然而然的选择。

  • 用法:
    Vuex 将全局状态放入state对象中,它本身是一颗状态树,组件中使用store实例的state访问这些状态;然后用配套的mutation方法修改这些状态,并且只能用mutation修改状态,在组件中调用commit方法提交mutation;如果应用中有异步操作或复杂逻辑组合,需要编写action,执行结束如果有状态修改仍需提交mutation,组件中通过dispatch派发action。最后是模块化,通过modules选项组织拆分出去的各个子模块,在访问状态(state)时需注意添加子模块的名称,如果子模块有设置namespace,那么提交mutation和派发action时还需要额外的命名空间前缀。

Vuex 基础使用

首先在Vue中添加 Vuex 插件

通过 vue-cli 添加了 Vuex 后,在项目的 src 目录下会多出一个 store 目录,目录下会有个 index.js

当然也通过 npm 进行安装 Vuex

bash
复制代码
npm install vuex --save

在开发环境开启严格模式 这样修改数据 就必须通过 mutation 来处理

在 package.json 文件 scripts 中可以设置环境,当我们处于开发环境时,可以开启严格模式

开启严格模式的方式也是很简单的一行代码就可以

strict:products.env.NODE_ENV !== 'production'

java
复制代码
/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  // 在开发环境开启严格模式 这样修改数据 就必须通过 mutation 来处理
  strict:products.env.NODE_ENV !== 'production',
  // 状态
  state: {
  },
  // 用来处理状态
  mutations: {
  },
  // 用于异步处理
  actions: {
  },
  // 用来挂载模块
  modules: {
  }
})

要使用 store 就在把 store 挂载到 Vue 中

把 store 挂载到 Vue 之后 ,所有的组件就可以直接从 store 中获取全局数据了

javascript
复制代码
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  // 挂载到vue 中
  store,
  render: (h) => h(App),
}).$mount('#app')

1.state

在 state 中添加数据

我们需要共享的状态都放在写在 state 对象里面

js
复制代码
/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  mutations: {},
  actions: {},
  modules: {},
})

组件中获取 state 中的数据

获取到 state 有两种方式

1.直接使用 this.$store.state[属性] ,(this 可以省略)
<template>
  <div id="app">
    {{ this.$store.state.name }}
    {{ this.$store.state.age }}
  </div>
</template>
2.使用 mapState

通过 mapStatestore 映射到 组件的计算属性,就相当于组件内部有了 state 里的属性

知道这里为啥要用 ...展开吗,到时候实现 mapState 时就知道了

<template>
  <div id="app">
    {{ name }}
    {{ age }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState
import { mapState } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  }
}
</script>

注意

当store 中的值 和 当前组件有相同的状态,我们可以在 mapState 方法里传递一个对象 而不是一个数组,在对象中给状态起别名

computed: {
    // name2 和 age2 都是别名
    ...mapState({ name2: 'name', age2: 'age'}])
}

2.Mutation

Store 中的状态不能直接对其进行操作,我们得使用 Mutation 来对 Store 中的状态进行修改,虽然看起来有些繁琐,但是方便集中监控数据的变化

state 的更新必须是 Mutation 来处理

我们现在 mutaions 里定义个方法

如果想要定义的方法能够修改 Store 中的状态,需要参数就是 state

js
复制代码
/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  mutations: {
    // 在这里定义 方法
    /**
     *
     * @param {*} state 第一个参数是 Store 中的状态(必须传递)
     * @param {*} newName 传入的参数 后面是多个
     */
    changeName(state, newName) {
      // 这里简单举个例子 修改个名字
      state.name = newName
    },
  },
  actions: {},
  modules: {},
})

在组件中使用 mutations 中的方法

同样有两种方法在组件触发 mutations 中的方法

1.this.$store.commit() 触发

methods 中定义一个方法,在这个方法里面进行触发 mutations 中的方法

<template>
  <div id="app">
    <button @click="handleClick">方式1 按钮使用 mutation 中方法</button>
    {{ name }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState
import { mapState } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
    handleClick() {
      // 触发 mutations 中的 changeName
      this.$store.commit('changeName', '小浪')
    }
  },
}
</script>

<style  scoped>
</style>
2.使用 mapMutations
<template>
  <div id="app">
    <button @click="changeName('小浪')">方式2 按钮使用 mutation 中方法</button>
    {{ name }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
	// 将 mutations 中的 changeName 方法映射到 methods 中,就能直接使用了 changeName 了
    ...mapMutations(['changeName'])
  },
}
</script>

<style  scoped>
</style>

3.Action

ActionMutation 区别

Action 同样也是用来处理任务,不过它处理的是异步任务,异步任务必须要使用 Action,通过 Action 触发 Mutation 间接改变状态,不能直接使用 Mutation 直接对异步任务进行修改

先在 Action 中定义一个异步方法来调用 Mutation 中的方法

/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  mutations: {
    // 在这里定义 方法
    /**
     *
     * @param {*} state 第一个参数是 Store 中的状态(必须传递)
     * @param {*} newName 传入的参数 后面是多个
     */
    changeName(state, newName) {
      // 这里简单举个例子 修改个名字
      state.name = newName
    },
  },
  actions: {
    /**
     *
     * @param {*} context 上下文默认传递的参数
     * @param {*} newName 自己传递的参数
     */
    // 定义一个异步的方法 context是 store
    changeNameAsync(context, newName) {
      // 这里用 setTimeout 模拟异步
      setTimeout(() => {
        // 在这里调用 mutations 中的处理方法
        context.commit('changeName', newName)
      }, 2000)
    },
  },
  modules: {},
})

在组件中是 Action 中的异步方法也是有两种方式

1.this.$store.dispatch()
<template>
  <div id="app">
    <button @click="changeName2('小浪')">方式1 按钮使用 action 中方法</button>
    {{ name }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState mapMutations
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
    changeName2(newName) {
      // 使用 dispatch 来调用 actions 中的方法
      this.$store.dispatch('changeNameAsync', newName)
    }
  },
}
</script>

<style  scoped>
</style>
2.使用 mapActions
<template>
  <div id="app">
    <button @click="changeNameAsync('小浪')">
      方式2 按钮使用 action 中方法
    </button>
    {{ name }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState mapMutations mapActions
import { mapState, mapMutations, mapActions } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 store 映射到当前组件的计算属性
    ...mapState(['name', 'age'])
  },
  methods: {
    // 映射 actions 中的指定方法 到 methods中,就可以在该组件直接使用
    ...mapActions(['changeNameAsync'])
  },
}
</script>

<style  scoped>
</style>

4.Getter

简介

Getter 类似于计算属性,但是我们的数据来源是 Vuex 中的 state ,所以就使用 Vuex 中的 Getter 来完成

应用场景

需要对 state 做一些包装简单性处理 展示到视图当中

先来写个 Getter

/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三',
    age: 21,
  },
  getters: {
    // 在这里对 状态 进行包装
    /**
     *
     * @param {*} state 状态 如果要使用 state 里面的数据,第一个参数默认就是 state ,名字随便取
     * @returns
     */
    decorationName(state) {
      return `大家好我的名字叫${state.name}今年${state.age}岁`
    },
  },
})

当然 Getter 也有两种方式导入

1.this.$store.getters[名称]
<template>
  <div id="app">
    {{ this.$store.getters.decorationName }}
  </div>
</template>
2.使用 mapGetters
<template>
  <div id="app">
    {{ decorationName }}
  </div>
</template>

<script>

// 从 Vuex 中导入 mapGetters
import { mapGetters } from 'vuex'
export default {
  name: 'App',
  computed: {
    // 将 getter 映射到当前组件的计算属性
    ...mapGetters(['decorationName'])
  },
}
</script>

5.Module

为了避免在一个复杂的项目 state 中的数据变得臃肿,Vuex 允许将 Store 分成不同的模块,每个模块都有属于自己的 stategetteractionmutation

我们这里新建一个 animal.js 文件

/* animal.js */

const state = {
  animalName: '狮子',
}
const mutations = {
  setName(state, newName) {
    state.animalName = newName
  },
}

//导出
export default {
  state,
  mutations,
}

store/index.js中的 modules 进行挂载这个模块

/* src/store/index.js */

// 导入 Vue
import Vue from 'vue'
// 导入 Vuex 插件
import Vuex from 'vuex'
// 引入模块
import animal from './animal'

// 把 Vuex 注册到Vue 上
Vue.use(Vuex)

export default new Vuex.Store({
  modules: {
    animal,
  },
})

然后我们就可以在组件中使用了

<template>
  <div id="app">
    {{ this.$store.state.animal.animalName }}
    <button @click="$store.commit('setName', '老虎')">改名</button>
  </div>
</template>

$store.state[在module中挂载的模块名][挂载的模块里的属性]

是不是觉得这种模式很复杂

img

添加命名空间

其实也可以使用 mapXXX 方法进行映射,不过写法有些许不同,先在导出的添加一个命名空间 namespaced: true

/* animal.js */

const state = {
  animalName: '狮子',
}
const mutations = {
  setName(state, newName) {
    state.animalName = newName
  },
}

export default {
  // 开启命名空间 方便之后使用 mapXXX
  namespaced: true,
  state,
  mutations,
}

方式2

<template>
  <div id="app">
    {{ animalName }}
    <button @click="setName('老鹰')">改名</button>
  </div>
</template>

<script>

// 从 Vuex 中导入 mapState mapMutations
import { mapState, mapMutations } from 'vuex'
export default {
  name: 'App',
  computed: {
    // mapState 使用方式和之前有些许不同,第一个是module挂载的模块名
    // 第二个参数是 animal 模块中的 state 属性
    ...mapState('animal', ['animalName'])
  },
  methods: {
    // mapMutations 使用方式也和之前有些许不同,第一个是module挂载的模块名
    // 第二个参数是 animal 模块中的 mutation 方法
    ...mapMutations('animal', ['setName'])
  },
}
</script>

14. 页面刷新后Vuex状态丢失怎么解决?

方法一: sessionStorage

将接口返回的数据保存在vuex的store里,也将这些信息也保存在sessionStorage里。注意的是vuex中的变量是响应式的,而sessionStorage不是,当你改变vuex中的状态,组件会检测到改变,而sessionStorage就不会了,页面要重新刷新才可以看到改变,所以应让vuex中的状态从sessionStorage中得到,这样组件就可以响应式的变化。

在store文件夹里面的js文件 示例如下

const state = {
  authInfo: JSON.parse(sessionStorage.getItem("COMPANY_AUTH_INFO")) || {}
}

const getters = {
  authInfo: state => state.authInfo,
}
const mutations = {
  SET_COMPANY_AUTH_INFO(state, data) {
    state.authInfo = data
    sessionStorage.setItem("COMPANY_AUTH_INFO", JSON.stringify(data))
  }
}

//actions 模块里无需使用 sessionStorage

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  //actions,
}

其实这里还可以用 localStorage,但是它没有期限;所以常用的还是sessionStorage,当浏览器关闭时会话结束。

方法二: 插件vuex-persist

import Vuex from "vuex";
// 引入插件
import VuexPersistence from "vuex-persist";

Vue.use(Vuex);
//  初始化
const state = {
	userName:'admin'
};
const mutations = {};
const actions = {};
// 创建实例
const vuexPersisted = new VuexPersistence({
	storage: window.sessionStorage,
  render:state=>({
  	userName:state.userName
    // 或
    ...state
  })
});

const store = new Vuex.Store({
	state,
  actions,
  mutations,
  // 数据持久化设置
  plugins:[vuexPersisted]
});

export default store;

15.router 和 route

router 和 route 的区别?

  1. $router是VueRouter的实例对象,是一个全局的路由对象,包含了所有路由的对象和属性。
  2. $route是一个跳转的路由对象,可以认为是当前组件的路由管理,指当前激活的路由对象,包含当前url解析得到的数据,可以从对象里获取一些数据,如:name,path,params,query等。

vue-router 的路由传参方式?

  1. 声明式导航 router-link
ruby
复制代码
<router-link :to="'/users?userId:1'"></router-link>
<router-link :to="{ name: 'users', params: { userId: 1 } }"></router-link>
<router-link :to="{ path: '/users', query: { userId: 1 } }"></router-link>

2. 编程式导航 router-push

*   通过`params`传参

<!---->

    php
    复制代码
    this.$router.push({
        name: 'users',
        params: {
            userId: 1
        }
    });
    // 路由配置
    {
        path: '/users',
        name: 'users',
        component: User
    }
    // 跳转后获取路由参数
    this.$route.params.userId // 为 1

*   通过`query`传参

<!---->

    php
    复制代码
    this.$router.push({
        path: '/users',
        query: {
            userId: 1
        } 
    });
    // 路由配置
    {
        path: '/users',
        name: 'users',
        component: User
    }
    // 跳转后获取路由参数
    this.$route.query.userId

*   动态路由

<!---->

    kotlin
    复制代码
    this.$router.push('/users/${userId}');
    // 路由配置
    {
        path: '/users/:userId',
        name: 'users',
        component: User
    }
    // 跳转后获取路由参数
    this.$route.params.userId

Vue Router中的常用路由模式和原理?

  1. hash 模式:
  • location.hash的值就是url中 # 后面的东西。它的特点在于:hash虽然出现url中,但不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
  • 可以为hash的改变添加监听事件window.addEventListener("hashchange", funcRef, false),每一次改变hash (window.location.hash),都会在浏览器的访问历史中增加一个记录,利用hash的以上特点,就可以实现前端路由更新视图但不重新请求页面的功能了。
    特点:兼容性好但是不美观
  1. history 模式:
    利用 HTML5 History Interface 中新增的pushState()replaceState()方法。
    这两个方法应用于浏览器的历史记录栈,在当前已有的backforwardgo 的基础上(使用popState()方法),他们提供了对历史记录进行修改的功能。
    为了实现前进、后退按钮的正常工作以及防止直接访问某个路由导致404错误,需要在服务器端进行适当的配置,将所有的请求都指向应用程序的入口文件,以便 Vue Router 能够接管并解析正确的路由
    特点:虽然美观,但是刷新会出现 404 需要后端进行配置。URL 易于阅读,也更利于 SEO。

hash 模式的实现原理:

Vue Router 中的 Hash 模式利用了浏览器对 #(哈希)符号的特殊处理方式来模拟前端路由。其实现原理主要包括以下几个要点:

 

URL 结构:

在 Hash 模式下,URL 包含一个哈希(#)符号,紧接着是路由信息。例如:example.com/#/home,/hom… 就是路由路径,位于哈希后面。

 

浏览器行为:

当用户点击链接或通过 JavaScript 改变 URL 的哈希部分时,浏览器并不会真正发起 HTTP 请求去服务器获取新内容,而是仅滚动到页面内与哈希值相对应的元素(通常用于页面内部锚点定位),或者在单页应用中,开发者可以通过监听 hashchange 事件来捕获这个变化。

 

Vue Router 监听:

Vue Router 在启动时会监听 window 对象上的 hashchange 事件。当哈希值发生改变时,Vue Router 解析新的哈希值,并根据它匹配已定义的路由表,决定应该激活哪个路由以及渲染对应的组件。

 

路由更新:

当匹配到新的路由时,Vue Router 更新内部的路由状态,并通知 Vue 组件进行相应视图的更新。这意味着尽管 URL 更改了,但实际上并没有整个页面刷新,而是局部视图进行了切换,实现了 SPA(Single Page Application,单页面应用程序)的无刷新跳转效果。

 

总之,在 Vue Router 的 Hash 模式中,它借助浏览器原生对哈希变化的支持来实现在客户端层面的路由管理和页面内容切换,避免了不必要的服务器通信。

 

History 模式的实现原理:

Vue Router 的 History 模式的实现原理主要基于 HTML5 的 History API,主要包括以下几个核心方法:

 

pushState():

history.pushState(state, title, url) 方法允许开发者在浏览器的历史记录栈中添加一个新的记录,同时更新当前 URL。这里的 state 参数用于存储任意类型的数据,标题 title 在现代浏览器中已废弃不用,实际操作时一般传空字符串,url 是即将改变的新路径,它会在地址栏显示,但不触发页面刷新。

 

replaceState():

类似于 pushState(), history.replaceState(state, title, url) 方法也会修改当前 URL,但它并不会新增一条历史记录,而是替换当前历史记录条目。

 

popstate 事件:

当浏览器的前进、后退按钮被点击或者调用 history.back()、history.forward()、history.go() 方法时,浏览器会触发 popstate 事件。Vue Router 监听这个事件,从而得知当前 URL 的变化并相应地切换路由和组件。

在 Vue Router 的 History 模式下,当用户点击一个路由链接或者通过编程方式调用 router.push('/new-path') 时,Vue Router 不是简单地更改地址栏的哈希值,而是使用 pushState() 或 replaceState() 更新浏览器的历史记录和当前 URL。同时,Vue Router 通过监听 popstate 事件来捕获浏览器的前进后退操作,并根据新的 URL 计算出对应的路由,进而动态加载对应的组件,实现了无刷新页面跳转和保持应用状态的同时,让 URL 地址呈现出标准的、不含哈希的路径形式。

为了使这一模式在所有环境下都能正常工作,特别是对于服务器端,还需要确保服务器能够正确地处理所有路由请求,将其重定向到应用的入口点,这样前端应用才能依据 URL 进行恰当的路由匹配和组件渲染。

动态路由?

很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。例如,我们有一个 User组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用动态路径参数(dynamic segment)来达到这个效果:{path: '/user/:id', compenent: User},其中:id就是动态路径参数。

16. 路由守卫

“导航”表示路由正在发生改变。vue-router提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

导航守卫就是路由跳转过程中的一些钩子函数。 路由跳转是一个大的过程,这个大的过程分为跳转前中后等等细小的过程,在每一个过程中都有一函数,这个函数能让你操作一些其他的事,这就是导航守卫。类似于Vue的生命周期钩子函数。

路由守卫的分类

路由守卫有多种类别,大致可以分为全局的,单个路由独享的,或者组件级的。以下是这几种类别的区别。

  • 全局守卫:是指路由实例上直接操作的钩子函数,特点是所有路由配置的组件都会触发,直白点就是触发路由就会触发这些钩子函数
  • 路由守卫:是指在单个路由配置的时候也可以设置的钩子函数
  • 组件守卫: 是指在组件内执行的钩子函数,类似于组件内的生命周期,相当于为配置路由的组件添加的生命周期钩子函数。

路由守卫中回调函数的参数

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

  • to: 即将要进入的目标
  • from: 当前导航正要离开的路由
  • next:涉及到next参数的钩子函数,必须调用next()方法来resolve这个钩子,否则路由会中断在这,不会继续往下执行。注意,这个参数是可选的。

一.全局路由守卫

守卫函数说明功能
全局前置守卫beforeEach在路由改变之前鉴权,登录状态核对,打点等相关操作
全局解析守卫beforeResolve在导航被确认之前 1. 所有的组件内守卫执行完毕 2. 异步路由组件已经解析完成beforeResolve方法可以确保路由组件及组件内回调被执行完成后再执行渲染 可以避免在渲染页面之前异步组件还未加载完成的情况 所以这个回调更多的是vue内部被回调
全局后置钩子afterEach在路由跳转完成后执行的回调 注意: 此时导航已经完成,无法通过这个回调来改变导航 所以它不是守卫函数,只是一个钩子函数比如页面滚动到指定位置、更新页面标题等
全局前置守卫
// 全局前置守卫
router.beforeEach((to, from, next) => { 
    if (to.name !== 'Login' && !isAuthenticated) {
        next({ name: 'Login' })
    }
    else next()
}
)
全局解析守卫
js
复制代码
router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... 处理错误,然后取消导航
        return false
      } else {
        // 意料之外的错误,取消导航并把错误传给全局处理器
        throw error
      }
    }
  }
})
全局后置钩子

和守卫不同的是,全局后置钩子不会接受 next 函数也不会改变导航本身:

js
复制代码
router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath)
})

二 .路由独享守卫

beforeEnter是路由独享守卫,该守卫只有在进入路由时触发,不会在 paramsqueryhash 改变时触发

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]

也可以将一个函数数组传递给 beforeEnter 以方便在不同路由中复用守卫函数

js
复制代码
// 在本例中removeQueryParams被复用了两次
const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

三.组件内守卫

组件内守卫指的是在路由组件内直接定义的导航守卫函数。 也就是说组件内守卫只能在路由组件中使用,在渲染到<router-view />的子组件中是不存在组件内守卫的。

守卫说明
beforeRouteEnter异步组件加载完毕,组件实例化之前 不可以访问组件实例 this
beforeRouteUpdate当前路由改变,但是该组件被复用时调用 如/users/1/users/2之间之间进行跳转的时候 可以访问组件实例 this
beforeRouteLeave离开该组件的时候 可以访问组件实例 this
beforeRouteEnter

beforeRouteEnter是唯一一个支持给 next 传递回调的守卫

  beforeRouteEnter(to, from) {
    // 在渲染该组件的对应路由被验证前调用
    // 不能获取组件实例 `this` !
    // 因为当守卫执行时,组件实例还没被创建!
  },
beforeRouteUpdate

在当前路由改变时,并且该组件被复用时调用,可以通过this访问实例。

  beforeRouteUpdate(to, from) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
    // 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
  },
beforeRouteLeave

离开守卫 通常用来预防用户在离开界面时没有保存修改

beforeRouteLeave (to, from) {
  const answer = window.confirm('是否确认离开')
  // 通过返回false,来取消离开操作
  if (!answer) return false
}

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

17.说一下 ref 的作用是什么?

ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:

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

所以常见的使用场景有:

  1. 基本用法,本页面获取 DOM 元素
  2. 获取子组件中的 data
  3. 调用子组件中的方法

18.接口请求一般放在哪个生命周期中?为什么要这样做?

接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间
  • SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
  • created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题。
区别
  1. created方法是在初始化页面之前对dom的操作。

  2. mounted方法是在初始化页面之后对dom的操作。

  3. 那么为什么要有这个区分哪? 有些需求就是要在页面加载之后才能请求到的,比如id,js中用document.getElementById("")(echart)

19.vue修饰符

  • 事件修饰符
  • 按键修饰符
  • 表单修饰符

事件修饰符

在事件处理程序中调用 event.preventDefaultevent.stopPropagation 方法是非常常见的需求。尽管可以在 methods 中轻松实现这点,但更好的方式是:methods 只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,vuev-on 提供了事件修饰符。通过由点 . 表示的指令后缀来调用修饰符。

常见的事件修饰符如下:

  • .stop:阻止冒泡。
  • .prevent:阻止默认事件。
  • .capture:使用事件捕获模式。
  • .self:只在当前元素本身触发。
  • .once:只触发一次。
  • .passive:默认行为将会立即触发。

按键修饰符

除了事件修饰符以外,在 vue 中还提供了有鼠标修饰符,键值修饰符,系统修饰符等功能。

  • .left:左键
  • .right:右键
  • .middle:滚轮
  • .enter:回车
  • .tab:制表键
  • .delete:捕获 “删除” 和 “退格” 键
  • .esc:返回
  • .space:空格
  • .up:上
  • .down:下
  • .left:左
  • .right:右
  • .ctrlctrl
  • .altalt
  • .shiftshift
  • .metameta

表单修饰符

vue 同样也为表单控件也提供了修饰符,常见的有 .lazy.number.trim

  • .lazy:在文本框失去焦点时才会渲染
  • .number:将文本框中所输入的内容转换为number类型
  • .trim:可以自动过滤输入首尾的空格

20.Vue SSR 的理解

SSR服务端渲染(Server Side Render),就是将 Vue 在客户端把标签渲染成 html 的工作放在服务端完成,然后再把 html 直接返回给客户端。

  • 优点:
    有着更好的 SEO,并且首屏加载速度更快。

  • 缺点:
    开发条件会受限制,服务器端渲染只支持 beforeCreate 和 created 两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于 Node.js 的运行环境。服务器会有更大的负载需求。