vue
常用高级特性、自定义v-model、slot插槽、mixin抽离公共代码、keep-alive缓存组件、$nextTick、动态组件、异步组件、vuex、vue-router等。vue中的响应式原理、组件渲染过程、模板渲染、虚拟DOMvdom、diff算法等。
Vue 高级特性
自定义v-model
:value与@input 结合实现
1
2
3
4
5
6
7<!-- input-price -->
<input type="text" :value="price"
@input="price=$event.target.value">
<!-- app.vue -->
<div id="app">
<input-price v-model="price"></input-price>
</div>slot
插槽 作用域插槽 具名插槽
mixin
抽离多个组件中相同的逻辑
mixin存在问题:1. 变量来源不明确,不利于阅读多 2. mixin可能造成命名冲突 3. mixin组件可能出现多对多关系,复杂度较高. vue3 composition API 解决这些问题
1
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
59
60
61
62
63
64
65
66
67
68
69
70<!-- mixin.js -->
export default {
data() {
return { city: '北京' }
},
methods: {
showName() { console.log(this.name) }
},
mounted() {
console.log('mixin mounted', this.name)
}
}
<!-- mixin demo -->
<template>
<div>
<p>{{name}} {{major}} {{city}}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可以添加多个,会自动合并起来
data() {
return {
name: 'tew',
major: 'web 前端'
}
},
methods: {},
mounted() {
console.log('component mounted', this.name)
}
}
</script>
<!--
当组件和混入对象含有同名选项时,选项会以恰当的方式进行合并
1. data中的数据对象在内部会进行递归合并, 并在冲突时优先使用组件中data定义的数据
2. 同名钩子函数(生命周期函数)将合并为一个数组, 都会被调用, 混入对象的钩子函数(生命周期函数)会在组件自身钩子之前调用
3. 值为对象的选项,如methdos, components和drictives,将被合并为同一个对象,键名冲突时,取组件自身对象的键值对
-->
<script>
var mixin = {
data () {
return { msg: 'hello' }
},
mounted () {
console.log('from mixin')
},
methods: {
test () { console.log('methods test from mixin') }
}
}
export default {
mixins: [mixin],
data () {
return { msg: 'world' }
},
mounted () {
console.log(this.msg, 'self') // world
this.test() // methods test from self
},
methods: {
test () { console.log('methods test from self') }
}
}
// 1. 'from mixin'; // 同名钩子函数 都将被调用 mixin钩子函数先执行
// 2. world self // 数据冲突时 优先使用组件自身数据
// 3. methods test from self // methods 对象键名冲突时 优先使用组件自身键值对
</script>keep-alive
缓存 频繁切换不需要重复渲染组件
1
2
3
4
5<keep-alive> <!-- tab 切换 -->
<KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
<KeepAliveStageB v-if="state === 'B'"/>
<KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>$nextTick与异步更新
由于异步渲染 会在DOM渲染完之后被触发,以获取最新DOM节点
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
refs
元素设置 ref="ul" 属性 获取DOM元素 this.$refs.ul
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/* <ul ref="ul">
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button> */
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul
console.log( ulElem.childNodes.length )
})
}动态组件
<component :is="componentname" />
- 异步组件 按需加载
components: { DescPopup: () => import('../components/descPopup')}
- vue 生命周期
- vuex
state mutation getters action
dispatch commit mapState mapMutations mapGetters mapActions
- vue-router
- 路由模式(hash(默认) H5 history(需要server端支持))
- 路由配置(动态路由(动态参数) 懒加载)
- 路由守卫
- event事件是 原生的event事件 事件被挂载到当前元素
Vue原理
Vue原理主要三部分: 组件化、响应式、vdom和diff
- 组件化和MVVM
数据驱动视图 M model V view VM 模型(指令 事件等将view和model链接起来)
响应式原理 核心API
Object.defineProperty
1
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// Object.defineProperty的缺点
// 1. 复杂对象需要深度监听 需要递归调用, 一次性计算量大
// 2. 无法监听新增属性/删除属性(Vue.set/Vue.delete)
// 3. 无法监听原生数组 需要特殊处理 重写了数组的7个方法
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
observer(value) // 深度监听
Object.defineProperty(target, key, { // 核心 API
get() { return value },
set(newValue) {
if (newValue !== value) {
observer(newValue) // 深度监听
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
return target // 不是对象或数组
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}vdom(Virtual DOM)
是实现vue
和react
的重要基石snabbdom vdom库
1
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// vdom 数据驱动视图 控制DOM操作
// vdom 用JS模拟DOM结构 计算出最小变更, 操作DOM
/* 真实DOM
<div id="div1" class="wrap">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>
*/
/* JS 模拟DOM
{
tag: 'div',
props:{ className: 'wrap', id: 'div1' },
children: [
{ tag: 'p', children: 'vdom' },
{
tag: 'ul',
props: { style:'font-size: 20px'},
children: [
{ tag: 'li', children: 'a' }
]
}
]
}
*/diff算法是vdom中最核心、最关键的部分 vdom树的diff对比
1
2
3
4// 只比较同一层级 不跨级比较
// tag不相同 则直接删除重建,不再深度比较
// tag和key 两者都相同 则认为是相同节点,不再深度比较
// vdom核心概念: h, vnode, ptach, diff, key等模板编译
with语法
1
2
3
4
5
6
7
8const obj = { a: 100, b: 200 }
// with 能改变 {} 内自由变量的查找方式 将 {} 内自由变量当做 obj 的属性来查找
with(obj){
console.log(a) // 100
console.log(b) // 200
console.log(c) // 报错
}
// with 慎用 他打破了作用域规则 易读性变差vue-template-complier
将模板编译为render函数1
2
3
4// 模板编译是将模板转化为某种JS代码
// 模板编译为render函数 执行render函数返回vnode
// 基于vnode在执行patch和diff
// webpack vue-loader 会在开发环境编译模板执行render函数生成vnode render函数代替template
1
2
3
4
5
6
7
8Vue.component('title', {
// template: '<div class="test"><a href="xxx">this is a tag</a></div>'
render: function(createElement){
return createElement('div',{ className: 'test'} , [
createElement(a, { attrs: { href: 'xxx' }}, 'this is a atg')
])
}
})
组件渲染/更新过程
初次渲染
- 解析模板为render函数
- 触发响应式, 监听data属性getter setter
- 执行render函数(会触发getter), 生成vnode, patch(elem, vnode)
更新过程
- 修改data, 触发setter
- 重新执行render函数, 生成newVnode
- patch(vnode, newVnode)
- 前段路由
hash
hash变化不会刷新页面, 会触发网页跳转,即浏览器的前进、后退
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
</body>H5 history
主要使用 history.pushState和window.onpopstate 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由 用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1')
})
// 监听浏览器前进、后退
window.onpopstate = (event) => {
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html
</script>
</body>
Vue面试题
v-show和v-if的区别
1
2
3// v-show 通过css display 控制显示和隐藏
// v-if 组件真正的渲染和销毁, 而不是显示和隐藏
// 频繁切换显示状态用 v-show, 否则用 v-if为何v-for中要用key
key可以提升vue的编译效率 提高复用
key不能是index和random, 因为diff算法中通过tag和key来判断, 是否是相同节点sameNode
当使用index索引做key时, 删除列表元素会有bug(bug产生原因就是vue就地复用策略导致的)
- 生命周期(父子组件生命周期)
加载渲染过程父beforeCreate->父created->父beforeMounted->子beforeCreate->子created->子beforeMount->子mounted->父mounted
子组件更新过程父beforeUpdate->子beforeUpdate->子updated->父updated
父组件更新过程父beforeUpdate->父updated
销毁过程父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
- Vue组件如何通讯
- 父子组件
props和$emit
- 自定义事件
event.$on event.$emit event.$off
- vuex
- provide和inject
- $parent和$children/$refs
- eventbus
- $attrs/$listeners
- dispatch/boardcast
- 描述组件渲染和和更新的过程
初次渲染
- 解析模板为render函数
- 触发响应式, 监听data属性getter setter
- 执行render函数(会触发getter), 生成vnode, patch(elem, vnode)
更新过程
- 修改data, 触发setter
- 重新执行render函数, 生成newVnode
- patch(vnode, newVnode)
双向数据绑定v-model的实现原理
:value与@input 结合实现
1
2
3
4
5
6
7<!-- input-price -->
<input type="text" :value="price"
@input="price=$event.target.value">
<!-- app.vue -->
<div id="app">
<input-price v-model="price"></input-price>
</div>computed有何特点
computed 会缓存, data不变不会重新计算 提高性能
如何将组件所有props传递给子组件
<com v-bind="$props">
何时使用异步组件
加载大组件 路由异步加载
何时需要使用
beforeDestroy
- 解绑自定义事件 event.$off
- 清楚定时器
- 解除自定义的DOM事件, 如scroll等
- vuex中action和mutation有何区别
- action中处理异步, mutation不可以处理异步只处理同步
- action可以整合多个mutation
- Vue常见性能优化方式
- 自定义事件 dom事件及时销毁
- 合理使用异步组件
- 合理使用keep-alive
- data层级不要太深