1、Vue基本原理
Vue 使用 数据劫持 + 订阅、发布的方式。
使用 Object.defineProperty 劫持数据的 setter、getter,
数据改变时,发布消息给订阅者,触发相应的回调,完成视图的跟新。
具体实现如下:
- Observer:将需要Observe 的数据进行遍历,包括子属性对象的数据,通过 Object.defineProperty() 劫持数据的 setter、getter。
- Compile:解析模版指令,将模版的变量替换成数据,然后初始化页面渲染视图,且在指令节点中绑定更新函数,添加监听数据的订阅者,当数据变化时,会触发图的跟新。
- Watcher:订阅者,主要做了如下两件事
- 自身实列化时将自身添加到 属性订阅器中,
- 数据变更,调用本身的 update()方法,并触发 compile的回调
- 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 数据的改变,控制视图的跟新,处理用户交互操作
View 和 Model 之间没有直接关联,
通过 viewModel 进行交互,viewModel 与 View 是双向绑定的。
所以 Model 数据变化可以通过 viewModel 触发 View 达到视图跟新
View 由于用户操作改变 也会 通过 ViewModel 给 Model 返回进行数据同步
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
MVC:
M - Model: 数据层,数据的相应操作及业务逻辑处理
V - View:视图层,数据的展示,UI界面的展示
C - Controller:两者的纽带,主要负责用户与应用的响应操作,
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 的解藕。
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 方法的定义有两种写法:
- 组件内局部定义 filters:{ 过滤器名: fn }
- 全局方法定义 Vue.filter('过滤器名',fn)
- filter 的调用,有两种方式:
- 使用 双括号
{{变量 | 过滤器名称(参数)}}的形式;- 在 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框架中自己的异步回调队列。
作用
- 如果是同步更新,则对此对一个或多个属性赋值,会频繁触发 UI/DOM 渲染,可以减少一些无用的渲染
- 同时由于 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优点:
- 页面切换快
页面每次切换跳转时,都不需要做 html 文件请求,这样节约了很多 http 发送延迟,在切换页面时速度会变快
- 用户体验好
页面片段间的切换快,包括移动设备。尤其在网络环境差的时候,因为组件已经预先加载好了,并不需要发送网络请求,所以用户体验好。
SPA缺点:
- 首屏加载慢
首屏需要请求一次 html,同时还需要发送一次 js 请求,两次请求回来了,首屏才会展示出来。向对于多页面应用首屏时间会慢很多。
- 不易于 SEO
SEO 效果差,因为 搜索引擎只识别 html 里的内容,不认识 js 内容,而单页面应用的内容都是靠 js 渲染生成出来的,搜索引擎不 识别这部分内容,也就不会给一个好的排名,会导致 SPA 应用做出来的网页排名相对较低。
什么是MPA:
MAP 多页面应用,指有多个独立页面的应用,每个页面必须重复加载 js、html、css 等相关资源。多页面应用跳转,需要整页资源刷新。
与 SPA 对比最大的不同即是页面路由切换由原生浏览器文档跳转控制,页面跳转是返回 HTML 的。
MPA 优点:
- 首屏加载速度快
访问页面时,服务器返回一个 html,页面就会展示出来,这个过程经历了一个 http 请求,所以页面展示速度非常快。
- SEO 效果好
搜索引擎在做网页排名的时候,需根据网页内容才能给网页权重,来进行网页的排名。搜索引擎识别 html 内容,每个页面的所有内容都放 在 html 中,有利于搜索引擎的排名。
MPA 缺点: 页面切换慢,影响用户体验
页面的每次跳转都需要发送一次 HTTP 请求,如果网络状态不好,页面来回切换的时候,会发生明显的卡顿,用户体验会变差。
24、对SSR理解
SSR
将页面渲染成完整的 HTML 工作在服务端完成后,再将完整的 HTML 返回给客户端显示。
使用SSR目的: SPA单页面的局限性:首次加载需要加载所以页面的JS库,导致首次加载页面缓慢,以及搜索引擎的劣势。
SSR 优势:
1、首屏加载更快
- SPA 需要等所有的JS文件下载后,才开始页面的渲染。
- SSR 是通过服务端将渲染好的页面直接返回到浏览器展示,不需等待 js 的下载,所以首屏加载会更快。
2、更好的SEO
- SPA 页面是靠js 渲染生成的,不被搜索引擎识别。
- SSR 是服务端直接返回给浏览器处理好的 HTML 文件,SEO 可以进行爬取。
17、Vue template 到 render 的过程(Vue 模板编译原理)
Vue 中的模板 template 无法被浏览器解析并渲染,因为它不属于浏览器标准,不是正确的 HTML语法。
Vue 模板编译就是 将 template 转换为 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的HTML元素。
- Vue模板编译过程主要如下:template 解析、优化静态节点、生成 render 函数
- Vue 在模板编译中,会执行 compileToFunction 将template 转化为render函数;
compileToFunctions 主要逻辑如下:
- 调用 parse 方法将 template 转换成 AST 抽象语法树。
- 其中 AST 表示 用 JavaScriipt 对象的形式描述整个模板
- 解析过程 - 利用正则表达式顺序将模板进行解析,遇到 开始标签、结束标签、文本标签,会执行对应的回调函数,以达到生成 AST 抽象语法树的目的。
- 再优化静态节点
- 对静态节点的优化,需要知道哪些是静态节点,对其做标签;以便于在再次渲染 进行 diff 比较时,会直接跳过这些静态节点,达到优化性能的目的。
- 深度遍历 AST,以查找出 AST 节点或根节点的静态节点/静态节点根,它们生成的 dom 永远不会改变,对运行时模板的更新有很大的优化作用。
- 生成代码
- 调用 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 在同一组件内调用,且存在钩子内部方法、参数重名,则后面覆盖前面
其缺点就是:
- 隐式引入,不利于阅读,代码也不利于维护:多个 minxins 的引入,其变量、方法会容易扰乱不知是调用的哪个。
- 多个 minxins 生命周期会融合在一块,重复的变量、方法会照成覆盖、冲突。
- minxins 可能会出现多对多关系,即一个组件引入多个 minxins,一个 minxin 被多个组件引入多现象。
使用方法:
/* minxins/common.js */
export default {
data(){
return {}
},
methods:{},
computed:{},
filters:{},
created(){},
mounted(){
console.log("我是mixins");
}
}
Vue3 保留了对 minxins 的支持,但是更推荐使用 hooks 的方式进行公共方法的封装,可以很好的优化 minxins 的缺点。
- 可以使用对象的解构显示引入变量、方法
- 可以用解构赋值内部命名方式,解决命名冲突问题
- 便于代码阅读
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 元素进行底层操作,就会用到自定义指令。
列:页面加载时,元素获取焦点
- 全局注册
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
- 局部注册
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 还未解析时 可能会出现一种叫做 “未编译模板闪现” 的情况。 一般这种情况比较短暂,一闪而过,优化如下:
- 可以在 el 挂载的标签下添加指令
v-cloak配合display: none可以在组件编译完成前对原始模板进行隐藏。- v-cloak 用于隐藏尚未完成编译的 DOM 模板。会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。
<p class="#app" v-cloak>
<p>{{value.name}}</p>
</p>
css 中:
[v-cloak] {
display: none;
}
27、MVVM 优缺点
优点:
- 分离 view 和 model,降低代码耦合,提高视图或逻辑的重用性: 比如 view 可 独立于 model 变化和修改,一个 viewModel 可绑定在不同的 view 上, 可以把一些逻辑放在一个 viewModel 中,让多个 view 复用这段视图逻辑 【vue2 使用 minxs,vue3 使用 hooks】。
- 提高可测试性:viewModel 的存在可以更好的编写测试代码
- 数据双向绑定,视图自动更新
缺点:
- 难调试
因为是双向绑定,界面异常可能是 view 也可能 是 model 照成的, 数据绑定使得 一个位置的 BUG 可能会被传递到别的位置,照成定位比较难, 且数据绑定是通过指令写入 view 模板中,无法进行 断点 debug。
- model 长期持有不被销毁时,其内存的不释放,就照成了内存的消耗。
- 大型图层应用程序,视图状态较多的情况,其构建和维护成本就会比较高。
28、VUE 生命周期
Vue有一个完整的生命周期,从开始的实列化,到初始化数据,再进行DOM挂载,挂载完成后进行视图渲染;数据的更新到再次的更新、渲染,到最后将组件进行卸载,这一系列的过程就加Vue 的生命周期。
- beforeCreate 进行Vue 实列化
此时只是实列化开始阶段,data 数据、methods方法、DOM 都获取不到
- created 实列化完成
- 完成实列化,实列上配置的 data、methods 方法、Watcher、props 都配置完成,可进行数据操作和方法的调用。
- 但是还未进行DOM的挂载,无法执行DOM操作
- beforeMount 挂载前阶段
- 此时模板已经在内存中编译完成,只是还未进行页面的更新渲染。也是无法进行DOM操作
- mounted 挂载完成
完成DOM挂载,页面和数据都是最新的,可进行 DOM 操作。
- beforeUpdate 更新前
响应式数据更新时调用,此时数据已经更新,只是页面还未更新渲染
- updated 更新完成
- 响应式数据更新导致的 vir-dom 重渲染和打补丁之后调用。
- 已经完成了视图的更新,数据和UI都是最新的,可在此进行依赖dom的一些操作
- 不赞成在此钩子进行状态更改,以避免导致无限循环
- beforeDestory【Vue3:beforeUnMount】卸载前
即将进行组件的卸载,此钩子还可以获取实列参数、方法。一般在此进行定时器清除、localStorage 的清除等
- Destoryed【Vue3:unMounted】卸载完成
组件被销毁,实列不可操作
- keep-alive 包裹的组件缓存,引发的生命周期
- activated:keep-alive 缓存组件,组件显示时的钩子
- deactivated:keep-alive 缓存组件,组件移除时的钩子
-
errorCaptured(err,instance,info){} 捕获后代组件传递的错误
-
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的区别
- created 完成实列化,在模板渲染成 html 前调用,即对数据进行初始化。
- mouted 完成挂载,在模板渲染成 html 后调用,即一般在初始化完成后,在对 dom 节点进行一些操作。
31、一般在哪个生命周期请求异步数据
- 可以在 created、beforeMount、mounted 中进行调用异步请求
- 因为这些钩子中实列已经生成,data 可以获取到,可对数据进行更新。
- 一般推荐在 create 中进行异步调用。
- 因为此时实列完成、还未进行挂载,可以更快速获取到服务端数据进行ui展示;
- 且 SSR 不支持 beforeMount、mounted 钩子,有利于保持一致性。
32、keep-alive 的生命周期有哪些
- 组件被 keep-alive 进行包裹,进行缓存,会走
activated、deactivated这两个钩子分别表示页面从缓存中展示、将页面缓存在内存中。 - 而之前的 beforeDestroy、destroy 不会再触发,因为 缓存组件被缓存在内存中不会进行实际销毁。
33、父子组件间通信
1、props、$emit
父向子传递数据
- 通过 props 传参数:在父组件中调用子组件,在模板标签中 通小写的
xx或xsx-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 文件,当路由被访问的时候才去加载对应的组件。这样可以有效分担首屏加载的能力。
- 使用动态导入的方式:函数rturn 出的url [箭头函数 + import]
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
- 组件按组分块: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 }],
})
- 组件按组分块: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、全局导航守卫
| 全局守卫 | 名称 | 参数 | 执行条件 | 使用场景 |
|---|---|---|---|---|
| 全局前置守卫 | beforeEach | to,from, next(可选) | 进入路由前执行 | 登录授权功能 |
| 全局解析守卫 | beforeResolve | to | 导航被确认之前调用 组件内的守卫和异步路由组件被解析后 | \ |
| 全局后置钩子 | afterEach | to, from | 进入路由之后执行 | \ |
2、路由独享守卫
| 路由独享守卫 | 参数 | 执行条件 | |
|---|---|---|---|
| 路由独享守卫 | beforeEnter | to,from, next(可选) | 只在进入路由时触发 |
3、组件内守卫
| 组件内守卫 | 参数 | 执行条件 | 使用场景 |
|---|---|---|---|
| beforeRouteEnter | to,from, next(可选) | 进入组件前触发,不可以访问 this | |
| beforeRouteUpdate | to,from, next(可选) | 组件复用时触发,可以访问 this | 复用组件时,不会触发钩子的创建执行,通过此方法进行一些必要的操作 |
| beforeRouteLeave | to,from, next(可选) | 离开组件时触发,可以访问 this | 1、误触离离开当前页面时,跳出提示框,可以选择是否确定离开页面 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 组件对状态的改变建立了一个完整的生态圈:
- 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']),
}