Vue 2.x 知识点总结
# Vue 跟 React 的异同点
相同点:
- 1.都使用了虚拟 dom
- 2.组件化开发
- 3.都是单向数据流(父子组件之间,不建议子修改父传下来的数据)
- 4.都支持服务端渲染
不同点:
- 1.React 的 JSX,Vue 的 template
- 2.数据变化,React 手动(setState),Vue 自动(初始化已响应式处理,Object.defineProperty)
- 3.React 单向绑定,Vue 双向绑定
- 4.React 的 Redux,Vue 的 Vuex
# MVVM 和 MVC
MVC
- Model(模型):负责从数据库中取数据
- View(视图):负责展示数据的地方
- Controller(控制器):用户交互的地方,例如点击事件等等 思想:Controller 将 Model 的数据展示在 View 上
MVVM
- VM:也就是 View-Model,做了两件事达到了数据的双向绑定 一是将【模型】转化成【视图】,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。二是将【视图】转化成【模型】,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
- 思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应 Vue 数据驱动的思想)
Vue 是 MVVM 框架,但是不是严格符合 MVVM,因为 MVVM 规定 Model 和 View 不能直接通信,而 Vue 的 ref 可以做到这点
# 为什么 data 是个函数并且返回一个对象呢?
data 之所以只一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行 data 函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染。
# 组件之间的传值方式
- 父组件传值给子组件,子组件使用 props 进行接收
- 子组件传值给父组件,子组件使用$emit+事件对父组件进行传值
- 组件中可以使用$parent和$children 获取到父组件实例和子组件实例,进而获取数据
- 使用$attrs和$listeners,在对一些组件进行二次封装时可以方便传值,例如 A->B->C
- 使用$refs 获取组件实例,进而获取数据
- 使用 Vuex 进行状态管理
- 使用 eventBus 进行跨组件触发事件,进而传递数据
- 祖孙组件:使用 provide 和 inject
- 使用浏览器本地缓存,例如 localStorage
- 父组件获取子组件数据:作用域插槽
# v-on 监听多个方法
<input type="text" v-on="{ input:onInput,focus:onFocus,blur:onBlur, }">
# Vue 的生命周期方法
- beforeCreate(支持服务端渲染) 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
- created(支持服务端渲染) 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
- beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
- mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
- beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
- updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
- beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
- destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
- activated keep-alive 专属,组件被激活时调用
- deactivated keep-alive 专属,组件被销毁时调用
- errorCaptured(支持服务端渲染) 当捕获来自子孙组件的错误时被调用,这个钩子有三个参数:错误对象,发生错误的组件实例,错误来源信息的字符串;可以返回 false 以防止错误继续向上传递
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
# 父子组件生命周期钩子函数执行顺序
加载渲染过程
- 父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
子组件更新过程
- 父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父组件更新过程
- 父 beforeUpdate->父 updated
销毁过程
- 父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
# v-if 和 v-show
- 1.v-if 是通过控制 dom 元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期,因为显隐决定了组件的生成和销毁,v-show 由 false 变为 true 的时候不会触发组件的生命周期。 v-if 由 false 变为 true 则会触发组件的 beforeCreate、create、beforeMount、mounted 钩子,由 true 变为 false 会触发组件的 beforeDestory、destoryed 方法。
- 2.v-show 是通过控制 dom 元素的 css 样式来实现显隐,不会销毁(display: none)
- 3.频繁或者大数量显隐使用 v-show,否则使用 v-if
# this.xxx 读取顺序
props -> methods -> data -> computed
# 组件中 name 的作用
- 项目使用 keep-alive 时,可搭配组件 name 进行缓存过滤
- DOM 做递归组件时需要调用自身 name
- vue-devtools 调试工具里显示的组件名称是由 vue 中组件 name 决定的
# computed 和 watch
# computed
本质是一个惰性求值的观察者computed watcher
。其内部通过 this.dirty
属性标记计算属性是否需要重新求值。
当 computed
的依赖状态发生改变时,就会通知这个惰性的 watcher
,computed watcher
通过 this.dep.subs.length
判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher
重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true
(当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy
(懒计算)特性。)
# watch
watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开 deep:true 选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听
watch: {
firstName: {
handler(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
},
// 代表在wacth里声明了firstName这个方法之后立即执行handler方法
immediate: true
}
}
watch: {
obj: {
handler(newName, oldName) {
console.log('obj.a changed');
},
immediate: true,
deep: true
}
}
// 字符串的形式监听
watch: {
'obj.a': {
handler(newName, oldName) {
console.log('obj.a changed');
},
immediate: true,
// deep: true
}
}
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
注意:Watcher
: 观察者对象 , 实例分为渲染 watcher
(render watcher
),计算属性 watcher
(computed watcher
),侦听器 watcher
(user watcher
)三种
# Vue 的数据频繁变化但只会更新一次
- 检测到数据变化
- 开启一个队列
- 在同一事件循环中缓冲所有数据改变
- 如果同一个 watcher (watcherId 相同)被多次触发,只会被推入到队列中一次
不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run
优化后:执行顺序 update -> queueWatcher -> 维护观察者队列(重复 id 的 Watcher 处理) -> waiting 标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新 DOM)
# 为什么访问 data 属性不需要带 data
访问属性代理 this.data.xxx 转换 this.xxx 的实现
/** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
function proxy( target, prop, key ) {
Object.defineProperty( target, key, {
enumerable: true,
configurable: true,
get () {
return target[ prop ][ key ];
},
set ( newVal ) {
target[ prop ][ key ] = newVal;
}
} );
}
2
3
4
5
6
7
8
9
10
11
12
13
# Vue 中的 key 到底有什么用
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 注意:在没有 key 的情况下,会更快。 引用官网的话:key 的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
vue 中在使用相同标签名元素的过渡切换时,也会使用到 key 属性,其目的也是为了让 vue 可以区分它们,否则 vue 只会替换其内部属性而不会触发过渡效果。
# 为什么不能用 index 做 key
如果 v-for
中的列表数据顺序被改变,在 diff
比较中将错误地复用旧节点,导致不必要的重新渲染步骤
<div v-for="(item, index) in list" :key="index">{{item.name}}</div>
list: [
{ name: '小明', id: '123' },
{ name: '小红', id: '124' },
{ name: '小花', id: '125' }
]
渲染为
<div key="0">小明</div>
<div key="1">小红</div>
<div key="2">小花</div>
现在我执行 list.unshift({ name: '小林', id: '122' })
渲染为
<div key="0">小林</div>
<div key="1">小明</div>
<div key="2">小红</div>
<div key="3">小花</div>
新旧对比
<div key="0">小明</div> <div key="0">小林</div>
<div key="1">小红</div> <div key="1">小明</div>
<div key="2">小花</div> <div key="2">小红</div>
<div key="3">小花</div>
可以看出,如果用index做key的话,其实是更新了原有的三项,并新增了小花,虽然达到了渲染目的,但是损耗性能
现在我们使用id来做key,渲染为
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>
现在我执行 list.unshift({ name: '小林', id: '122' }),渲染为
<div key="122">小林</div>
<div key="123">小明</div>
<div key="124">小红</div>
<div key="125">小花</div>
新旧对比
<div key="122">小林</div>
<div key="123">小明</div> <div key="123">小明</div>
<div key="124">小红</div> <div key="124">小红</div>
<div key="125">小花</div> <div key="125">小花</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
47
48
49
50
51
52
# vue-router 路由模式
默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境) 可选值: "hash" | "history" | "abstract" 配置路由模式:
hash:
- 基于 location.hash 来实现的,location.hash 的值就是 URL 中#后面的内容。其实现原理就是监听#后面的内容来发起 Ajax 请求来进行局部更新,而不需要刷新整个页面。
- 使用 hashchange 事件来监听 URL 的变化,以下这几种情况改变 URL 都会触发 hashchange 事件:浏览器前进后退改变 URL、a 标签改变 URL、window.location 改变 URL。
history:
- history 提供了 pushState 和 replaceState 两个方法来记录路由状态,通过这两个 API 可以改变 url 地址且不会发送请求。
- history 提供类似 hashchange 事件的 popstate 事件,但 popstate 事件有些不同:通过浏览器前进或者后退改变 URL 时会触发 popstate 事件,通过 pushState/replaceState 或 a 标签改变 URL 不会触发 popstate 事件。可以拦截 pushState/replaceState 的调用和 a 标签的点击事件来检测 URL 变化,所以监听 URL 变化可以实现,只是没有 hashchange 那么方便。
- pushState(state, title, url) 和 replaceState(state, title, url)都可以接受三个相同的参数。
- 用了 HTML5 的实现,单页路由的 url 就不会多出一个 # ,变得更加美观。但因为没有 # 号,所以当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求。为了避免出现这种情况,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
# params 和 query 的区别
- url 地址显示:query 在浏览器地址栏中显示参数,params 则不显示
- query 刷新不会丢失 query 里面的数据、params 刷新会丢失 params 里面的数据。
//query语法:
this.$router.push({path:"地址",query:{id:"123"}}); //这是传递参数
this.$route.query.id; //这是接受参数
//params语法:
this.$router.push({name:"地址",params:{id:"123"}}); //这是传递参数
this.$route.params.id; //这是接受参数
2
3
4
5
6
# 动态组件
动态组件通过 is 特性实现。适用于根据数据、动态渲染的场景,即组件类型不确定。
<template>
<div v-for="val in componentsData" :key="val.id">
<component :is="val.type">
</div>
</template>
<script>
import CustomTitle from './CustomTitle'
import CustomText from './CustomText'
import CustomImage from './CustomImage'
export default {
data() {
return {
componentsData: [{
id: 1,
type: 'CustomTitle'
},{
id: 2,
type: 'CustomText'
},{
id: 3
type: 'CustomImage'
}]
}
}
}
</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
# 路由钩子函数执行顺序
# 在失活的组件里调用 beforeRouteLeave 组件守卫。
//A.vue
beforeRouteLeave(to, from) {
console.log('🚀🚀~ beforeRouteLeave');
},
2
3
4
# 调用全局的 beforeEach 全局守卫
router.beforeEach((to, from, next) => {
console.log('🚀🚀~ to:', to);
console.log('🚀🚀~ from:', from);
next();
})
2
3
4
5
# 在重用的组件里调用 beforeRouteUpdate 组件守卫。
//A.vue
beforeRouteUpdate(to, from) {
console.log('🚀🚀~ beforeRouteUpdate');
},
2
3
4
# 在路由配置里调用 beforeEnter 路由守卫
//index.js
{
path: '/a',
component: () => import('../components/A.vue'),
beforeEnter: (to, from) => {
console.log('🚀🚀~ beforeEnter ');
},
},
2
3
4
5
6
7
8
# 解析异步路由组件
# 在被激活的组件里调用 beforeRouteEnter 组件守卫
//A.vue
beforeRouteEnter(to, from,next) {
console.log('🚀🚀~ beforeRouteEnter');
},
2
3
4
# 调用全局的 beforeResolve 全局守卫
router.beforeResolve((to, from, next) => {
next();
})
2
3
# 调用全局的 afterEach 全局守卫
router.afterEach((to, from) => {
console.log('🚀🚀~ afterEach:');
})
2
3
# 触发 DOM 更新
# v-if 和 v-for 不建议用在同一标签
在 Vue2 中,v-for 优先级是高于 v-if 的
<div v-for="item in [1, 2, 3, 4, 5, 6, 7]" v-if="item !== 3">
{{item}}
</div>
2
3
上面的写法是 v-for 和 v-if 同时存在,会先把 7 个元素都遍历出来,然后再一个个判断是否为 3,并把 3 给隐藏掉,这样的坏处就是,渲染了无用的 3 节点,增加无用的 dom 操作,建议使用 computed 来解决
<div v-for="item in list">
{{item}}
</div>
computed() {
list() {
return [1, 2, 3, 4, 5, 6, 7].filter(item => item !== 3)
}
}
2
3
4
5
6
7
8
9
# 设置动态 class,动态 style
动态class对象:<div :class="{ 'is-active': true, 'red': isRed }"></div>
动态class数组:<div :class="['is-active', isRed ? 'red' : '' ]"></div>
动态style对象:<div :style="{ color: textColor, fontSize: '18px' }"></div>
动态style数组:<div :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"></div>
2
3
4
# 不需要响应式的数据
数据量大的死数据,如果都进行响应式处理,那会消耗大量性能
// 方法一:将数据定义在data之外
data () {
this.list1 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list2 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list3 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list4 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
this.list5 = { xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx }
return {}
}
// 方法二:Object.freeze()
data () {
return {
list1: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list2: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list3: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list4: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
list5: Object.freeze({xxxxxxxxxxxxxxxxxxxxxxxx}),
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 对象新属性无法更新视图,删除属性无法更新视图
- 原因:Object.defineProperty 没有对对象的新属性进行属性劫持
- 对象新属性无法更新视图:使用 Vue.$set(obj, key, value),组件中this.$set(obj, key, value)
- 删除属性无法更新视图:使用 Vue.$delete(obj, key),组件中this.$delete(obj, key)
# 直接 arr[index] = xxx 无法更新视图
- 原因:Vue 没有对数组进行 Object.defineProperty 的属性劫持,所以直接 arr[index] = xxx 是无法更新视图的
- 使用数组的 splice 方法,arr.splice(index, 1, item)
- 使用 Vue.$set(arr, index, value)
# 插槽
# 单个插槽
//子组件 : (假设名为:child)
<template>
<div class= 'child'>
<slot>子组件默认内容,父组件不在子组件写数据就会默认显示这个</slot>
</div>
</template>
//父组件:(引用子组件 child)
<template>
<div class= 'app'>
<child>
林三心
</child>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 具名插槽 (子组件多个对应插入内容)
//子组件 : (假设名为:child)
<template>
<div class= 'child'>
<slot name='one'> 这就是默认值1</slot>
<slot name='two'> 这就是默认值2 </slot>
<slot name='three'> 这就是默认值3 </slot>
</div>
</template>
//父组件:(引用子组件 child)
<template>
<div class= 'app'>
<child>
<template v-slot:"one"> 这是插入到one插槽的内容 </template>
<template v-slot:"two"> 这是插入到two插槽的内容 </template>
<template v-slot:"three"> 这是插入到three插槽的内容 </template>
</child>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 作用域插槽 (父组件在子组件处使用子组件 data)
//子组件 : (假设名为:child)
<template>
<div class= 'child'>
<slot name= 'one' :value1='child1'> 这就是默认值1</slot> //绑定child1的数据
<slot :value2='child2'> 这就是默认值2 </slot> //绑定child2的数据,这里我没有命名slot
</div>
</template>
new Vue({
el:'child',
data:{
child1:'数据1',
child2:'数据2'
}
})
//父组件:(引用子组件 child)
<template>
<div class='app'>
<child>
<template v-slot:one='slotone'>
{{ slotone.value1 }} // 通过v-slot的语法 将子组件的value1值赋值给slotone
</template>
<template v-slot:default='slotde'>
{{ slotde.value2 }} // 同上,由于子组件没有给slot命名,默认值就为default
</template>
</child>
</div>
</template>
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
# nextTick 的用处
- 响应式数据更新后,想要立即拿到页面上对应的 DOM 数据,需要用到 nextTick
- Vue 采用的是异步更新的策略,通俗点说就是,同一事件循环内多次修改,会统一进行一次视图更新,这样才能节省性能
- .Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发 生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。nextTick 方法会在队列中加入一个回调函数,确保该函数在前面的 dom 操作完成后才调用。
- microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕;
- 因为兼容性问题,vue 不得不做了 microtask 向 macrotask 的降级方案:Promise、MutationObserver、setImmediate、setTimeout.
# keep-alive
keep-alive
组件接受三个属性参数:include
、exclude
、max
include
指定需要缓存的组件name
集合,参数格式支持String
,RegExp
,Array
。当为字符串的时候,多个组件名称以逗号隔开。exclude
指定不需要缓存的组件name
集合,参数格式和include
一样。max
指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String
、Number
。
# 原理
keep-alive 实例会缓存对应组件的 VNode,如果命中缓存,直接从缓存对象返回对应 VNode LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)
# vuex 的属性
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。 Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。 Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。 Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。 Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
# vuex 中的数据在页面刷新后数据消失
用 sessionstorage 或者 localstorage 存储数据,或者用 vuex-persist 插件
# Vuex 的严格模式
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
在 Vuex.Store 构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
2
3
# 组件中使用 Vuex 的 getter 和 mutation
// 使用mapGetters辅助函数, 利用对象展开运算符将getter混入computed 对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
2
3
4
5
6
7
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
然后调用this.setNumber(10)相当调用this.$store.commit('SET_NUMBER',10)
2
3
4
5
6
7
# mutation 和 action
- action 提交的是 mutation,而不是直接变更状态。mutation 可以直接变更状态
- action 可以包含任意异步操作。mutation 只能是同步操作
- 提交方式不同
action 是用this.store.dispatch('ACTION_NAME',data)来提交。
mutation是用this.$store.commit('SET_NUMBER',10)来提交
2
接收参数不同,mutation 第一个参数是 state,而 action 第一个参数是 context,其包含了
{
state, // 等同于 `store.state`,若在模块中则为局部状态
rootState, // 等同于 `store.state`,只存在于模块中
commit, // 等同于 `store.commit`
dispatch, // 等同于 `store.dispatch`
getters, // 等同于 `store.getters`
rootGetters // 等同于 `store.getters`,只存在于模块中
}
2
3
4
5
6
7
8
9
# Vue 的 SSR
- SSR 就是服务端渲染
- 基于 nodejs serve 服务环境开发,所有 html 代码在服务端渲染
- 数据返回给前端,然后前端进行“激活”,即可成为浏览器识别的 html 代码
- SSR 首次加载更快,有更好的用户体验,有更好的 seo 优化,因为爬虫能看到整个页面的内容,如果是 vue 项目,由于数据还要经过解析,这就造成爬虫并不会等待你的数据加载完成,所以其实 Vue 项目的 seo 体验并不是很好
# Vue 的响应式原理
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
- 2、compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- 3、Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ① 在自身实例化时往属性订阅器(dep)里面添加自己 ② 自身必须有一个 update()方法 ③ 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。
- 4、MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
# 为什么只对对象劫持,而要对数组进行方法重写
因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案
# vue 渲染过程
1.调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
parse 使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST。模板 -> AST (最消耗性能)
optimize 遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 的性能
generate 将最终的 AST 转化为 render 函数字符串
- 2.调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
- 3.调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
# prop 验证,和默认值
props: {
visible: {
default: true,
type: Boolean,
required: true
},
},
2
3
4
5
6
7
# vue 中怎么重置 data
- 使用 Object.assign(),vm.data 可以获取当前状态下的 data,vm.data 可以获取当前状态下的 data
- vm.options.data(this)可以获取到组件初始化状态下的 data。
Object.assign(this.$data, this.$options.data(this)) // 注意加this,不然取不到
data(){ a: this.methodA } 中的this.methodA。
2
3
4
# Vue.set 方法的原理
function set(target, key, val) {
// 判断是否是数组
if (Array.isArray(target)) {
// 判断谁大谁小
target.length = Math.max(target.length, key)
// 执行splice
target.splice(key, 1, val)
return val
}
const ob = target.__ob__
// 如果此对象没有不是响应式对象,直接设置并返回
if (key in target && !(key in target.prototype) || !ob) {
target[key] = val
return val
}
// 否则,新增属性,并响应式处理
defineReactive(target, key, val)
return val
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Vue.delete 方法的原理
function del (target, key) {
// 判断是否为数组
if (Array.isArray(target)) {
// 执行splice
target.splice(key, 1)
return
}
const ob = target.__ob__
// 对象本身就没有这个属性,直接返回
if (!(key in target)) return
// 否则,删除这个属性
delete target[key]
// 判断是否是响应式对象,不是的话,直接返回
if (!ob) return
// 是的话,删除后要通知视图更新
ob.dep.notify()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 冷门的知识点
# 子组件改变 props 里的数据
- 如果修改的是基本类型,则会报错
- 改变的 props 数据是引用类型,不报错,并且父级数据会跟着变
# props 怎么自定义验证
props: {
num: {
default: 1,
validator: function (value) {
// 返回值为true则验证不通过,报错
return [
1, 2, 3, 4, 5
].indexOf(value) !== -1
}
}
}
2
3
4
5
6
7
8
9
10
11
# watch 的 immediate 属性有什么用
比如平时 created 时要请求一次数据,并且当搜索值改变,也要请求数据
一般写法
created(){
this.getList()
},
watch: {
searchInputValue(){
this.getList()
}
}
2
3
4
5
6
7
8
使用 immediate 可以这么写,当它为 true 时,会初始执行一次
watch: {
searchInputValue:{
handler: 'getList',
immediate: true
}
}
2
3
4
5
6
# watch 监听一个对象时,如何排除某些属性的监听
下面代码是,params 发生改变就重新请求数据,无论是 a,b,c,d 属性改变
data() {
return {
params: {
a: 1,
b: 2,
c: 3,
d: 4
},
};
},
watch: {
params: {
deep: true,
handler() {
this.getList;
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
只想要 a,b 改变时重新请求,c,d 改变时不重新请求
mounted() {
Object.keys(this.params)
.filter((_) => !["c", "d"].includes(_)) // 排除对c,d属性的监听
.forEach((_) => {
this.$watch((vm) => vm.params[_], handler, {
deep: true,
});
});
},
data() {
return {
params: {
a: 1,
b: 2,
c: 3,
d: 4
},
};
},
watch: {
params: {
deep: true,
handler() {
this.getList;
},
},
}
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
# 审查元素时发现 data-v-xxxxx
这是在标记 vue 文件中 css 时使用 scoped 标记产生的,因为要保证各文件中的 css 不相互影响,给每个 component 都做了唯一的标记,所以每引入一个 component 就会出现一个新的'data-v-xxx'标记
# computed 如何实现传参
// html
<div>{{ total(3) }}
// js
computed: {
total() {
return function(n) {
return n * this.num
}
},
}
2
3
4
5
6
7
8
9
10
11
12
# vue 的 hook 的使用
常用的使用定时器的方式
export default{
data(){
timer:null
},
mounted(){
this.timer = setInterval(()=>{
//具体执行内容
console.log('1');
},1000);
}
beforeDestory(){
clearInterval(this.timer);
this.timer = null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
不好的地方在于:得全局多定义一个 timer 变量,可以使用 hook 这么做:
export default{
methods:{
fn(){
const timer = setInterval(()=>{
//具体执行代码
console.log('1');
},1000);
this.$once('hook:beforeDestroy',()=>{
clearInterval(timer);
timer = null;
})
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
子组件需要在 mounted 时触发父组件的某一个函数,平时都会这么写:
//父组件
<rl-child @childMounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},
// 子组件
mounted () {
this.$emit('childMounted')
},
2
3
4
5
6
7
8
9
10
11
12
13
使用 hook
//父组件
<rl-child @hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
},
2
3
4
5
6
7
8
# provide 和 inject 是响应式的吗
// 祖先组件
provide(){
return {
// keyName: { name: this.name }, // value 是对象才能实现响应式,也就是引用类型
keyName: this.changeValue // 通过函数的方式也可以[注意,这里是把函数作为value,而不是this.changeValue()]
// keyName: 'test' value 如果是基本类型,就无法实现响应式
}
},
data(){
return {
name:'张三'
}
},
methods: {
changeValue(){
this.name = '改变后的名字-李四'
}
}
// 后代组件
inject:['keyName']
create(){
console.log(this.keyName) // 改变后的名字-李四
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# mixin 数据冲突
- 当组件 data 选项与混入 data 选项冲突时以组件 data 优先
- 当组件和 mixin 同时定义生命周期选项,两个都会触发,而且 mixin 会先触发.
- 组件和 mixin 同时定义相同方法,会使用组件方法,会覆盖 mixin.
# Vue 的 el 属性和$mount 优先级
new Vue({
router,
store,
el: '#app',
render: h => h(App)
}).$mount('#ggg')
// el优先级 > $mount
2
3
4
5
6
7
8
# 动态指令和参数
<template>
...
<aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
...
data(){
return{
...
someEvent: someCondition ? "click" : "dbclick",
someProps: someCondition ? "num" : "price"
}
},
methods: {
handleSomeEvent(){
// handle some event
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 相同的路由组件如何重新渲染
经常遇到的情况是,多个路由解析为同一个 Vue 组件。问题是,Vue 出于性能原因,默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化。
const routes = [
{
path: "/a",
component: MyComponent
},
{
path: "/b",
component: MyComponent
},
];
2
3
4
5
6
7
8
9
10
如果依然想重新渲染,可以使用 key
<template>
<router-view :key="$route.path"></router-view>
</template>
2
3
# 自定义 v-model
默认情况下,v-model 是 @input 事件侦听器和 :value 属性上的语法糖。但是,可以在 Vue 组件中指定一个模型属性来定义使用什么事件和 value 属性
export default: {
model: {
event: 'change',
prop: 'checked'
}
}
2
3
4
5
6
# 获取 data 中某一个数据的初始状态
data() {
return {
num: 10
},
mounted() {
this.num = 1000
},
methods: {
howMuch() {
// 计算出num增加了多少,那就是1000 - 初始值
// 可以通过this.$options.data().xxx来获取初始值
console.log(1000 - this.$options.data().num)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 计算变量时,methods 和 computed 哪个好
<div>
<div>{{howMuch1()}}</div>
<div>{{howMuch2()}}</div>
<div>{{index}}</div>
</div>
data: () {
return {
index: 0
}
}
methods: {
howMuch1() {
return this.num + this.price
}
}
computed() {
howMuch2() {
return this.num + this.price
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
computed 会好一些,因为 computed 会有缓存。例如 index 由 0 变成 1 ,那么会触发视图更新,这时候 methods 会重新执行一次,而 computed 不会,因为 computed 依赖的两个变量 num 和 price 都没变。
# 父组件如何监听子组件生命周期
$emit 实现
// 父组件
<template>
<div class="parent">
<Child @mounted="doSomething"/>
</div>
</template>
<script>
export default {
methods: {
doSomething() {
console.log('父组件监听到子组件 mounted 钩子函数')
}
}
}
</script>
//子组件
<template>
<div class="child">
</div>
</template>
<script>
export default {
mounted() {
console.log('触发mounted事件...')
this.$emit("mounted")
}
}
</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
@hook 实现
// 父组件
<template>
<div class="parent">
<Child @hook:mounted="doSomething"/>
</div>
</template>
<script>
export default {
methods: {
doSomething() {
console.log('父组件监听到子组件 mounted 钩子函数')
}
}
}
</script>
//子组件
<template>
<div class="child">
</div>
</template>
<script>
export default {
mounted() {
console.log('触发mounted事件...')
}
}
</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
# 函数式组件
- 无状态
- 无法实例化
- 内部没有任何生命周期处理函数
- 轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修改)
- 在 template 标签里标明 functional
- 只接受 props 值
- 不需要 script 标签
<template functional>
<div>
<p v-for="(item,index) in props.items" :key="index" @click="props.itemClick(item)" />
</div>
</template>
2
3
4
5
# 监听 Vuex 里面的数据的变更
- 在
.vue
文件中可以直接用watch
来监听 - 那么如何在
js
或者其他文件中监听?有两种方法
第一种:store.subscribe();subscribe 是 vuex 的一个实例方法,它通过订阅 store 的 mutation,接收一个函数作为参数,会在每个 mutation 完成后调用
- 明显的缺点就是由于每个 mutation 方法完成后都会调用,所以如果只想订阅某个特定的数据需要写逻辑来判断
import store from "../src/store/index";
const subFun = store.subscribe((mutation, state) => {
console.log("rd: state", state);
console.log("rd: mutation", mutation);
console.log("rd: mutation.type", mutation.type); // type就是我们当前执行的mutation方法的方法名
console.log("rd: mutation.payload", mutation.payload); // payload就是我们当次执行mutation所传入的值
});
setTimeout(() => {
subFun(); // 调用返回值可以取消订阅
}, 10000);
2
3
4
5
6
7
8
9
10
11
12
13
14
第二种方法:store.watch(); 相比于上一种方法可以监听特定的数据
import store from "../store/index";
const watchFun = store.watch(
state => state.pathName,
(newValue, oldValue) => {
console.log("search string is changing");
console.log("rd: newValue", newValue);
console.log("rd: oldValue", oldValue);
}
);
setTimeout(() => {
watchFun();
}, 10000);
2
3
4
5
6
7
8
9
10
11
12
13
14
- 在相应的 js 文件引入 store
- 调用 store 的 watch 实例方法,第一个函数参数返回一个需要监听的 state 中的值(比如我想监听 vuex 中的 pathName 的变化情况,就返回该值)
- 第二个参数同 vue 的 watch,接收 2 个参数代表新旧值
- 通过变量 watchFun 接收 watch 的返回值,调用该方法会停止监听
# Vuex 如何知道 State 是通过 Mutation 修改还是外部修改
Vuex 中修改 state 唯一渠道是执行 commit 方法,底层通过执行 this._withCommit(fn),且设置_committing 标识符为 true,才能修改 state,修改完还需要将标识符置为 false。外部修改是无法设置标识位的,所以通过 watch 监听 state 变化,来判断修改的合法性。
# new Vue 后整个的流程
- initProxy:作用域代理,拦截组件内访问其它组件的数据。
- initLifecycle:建立父子组件关系,在当前组件实例上添加一些属性和生命周期标识。如[Math Processing Error]parent,parent,refs,$children,_isMounted 等。
- initEvents:对父组件传入的事件添加监听,事件是谁创建谁监听,子组件创建事件子组件监听
- initRender:声明[Math Processing Error]slots 和 slots 和 createElement()等。
- initInjections:注入数据,初始化 inject,一般用于组件更深层次之间的通信。
- initState:重要)数据响应式:初始化状态。很多选项初始化的汇总:data,methods,props,computed 和 watch。
- initProvide:提供数据注入。
- 01
- 2023/07/03 00:00:00
- 02
- 2023/04/22 00:00:00
- 03
- 2023/02/16 00:00:00