# Vue基础
# computed、watch与methods
computed:
- 支持缓存,计算属性是基于响应式依赖进行缓存的,也就是data中或父组件传过来的props中的数据,只有在响应式数据发生变化才会重新计算
- 不支持异步,当computed中有异步操作时,无法监听数据的变化
- 计算属性中函数的返回值就是属性的属性值
应用场景
:需要进行数值计算,且依赖于其他数据,可以利用computed的缓存特性,避免每次获取值都要重新计算。
watch:
- 不支持缓存,只要监听的数据变化,就会触发,监听数据必须是响应式数据
- 支持异步监听,监听函数接收两个值:新值和旧值
应用场景
:需要在数据变化时执行异步或开销较大的操作,watch可以限制执行该操作的频率,并在得到最终结果前设置中间状态,这是计算属性无法做到的。
可以将同一函数定义为 method 或 computed,对于最终的结果,两种方式都是相同的。但是计算属性可以缓存,而method调用总会执行该函数。
# v-if、v-show与v-html
v-if:
- v-if是惰性的,如果初始条件为假,则渲染时会忽略对应节点;只有在条件第一次变为真时才开始局部编译
- v-if切换有一个局部编译/卸载的过程,切换过程中会销毁和重建内部的事件监听和子组件
- v-if有更高的切换消耗,适合条件不容易被改变的情况
v-show:
- 无论首次条件是否为真,都被编译,然后缓存,而且DOM元素保留
- v-show是通过设置DOM元素的display样式属性控制显隐的
- v-show有更高的初始渲染消耗,适合需要频繁切换的场景
v-html:会先移除节点下的所有子节点,调用html方法,通过addProp给节点设置innerHTML为v-html的值。
# v-if和v-for的优先级问题
- 在Vue2.x中,v-for优先于v-if,如果同时出现,每次渲染都会执行循环再判断条件,这样渲染就不可避免,浪费性能。
- 在Vue3.x中,v-if优先于v-for。
由于语法上存在歧义,建议避免在同一元素上同时使用两者。更好的办法是通过创建计算属性筛选出列表,一次创建可见元素。
# v-model语法糖
# data为什么是一个函数而不是对象
Vue组件可能存在多个实例,如果使用对象形式定义data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例,这是不合理的;采用函数形式定义,在initData时会将其作为工厂函数返回全新data对象,有效规避多实例之间状态污染问题。
而在Vue根实例创建过程中则不存在该限制,也是因为根实例只能有一个,不需要担心这种情况。
# slot插槽
# 对keep-alive组件的理解
keep-alive是Vue提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。这也可以使用v-show来达到目的,但是v-show在初始渲染时会全部渲染,而keep-alive只有在被包裹的组件第一次切换时才被渲染并保存在内存中。
keep-alive的生命周期:
如果为一个组件包裹了keep-alive,它会多出两个生命周期: activated
和 deactivated
。
- 当组件被切换时,会被缓存到内存中、触发 deactivated 生命周期;
- 当组件被切换回来时,再去缓存里找这个组件、触发 activated 钩子函数。
彻底揭秘keep-alive原理 (opens new window)
# $nextTick的原理及作用
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
# vm.$set()
在Vue2.x,如果直接给vue中data的对象添加属性,或直接设置数组的某项值,会发现视图并未刷新。这是因为defineProperty的限制(Vue3的proxy可以完美解决这些问题),如果在Vue实例创建时没有声明,就不会被Vue转换成响应式属性,自然就不会触发视图的更新,这个时候就要使用Vue的全局api $set()
。
this.$set(要改变的数组/对象, 索引/key, value);
this.$set(this.arr, 0, "1"); // 改变数组
this.$set(this.obj, "c", "o"); // 改变对象
2
3
4
vm.$set()实现原理:
- 如果目标是数组,直接使用数组的splice方法触发响应式(该方法已被vue重写)
- 如果目标是对象,会先判断属性是否存在,对象是否为响应式。如果要对属性进行响应式处理,则通过调用
defineReactive
方法(该方式就是Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加getter和setter功能所调用的方法)
# Vue如何收集依赖
# 单页面应用与多页面应用
- SPA单页面应用:只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
- 优点:
- 用户体验好、内容改变不需要重新加载整个页面
- 避免了不必要的重复渲染,SPA相对对服务器压力小
- 缺点:
- 初次加载耗时长
- 前进后退路由管理:SPA在一个页面中显示所有内容,所以不能使用浏览器的前进后退功能,所有页面切换需要自己建立堆栈管理
- 不利于SEO,所有内容都在一个页面中动态替换显示
- 优点:
- MPA多页面应用:有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。
# 预渲染
如果仅希望改善网站的一些推广页面 (例如/
、/about
、/contact
等) 的SEO,预渲染比SSR更加合适。预渲染可以在构建时为指定的路由生成静态HTML文件,通俗来讲就是把希望被爬虫抓取的页面通过预渲染插件(webpack中的prerender-spa-plugin
插件)来提前渲染出来,好让爬虫抓取这些内容,改善SEO。
# SSR
SSR即服务端渲染:将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后把html直接返回给客户端。它通常是由内容呈现时间对应用的重要程度决定的,SSR可以实现最佳的初始加载性能。
优点:
- 更好的SEO,因为搜索引擎爬虫会直接读取完整的渲染出来的页面。
- 更快的内容呈现,尤其是网络连接缓慢或设备运行速度缓慢的时候。服务端标记不需要等待所有的 JavaScript 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容。
缺点:
- 开发条件会受限,浏览器特有的代码只能在特定的生命周期钩子中使用;一些外部的库在服务端渲染应用中可能需要经过特殊处理。SSR只支持调用
beforeCreate
和created
两个钩子,其他只会在客户端执行。 - 不同于一个完全静态的 SPA 可以部署在任意的静态文件服务器,服务端渲染应用需要一个能够运行 Node.js 服务器的环境。
- 更多的服务端负载。在 Node.js 中渲染一个完整的应用会比仅供应静态文件产生更密集的 CPU 运算。所以如果流量很高,需要能够承担相应负载的服务器并采取缓存策略。
# 生命周期
从Vue实例创建、运行、到销毁期间,会运行一些叫生命周期钩子的函数。主要的生命周期分类:
- 创建期间:beforeCreate、created、beforeMount、mounted
- beforeCreate:在实例初始化之后,data的响应式追踪、event/watcher之前被调用,也就是说不能访问到data、computed、watch、methods上的数据和方法。
- created:实例创建完成之后被调用,实例上配置的options包括data、computed、watch、methods等都配置完成了,但是此时渲染节点还未挂载到DOM,所以不能访问到与实例绑定的 $el 属性对应的节点,如果想要与DOM进行交互,可以通过vm.$nextTick来访问DOM。
- beforeMount:在挂载前调用,相关的render函数首次被调用。完成以下配置:编译模板,把data里的数据和模板生成html。此时还没有挂载到html页面上。
- mounted:在挂载后调用,此时真实的DOM已经挂载完毕,数据已经被监测,可以访问DOM节点。
- 运行期间:beforeUpdate、updated
- beforeUpdate:响应式数据更新时调用,发生在虚拟DOM重新渲染和打补丁(patch)之前。这里可以进一步变更状态,不会触发附加的重渲染过程。
- updated:发生在DOM更新完成之后,这期间避免更改数据,可能导致更新无限循环。
- 销毁期间:beforeDestroy、destroyed(beforeUnmount、unmounted)
- beforeDestroy/beforeUnmount:实例销毁前调用,这里可以进行善后首尾工作,如清除定时器。
- destroyed/unmounted:实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
异步请求
可以在 created、beforeMount、mounted 中进行,这里data可以访问,且可以将服务器返回的数据进行赋值。如果异步请求不依赖DOM,推荐在created中调用,有以下优点:
- 更快获取到服务端数据,减少页面loading时间;
- SSR不支持beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性。
Vue3的生命周期销毁阶段的钩子函数有了些变动,且新添了一些其他的生命周期函数 (opens new window)。下图就是Vue3的生命周期图:
# 父子组件生命周期钩子执行顺序
- 加载渲染过程:父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
- 子组件更新过程:父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
- 父组件更新过程:父 beforeUpdate->父 updated
- 销毁过程:父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
# 组件通信
# props/$emit
父组件通过 props
向子组件传递数据,子组件通过 $emit
和父组件通信。
父组件向子组件传值:
- props可以为字符串数组或对象(键值分别为名称和数据类型)
- 基本使用:父组件中使用子组件的时候给子组件添加要传入的属性,子组件通过props来接收父组件传入的属性
单向数据流
:props只能是父组件向子组件传值,反过来不行子组件避免修改prop
:js中对象和数组是通过引用传入的,所以在子组件中应该避免修改任何prop,防止应用的数据流向难以理解传递静态or动态的prop
:如果使用子组件时传入的属性是静态值,可以不适用 v-bind ;如果是动态的值,则使用 v-bind 绑定props 属性命名规则
:在 props 中使用驼峰形式,DOM模板中需要使用短横线的形式。如果使用字符串模板,就没有这个限制了
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}
<!-- 子组件 -->
<template>
<div id="son">
<p>{{postTitle}}</p>
<p>{{msg}}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default {
name: "son",
<!-- props使用驼峰 -->
props: ["postTitle", "msg", "fn"];
};
</script>
<!-- 父组件 -->
<template>
<div id="father">
<!-- 传递静态or动态属性,模板中使用横线分隔方式 -->
<son post-title="hello!" :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {
msgData: "父组件数据";
},
methods: {
myFunction() {
console.log("vue");
}
},
components: {
son
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
子组件向父组件传值:
- 在父组件定义函数,在子组件通过
$emit('函数名', 要传的参数)
调用
<template>
<div id="example">
<advice-component v-on:advise="showAdvice"></advice-component>
</div>
</template>
<script>
import adviceComponent from './text/adviceComponent.vue'
// 父组件实例
const app = createApp({
methods: {
showAdvice(advice) {
alert(advice)
}
}
})
// 注册子组件
app.component('adviceComponent', {
emits: ['advise'], // vue3提供的,与props类似,定义一个组件可以向其父组件触发的事件
data() {
return {
adviceText: 'Some advice'
}
},
//
template: `
<div>
<input type="text" v-model="adviceText">
<button v-on:click="$emit('advise', adviceText)">
Click me for sending advice
</button>
</div>
`
})
app.mount('#example')
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
props传入函数与$emit调用的区别?
# eventBus事件总线($emit/$on)
eventBus适用于父子组件、非父子组件等之间的通信,一个组件通过$emit('事件名', 传递的参数对象)发出事件,另一个组件通过$on('相同的事件名', function(传递过来的参数对象){...})。
Vue3.x已经移除了 $on,$off(移除事件总线监听器) 和 $once 方法。绝大多数情况下不建议使用eventBus在组件间通信,长期看它很难维护,可以通过其他方式来解决,具体看事件总线 (opens new window)。
# 依赖注入(provide/inject)
该方法用于父子or祖孙组件之间通信,层数很深
的情况下可以用这种方式传值。provide/inject 是Vue提供的两个钩子,和data、methods是同级的。
- provide 钩子用来让父组件发送数据或方法
- inject 钩子用来给子组件接收数据或方法
注:默认情况下依赖注入所提供的属性是非响应式的,可以通过computed、ref或reactive来解决。
// 祖父组件
provide() {
return {
num: Vue.computed(() => this.num)
// 让num变成响应式的
// 这里的num是父组件data中的数据,provide必须是一个函数,如果是一个对象就不能传递data中的数据
};
}
// 子孙组件
inject: ['num'] // 在子组件中通过this.num来调用
// 访问父组件中的所有属性
provide() {
return {
app: this // 直接返回父组件实例
};
}
data() {
return {
num: 1
};
}
// 子孙组件
inject: ['app']
console.log(this.app.num)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ref/$refs
实现父子组件之间的通信。
ref
被用来给元素或子组件注册引用信息。引用信息会被注册在父组件的$refs
对象上。
- 如果在普通的DOM元素上使用,引用指向的就是这个DOM元素
- 如果用在子组件上,引用就指向组件实例
注:因为 ref 本身是作为渲染函数的结果而创建的,在初始渲染时你不能访问它们——它们还不存在!$refs
也是非响应式的,因此不应该试图用它在模板中做数据绑定。
<!-- vm.$refs.p 会是 DOM 节点 -->
<p ref="p">hello</p>
<!-- vm.$refs.child 会是子组件实例 -->
<child-component ref="child"></child-component>
<!-- 当动态绑定时,我们可以将 ref 定义为回调函数,显式地传递元素或组件实例 -->
<child-component :ref="(el) => child = el"></child-component>
2
3
4
5
6
7
8
# $parent/$children
- $parent:可以让组件访问上一级父组件的属性和方法(是一个对象)
- $root:访问根组件的实例
- $children:可以让组件访问所有直接子组件实例(是一个数组),但是并不能保证顺序且访问的数据也不是响应式的,在Vue3中已被移除,可以使用 $refs 替换
# $attrs/$listeners
Vue引入了$attrs / $listeners
,实现组件之间的跨代通信。如A是B组件的父组件,B是C组件的父组件,可以使用该方法实现组件A给组件C传递数据。
- inheritAttrs:默认为true,继承父组件除 props 之外的所有属性;如果设为false,只继承class属性
- $attrs:
- vue2.x:继承所有父组件属性(除prop传递的属性、class和style),一般用在子组件的子元素上
- vue3.x:继承所有父组件属性,包括class和style,还有监听的事件!如果在子组件的子元素使用v-bind绑定了$attrs,会将class和style一起应用到该元素上,可能会造成视觉效果的破坏
- $listeners:
- vue2.x:该属性是一个对象,包含作用在这个组件上的所有监听器,使用
v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件) - vue3.x:该对象已被移除,事件监听器成为了
$attrs
的一部分,可以直接通过$attrs调用
- vue2.x:该属性是一个对象,包含作用在这个组件上的所有监听器,使用
<!-- A组件 -->
<template>
<div id="app">
<!-- 此处监听了两个事件,可以在B组件或者C组件中直接触发 -->
<child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
</div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
components: { Child1 },
methods: {
onTest1() {
console.log('test1 running');
},
onTest2() {
console.log('test2 running');
}
}
};
</script>
<!-- B组件 -->
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
props: ['pChild1'],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit('test1'); // 触发APP.vue中的test1方法
}
};
</script>
<!-- C组件 -->
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {
props: ['pChild2'],
inheritAttrs: false,
mounted() {
this.$emit('test2');// 触发APP.vue中的test2方法
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
- C组件中能直接触发test的原因在于B组件调用C组件时 使用 v-on 绑定了
$listeners
属性(子组件继承父组件的事件) - 在B组件中通过v-bind 绑定
$attrs
属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
# 总结
父子组件间通信:
- 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过 $refs 组件名来获得子组件,子组件通过 $parent 获得父组件,这样也可以实现通信。
- 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。
兄弟组件间通信:
- (不推荐)使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
- 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。
任意组件之间:
- (不推荐)使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
- 使用Vuex
# 路由
# 路由懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
component
(和 components
) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。
// 非懒加载
import List from '@/components/list.vue'
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
// 1. 箭头函数 + import动态加载
const List = () => import('@/components/list.vue')
const router = new VueRouter({
routes: [
{ path: '/list', component: List }
]
})
// 2. 箭头函数 + require动态加载
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/components/list'], resolve)
}
]
})
// 3. webpack的require.ensure(按模块划分懒加载)
// 后面的'list'为ChunkName,多个路由指定相同的chunkName,会合并打包成一个js文件。
const List = resolve => require.ensure([], () => resolve(require('@/components/list')), 'list');
const router = new Router({
routes: [
{
path: '/list',
component: List,
name: 'list'
}
]
}))
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 路由的hash和history模式
# hash模式(默认的路由模式)
hash模式是开发中默认的模式,location.hash值就是url中#
后面的一串东西。
特点:
- hash虽然出现在URL里,但是不会被包含在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面。
- 这种模式浏览器支持度很好,低版本的IE浏览器也支持。
- 兼容性好但不美观。
hash模式的主要原理就是onhashchange
事件:
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
}
2
3
4
- 在页面hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应代码。
- hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。
# history模式
history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
特点:虽然看着更美观了,但是history模式需要后台配置支持,如果后台没有正确配置(没有相应的路由或资源),访问时会返回404。
修改历史状态:利用了HTML5 History Interface 中新增的 pushState()
和replaceState()
方法。
这两个方法应用于浏览器的历史记录栈,提供对历史记录进行修改的功能。但调用他们进行修改时,虽然修改了URL,但浏览器不会立即向后端发送请求,这样就可以实现“更新视图但不重新请求页面”的效果。
切换历史状态:包括forward()、back()、go()三个方法,对应浏览器的前进、后退、跳转操作。
进行以下配置就能切换到history模式(后端也要配置):
const router = new VueRouter({
mode: 'history',
routes: [...]
})
2
3
4
# 两种模式对比
调用history.pushState()相比于直接修改hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- pushState() 可额外设置 title 属性供后续使用。
hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对应的路由处理,将返回404错误。
# 获取页面的hash变化
监听$route的变化
// 监听,当路由发生变化的时候执行 watch: { $route: { handler: function(val, oldVal){ console.log(val); }, // 深度观察监听 deep: true } },
1
2
3
4
5
6
7
8
9
10window.location.hash读取#值
window.location.hash的值可读可写,读取来判断状态是否改变;写入可以在不重载网页的前提下,添加一条历史访问记录。
# $route和$router的区别
- $route是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数
- $router 是“路由实例对象”,包括了路由的跳转方法,钩子函数等。
# 路由传参
需求:把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。
# params
配置路由:
{ path: '/user/:userId', name: 'user', component: user }
1
2
3
4
5传入参数
// 1 <router-link :to="'/user/'+userId">用户</router-link> // 2 // 这里的name为路由配置中的name,params为要传入的参数对象 <router-link :to="{name: 'user', params: {userId: userId}}">用户</router-link> // 3 this.$router.push('/user/'+userId) // 4 this.$router.push({name: 'user', params: {userId: userId}})
1
2
3
4
5
6
7
8
9
注:
- 解析后的地址为
/user/1
这样的形式 - 获取参数:
this.$route.params.userId
- params对象可以传入多个参数,但是每个参数都要在路由中进行配置。如果未配置,参数会在刷新后丢失
- params传参,只能用路由的"name"访问,如果用"path",params不起作用
# query
query不用配置路由,直接传参就行。可以使用
<router-link :to="...">按钮</router-link>
this.$router.push(...)
// ...里面可以填入以下内容
// 1
{name: 'user', query: {id: id}}
// 2
{path: '/user', query: {id: id}}
// 3
'/user?id='+id
2
3
4
5
6
7
注:
- 解析后的地址为
/user?id=1
这样的形式 - 获取参数:
this.$route.query.userId
- query使用"name"或"path"引入都可以
# 两者对比
- params是路由的一部分,如果配置了一定要传;query是拼接在url后面的参数,没传也没关系。
- params、query不设置也可以传参,但是params不设置时,刷新页面或返回参数会丢失;query不会。
# Vue-router跳转和location.href
location.href=url
:简单方便,但是会刷新页面history.pushState(url)
:不会刷新页面,静态跳转router.push(url)
:引入router,使用该方法来跳转。使用了 diff 算法,实现了按需加载,减少了dom的消耗。使用router方法和使用 history.pushState() 没什么差别,因为vue-router就是用了 history.pushState(),尤其是history模式下。
# 导航守卫
- 全局守卫/钩子:beforeEach、beforeResolve、afterEach
- 路由独享守卫:beforeEnter
- 组件内守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
导航行为被触发到导航完成的整个过程:
- 导航被触发,此时导航未被确认。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件(如果有)。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+),表示解析阶段完成。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 非重用组件,开始组件实例(及keep-alive)的生命周期:beforeCreate、created、beforeMount、(deactivated)、mounted、(activated)。
- 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。 - 导航完成。
# 对前端路由的理解
在前端技术早期,一个url对应一个页面,如果想从页面A切换到页面B,必然伴随着页面刷新。用户只有在刷新页面的情况下,才能重新去请求数据。
ajax的出现,让用户可以在不刷新页面的情况下发起请求;也出现了“不刷新页面即可更新页面内容”这种需求。在这种背景下出现了SPA(单页面应用),但是刚出现SPA时,人们并没有考虑到“定位”这个问题:
- SPA不会“记住”你的操作:在内容切换前后,页面的url都是一样的,SPA不知道当前页面“进展到哪一步”,一刷新页面,一切操作就会被清零。
- 由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息。
为了解决这个问题,前端路由出现了。它可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步,即为 SPA 中的各个视图匹配一个唯一标识。这意味着用户前进、后退触发的新内容,都会映射到不同的 URL 上去。此时即便他刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。实现这个目的首先要解决两个问题:
- 当用户刷新页面时,浏览器会默认根据当前 URL 对资源进行重新定位(发送请求)。这个动作对 SPA 是不必要的,因为我们的 SPA 作为单页面,无论如何也只会有一个资源与之对应。此时若走正常的请求-刷新流程,反而会使用户的前进后退操作无法被记录。
- 单页面应用对服务端来说,就是一个URL、一套资源,那么如何做到用“不同的URL”来映射不同的视图内容呢?
从这两个问题来看,服务端已经完全救不了这个场景了。所以要靠咱们前端自力更生,不然怎么叫“前端路由”呢?作为前端,可以提供这样的解决思路:
- 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。
- 感知 URL 的变化。这里不是说要改造 URL、凭空制造出 N 个 URL 来。而是说 URL 还是那个 URL,只不过我们可以给它做一些微小的处理——这些处理并不会影响 URL 本身的性质,不会影响服务器对它的识别,只有我们前端感知的到。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。
# Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,其状态存储是响应式的。每一个Vuex应用的核心就是store(仓库)。
- 改变store中的状态唯一途径就是显式提交 mutation,该方法只能进行同步操作,且方法名全局唯一。
- Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。
const store = new Vuex.Store({
// 基本数据
state: {
count: 0
},
// 从基本数据派生出的数据
getters: {
countPlus: state => {
return state.count + 1
}
},
// 提交更改数据的方法,同步
mutations: {
increment: (state, payload) => {
state.count += payload
}
},
// Action可以包含任意异步操作,提交的是mutation,而不是直接变更状态。
actions: {
increment (context) {
// 写异步代码
...
context.commit('increment')
}
}
})
new Vue({
el: '.app',
store,
computed: {
count: function() {
return this.$store.state.count
}
},
methods: {
increment: function() {
this.$store.commit('increment', 10)
}
},
template: `
<div>
{{ count }}
<button @click='increment'>点我</button>
</div>
`
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 为什么Vuex的mutation中不能做异步操作?
- Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
- 每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
# vuex和localStorage的区别
- vuex存储在内存中;localStorage以文件方式存在本地,只能存储字符串类型的数据。读取内存比读取硬盘速度要快。
- vuex用于组件之间传值,能做到数据的响应式;localStorage是将数据存储到浏览器中,一般是在跨页面传递数据时使用
- 属性页面时vuex存储的值会丢失,localStorage不会
# Vue3.0
# 响应式
# 双向数据绑定
Vue是采用数据劫持
结合发布者-订阅者模式
的方式来实现双向数据绑定的。
当Vue实例创建时,Vue会遍历data中的属性,用Object.defineProperty(vue3.0使用proxy)将它们转化为getter/setter,在数据变动时发布消息给订阅者,触发相应的监听回调。每个组件实例都有相应的 watcher 实例。主要分为以下几个步骤:
- 需要Observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter/getter,给这个对象的某个值赋值,就会触发setter,这样就可以监听到数据变化。
- Compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,
添加监听数据的订阅者
,一旦数据有变动,收到通知,更新视图。 - Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
- 在自身实例化时往订阅器管理员(dep)里面添加自己
- 自身必须有一个update()方法,待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退
# Object.defineProperty与Proxy
Object.defineProperty缺点:
- 无法监听数组的变化,Vue内部通过重写数组的8种方法来检测数组变化。除了这8种,其他数组属性都检测不到,比如
通过下标方式修改数组数据
或者给对象新增属性
,这都不能触发组件的重新渲染。 - 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。
Proxy的优点:
- Proxy可以直接劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能都远胜Object.defineProperty。
- Proxy可以无压力监听数组的变化。
- Proxy有13中拦截方式,是Object.defineProperty不具备的。
- 如果对象在vue实例创建后新增属性,一样可以监听到,不需要使用
Vue.$set
或Vue.$delete
触发响应式。 - 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示。
Proxy 唯一的缺点是兼容性的问题,因为它是 ES6 的语法。
可参考面试官: 实现双向绑定Proxy比defineproperty优劣如何? (opens new window)
# Vue模板编译
# 虚拟DOM
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。其设计最初的目的就是跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,使用vue内部封装的diff算法来比较现在的虚拟DOM会与缓存的虚拟DOM,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
# 为什么要用虚拟DOM?
- Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
- 跨平台:Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
# Diff算法原理
Vnode最大的用途就是在数据变化前后生成真实DOM对应的虚拟DOM节点,然后就可以对比新旧两份VNode,找出差异所在,然后更新有差异的DOM节点,最终达到以最少操作真实DOM更新视图的目的。
在新老虚拟DOM对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
# Vue中key的作用
vue 中 key 值的作用可以分为两种情况来考虑:
- 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
- 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
- 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
- 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行对比,从而找到相应的旧节点。
# 为什么不建议用index作为key?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。