说说你对vue的理解
vue 是一套用于构建用户界面的渐进式框架,vue 的核心库只关注视图层。【渐进式是什么】
是基于MVVM模式实现的一套框架,
在vue中:Model:指的是js中的数据,如对象,数组等等。
View:指的是页面视图viewModel:指的是vue实例化对象,
viewModel:是连接view和model的桥梁,通常要实现一个observer观察者,当数据发生变化,ViewModel能够监听到数据的这种变化,然后通知到对应的视图做自动更新,而当用户操作视图,ViewModel也能监听到视图的变化,然后通知数据做改动,这实际上就实现了数据的双向绑定;
在vue2中实现数据双向绑定是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应监听回调
【使用这种方式有什么问题】。Vue 2 使用 getter / setters 完全是出于支持旧版本浏览器的限制。而在 Vue 3 中则使用了 Proxy 来创建响应式对象【和原来有什么区别,存在什么问题】,仅将 getter / setter 用于 ref
总体来说,【vue的优点和缺点】
怎么理解vue的渐进式
渐进式就是框架分层,可以一部分嵌入,你可以只用核心的视图渲染功能来开发,也可以使用全家桶,慢慢的过渡
从里向外分别是:视图层渲染–组件机制–路由机制–状态管理–构建工具
·
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染
因为 Object.defineProperty 不能拦截到这些操作。
Object.defineProperty,没有对对象的新属性和数组进行属性劫持;更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
包括七个方法:push,pop,shift,unshift,sort,reverse,splice(Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化)
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
①对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?
this.$set(this.obj, 'b', 'obj.b')
this.$delete(obj, key)
set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
②直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?
arr.splice(index, 1, item) 或者 Vue.$set(arr, index, value)
实例:
点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局 api $set():
addObjB () (
this.$set(this.obj, 'b', 'obj.b')
console.log(this.obj)
}
defineProperty和proxy的区别
-
Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。所以我们在编写响应式函数的时候,defineProperty 需要用for in 去给每个属性添加监听。Proxy 可以劫持整个对象,并返回一个新的对象
-
对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。
-
无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应,Proxy 可以监听到,
-
Proxy 不兼容 IE,Object.defineProperty 不兼容 IE8 及以下。
-
Proxy 使用上比 Object.defineProperty 方便多。对象只有get和set而proxy有多达 13 种拦截方法
vue的优点?
优点:
①灵活性:vue渐进式如果我们的应用足够小,可以只使用 vue 的核心库即可;随着应用的规模不断扩大,可以根据需求引入 vue-router、vuex、vue-cli 等其他工具。
②轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb
③ 简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
并且支持双向数据绑定、组件化、数据和结构的分离、引入虚拟DOM运行速度快;
④组件化:Vue 通过组件,把一个单页应用中的各种模块拆分到一个一个单独的组件(component)中,我们只要先在父级应用中写好各种组件标签。组件化开发的优点:提高开发效率、方便重复使用、简化调试步骤、提升整个项目的可维护性、便于协同开发。
⑤虚拟DOM:在传统开发中,用 JQuery 或者原生的 JavaScript DOM 操作函数对 DOM 进行频繁操作的时候,浏览器要不停的渲染新的 DOM 树,导致在性能上面的开销特别的高。【虚拟dom有什么好处】
⑥ 双向数据绑定: 通过 MVVM 思想实现数据的双向绑定,Vue 会自动对页面中某些数据的变化做出响应。
缺点:
① 和其他框架一样不利于 SEO,不适合做需要浏览器抓取信息的电商网站,比较适合做后台管理系统。
② SPA的特性,会在首页的时候将所有的js、css数据全部加载,当项目过于庞大时,这个白屏时间就会比较明显。
什么是虚拟DOM
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
真实DOM∶ 生成HTML字符串+ 重建所有的DOM元素
Virtual DOM∶ 生成vNode+ DOMDiff+必要的DOM更新
为什么要使用虚拟DOM
1、保证性能下限,在不进行手动优化的情况下,提供过得去的性能
2、跨平台 Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
diff算法的流程?
真实的 DOM 首先会映射为虚拟 DOM;
当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
根据 patch 去更新真实的 DOM,反馈到用户的界面上。
Js操作Dom的代价
用我们传统的开发模式,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting;Dom树消耗性能是因为Dom树模块和js模块之间跨模块通信需要成本,并且之间操作Dom会引起浏览器重排和重绘
vue同时绑定多个事件
如果是不同类型的事件,可以通过键值对的形式
v-on=“{click:clickChange,mouseover:mouseChange}”
如果是同一个类型的事情,直接逗号隔开
@click="submitData(),cancle()
vue中几种绑定方式:
1.v-bind 动态绑定
2.{{ 插值语法 }} 单向绑定
3.v-model双向绑定
4.v-once 一次性绑定
mixin是什么
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}
// 定义一个使用混入对象的组件
var Component = Vue.extend({
mixins: [myMixin]
})
var component = new Component() // => "hello from mixin!"
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
混入对象的钩子将在组件自身钩子之前调用。
全局组件和局部组件的区别
不同点
一、注册位置不一样
全局注册写在src/main.js里边
局部注册哪儿用,写在那,如:src/App.vue
二、语法不一样
全局注册
Vue.component(‘myheader’,header),
局部注册
export default {
name: 'app',
components:{
'myheader':Header,
}
三、作用域不一样
如题,全局是哪儿都可以用,局部是哪儿注册哪儿用。
相同点:导入方式一样
import Header from './components/header'
什么是nextTick ?
在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到更新周期的 “下个时机”才一起执行。这样是为了确保缓存时去除重复数据对于避免不必要的计算和 DOM 操作;
nextTick 回调会等待一个状态改变后的 DOM 更新完成,在这个函数中可以拿到更新后的DOM
import { nextTick } from 'vue'
function increment() {
state.count++
nextTick(() => {
// 访问更新后的 DOM
})
}
如何创建一个浅层的响应式
在 Vue 中,状态都是默认深层响应式的。这意味着即使在更改深层次的对象或数组,你的改动也能被检测到。
function shallowReactive<T extends object>(target: T): T
一个浅层响应式对象里只有根级别的属性是响应式的。
ref 和 reactive 的区别
reactive() 返回的是一个原始对象的 Proxy,只有代理对象是响应式的,更改原始对象不会触发更新。
仅对对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效。
不可以随意地“替换”一个响应式对象,因为这将导致对初始引用的响应性连接丢失
let state = reactive({ count: 0 })
// 上面的引用 ({ count: 0 }) 将不再被追踪(响应性连接已丢失!)
state = reactive({ count: 1 })
当我们将响应式对象的属性赋值或解构至本地变量时,或是将该属性传入一个函数时,我们会失去响应性
const state = reactive({ count: 0 })
// n 是一个局部变量,同 state.count
// 失去响应性连接
let n = state.count
// 不影响原始的 state
n++
// count 也和 state.count 失去了响应性连接
let { count } = state
// 不会影响原始的 state
count++
// 该函数接收一个普通数字,并且
// 将无法跟踪 state.count 的变化
callSomeFunction(state.count)
ref() 方法允许我们创建可以使用任何值类型的响应式 ref, 通过.value获取,在模板中并且获取的是ref的顶层属性时能自动解包;
import { ref } from 'vue'
const count = ref(0)
<template>
<button @click="increment">
{{ count }} <!-- 无需 .value -->
</button>
</template>
一个包含对象类型值的 ref 可以响应式地替换整个对象,一个包含对象类型值的 ref 可以响应式地替换整个对象
什么是计算属性?
模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。
<script setup>
import { reactive, computed } from 'vue'
const author = reactive({
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
})
// 一个计算属性 ref
const publishedBooksMessage = computed(() => {
return author.books.length > 0 ? 'Yes' : 'No'
})
</script>
<template>
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
</template>
computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value。
计算属性和方法的区别
1.计算属性是一个属性 必须要有返回值 methods不一定
2.计算属性在页面渲染时 不需要加括号 methods必须要加
3.计算属性有缓存,一个计算属性仅会在其响应式依赖更新时才重新计算。 methods没有缓存 从性能上来讲 计算属性更具有优势
v-show和v-if的区别
v-if 是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
v-if 和 v-for 的优先级
在vue2中,v-for的优先级是高于v-if的,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能;另外需要注意的是在vue3则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常。
解决办法:
- 将原来使用v-if判断的项目使用计算属性过滤,只要遍历过滤后的列表
- 在外层包裹template,在外层循环,内层判断
<!--
这会抛出一个错误,因为属性 todo 此时
没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
在外新包装一层 <template> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):
template
<template v-for="todo in todos">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
你的接口请求一般放在哪个生命周期中?为什么要这样做?
接口请求可以放在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
但是推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
能更快获取到服务端数据,减少页面 loading 时间
SSR 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于代码的一致性
created 是在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图。如果在 mounted 钩子函数中请求数据可能导致页面闪屏问题
为什么v-for 要加key
为了高效的更新渲染虚拟 DOM。它能够根据key,直接找到具体位置进行操作
用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。
vue 如何检测数组的变化
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
当遇到的是非变更方法时,我们需要将旧的数组替换为新的:
js
// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/))
如果不想变更原数组,可以创建返回已过滤或已排序数组的计算属性
说说你对生命周期的理解
什么是 vue 生命周期?
对于 vue 来讲,生命周期就是一个 vue 实例从创建到销毁的过程
vue 生命周期的作用是什么?
给予了开发者在不同的生命周期阶段添加业务代码的能力
vue 生命周期有几个阶段?
它可以总共分为 8 个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。
beforeCreate:是 new Vue( ) 之后触发的第一个钩子,在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问。
created:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发 updated 函数。可以做一些初始数据的获取,在当前阶段无法与 DOM 进行交互,如果非要想,可以通过 vm.$nextTick 来访问 DOM 。
beforeMount:发生在挂载之前,在这之前 template 模板已导入渲染函数编译。而当前阶段虚拟 DOM 已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发 updated。
mounted:在挂载完成后发生,在当前阶段,真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点,使用 $refs 属性对 DOM 进行操作。
beforeUpdate:发生在更新之前,也就是响应式数据发生更新,虚拟 DOM 重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
updated:发生在更新完成之后,当前阶段组件 DOM 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
beforeDestroy:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed:发生在实例销毁之后,这个时候只剩下了 DOM 空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
第一次页面加载会触发哪几个钩子?
会触发 4 个钩子,分别是:beforeCreate、created、beforeMount、mounted
DOM 渲染在哪个周期就已经完成?
DOM 渲染是在 mounted 阶段完成,此阶段真实的 DOM 挂载完毕,数据完成双向绑定,可以访问到 DOM 节点。
vue3中的生命周期
vue2中执行顺序
beforeCreate=>created=>beforeMount =>mounted=>beforeUpdate
=>updated=>beforeDestroy=>destroyed
vue3中执行顺序 setup=>onBeforeMount=>onMounted=>onBeforeUpdate=>onUpdated=>onBeforeUnmount=>onUnmounted
其中 vue3中的setup相当于vue2中beforeCreate 与created 但是的执行在beforeCreate 与created之前,所以setup无法使用 data 和 methods 中的数据和方法
另外还有 keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数;在vue3中对应的分别是 onActivated 和 onDeactivated
父子组件的生命周期执行顺序:
我们可以发现父子组件在加载的时候,执行的先后顺序为父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。
wacth 监听器
watch 属性监听 是一个对象,键是需要观察的属性,值是对应回调函数,主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,监听属性的变化,需要在数据变化时执行异步或开销较大的操作时使用
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
},
{ deep: true },
{ immediate: true }
),
watchEffect()
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。
watch 和 watchEffect() 的区别
① watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。watchEffect()自动追踪所有能访问到的响应式属性。
② watch 会避免在发生副作用时追踪依赖;watchEffect,则会在副作用发生期间追踪依赖。
computed与watch的区别
computed 计算属性 属性的结果会被缓存,当computed中的函数所依赖的属性没有发生改变的时候,那么调用当前函数的时候结果会从缓存中读取。除非依赖的响应式属性变化时才会重新计算,主要当做属性来使用 computed中的函数必须用return返回最终的结果 computed更高效,优先使用
区别
- computed 支持缓存,只有依赖数据发生改变,才会重新进行计算; watch不支持缓存,数据变,直接会触发相应的操作;
- 不支持异步,当computed内有异步操作时无效,无法监听数据的变化;watch支持异步;
- computed第一次加载时就监听;watch默认第一次加载时不监听。
- computed是计算属性;watch是监听,监听data中的数据变化。
- computed 有getter 和 setter 方法,watch 可一设置deep:true, immediate:true
vue常见指令
1、v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。
2、v-show:根据表达式之真假值,切换元素的 display CSS 属性。
3、v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0以上必须需配合 key值 使用。
4、v-bind:动态地绑定一个或多个特性,或一个组件 prop 到表达式。
5、v-on:用于监听指定元素的DOM事件,比如点击事件。绑定事件监听器。
6、v-model:实现表单输入和应用状态之间的双向绑定
7、v-pre:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
8、v-once:只渲染元素和组件一次。随后的重新渲染,元3 素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
为什么data必须是函数
JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,其中一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
组件中的 data 写成一个函数,数据以函数返回值形式定义。这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。
vue3中props
第一步:传递
<BlogPost title="My journey with Vue" />
第二步:声明
如果使用的<script setup> 那么可以使用defineProps
<script setup>
defineProps(['title'])
</script>
如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}
第三步:使用
<template>
<h4>{{ title }}</h4>
</template>
子组件改变父组件中的数据
第一步:向父组件传递一个方法
<BlogPost
...
@enlarge-text="postFontSize += 0.1"
/>
第二步:声明
如果使用了<script setup>,可以使用defineProps
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>
如果你没有在使用 <script setup>,你可以通过 emits 选项定义组件会抛出的事件。你可以从 setup() 函数的第二个参数,即 setup 上下文对象上访问到 emit 函数
export default {
emits: ['enlarge-text'],
setup(props, ctx) {
ctx.emit('enlarge-text')
}
}
第三步:使用
<template>
<div class="blog-post">
<h4>{{ title }}</h4>
<button @click="$emit('enlarge-text')">Enlarge text</button>
</div>
</template>
props 单向数据流
所有的 props 都遵循着单向绑定原则,props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
插槽slots
通过插槽子组件只负责渲染由父组件提供的内容;
在子组件中使用 元素定义一个插槽出口 (slot outlet)
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
可以在插槽中指定默认内容
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>
父组件使用时传入的内容会被放入插槽中
<FancyButton>
Click me! <!-- 插槽内容 -->在这里插入代码片
</FancyButton>
插槽的作用域?
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。
插槽内容无法访问子组件的数据
然而在某些场景下插槽的内容可能想要同时使用父组件域内和子组件域内的数据,可以使用作用域插槽;向具名插槽中传入 props:<slot name="header" message="hello"></slot>
如果你混用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。
这么写无效
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message 属于默认插槽,此处不可用 -->
<p>{{ message }}</p>
</template>
</MyComponent>
应该这么写
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</MyComponent>
具名插槽?
用于将多个插槽内容传入到各自目标插槽的出口
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
provide 依赖注入
解决多层嵌套组件 传值的问题
一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。
<script setup>
import { provide } from 'vue'
provide('location', {
location,
updateLocation
})
</script>
注入
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
如果注入方组件想要改变依赖方的数据,最好传递一个方法进去
keep-alive的使用
什么是keep-alive?
keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。
keep-alive的常用属性有哪些?
keep-alive 具有 include 和 exclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存?
与keep-alive相关的生命周期函数是什么,什么场景下会进行使用
受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activated 和 deactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
keep-alive的实现原理是什么?
// keep-alive 内部的声明周期函数
created () {
this.cache = Object.create(null)
this.keys = []
}
key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值
cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM
在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。
当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素。
命令式和声明式的区别
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
<body>
<script type="text/javascript">
let body = document.getElementsByTagName("body");
let head = document.createElement("h1");
head.innerHTML = "Hello World!";
body[0].appendChild(head);
</script>
</body>
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
<body>
<div id="test"></div>
<script src="/src/babel.min.js"></script>
<script src="/src/react.development.js"></script>
<script src="/src/react-dom.development.js"></script>
<script type="text/babel">
ReactDOM.render((
<h1>Hello World!</h1>
), document.getElementById("test"))
</script>
</body>
不需要响应式的数据应该怎么处理?
// 方法一:将数据定义在data的return函数之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx})
}
}
Vue初始化页面闪动
当网络较慢,网页还在加载 Vue.js ,而导致 Vue 来不及渲染,这时页面就会显示出 Vue 源代码。我们可以使用 v-cloak 指令来解决这一问题。可以使用 v-cloak 指令设置样式,这些样式会在 Vue 实例编译结束时,从绑定的 HTML 元素上被移除。
<div id="app" v-cloak>
{{context}}
</div>
css 代码
[v-cloak]{
display: none;
}
v-on 常见修饰符
.once - 只触发一次回调。
.prevent - 调用 event.preventDefault()阻止默认行为
.stop - 调用 event.stopPropagation()阻止冒泡
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.capture - 添加事件侦听器时使用 capture 模式,默认情况下是事件冒泡, 如果想变成事件捕获, 那么就需要使用.capture修饰符
vue 过滤器
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在插值表达式 {{ }} 和 v-bind 表达式 中,然后放在操作符“ | ”后面进行指示。
例如,在显示金额,给商品价格添加单位:
如何自定义全局过滤器
Vue.filter("过滤器名称", 过滤器处理函数):
注意点: 默认情况下处理数据的函数接收一个参数, 就是当前要被处理的数据
<!--Vue会把name交给指定的过滤器处理之后, 再把处理之后的结果插入到指定的元素中-->
<p>{{name | formartStr1 | formartStr2}}</p>
Vue.filter("formartStr1", function (value) {
value = value.replace(/学院/g, "大学");
return value;
}
vue组件之间如何通信
props / $emit
父组件通过props向子组件传递数据,子组件通过$emit和父组件通信
父组件:
<template>
<div class="section">
<com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
子组件
<template>
<div>
<div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
</div>
</template>
<script>
export default {
props: ['articles'],
methods: {
emitIndex(index) {
this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
}
}
}
</script>
eventBus事件总线($emit / $on)
eventBus事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下
(1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件
假设有两个兄弟组件firstCom和secondCom:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
components: { firstCom, secondCom }
}
</script>
在firstCom组件中发送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import {EventBus} from './event-bus.js' // 引入事件中心
export default {
data(){
return{
num:0
}
},
methods:{
add(){
EventBus.$emit('addition', {
num:this.num++
})
}
}
}
</script>
(3)接收事件
在secondCom组件中发送事件:
<template>
<div>求和: {{count}}</div>
</template>
<script>
import { EventBus } from './event-bus.js'
export default {
data() {
return {
count: 0
}
},
mounted() {
EventBus.$on('addition', param => {
this.count = this.count + param.num;
})
}
}
</script>
在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
ref / $refs
这种方式也是实现父子组件之间的通信。
ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default {
data () {
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
在父组件中:
<child ref="child"></component-a>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
mounted () {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
}
}
</script>
$parent / $children
使用$parent
可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
使用$children
可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。
在子组件中:
<template>
<div>
<span>{{message}}</span>
<p>获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Vue'
}
},
computed:{
parentVal(){
return this.$parent.msg;
}
}
}
</script>
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import child from './child.vue'
export default {
components: { child },
data() {
return {
msg: 'Welcome'
}
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = 'JavaScript'
}
}
}
</script>
依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了
Vue 的响应式原理
在 newVue() 后, Vue 会调用 _init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行 getter 函数,从而将Watcher添加到依赖中进行依赖收集。而在当被赋值的时候会执行 setter函数, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图
v-modle绑定的原理
v-model 本质就是 :value + input 方法的语法糖。可以通过 model 属性的 prop 和 event 属性来进行自定义。原生的 v-model,会根据标签的不同生成不同的事件和属性。
例如:
text 和 textarea 元素使用 value 属性和 input 事件
checkbox 和 radio 使用 checked 属性和 change 事件
select 字段将 value 作为 prop 并将 change 作为事件
以输入框为例,当用户在输入框输入内容时,会触发 input 事件,从而更新 value。而 value 的改变同样会更新视图,这就是 vue 中的双向绑定。双向绑定的原理,其实现思路如下:
首先要对数据进行劫持监听,所以我们需要设置一个监听器 Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者 Watcher 看是否需要更新。
因为订阅者是有很多个,所以我们需要有一个消息订阅器 Dep 来专门收集这些订阅者,然后在监听器 Observer 和订阅者 Watcher 之间进行统一管理的。
接着,我们还需要有一个指令解析器 Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者 Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者 Watcher 接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
因此接下去我们执行以下 3 个步骤,实现数据的双向绑定:
实现一个监听器 Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
实现一个订阅者 Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
实现一个解析器 Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
构建 vue-cli 工程都用到了哪些技术?他们的作用分别是什么?
vue.js:vue-cli 工程的核心,主要特点是双向数据绑定和组件系统。
vue-router:vue 官方推荐使用的路由框架。
vuex:专为 Vue.js 应用项目开发的状态管理器,主要用于维护 vue 组件间共用的一些 变量 和 方法。
axios(或者 fetch、ajax):用于发起 GET 、或 POST 等 http请求,基于 Promise 设计。
vux等:一个专为vue设计的移动端UI组件库。
webpack:模块加载和vue-cli工程打包器。
eslint:代码规范工具
vue-cli 工程常用的 npm 命令有哪些?
下载 node_modules 资源包的命令:npm install
启动 vue-cli 开发环境的 npm命令:npm run dev
vue-cli 生成 生产环境部署资源 的 npm命令:npm run build
用于查看 vue-cli 生产环境部署资源文件大小的 npm命令:npm run build --report
vue2.x 和 vuex3.x 渲染器的 diff 算法分别说一下?
简单来说,diff 算法有以下过程
同级比较,再比较子节点
先判断一方有子节点一方没有子节点的情况(如果新的 children 没有子节点,将旧的子节点移除)
比较都有子节点的情况(核心 diff)
递归比较子节点
正常 Diff 两个树的时间复杂度是 O(n^3),但实际情况下我们很少会进行跨层级的移动 DOM,所以 Vue 将 Diff 进行了优化,从O(n^3) -> O(n),只有当新旧 children 都为多个子节点时才需要用核心的 Diff 算法进行同层级比较。
Vue2 的核心 Diff 算法采用了双端比较的算法,同时从新旧 children 的两端开始进行比较,借助 key 值找到可复用的节点,再进行相关操作。相比 React 的 Diff 算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x 借鉴了 ivi 算法和 inferno 算法
在创建 VNode 时就确定其类型,以及在 mount/patch 的过程中采用位运算来判断一个 VNode 的类型,在这个基础之上再配合核心的 Diff 算法,使得性能上较 Vue2.x 有了提升。该算法中还运用了动态规划的思想求解最长递归子序列。
Vue与React的区别?
共同点:
①都使用虚拟dom。
②提供了响应式和组件化的视图组件。
③把注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。(vue-router、vuex、react-router、redux等等)
区别:
1、数据是否可变
React:整体是函数式的思想,在react中,是单向数据流,推崇结合immutable来实现数据不可变。
Vue:的思想是响应式的,也就是基于是数据可变的,通过对每一个属性建立Watcher来监听,当属性变化的时候,响应式的更新对应的虚拟dom。
2、编译&写法
React:思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等。
Vue:把html,css,js组合到一起,用各自的处理方式,Vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
3、重新渲染和优化
当组件的状态发生变化时,React的机制会触发整个组件树的重新呈现。您可能需要使用额外的属性来避免不必要地重新渲染子组件。虽然Vue的重新渲染功能是开箱即用的,但Vue提供了优化的重新渲染,其中系统在渲染过程中跟踪依赖关系并相应地工作。重新渲染是Vue最显着的特征,也使其成为全世界开发人员广泛接受的框架。
4、响应式原理不同:Vue依赖收集,自动优化,数据可变。Vue递归监听data的所有属性,直接修改。当数据改变时,自动找到引用组件重新渲染。React基于状态机,手动优化,数据不可变,需要setState驱动新的state替换老的state。当数据改变时,以组件为根目录,默认全部重新渲染, 所以 React 中会需要 shouldComponentUpdate 这个生命周期函数方法来进行控制
5、框架本质不同;
Vue本质是MVVM框架,是由MVC发展来的;
React是前端组件框架,是由后端组件演化而来的。
6、构建工具不同
React ==> Create React APP
Vue ==> vue-cli
vue性能优化
(1)编码阶段
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
v-if和v-for不能连用
如果需要使用v-for给每项元素绑定事件时使用事件代理
SPA 页面采用keep-alive缓存组件
在更多的情况下,使用v-if替代v-show
key保证唯一
使用路由懒加载、异步组件
防抖、节流
第三方模块按需导入
长列表滚动到可视区域动态加载
图片懒加载
(2)SEO优化
预渲染
服务端渲染SSR
(3)打包优化
压缩代码
Tree Shaking/Scope Hoisting
使用cdn加载第三方模块
多线程打包happypack
splitChunks抽离公共文件
sourceMap优化
(4)用户体验
骨架屏
PWA
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
vue 中的 spa 应用如何优化首屏加载速度?
优化首屏加载可以从这几个方面开始:
请求优化:CDN 将第三方的类库放到 CDN 上,能够大幅度减少生产环境中的项目体积,另外 CDN 能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
缓存:将长时间不会改变的第三方类库或者静态资源设置为强缓存,将 max-age 设置为一个非常长的时间,再将访问路径加上哈希达到哈希值变了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户的体验
gzip:开启 gzip 压缩,通常开启 gzip 压缩能够有效的缩小传输资源的大小。
http2:如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的 tcp 连接数量是有限制的(chrome 为 6 个)超过规定数量的 tcp 连接,则必须要等到之前的请求收到响应后才能继续发送,而 http2 则可以在多个 tcp 连接中并发多个请求没有限制,在一些网络较差的环境开启 http2 性能提升尤为明显。
懒加载:当 url 匹配到相应的路径时,通过 import 动态加载页面组件,这样首屏的代码量会大幅减少,webpack 会把动态加载的页面组件分离成单独的一个 chunk.js 文件
预渲染:由于浏览器在渲染出页面之前,需要先加载和解析相应的 html、css 和 js 文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响体积优化
合理使用第三方库:对于一些第三方 ui 框架、类库,尽量使用按需加载,减少打包体积
使用可视化工具分析打包后的模块体积:webpack-bundle- analyzer 这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
提高代码使用率:利用代码分割,将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程
封装:构建良好的项目架构,按照项目需求就行全局组件,插件,过滤器,指令,utils 等做一 些公共封装,可以有效减少我们的代码量,而且更容易维护资源优化
图片懒加载:使用图片懒加载可以优化同一时间减少 http 请求开销,避免显示图片导致的画面抖动,提高用户体验
使用 svg 图标:相对于用一张图片来表示图标,svg 拥有更好的图片质量,体积更小,并且不需要开启额外的 http 请求
压缩图片:可以使用 image-webpack-loader,在用户肉眼分辨不清的情况下一定程度上压缩图片
vue3 新特征
1.原来的options API 变成了 组合是 Composition API 好处就是将响应式数据和相关的业务逻辑结合到一起,逻辑更加清晰和便于组件复用
2.不再要求必须只能一个根组件,因为会生成一个虚拟元素fragement 减少层次
3.vue2和3 响应式原理的对比,3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
在vue2对于对象类型的数据通过数据劫持实现读取,数组类型重写常用的数组方法,对数新增和删除页面不会更新
在vue3中对象通过proxy代理拦截属性的增删改查.通过reflect对被代理的对象操作
4.生命周期,所以Vue3 没有提供单独的 onBeforeCreate 和 onCreated 方法,因为 setup 本身是在这两个生命周期之前执行的,Vue3 建议我们直接在 setup 中编写这两个生命周期中的代码。用setup代替了 beforeCreate 和 created 这两个生命周期
5.v-if 和 v-for 优先级变化
6.新增指令 v-memo,可以缓存 html 模板,比如 v-for 列表不会变化的就缓存,简单说就是用内存换时间
7. 源码用 typescript 重写, 有更好的 类型推导 (类型检测更为严格, 更稳定)
8.支持了tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,主要是利用es6的moudlue语法来判断哪些模块已经加载,判断那些模块和变量未被使用或者引用,进而删除对应代码
9.新增组件teleport 是一种能够将我们的组件html结构移动到指定位置的技术,可以让子组件能够在视觉上跳出父组件(如父组件overflow:hidden)
Composition API和 Options API 的区别
① 选项式API一个功能往往需要在不同的vue配置项中定义属性和方法,比较分散,不利于维护和复用;在vue3 Composition API中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起,逻辑清晰,方便组件复用
② Composition API中见不到this的使用,setup函数的执行并没有绑定实例对象减少了this指向不明的情况
③ Composition API对 tree-shaking 友好,代码也更容易压缩
Vue在created和mounted这两个生命周期中请求数据有什么区别呢?
在created中,页面视图未出现,如果请求信息过多,页面会长时间处于白屏状态,DOM节点没出来,无法操作DOM节点。在mounted不会这样,比较好。
单页面和多页面应用的区别
SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
vue-router 相关
路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。
- Vue-Router 的懒加载如何实现
import List from '@/components/list.vue'
方案一(常用):使用箭头函数+import动态加载:const List = () => import('@/components/list.vue')
方案二:使用箭头函数+require动态加载 : component: resolve => require(['@/components/list'], resolve)
方案三:使用webpack的require.ensure技术,也可以实现按需加载。 这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
router和route的区别
1.router是VueRouter的一个对象,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所有的路由包含了许多关键的对象和属性。
2.route是一个跳转的路由对象,每一个路由都会有一个route对象,是一个局部的对象,可以获取对应的name,path,params,query等
$route.path 字符串,等于当前路由对象的路径,会被解析为绝对路径,如 “/home/news”
$route.params 对象,包含路由中的动态片段和全匹配片段的键值对
$route.query 对象,包含路由中查询参数的键值对。
例如,对于 /home/news/detail/01?favorite=yes,会得到$route.query.favorite == 'yes'
$route.router 路由规则所属的路由器(以及其所属的组件)。
$route.matched 数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
$route.name 当前路径的名字,如果没有使用具名路径,则名字为空。
vue中路由传参的方式
方式一:params
params(推荐):命名的路由,params 必须和 name 搭配使用
this.$router.push({ name:'user',params: { userId: 123 }})
获取 this.$route.params (已经是$route而不是$router )
方式二: query
query:带查询参数,变成 /register?plan=private
this.$router.push({ path: 'register', query: { plan: 'private' }})
this.$route.query
方式三: meta
meta方式:路由元信息
export default new Router({
routes: [
{
path: '/user',
name: 'user',
component: user,
meta:{
title:'个人中心'
}
}
]
this.$route.meta
query 和 params 传参的区别 ?
1.query要用path来引入,params要用name来引入
2. query更加类似于我们ajax中get传参,params则类似于post,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示
3. query刷新不会丢失query里面的数据 params刷新会丢失 params里面的数据
Vue的路由实现:hash模式 和 history模式
第一种:利用H5的history API实现
主要通过history.pushState 和 history.replaceState来实现,不同之处在于,pushState会增加一条新的历史记录,而replaceState则会替换当前的历史记录
第二种:利用url的hash实现
我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,路由里的 # 不叫锚点,我们称之为 hash,我们说的就是hash,主要利用监听哈希值的变化来触发事件 —— hashchange 事件来做页面局部更新
vue-router有哪几种路由守卫?
路由守卫主要是在路由跳转进入和离开之前做一些拦截操作,比如判断用户是否登录,该页面用户是否有权限浏览等等
路由分为分为全局守卫、组件内守卫和独享守卫三种
全局守卫:又包括全局前置守卫、解析守卫、后置守卫
beforeEach 全局前置守卫
router.beforeEach(async (to, from) => {
if (
// 检查用户是否已登录
!isAuthenticated &&
// ❗️ 避免无限重定向
to.name !== 'Login'
) {
// 将用户重定向到登录页面
return { name: 'Login' }
}
})
全局解析守卫beforeResolve
在对应 的路由组件在被解析之后,确认之前调用
router.beforeResolve 是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
全局后置守卫 afterEach
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
路由独享受守卫
只有在 从一个不同的 路由导航时,才会被触发;同一路由修改参数不会触发
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内守卫
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
在渲染该组件的对应路由被验证前调用
不能获取组件实例 `this` !
因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
在当前路由改变,但是该组件被复用时调用
举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
在导航离开渲染该组件的对应路由时调用
与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
讲一下完整的导航守卫流程?
导航被触发。
在失活的组件里调用离开守卫beforeRouteLeave(to,from,next)。
调用全局的beforeEach( (to,from,next) =>{} )守卫。
在重用的组件里调用 beforeRouteUpdate(to,from,next) 守卫。
在路由配置里调用beforeEnter(to,from,next)路由独享的守卫。
解析异步路由组件。
在被激活的组件里调用beforeRouteEnter(to,from,next)。
在所有组件内守卫和异步路由组件被解析之后调用全局的beforeResolve( (to,from,next) =>{} )解析守卫。
导航被确认。
调用全局的afterEach( (to,from) =>{} )钩子。
触发 DOM 更新。
路由导航守卫和Vue实例生命周期钩子函数的执行顺序?
路由导航守卫都是在Vue实例生命周期钩子函数之前执行的
讲一下导航守卫的三个参数的含义?
to:即将要进入的目标 路由对象。
from:当前导航正要离开的路由对象。
next:函数,必须调用,不然路由跳转不过去。
next():进入下一个路由。
next(false):中断当前的导航。
next(‘/’)或next({ path: ‘/’ }) : 跳转到其他路由,当前导航被中断,进行新的一个导航。
怎么在组件中监听路由参数的变化?
有两种方法可以监听路由参数的变化,但是只能用在包含的组件内。
第一种watch: {
'$route'(to, from) {
//这里监听
},
},
第二种beforeRouteUpdate (to, from, next) {
//这里监听
},
什么是命名视图?
有时候想同时 (同级) 展示多个视图,而不是嵌套展示,例如创建一个布局,有 sidebar (侧导航) 和 main (主内容) 两个视图,这个时候命名视图就派上用场了。你可以在界面中拥有多个单独命名的视图,而不是只有一个单独的出口。如果 router-view 没有设置名字,那么默认为 default。
<router-view class="view left-sidebar" name="LeftSidebar"></router-view>
<router-view class="view main-content"></router-view>
<router-view class="view right-sidebar" name="RightSidebar"></router-view>
一个视图使用一个组件渲染,因此对于同个路由,多个视图就需要多个组件。确保正确使用 components 配置 (带上 s):
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
],
})
路由之间是怎么跳转的?有哪些方式?
声明式 通过使用内置组件来跳转
编程式 通过调用router实例的push方法router.push({ path: ‘/home’ })或replace方法router.replace({ path: ‘/home’ })
vuex相关
什么是vuex
Vuex是一个专为vue.js应用程序开发的状态管理模式。它解决了组件之间同一状态的共享问题。因为它采用的是集中式存储管理应用的所有组件状态,所以组件就可以和store通讯了。其实Vuex就是用来管理组件之间通信的一个插件
在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation进行,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
Vuex有哪几种属性?
有五种,分别是 State、 Getter、Mutations 、Actions、 Modules
state => 基本数据(数据源存放地) 访问:this.$store.state.count
getters => 从基本数据派生出来的数据
getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } }
mutations => 提交更改数据的方法,同步
actions => 像一个装饰器,包裹mutations,使之可以异步。
modules => 模块化Vuex
Vuex 为什么要分模块并且加命名空间
模块:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
Vuex和单纯的全局对象有什么区别?
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
Mutation 和 Action 的区别
Mutation专注于修改State,理论上是修改State的唯一途径;Action业务代码、异步请求。
Mutation:必须同步执行;Action:可以异步,但不能直接操作State。
在视图更新时,先触发actions,actions再触发mutation
当触发一个类型为 increment 的 mutation 时,需要调用此函数:
store.commit('increment')
Action 提交的是 mutation,而不是直接变更状态。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Vuex的严格模式是什么,有什么作用,如何开启?
在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
在Vuex.Store 构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中?
一、如果请求来的数据是不是要被其他组件公用,仅仅在请求的组件内使用,就不需要放入vuex 的state里。
二、如果被其他地方复用,这个很大几率上是需要的,如果需要,请将请求放入action里,方便复用,并包装成promise返回,在调用处