0. vue的diff算法
- diff算法比较:
- 只进行同层级比较执行patch(不同则替换),
- 如果节点不相同(主要根据标签和key等是否一致),则直接生成新节点(不复用)替换原节点dom。
- 如果节点相同(复用),执行patchVnode,根据新节点对旧节点修改属性,文本和子节点children(删除或者新增children的子节点dom)
- 相同节点更新时,如果新旧vnode都有子节点,则对应的子节点需要执行updateChildren( 四种比较方式+key查询在此过程),来更新子节点的属性和data等,并将此节点移动到对应的位置。
在updateChildren中,如果没找到可复用的节点,则是生成新节点dom后插入到oldStartVnode前面,** 不是替换 **。 - 最后在updateChildren中,如果原虚拟节点先比较完,说明新虚拟节点都是新增的直接新建插入,如果新虚拟节点先比较完毕,说明剩下的旧虚拟节点都应该删除。
// 是否是相同节点的判断函数
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
diff过程时,都没找到该newVnode则新建并插入,newStart++,oldStart并不发生改变
1. Vue中列表组件写key的作用
主要用于diff时,快速找到要复用的节点,找不到就新生成,没有加key永远都认为是一个相同的节点,会直接修改当前节点(tag一致时), diff算法中,新旧虚拟dom首先进行头头,尾尾,头尾,尾头对比(交叉对比),四种比较如果都没找到相同节点,则会1:如果有key,则使用map(map是key=>节点位置的index)映射,去看看节点列表中是否有要复用的节点(如果有则直接复用该节点)。2:如果上面都没有则直接根据新的虚拟节点列表的头指针位置的vnode生成新的节点,并插入Dom对应位置。
// vue项目 src/core/vdom/patch.js -488行
// 以下是为了阅读性进行格式化后的代码
// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
// map 方式获取原节点,进行服用
idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
// 新建节点并插入
}
2. 父子组件通信的方式
props和$emit
this.$refs.child 和 this.$parent
this.$ref和this.$parent(this.$child获取子组件数组,效果跟this.$refs一样,vue3中废弃)都能获取实例,直接访问data和method
eventBus
主要是使用一个新的Vue的实例的 $on 和 $emit方法,只监听一次用$once,$off移出监听(或者自己实现EventEmitter)
-
EventBus.$off('事件名', callback),只移除这个回调的监听器。 -
EventBus.$off('事件名'),移除该事件所有的监听器。 -
EventBus.$off(), 移除所有的事件监听器,注意不需要添加任何参数。 缺点 -
大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。
-
A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。
优点
- 解决了多层组件之间繁琐的事件传播。
- 使用原理十分简单,代码量少。
// 两种引入方式:全局和局部
// 局部:创建一个EventBus.js文件
import Vue from 'vue' // 引入vue
const EventBus = new Vue() // 创建实例
export default EventBus // 导出
// 全局:
import Vue from 'vue' // 引入vue
Vue.prototype.$EventBus = new Vue();
// a.vue,监听
<template>
<div>
<h3>页面A</h3>
<router-link to="/b">
跳转B页面
</router-link>
</div>
</template>
<script>
export default {
created () {
console.log('----A页面监听事件----')
// 使用Vue原型链引入
this.$EventBus.$on('getNum', (num) => {
console.log('num', num)
})
},
// a.vue 添加$off方法,防止内存泄露
beforeDestroy () {
console.log('----A页面销毁监听事件----')
this.$EventBus.$off('getNum')
}
}
</script>
// b.vue,触发
<template>
<div>
<h3>页面B</h3>
<router-link to="/a">
跳转A页面
</router-link>
</div>
</template>
<script>
// import { EventBus } from "../Bus.js"; 局部使用
export default {
created () {
console.log('----B页面触发事件----')
// 使用Vue原型链引入
this.$EventBus.$emit('getNum', num)
})
}
}
</script>
provide/inject,$attrs和$listeners(父子,爷孙等组件)
provide/inject是vue 2.2.0 新出的api,主要用于父、子、孙及所有后代之间的通信。通过provide属性在父组件中提供指定属性,在需要的子孙组件(不论组件嵌套有多深)中通过inject注入所需属性。
// parent组件
<template>
<div>
<div>{{test}}</div>
<button @click="changeTest">修改test的值</button>
<son></son>
</div>
</template>
<script>
import Son from "./Son";
export default {
name: "parent",
components: { Son },
provide() {
return {
injectData: this.test
};
},
data() {
return {
test: "测试",
};
},
methods: {
changeTest() {
this.test = "测试后";
}
}
};
</script>
//son组件
<template>
<div>{{injectData}}</div>
</template>
<script>
export default {
name: "son",
inject: ["injectData"],
mounted() {
// eslint-disable-next-line
console.log(this.injectData)
},
};
</script>
vuex(也可以非父子组件)
3. vue3的ref, reactive, toRef, toRefs
- ref: 用于将基础类型(引用类型也行)的数据转成响应式,(除了模板中,其他地方都是.value获取和修改值)
<template>
<div>
<div>countRef:{{countRef}}</div>
<div>objCountRef:{{objCountRef.count}}</div>
<div>爱好:{{hobbyRef.join('---')}}</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'refdemo',
setup () {
// 值类型
const countRef = ref(1)
// 对象
const objCountRef = ref({ count: 1 })
// 数组
const hobbyRef = ref(['爬山', '游泳'])
setTimeout(() => {
// 通过value改变值
countRef.value = 2
objCountRef.value.count = 3
hobbyRef.value.push('吃饭')
}, 4000)
return {
countRef,
objCountRef,
hobbyRef
}
}
}
</script>
- reactive: 用于将引用类型的数据转成响应式,解构时丢失响应式
<template>
<p>ref demo {{ageRef}} {{state.name}}</p>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
name: 'Ref',
setup(){
const ageRef = ref(18)
const nameRef = ref('monday')
// ref的值,可以作为reactive的属性
const state = reactive({
name: nameRef
})
setTimeout(() => {
console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
// .value
ageRef.value = 20
nameRef.value = 'mondaylab'
console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
},1500)
return{
ageRef,
state
}
}
}
</script>
- toRef:用于将reactive之后的响应式对象的
某个key的value单独拿出来(保持响应式),跟原值是引用关系
<template>
<p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>
<script>
import { ref, toRef, reactive, computed } from 'vue'
export default {
name: 'ToRef',
setup() {
const state = reactive({
age: 18,
name: 'monday'
})
//实现某一个属性的数据响应式
const ageRef = toRef(state, 'age')
setTimeout(() => {
// 由于引用关系,ageRef.value也变化
state.age = 20
}, 1500)
setTimeout(() => {
ageRef.value = 25 // .value 修改值
}, 3000)
return {
state,
ageRef
}
}
}
</script>
- toRefs:用于将reactive之后的响应式对象的
所有key的value拿出来,在return的时候可以解构
<template>
<p>toRefs demo {{age}} {{name}}</p>
</template>
<script>
import { ref, toRef, toRefs, reactive } from 'vue'
export default {
name: 'ToRefs',
setup() {
const state = reactive({
age: 18,
name: 'monday'
})
const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象(理解:应该是对象变成普通对象,但是属性还是响应式的)
setTimeout(() => {
console.log('age', state.age, 'name', state.name)
state.age = 20,
state.name = '周一'
console.log('age', state.age, 'name', state.name)
}, 1500)
// 这样之后,不用在写state.age等,直接写age
return ...stateAsRefs
}
}
</script>
4. vue2、3的数据劫持实现
- Object.defineProperty
// 变成响应式数据
let oldArrayProto = Array.prototype
// 给新对象的__proto__设置为oldArrayProto
let newArrayProto = Object.create(oldArrayProto)
let methods = ['push', 'shift', 'unshift', 'pop', 'splice', 'sort', 'reverse']
methods.forEach(methodName => {
newArrayProto[methodName] = function () {
console.log('视图更新')
oldArrayProto[methodName].call(this, ...arguments)
}
})
function observer(target) {
if (typeof target !== 'object' || typeof target === 'null') {
return target
}
for (let key in target) {
target.hasOwnProperty(key) && defineReactive(target, key, target[key]);
}
}
function defineReactive(target, key, value) {
// 深度监听
observer(value);
// 数组方法劫持
if (Array.isArray(target)) {
target.__proto__ = newArrayProto
return;
}
// 以对象的key -> value劫持
Object.defineProperty(target, key, {
get() {
// observer(value)
console.log('get', key, value);
return value
},
set(newValue) {
// 新值是引用类型需要重新劫持
observer(newValue)
if (newValue !== value) {
value = newValue
console.log('视图更新')
}
}
})
}
const data = {
name: 'zhm',
age: 10,
friend: {
friendName: '测试'
},
colors: [0, 1, 2]
}
// 把数据变成响应式
observer(data)
- Proxy
const reactive = function(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const observer = new Proxy(obj, {
get(target, key) {
console.log('get', key)
const res = Reflect.get(target, key);
// 深层代理,不然的话value为引用值时,没有被代理,虽然通过proxy.a.b能触发get
// 但是相当于proxy.a触发代理的get, 返回的是未代理的对象a,然后再a.b访问属性b
return typeof res === 'object' ? reactive(res) : res;
},
set(target, key, value) {
console.log('set', key);
Reflect.set(target, key, value)
}
})
return observer;
}
const reactive2 = function(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let tmpValue;
}
const o = {
name: 'sx',
age: '122',
obj: {
h: 'aaa'
}
}
const oo = reactive(o);
5. v-if和v-show的区别
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中会销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;