常用知识
# 日常工作知识
# 1. Vue 中 v-if 和 v-for 的优先级
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true 值的时候被渲染。
v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用
item in items
形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名,在 v-for 的时候,建议设置 key 值,并且保证每个 key 值是独一无二的,这便于 diff 算法进行优化。Vue2 中 v-for 的优先级高于 v-if,如果两者同时出现会导致 for 循环始终被执行,浪费性能,不建议同时使用。
可以使用 template 包裹 v-for 所在的元素标签,在 template 层使用 v-if 判断。
从 Vue2 源码中查看两者的优先级(源码位置:\vue-dev\src\compiler\codegen\index.js 大致 56 行)
export function genElement (el: ASTElement, state: CodegenState): string { if (el.parent) { el.pre = el.pre || el.parent.pre } if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Vue3 中 v-if 的优先级高于 v-for,此时两者是不可出现在同一个元素上的。
结论:Vue2 中 v-for 的优先级高于 v-if,Vue3 中 v-if 的优先级高于 v-for ,尽量避免在同一个元素上面同时使用 v-if 和 v-for,建议使用计算属性替代。
# 2. Vue2.x 中 data 为什么是一个函数而不是一个对象
# 2.1 原因
Vue 根实例的时候定义 data 属性既可以是一个对象,也可以是一个函数(根实例是单例),不会产生数据污染情况。
在定义好一个组件时,Vue 最终都会通过 Vue.extend()构成组件实例,如果 data 是一个对象,会导致多个组件实例共用同一个 data 数据(多个组件实例指向同一个内存地址,不存在数据隔离),会导致组件之间的数据互相影响,产生数据污染。
Vue 组件实例 data 必须是函数,目的是为了防止多个组件实例对象之间共用一个 data,产生数据污染。采用函数的形式, initData 初始化时会将其作为工厂函数都会返回全新 data 对象。
# 2.2 原理分析
Vue 初始化 data (源码位置:/src/core/instance/state.js)
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} ... }
1
2
3
4
5
6
7Vue 组件在创建时,自定义组件会进入
mergeOptions
进行选项合并(源码位置:/src/core/util/options.js)Vue.prototype._init = function (options?: Object) { ... // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17会对定义的 data 进行数据校验,此时的 VM 实例为 undefined,会进入 if 判断,若 data 类型不是 function,则会出现警告。(源码位置:/src/core/instance/init.js)
strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { if (childVal && typeof childVal !== "function") { process.env.NODE_ENV !== "production" && warn( 'The "data" option should be a function ' + "that returns a per-instance value in component " + "definitions.", vm ); return parentVal; } return mergeDataOrFn(parentVal, childVal); } return mergeDataOrFn(parentVal, childVal, vm); };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3. Vue 中如何创建一个非响应式对象
# Vue 2 中实现方式
方式一:将数据定义在 Vue 实例之外(即 export default 之外),注意,这样定义的数据不能在 template 模板内使用,且此变量会被所有实例对象共享,当其中一个实例对象修改了此数据时,另外的实例对象的数据也会被修改,且此方式可能会导致内存泄露,需要谨慎使用。
const test = { //... } export default { //... }
1
2
3
4
5
6
7方式二:将数据定义在 created 声明周期钩子函数中,这样定义的数据可以在 template 中使用,但数据的定义被分在了两个地方。
export default { data() { return { // ... //一些属性 } }, created() { this.test = { // ··· //一些属性 } } // ··· //一些属性 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14方式三:通过 Object.defineProperty 设置目标对象的 configurable 属性为 false,此时可以使用 this.test 访问变量并可在 template 模板中使用。
export default { data() { const data = { test: { // ... //一些属性 } //··· // 其他属性 } Object.defineProperty(data, 'test', { configurable: false }) return data } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17方式四:使用
Object.freeze()或Object.preventExtensions()或Object.seal()
对数据进行冻结,冻结之后此对象再也不能被修改了。这三个方法返回传递的对象,而不是创建一个被冻结的副本。但需要注意,这三种方法都只是浅冻结即只冻结一层,如果存在嵌套对象则深层对象仍然可以改变。export default { data() { return { testData: Object.freeze({}) } } }
1
2
3
4
5
6
7- 当被冻结对象改变时,重新调用 Object.freeze()赋值
updateTestData (newTestData) { this.testData = Object.freeze(newBigData) }
1
2
3方式五:将数据定义在组件的自定义属性上,此时通过
this.$options.testData
就可以正常访问数据,但此时的数据也是被定义在了两个地方,且会增加调用链,如果数据更改,则需要手动调用 this.$forceUpdate()才能使模板更新。<template> <div>{{ $options.testData.test }}</div> </template> <script> export default { // 自定义属性 testData: { test: 'xxx' }, data() { return {} }, methods: { doSomething() { return this.$options.testData } } } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 3.2 Vue3 中实现方式
在 Vue3 中,可以使用 markRaw 标记一个对象,使其永远不会转换为 proxy。返回对象本身,具体可参考 官方文档 (opens new window)。
const foo = markRaw({}) console.log(isReactive(reactive(foo))) // false // 嵌套在其他响应式对象中时也可以使用 const bar = reactive({ foo }) console.log(isReactive(bar.foo)) // false
1
2
3
4
5
6
7
8
9
# 4. Vue nextTick 实现原理
# 4.1 nextTick 是什么,为什么要有这个概念的出现?
Vue 在更新 DOM 时是异步执行的,当监听到数据发生变化的时候不会立即去更新 DOM,而是开启一个异步更新任务队列,并缓存在同一事件循环中发生的所有数据变更,如果一直修改相同数据,异步操作队列还会进行去重,在同一事件循环中的所有数据变化完成之后再统一执行更新 DOM 操作;
这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作 DOM 的次数,提高性能。
Vue.nextTick 是全局方法,vm.$nickTick 是实例方法,两者使用方式类似,不同的是 vm.$nickTick 会将回调的 this 自动绑定到调用它的实例上。
# 4.2 使用场景
如果想要在修改数据后立刻得到更新后的 DOM 结构,可以使用 Vue.nextTick(),放在 nextTick 当中的操作不会立即执行,而是等数据更新、DOM 更新完成之后再执行,
第一个参数为:回调函数(可以获取最近的 DOM 结构)
第二个参数为:执行函数上下文
export default {
data() {
return {
msg: '哈哈哈哈'
}
},
created() {
this.$nextTick(() => {
// ...DOM 更新后做某些事
})
},
methods: {
test() {
// 修改数据
this.msg = 'changed'
// DOM 还没有更新
this.$nextTick(function() {
// DOM 现在更新了
// `this` 绑定到当前实例
console.log('新的值', this.msg)
})
}
}
}
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
# 4.3 执行过程
nextTick 提供了四种异步方法 Promise.then、MutationObserver、setImmediate、setTimeout(fn,0),优先级由高到低
把回调函数放入 callbacks 等待执行,callbacks 也就是异步操作队列
将执行函数放到微任务或者宏任务中
事件循环到了微任务或者宏任务,执行函数依次执行 callbacks 中的回调
# 5. Vue 组件之间通信的方式
# 5.1 props 父传子
# 5.2 $emits 子传父
# 5.3 ref 父操作子
# 5.4 provide/inject 父子或祖孙
- provide/ inject 是 Vue2.2.0 新增的 api, 简单来说就是父组件中通过 provide 来提供变量, 然后再子组件中通过 inject 来注入变量。
假设有三个组件: A.vue、B.vue、C.vue 其中 C 是 B 的子组件,B 是 A 的子组件
// A.vue
<template>
<div>
<comB></comB>
</div>
</template>
<script>
import comB from '../components/test/comB.vue'
export default {
name: 'A',
provide: {
for: 'demo'
},
components: {
comB
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// B.vue
<template>
<div>
{{ demo }}
<comC></comC>
</div>
</template>
<script>
import comC from '../components/test/comC.vue'
export default {
name: 'B',
inject: ['for'],
data() {
return {
demo: this.for
}
},
components: {
comC
}
}
</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
// C.vue
<template>
<div>
{{ demo }}
</div>
</template>
<script>
export default {
name: 'C',
inject: ['for'],
data() {
return {
demo: this.for
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 5.5 eventBus 事件总线 兄弟组件
Vue3 不再提供$on 与 emit 函数,Vue 实例不再实现事件接口,具体可查询官网 (opens new window)。Vue 官方已不推荐使用全局事件总线在组件之间通信,如果想用,可以采用其他库来实现,例如 mitt.js (opens new window)
# 5.6 $children $parent 父子之间传值
在#app 上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent 得到的是 undefined,而在最底层的子组件拿$children 是个空数组。$children 的值是数组,而$parent 是个对象。
# 5.7 $attrs $listeners 父子或祖孙
Vue3 中已删除 $listeners 对象,事件监听器现在只是属性,以 on 为前缀,因此是 $attrs 对象的一部分,
# 5.8 Vuex
# 5.9 localStorage、sessionStorage
# 参考链接
# 6. 函数的防抖和节流
# 6.1 什么是防抖、节流?
防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着 N 秒内函数只会被执行一次(最后一次),如果 N 秒内再次被触发,则重新计算延迟时间。
节流函数的作用是规定一个单位时间,在这个单位时间内最多只能触发一次函数执行,如果这个单位时间内多次触发函数,只能有一次生效。
函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于间隔时间执行,在一个单位时间内,只能触发一次函数,如果这个单位时间内触发多次函数,只有一次生效。
节流防抖就好比乘电梯,比如 delay 是 10 秒,防抖就是电梯每进来一个人就要等 10 秒再运行,而节流就是电梯保证每 10 秒可以运行一次。
# 6.2 常见使用场景
防抖的应用场景:(防抖可能用于无法预知的用户主动行为,如用户输入内容去服务端动态搜索结果。用户打字的速度等是无法预知的,具有非规律性。)
搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
搜索联想功能
窗口大小 Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
节流的应用场景:(节流可能用于一些非用户主动行为或者可预知的用户主动行为,如用户滑动商品橱窗时发送埋点请求、滑动固定的高度是已知的逻辑,具有规律性。)
滚动加载,加载更多或滚到底部监听
高频点击提交,表单重复提交
# 6.3 用法
- 防抖
/**
* @description 函数防抖
* @param {fn : function} 回调执行函数
* @param {delay : number} 延迟时间
* @return function
*/
export function debounce(fn, delay = 200) {
let timer
return function() {
let that = this
let args = arguments
timer && clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(that, args)
}, delay)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 节流
/**
* @description 函数节流
* @param {fn : function} 回调执行函数
* @param {delay : number} 延迟时间
* @return function
*/
export function throttle(fn, delay = 200) {
let timer
return function() {
let that = this
let args = arguments
if (timer) {
return
}
timer = setTimeout(function() {
fn.apply(that, args)
timer = null
}, delay)
}
}
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
# 参考链接
# 7. Vue 中计算属性 computed 和 watch 的区别
computed
computed 会被缓存,不支持异步。
注意,computed 在****响应式****的依赖项发生变化时会触发 getter,前提是 computed 里的值必须要在 template 模板里使用,如果在模板中没有使用 computed 里的值,该计算属性中的 getter 是不会触发的。
如果使用 v-model 绑定 computed 计算后的值,再修改这个计算后的值时,会依次触发 setter --> getter(不一定会触发) --> updated,但并不意味着所有的修改 computed 计算后的值的操作都会依次执行这个顺序,只有在 setter 中修改了 computed 计算所依赖的响应式数据时才会触发 getter。
watch
watch 不会被缓存,可以支持异步操作。
Vue 实例会在实例化的时候调用 $watch(),
# 8. 元素透明实现方式,有何区别?
方式一:使用 opacity 设置元素的不透明度,取值从 0.0 到 1.0,值越小,越透明。
方式二:使用 rgba 设置元素的颜色,其中 a 为元素的透明度,取值从 0.0 到 1.0,0.0 表示完全透明,1.0 为完全不透明,不可继承。
两者都是 css3 新增的属性,opacity 会继承父元素的 opacity 属性,且子元素设置的 opacity 属性只有再小于或等于父元素的 opacity 属性值时才会生效,否则不生效。rgba 属性则只会对当前设置的元素本身生效,不会对子元素产生影响。
# 9. 两个一维数组合并为一个二维数组
let data = ['哈哈哈','嘿嘿嘿','哎呦喂']
let array = ['3','4','5']
data = data.map((currentValue,index)=>[currentValue,array[index]])
//方式二
data = Array.from(data,(item,index)=> [item,array[index]])
console.log(JSON.stringify(data))
2
3
4
5
6
7
8
9