跳到主要内容

2.1-Vue面试题

Vue三大功能

  • 响应式:监听到数据变化
  • 模板引擎:解析模板
  • 渲染原理:如何将监听到的数据变化和解析后的 HTML 渲染

响应式:vue2 使用的是 Object.defineProperty,vue3 使用的是 Proxy

模板引擎:使用正则进行解析,解析成 JS 代码,然后使用

渲染原理:虚拟 DOM

简单

组件通讯方式有哪些方法

  • props$emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的
  • $parent$children 获取当前组件的父组件和当前组件的子组件
  • $attrs$listeners A->B->C。Vue 2.4 开始提供了 $attrs$listeners 来解决这个问题
  • 父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。
  • $refsref 获取组件实例,变量在组合式 API 中需要 defineExpose 或者显示导出
  • envetBus 兄弟组件数据传递,这种情况下可以使用事件总线的方式
  • vuex 状态管理

nextTick 原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

相关代码如下

let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听 dom 变化,也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}

export function nextTick(cb) {
// 除了渲染 watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用 nextTick 只会执行一次异步 等异步队列清空之后再把标志变为 false
pending = true;
timerFunc();
}
}

vue3.0 了解多少

  • 响应式原理的改变 Vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
  • 组件声明方式 Vue3.x 新增了 Composition API(组合式 API),Vue2 是选项式 API。
  • 模板语法变化 slot 具名插槽语法,自定义指令 v-model 升级
  • 其它方面的更改 Suspense 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。基于 treeshaking 优化,提供了更多的内置功能。

Vue diff 原理

  • 先对比标签(标签是否相同)
  • 元素标签不同直接替换
  • 元素标签相同
    • 标签一致后查看属性是否一致(不一致的进行替换
    • 对比子元素
      • 都有子元素,进行指针方式对比是否为同一个节点,是则复用
      • 新有子元素,旧无子元素,将子元素虚拟节点转换为真实节点后进行插入
      • 新的无子元素,旧有子元素,直接清空 innerHtml

MVC 和 MVVM 区别

MVC

MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范

  • Model(模型):是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据
  • View(视图):是应用程序中处理数据显示的部分。通常视图是依据模型数据创建的
  • Controller(控制器):是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据
img

MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。

MVVM

MVVM 新增了 VM 类

  • ViewModel 层:做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。

imgmvvm.png

MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想)

整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性

注意:Vue 并没有完全遵循 MVVM 的思想 这一点官网自己也有说明:

引自官网:https://v2.cn.vuejs.org/v2/guide/instance.html

严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。

Vue3 官网已经找不到关于 MVVM 模型的描述

data 为什么是函数

组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,会造成一个组件的实例值改变,其它组件实例全都会变的结果。

生命周期方法有哪些一般在哪一步发请求

  • beforeCreate:在实例初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
  • created:实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。此时 DOM 还没有渲染,如果非要想与进行交互,可以通过 nextTick 来访问 DOM
  • beforeMount:在挂载开始之前被调用:render 函数首次被调用。
  • mounted:在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点。
  • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated:发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

还有两个生命周期的方法只在 keep-alive 的组件中触发,缓存树中的后代组件也同样适用。

  • activated:在首次挂载,以及组件从不活跃再次到活跃状态时调用
  • deactivated:在从 DOM 上移除、进入缓存组件以及组件卸载时调用

异步请求通常在哪个生命周期发起?

可以在钩子函数 createdbeforeMountmounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 DOM 推荐在 created 钩子函数中调用异步请求,原因如下:

  • 在 created 钩子函数中调用异步请求能更快获取到服务端数据,减少页面 loading 时间
  • 只有 beforeCreatecreated 这两个钩子会在 SSR 期间被调用,SSR 不支持 beforeMountmounted 钩子函数。

v-if 和 v-show 的区别

v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。

v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏(display:none

使用场景

v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景

v-show 适用于需要非常频繁切换条件的场景

扩展补充:display:nonevisibility:hiddenopacity:0 之间的区别?

  • display:none:DOM 隐藏
  • visibility:hidden:不可见,且无法触发事件
  • opacity:0:只是不可见,还可以触发 DOM 的事件,比如 input

内置指令都有那些

  • v-text:更新元素的文本内容。
  • v-html:更新元素的 innerHTML
  • v-show:基于表达式值的真假,改变元素的可见性。
  • v-ifv-else-ifv-else:基于表达式值的真假,条件性地渲染元素或者模板片段。
  • v-for :基于原始数据多次渲染元素或模板块。
  • v-on:给元素绑定事件监听器。
  • v-bind:动态的绑定一个或多个 attribute,也可以是组件的 prop。
  • v-model:在表单输入元素或组件上创建双向绑定。
  • v-slot:用于声明具名插槽或是期望接收 props 的作用域插槽。
  • v-pre:跳过该元素及其所有子元素的编译。
  • v-once:该组件及所有节点只渲染一次,首次渲染后,不会随着数据的变化重新渲染,而是被视为静态内容。
  • v-memo:缓存一个模板的子树。传入一个数组,数组中的每个值都与最后一次的渲染相同,整个子树将跳过更新。
  • v-cloak:用于隐藏尚未完成编译的 DOM 模板(仅在不需要构建的环境下使用

v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。

自定义指令

如何写一个自定义指令

指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。

自定义指令生命周期和组件相同,分别是 created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted

app.directive('focus', {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
})
  • el:指令绑定到的元素。这可以用于直接操作 DOM。
  • binding:一个对象,包含以下属性。
    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdateupdated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。
  • prevNode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdateupdated 钩子中可用。

当应用到一个多根组件时,指令将会被忽略且抛出一个警告。

原理

  • 在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
  • 通过 genDirectives 生成指令代码
  • 在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
  • 当执行指令对应钩子函数时,调用对应指令定义的方法

怎样理解 Vue 的单向数据流

数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法,开发环境会报警告

如果实在要改变父组件的 prop 值 可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用 $emit 通知父组件去修改

computed 和 watch 的区别和运用的场景

computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容,它可以设置 getter 和 setter。

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

计算属性一般用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑

v-if 与 v-for 为什么不建议一起使用

2.x 版本中在一个元素上同时使用 v-ifv-for 时,v-for 会优先作用。

3.x 版本中 v-if 总是优先于 v-for 生效。

v-for 和 v-if 不要在同一个标签中使用,因为容易导致逻辑混乱。

Vue 事件绑定原理

原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的 $on 实现的。如果要在组件上使用原生事件,需要加 .native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。

emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器

手写发布订阅原理 传送门

Scoped 样式穿透是什么

scoped 是 style 标签的一个属性,当在 style 标签中定义了 scoped 时,style 标签中的所有属性就只作用于当前组件的样式,实现组件样式私有化,从而也就不会造成样式全局污染。

项目开发中,多数情况下不能避免引用第三方组件,而第三方组件的样式又不全是我们想要的,就需要在组件中局部修改第三方组件的样式,但同时又不想去除 scoped 属性和避免样式污染。此时只能通过穿透 scoped,写法如下。

<style scoped>
外层 > 第三方组件 {
样式
}
</style>

中等

为什么要使用 proxy

Vue3.0 和 2.0 的响应式原理区别

在 Vue2 中, Object.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶

  • 不需用使用 Vue.$setVue.$delete 触发响应式。
  • 全方位的数组变化检测,消除了 Vue2 无效的边界情况。
  • 支持 Map,Set,WeakMap 和 WeakSet。

Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶

  • get 收集依赖
  • Set、delete 等触发依赖
  • 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑。

Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。

相关代码如下

import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
import { isObject } from "./util"; // 工具方法

export function reactive(target) {
// 根据不同参数创建不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {
if (!isObject(target)) {
return target;
}
const observed = new Proxy(target, baseHandler);
return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {
return function get(target, key, receiver) {
// 对获取的值进行放射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回当前对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {
console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {
console.log("属性值被修改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此方法
set, // 当修改属性时调用此方法
};

相比 Vue 2,Vue 3 有哪些性能方面的提升。

对于这个问题,我们可以从编译阶段、源码体积和响应式系统三个方面进行回答。

在编译阶段,Vue 3 做了如下一些优化:

  • diff 算法优化
  • 静态提升
  • 事件监听缓存
  • SSR 优化

diff 算法优化

vue3 在 diff 算法中相比 vue2 增加了静态标记。关于这个静态标记,其作用是为了会发生变化的地方添加一个 flag 标记,下次发生变化的时候直接找该地方进行比较,性能得到了进一步的提高。

静态提升

Vue3 中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用。

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化。开启了缓存后,没有了静态标记,也就是说下次 diff 算法的时候直接使用。

*SSR优化*

当静态内容大到一定量级时候,会用 createStaticVNode 方法在客户端去生成一个 static node,这些静态 node,会被直接 innerHtml,就不需要创建对象,然后根据对象渲染。

详细内容参考:https://vue3js.cn/interview/vue3/goal.html#%E4%B8%80%E3%80%81%E8%AE%BE%E8%AE%A1%E7%9B%AE%E6%A0%87

谈谈你对 slot 的理解,以及 slot 的使用场景

在 HTML 中 slot 元素 ,作为 Web Components 技术套件的一部分,是 Web 组件内的一个占位符。该占位符可以在后期使用自己的标记语言填充:

<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>

template 不会展示到页面中,需要用先获取它的引用,然后才会添加到 DOM 中。

customElements.define('element-details',
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})

slot:插槽,我们可以理解为 slot 在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填入(替换组件模板中 slot 位置)。

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理。如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情。通过 slot 插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

Vue 的父子组件生命周期钩子函数执行顺序

  • 加载渲染过程

父 beforeCreate -> 父 created -> 父 beforeMount-> 子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted

  • 子组件更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

  • 父组件更新过程

父 beforeUpdate ->父 updated

  • 销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

大量数据渲染时如何优化

我们可以从以下几个方面进行考虑:

  • 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树。
  • 处理时要根据情况做不同处理。
  • 可以采取分页的方式获取,避免渲染大量数据。
  • vue-virtual-scroller(opens new window)等虚拟滚动方案,只渲染视口范围内的数据。
  • 如果不需要更新,可以使用 v-once 方式只渲染一次。
  • 通过 v-memo(opens new window)可以缓存结果,结合 v-for 使用,避免数据变化时不必要的 VNode 创建。
  • 可以采用懒加载方式,在用户需要的时候再加载数据,比如 tree 组件子树的懒加载。

还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以 v-once 处理,需要更新可以 v-memo 进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案。

v-for 为什么要加 key

如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

更准确:因为带 key 就不是直接复用了,在 sameNode 函数 a.key === b.key 对比中可以避免直接复用的情况。所以会更加准确。

更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

相关代码如下

// 判断两个 vnode 的标签和 key 是否相同 如果相同就认为是同一节点进行      复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}

// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);

Vue是如何解决跨域问题的

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

所谓同源(即指在同一个域)需要:协议相同(protocol)、主机相同(host)、端口相同(port)

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

Vue 解决跨域的方法有很多,常用有三种:JSONP、CORS、Proxy

而在 Vue 项目中,我们主要针对 CORS 或 Proxy 这两种方案进行展开:

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的 HTTP 头组成,这些 HTTP 头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应 CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源,只要后端实现了 CORS,就实现了跨域。

app.use(async (ctx, next)=> {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
})

Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击。

方案一

如果是通过 vue-cli 脚手架工具搭建项目,我们可以通过 webpack 为我们起一个本地服务器作为请求的代理对象。通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果 web 应用和接口服务器不在一起仍会跨域。

在 vue.config.js 文件,新增以下代码:

module.exports = {
devServer: {
host: '127.0.0.1',
port: 8084,
open: true,// vue项目启动时自动打开浏览器
proxy: {
'/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
'^/api': ""
}
}
}
}
}

通过 axios 发送请求中,配置请求的根路径。

axios.defaults.baseURL = '/api'

方案二

通过配置 nginx 实现代理:

server {
listen 80;
# server_name www.josephxia.com;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

谈谈你对 Vue、Angular 以及 React 的理解

首先,我们来看一下 Vue 与 AngularJS 的区别

  • Angular 采用 TypeScript 开发, 而 Vue 可以使用 javascript 也可以使用 TypeScript
  • AngularJS 依赖对数据做脏检查,所以 Watcher 越多越慢;Vue.js 使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
  • AngularJS 社区完善,Vue 内置了很多默认的模版和语法,学习成本较小。

接下来,我们看一下 Vue 与 React 的区别

相似点:

  • Virtual DOM。其中最大的一个相似之处就是都使用了 Virtual DOM,也就是能让我们通过操作数据的方式来改变真实的 DOM 状态。因为其实 Virtual DOM 的本质就是一个 JS 对象,它保存了对真实 DOM 的所有描述,是真实 DOM 的一个映射,所以当我们在进行频繁更新元素的时候,改变这个 JS 对象的开销远比直接改变真实 DOM 要小得多。
  • 组件化开发。它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
  • Props。Vue 和 React 中都有 props 的概念,允许父组件向子组件传递数据。
  • 构建工具、Chrome 插件、配套框架。还有就是它们的构建工具以及 Chrome 插件、配套框架都很完善。

不同点:

  • 模版编写。最大的不同就是模版的编写,Vue 鼓励你去写近似常规 HTML 的模板,React 推荐你使用 JSX 去书写。
  • 状态管理与对象属性。在 React 中,应用的状态是比较关键的概念,也就是 state 对象,它允许你使用 setState 去更新状态。但是在 Vue 中,state 对象并不是必须的,数据是由 data 属性在 Vue 对象中进行管理。
  • 虚拟 DOM 的处理方式不同。Vue 中的虚拟 DOM 控制了颗粒度,组件层面走 watcher 通知,而组件内部走 vdom 做 diff,这样,既不会有太多 watcher,也不会让 vdom 的规模过大。而 React 走了类似于 CPU 调度的逻辑,把 vdom 这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做 diff。

Vuex 页面刷新数据丢失怎么解决

需要做 vuex 数据持久化 一般使用本地存储的方案来保存数据 可以自己设计存储方案 也可以使用第三方插件

推荐使用 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中

vue-router 中如何保护路由

const router = createRouter({ ... })
// 路由守卫
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
// 路由独享守卫
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]

以下是触发钩子函数的完整顺序:路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入 b 组件。

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等。
  • beforeEnter:路由独享守卫
  • beforeRouteEnter:路由组件的组件进入路由前钩子。
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate:组件生命周期,不能访问 this。
  • created:组件生命周期,可以访问 this,不能访问 DOM。
  • beforeMount:组件生命周期
  • deactivated:离开缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
  • mounted:访问/操作 dom。
  • activated:进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
  • 执行 beforeRouteEnter 回调函数 next。

为什么要使用函数式组件,有什么优势

函数组件的特点:

  • 函数式组件需要在声明组件是指定 functional:true
  • 不需要实例化,所以没有 this,this 通过 render 函数的第二个参数 context 来代替。
  • 没有生命周期钩子函数,不能使用计算属性,watch。
  • 不能通过 $emit 对外暴露事件,调用事件只能通过 context.listeners.click 的方式调用外部传入的事件。
  • 因为函数式组件是没有实例化的,所以在外部通过 ref 去引用组件时,实际引用的是 HTMLElement。
  • 函数式组件的 props 可以不用显示声明,所以没有在 props 里面声明的属性都会被自动隐式。解析为 prop,而普通组件所有未声明的属性都解析到 $attrs 里面,并自动挂载到组件根元素上面(可以通过 inheritAttrs 属性禁止)。

相比普通的类组件,函数组件有如下的一些优势:

  • 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件;
  • 函数式组件结构比较简单,代码结构更清晰;
Vue.component('functional',{ // 构造函数产生虚拟节点的
functional:true, // 函数式组件 // data={attrs:{}}
render(h){
return h('div','test')
}
})
const vm = new Vue({
el: '#app'
})

Vue 的 Tree shaking

Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。

在 Vue2 中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。而 Vue3 源码引入 tree shaking 特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中。

Tree shaking 是基于 ES6 模板语法( import 与 exports ),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。Tree shaking 无非就是做了两件事:

  • 编译阶段利用 ES6 Module 判断哪些模块已经加载;
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码;

讲一讲 Vue 的 SSR

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

优点:

SSR 有着更好的 SEO、并且首屏加载速度更快

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

要求服务端性能需求更大,更大的负载

vue 中使用了哪些设计模式

  • 工厂模式 - 传入参数即可创建实例。虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
  • 单例模式 - 整个程序有且仅有一个实例。vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
  • 发布-订阅模式:(vue 事件机制)
  • 观察者模式:(响应式数据原理)
  • 装饰模式:(@装饰器的用法)
  • 策略模式:策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略

你都做过哪些 Vue 的性能优化

这里只列举针对 Vue 的性能优化 整个项目的性能优化是一个大工程

  • 对象层级不要过深,否则性能就会差
  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
  • v-if 和 v-show 区分使用场景
  • computed 和 watch 区分使用场景
  • v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
  • 大数据列表和表格性能优化-虚拟列表/虚拟表格
  • 防止内部泄漏,组件销毁后把全局变量和事件销毁
  • 图片懒加载
  • 路由懒加载
  • 第三方插件的按需引入
  • 适当采用 keep-alive 缓存组件
  • 防抖、节流运用
  • 服务端渲染 SSR or 预渲染

Class 与 Style 如何实现动态绑定

Class 可以通过对象语法和数组语法进行动态绑定,比如:

//对象语法
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: {
isActive: true,
hasError: false
}
//数组语法
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}

Style 也可以通过对象语法和数组语法进行动态绑定,比如:

//对象语法
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
//数组语法
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}

vue3 中也可以对 style 标签中的内容进行绑定,实际的值会被编译成哈希化的 CSS 自定义属性,因此 CSS 本身仍然是静态的。自定义属性会通过内联样式的方式应用到组件的根元素上,并且在源值变更的时候响应式地更新。

<script setup>
import { ref } from 'vue'
const theme = ref({
color: 'red',
})
</script>

<template>
<p>hello</p>
</template>

<style scoped>
p {
color: v-bind('theme.color');
}
</style>

什么是函数式组件

函数组件的特点:

  • 在渲染过程中不会创建组件实例 (也就是说,没有 this)
  • 也不会触发常规的组件生命周期钩子,不能使用计算属性,watch。
  • 大多数常规组件的配置选项在函数式组件中都不可用,除了 propsemits
  • 因为函数式组件是没有实例化的,所以在外部通过 ref 去引用组件时,实际引用的是 HTMLElement。
  • 函数式组件的 props 可以不用显示声明,所以没有在 props 里面声明的属性都会被自动隐式。解析为 prop,而普通组件所有未声明的属性都解析到 $attrs 里面,并自动挂载到组件根元素上面(可以通过 inheritAttrs 属性禁止)。

相比普通的类组件,函数组件有如下的一些优势:

  • 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件;
  • 函数式组件结构比较简单,代码结构更清晰;

如果你将一个函数作为第一个参数传入 h,它将会被当作一个函数式组件来对待。

function MyComponent(props, { slots, emit, attrs }) {
// ...
}
MyComponent.props = ['value']
MyComponent.emits = ['click']

困难

Vue.mixin 的使用场景和原理

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue 的 mixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

相关代码如下

export default function initMixin(Vue){
Vue.mixin = function (mixin) {
// 合并对象
this.options=mergeOptions(this.options,mixin)
};
}
};

// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
];

// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
const options = {};
// 遍历父亲
for (let k in parent) {
mergeFiled(k);
}
// 父亲没有 儿子有
for (let k in child) {
if (!parent.hasOwnProperty(k)) {
mergeFiled(k);
}
}

//真正合并字段方法
function mergeFiled(k) {
if (strats[k]) {
options[k] = strats[k](parent[k], child[k]);
} else {
// 默认策略
options[k] = child[k] ? child[k] : parent[k];
}
}
return options;
}

Vue.mixin 原理详解 传送门

nextTick 使用场景和原理

nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

相关代码如下

let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}

export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}

keep-alive 使用场景和原理

keep-alive 是 Vue 内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。

  • 常用的两个属性 include/exclude,允许组件有条件的进行缓存。
  • 两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
  • keep-alive 的中还运用了 LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰。

相关代码如下

export default {
name: "keep-alive",
abstract: true, //抽象组件

props: {
include: patternTypes, //要缓存的组件
exclude: patternTypes, //要排除的组件
max: [String, Number], //最大缓存数
},

created() {
this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode}
this.keys = []; //缓存组件的key集合 [a,b]
},

destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},

mounted() {
//动态监听include exclude
this.$watch("include", (val) => {
pruneCache(this, (name) => matches(val, name));
});
this.$watch("exclude", (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},

render() {
const slot = this.$slots.default; //获取包裹的插槽默认值
const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
// 不走缓存
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除里面
(exclude && name && matches(exclude, name))
) {
//返回虚拟节点
return vnode;
}

const { cache, keys } = this;
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
if (cache[key]) {
//通过key 找到缓存 获取实例
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key); //通过LRU算法把数组里面的key删掉
keys.push(key); //把它放在数组末尾
} else {
cache[key] = vnode; //没找到就换存下来
keys.push(key); //把它放在数组末尾
// prune oldest entry //如果超过最大值就把数组第0项删掉
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}

vnode.data.keepAlive = true; //标记虚拟节点已经被缓存
}
// 返回虚拟节点
return vnode || (slot && slot[0]);
},
};

扩展补充:LRU 算法是什么?

imglrusuanfa.png

LRU 的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key 重新插入到 this.keys 的尾部,这样一来,this.keys 中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys 中第一个缓存的组件。

Vue.set 方法原理

了解 Vue 响应式原理的同学都知道在两种情况下修改数据 Vue 是不会触发视图更新的

1.在实例创建之后添加新的属性到实例上(给响应式对象新增属性)

2.直接更改数组下标来修改数组的值

Vue.set 或者说是$set 原理如下

因为响应式数据 我们给对象和数组本身都增加了__ob__属性,代表的是 Observer 实例。当给对象新增不存在的属性 首先会把新的属性进行响应式跟踪 然后会触发对象__ob__的 dep 收集到的 watcher 去更新,当修改数组索引时我们调用数组本身的 splice 方法去更新数组

相关代码如下

export function set(target: Array | Object, key: any, val: any): any {
// 如果是数组 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;

// 如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val;
return val;
}
// 将属性定义成响应式的
defineReactive(ob.value, key, val);
// 通知视图更新
ob.dep.notify();
return val;
}

响应式数据原理详解 传送门

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

相关代码如下

export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}

Vue 组件原理详解 传送门

Vue 修饰符有哪些

事件修饰符

  • .stop 阻止事件继续传播
  • .prevent 阻止标签默认行为
  • .capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
  • .self 只当在 event.target 是当前元素自身时触发处理函数
  • .once 事件将只会触发一次
  • .passive 告诉浏览器你不想阻止事件的默认行为

v-model 的修饰符

  • .lazy 通过这个修饰符,转变为在 change 事件再同步
  • .number 自动将用户的输入值转化为数值类型
  • .trim 自动过滤用户输入的首尾空格

键盘事件的修饰符

  • .enter
  • .tab
  • .delete (捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

系统修饰键

  • .ctrl
  • .alt
  • .shift
  • .meta

鼠标按钮修饰符

  • .left
  • .right
  • .middle

Vue 模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步

第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

相关代码如下

export function compileToFunctions(template) {
// 我们需要把html字符串变成render函数
// 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
// 很多库都运用到了ast 比如 webpack babel eslint等等
let ast = parse(template);
// 2.优化静态节点
// 这个有兴趣的可以去看源码 不影响核心功能就不实现了
// if (options.optimize !== false) {
// optimize(ast, options);
// }

// 3.通过ast 重新生成代码
// 我们最后生成的代码需要和render函数一样
// 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
// _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
let code = generate(ast);
// 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值
let renderFn = new Function(`with(this){return ${code}}`);
return renderFn;
}

模板编译原理详解 传送门

生命周期钩子是如何实现的

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

相关代码如下

export function callHook(vm, hook) {
// 依次执行生命周期对应的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}

// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); //初始化数据之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};

生命周期实现详解 传送门

函数式组件使用场景和原理

函数式组件与普通组件的区别

1.函数式组件需要在声明组件是指定 functional:true
2.不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
3.没有生命周期钩子函数,不能使用计算属性,watch
4.不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
5.因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
6.函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

优点 1.由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件 2.函数式组件结构比较简单,代码结构更清晰

使用场景:

一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件

“高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件

相关代码如下

if (isTrue(Ctor.options.functional)) {
// 带有functional的属性的就是函数式组件
return createFunctionalComponent(Ctor, propsData, data, context, children);
}
const listeners = data.on;
data.on = data.nativeOn;
installComponentHooks(data); // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)

能说下 vue-router 中常用的路由模式实现原理吗

hash 模式

  1. location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
  2. 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false); 

每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

特点:兼容性好但是不美观

history 模式

利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

特点:虽然美观,但是刷新会出现 404 需要后端进行配置

手写 Vue.extend

//  src/global-api/initExtend.js
import { mergeOptions } from "../util/index";
export default function initExtend(Vue) {
let cid = 0; //组件的唯一标识
// 创建子类继承Vue父类 便于属性扩展
Vue.extend = function (extendOptions) {
// 创建子类的构造函数 并且调用初始化方法
const Sub = function VueComponent(options) {
this._init(options); //调用Vue初始化方法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor指向自己
Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options
return Sub;
};
}

具体可以看看这篇 手写 Vue2.0 源码(八)-组件原理

vue-router 中路由方法 pushState 和 replaceState 能否触发 popSate 事件

答案是:不能

pushState 和 replaceState

HTML5 新接口,可以改变网址(存在跨域限制)而不刷新页面,这个强大的特性后来用到了单页面应用如:vue-router,react-router-dom 中。

注意:仅改变网址,网页不会真的跳转,也不会获取到新的内容,本质上网页还停留在原页面

window.history.pushState(state, title, targetURL);
@状态对象:传给目标路由的信息,可为空
@页面标题:目前所有浏览器都不支持,填空字符串即可
@可选url:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data

window.history.replaceState(state, title, targetURL);
@类似于pushState,但是会直接替换掉当前url,而不会在history中留下记录
复制代码

popstate 事件会在点击后退、前进按钮(或调用 history.back()、history.forward()、history.go()方法)时触发

注意:用 history.pushState()或者 history.replaceState()不会触发 popstate 事件

相比 Vue2.x,Vue 3 有哪些性能方面的提升。

对于这个问题,我们可以从编译阶段、源码体积和响应式系统三个方面进行回答。

在编译阶段,Vue 3 做了如下一些优化:

  • diff 算法优化
  • 静态提升
  • 事件监听缓存
  • SSR 优化

*diff 算法优化*

vue3 在 diff 算法中相比 vue2 增加了静态标记。关于这个静态标记,其作用是为了会发生变化的地方添加一个 flag 标记,下次发生变化的时候直接找该地方进行比较,性能得到了进一步的提高。

图片

静态提升

Vue3 中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用。

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化。开启了缓存后,没有了静态标记,也就是说下次 diff 算法的时候直接使用。

*SSR优化*

当静态内容大到一定量级时候,会用 createStaticVNode 方法在客户端去生成一个 static node,这些静态 node,会被直接 innerHtml,就不需要创建对象,然后根据对象渲染。

Vue-router 路由钩子在生命周期的体现

有的时候,需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录。为此有很多种方法可以植入路由的导航过程:全局的,单个路由独享的,或者组件级的。

1,全局路由勾子

vue-router 全局有三个路由钩子:

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

具体来说,为了实现登录拦截,我们可以这么做:

  1. beforeEach,判断是否登录了,没登录就跳转到登录页;
  2. afterEach,跳转之后滚动条回到顶部;

2,单个路由独享钩子

beforeEnter 如果不想全局配置守卫的话,可以为某些路由单独配置守卫,有三个参数∶ to、from、next,代码如下。

export default [    
{
path: '/',
name: 'login',
component: login,
beforeEnter: (to, from, next) => {
console.log('即将进入登录页面')
next()
}
}
]

3, 组件内钩子

组件内钩子主要有三个:beforeRouteUpdate、beforeRouteEnter、beforeRouteLeave。这三个钩子都有三个参数∶to、from、next。

  • beforeRouteEnter∶ 进入组件前触发
  • beforeRouteUpdate∶ 当前地址改变并且改组件被复用时触发,举例来说,带有动态参数的路径 foo/∶id,在 /foo/1 和 /foo/2 之间跳转的时候,由于会渲染同样的 foa 组件,这个钩子在这种情况下就会被调用
  • beforeRouteLeave∶ 离开组件被调用

注意点,beforeRouteEnter 组件内还访问不到 this,因为该守卫执行前组件实例还没有被创建,需要传一个回调给 next 来访问,例如:

beforeRouteEnter(to, from, next) {      
next(target => {
if (from.path == '/classProcess') {
target.isFromProcess = true
}
})
}

以下是触发钩子函数的完整顺序:路由导航、keep-alive、和组件生命周期钩子结合起来的,触发顺序,假设是从a组件离开,第一次进入 b 组件。

  • beforeRouteLeave:路由组件的组件离开路由前钩子,可取消路由离开。
  • beforeEach:路由全局前置守卫,可用于登录验证、全局路由 loading 等。
  • beforeEnter:路由独享守卫
  • beforeRouteEnter:路由组件的组件进入路由前钩子。
  • beforeResolve:路由全局解析守卫
  • afterEach:路由全局后置钩子
  • beforeCreate:组件生命周期,不能访问 tAis。
  • created;组件生命周期,可以访问 tAis,不能访问 dom。
  • beforeMount:组件生命周期
  • deactivated:离开缓存组件 a,或者触发 a 的 beforeDestroy 和 destroyed 组件销毁钩子。
  • mounted:访问/操作 dom。
  • activated:进入缓存组件,进入 a 的嵌套子组件(如果有的话)。
  • 执行 beforeRouteEnter 回调函数 next。

参考文章

作者链接
Big shark@LXhttps://juejin.cn/post/6961222829979697165