v-text/v-html/v-once/v-show/v-if/v-for/v-bind/v-on
beforeCreate()
- 已有DOM节点:可以
- data选项:不可以
- 虚拟DOM节点:不可以
created():掌握
- 已有DOM节点:可以
- data选项:可以
- 虚拟DOM节点:不可以
beforeMount()
- 已有DOM节点:可以
- data选项:可以
- 虚拟DOM节点:不可以
mounted():掌握
- 已有DOM节点:可以
- data选项:可以
- 虚拟DOM节点:可以
父子组件传值
解决方案:自定义属性
语法结构:子组件中通过props
选项接受自定义属性
① 编辑子组件,添加自定义属性
<template>
<p>
data中的数据:{{ msg }}
</p>
<p>
自定义属性数据:{{ dataFromParent }}
</p>
</template>
<script>
export default {
// 声明自定义属性:一个组件可能会存在多个属性,第一种格式通过数组接受数据
// 定义的属性名称,用于存储接受的数据,可以和data选项中的变量一样使用;不能和data选项变量重名
props: ['dataFromParent'],
data() {
return {
msg: "xxxx"
}
}
....
}
</script>
② 编辑父组件,通过属性传递数据
<template>
<!-- 父组件给子组件传递数据 -->
<My-child :dataFromParent="info"></My-child>
<My-child :data-from-parent="info"></My-child>
</template>
<script>
import MyChild from "./MyChild"
export default {
data() {
info: "父组件数据"
}
}
</script>
③ 自定义属性扩展
自定义属性通过props
选项定义,除了基本语法定义,还可以给自定义属性添加类型约束和默认值
- 类型检查
export default {
props: {
dataFromParent: String
}
}
- 默认数据
export default {
props: {
dataFromParent: String,
// default: "默认值"
default: function() {
return "默认值"
}
}
}
- 数据校验
export default {
props: {
dataFromParent: String,
default: "默认值",
validator: function(value) {
//执行自定义属性接受的数据的校验,可以在控制台产生校验信息
return true/false
}
}
}
子父组件传值
解决方案:自定义事件
基本语法:通过实例选项的this.$emit()
触发自定义事件
① 子组件触发事件
<template>
<button @click="sendData">
发送数据给父组件
</button>
</template>
<script>
export default {
data() {
return {
msg: "子组件数据"
}
},
methods: {
sendData() {
// 触发自定义事件,传递数据
this.$emit("childEvent", this.msg)
}
}
}
</script>
② 父组件监听事件
<template>
<!-- 标签节点上,通过监听自定义事件,接受子组件数据 -->
<My-child @childEvent="getData"></My-child>
<My-child @child-event="getData"></My-child>
</template>
<script>
import MyChild from "./MyChild"
export default {
data() {
return {
dataFromChild: ""
}
},
methods: {
getData(value) {
this.dataFromChild = value
}
},
components: {
MyChild
}
}
</script>
(4) 组件之间传值
组件之间的传值,任意一个项目中的两个组件都有直接或者间接的关系,完全可以通过父子组件传值完成不同组件之间的传值,如图所示:
组件之间的传值,完全可以通过自定义事件中心,避免多个组件参与传递数据的复杂度!
① 定义事件中心
创建:src/utils/events.js
:创建一个空白的vue
实例,主要用于数据传递
/**
* 自定义事件中心
* events.js
*/
import Vue from "vue"
const vm = new Vue()
export default vm
② 组件1发送数据
<template>
<button @click="sendData2">
发送数据给其他组件
</button>
</template>
<script>
// 导入事件中心
import vm from "../utils/event"
export default {
data() {
return {
msg: "子组件数据"
}
},
methods : {
sendData2() {
// 事件中心 触发自定义事件
vm.$emit("customEvent", this.msg)
}
}
}
</script>
③ 组件2接受数据
<template>
<p>
等待接受数据:{{ }}
</p>
</template>
<script>
// 导入事件中心
import vm from "../utils/event"
export default {
created() {
// 组件加载的时候执行函数,监听自定义事件
this.getData();
},
data() {
return {
dataFromOther: "等待接受"
}
},
methods: {
getData() {
vm.$on("customEvent", value => {
// 接受自定义事件传递的数据
this.dataFromOther = value
})
}
}
}
</script>
小总结:关于父子组件传值问题
父子组件之间的传值不涉及页面视图的切换!
一定是在单个页面组件中,已经存在父子组件嵌套关系,才是父子组件传值的操作场景!
问题:如果出现多个页面组件切换,如何完成数据传递? 解决方案:路由传参
vuex
state mutations同步函数 actions异步函数 getters
vuex
解耦合:辅助函数
(1)state
选项:
vue
组件中,需要通过耦合语法访问数据
this.$store.state.brandList
vuex
中提供了一个函数mapState
,用于将数据进行解耦合访问
<template>
<p>
{{ $store.state.brandList }}
</p>
<p>
{{ brandList }}
</p>
</template>
<script>
import {mapState} from "vuex"
export default {
computed: {
...mapState({
brandList: state=> state.brandList
})
}
}
</script>
(2) actions
选项
vue
组件中,需要通过耦合语法访问函数
this.$store.dispath('getBrandListActions')
vuex
中提供了辅助函数mapActions
,通过解耦合的方式访问对应的函数操作
<template>
</template>
<script>
import {mapActions} from "vuex"
export default {
created(){
// 原始耦合
this.$store.dispatch('getBrandListActions')
// 解耦合
this.getBrandListActions()
},
methods: {
...mapActions(['getBrandListActions'])
}
}
</script>
(3) getters
选项
vue
组件中,需要通过耦合语法访问函数
this.$store.getters.函数名称
vuex
提供了解耦合语法mapGetters
提供了解耦合访问方式
<template>
<p>
{{ $store.getters.brandCounter }}
</p>
<p>
{{ brandCounter }}
</p>
</template>
<script>
import {mapGetters} from "vuex"
export default {
computed: {
...mapGetters(['brandCounter'])
}
}
</script>
(4)mutations
选项[了解]
vue
组件中,需要通过耦合语法访问函数
this.$store.commit('mutations函数')
vuex
提供解耦合辅助函数mapMutations
,提供解耦合访问方式
import {mapMutations} from "vuex"
export default {
methods: {
...mapMutations(['getBrandListMutations'])
},
create() {
// 1、原始
this.$store.commit('getBrandListMutations')
// 2、解耦合
this.getBrandListMutations()
}
}
vue3
vue2 vue3
beforeCreate setup()
created setup()
beforeMount onbeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
activated onActivated
deactivated onDeactivated
errorCaptured onErrorCaptured
import {watch computed} from 'vue'
export default{
name:'App',
setup(){
watch(
()=>{},()=>{}
)
const a=computed(()=>{})
}
}
//vue3.2
<script setup>
import {watch compted} from 'vue'
watch(
()=>{},()=>{}
)
const a=computed(()=>{})
</script>
setup存在的意义,就是为了使用新增的组合API,并且这些组合API只能在setup内使用
setup调用的时机是创建组件实例然后初始化props,紧接着就是调用setup函数,从生命周期钩子的角度看,他会在beforeCreate钩子之前被调用,所以setup内拿不到this上下文
除去beforeCreate和created,在setup方法中,有9个旧的生命周期钩子,可以在setup中访问
1.setup():开始创建组件之前,在beforeCreate和created之前执行,创建的是date和method
2.onBeforeMount():组件挂载到节点之前执行的函数
3.onMounted():组件挂载完成之后执行的函数
4.onBeforeUpdate():组件更新之前执行的函数
5.onUpdated():组件更新完成之后执行额函数
6.onBeforeUnmount():组件卸载之前执行的函数
7.onUnmounted():组件卸载完成之后执行的函数
8.onActived():被keep-alive缓存的组件激活是调用
9.onDeactivated():被 keep-alive 缓存的组件停用时调用,比如从 A 组件,切换到 B 组件,A 组件消失时执行。
10.onErrorCaptured():当捕获一个来自子孙组件的异常时激活钩子函数。
组件如何接受外部传入的参数
Vue 3.2 为我们在 script 内默认添加了三个方法,这三个方法分别是**defineProps **、 **defineEmits **和 defineExpose
defineProps
我们现在需要一个父子组件的传值例子,修改上述 src/App.vue 文件如下:
<template>
<Test :count="count"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'
const count = ref(0)
</script>
在 src/components 下新增 Test.vue 组件:
<template>
<div>{{ props.count }}</div>
</template>
<script setup>
const props = defineProps({ count: Number })
console.log('props', props)
</script>
在页面中我们打印了 props,可以发现它被 Proxy 代理过,这是 Vue3 实现响应式的核心 API,也就是说从父亲组件传到子组件的 count 变量,已经是响应式数据。
并且在子组件内,可以通过 watchEffect 和 watch 观察到数据的变化情况,我们来试试让数据在父组件变化起来,分别做如下修改:
// App.vue
<template>
<Test :count="count"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'
const count = ref(0)
setTimeout(() => {
count.value = 100
}, 2000)
</script>
// Test.vue
<template>
<div>{{ props.count }}</div>
</template>
<script setup>
import { watchEffect } from 'vue'
const props = defineProps({ count: Number })
watchEffect(() => {
console.log('props.count = ', props.count)
})
</script>
defineEmits
该属性的作用是在子组件获取父组件传递进来的方法,我们同样用一个例子来演示该属性的作用,在 App.vue 添加一个 add 方法如下:
<template>
<Test :count="count" @add="add"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'
const count = ref(0)
const add = () => {
count.value += 1
}
</script>
这里声明 add 方法,就不用再像 setup 函数那样,将其 return 出去。 在子组件内,通过 defineEmits 接受方法,如下所示:
<template>
<div>{{ props.count }}</div>
<button @click="add">+1</button>
</template>
<script setup>
const props = defineProps({ count: Number })
// 获取父组件传进来的add方法
const emit = defineEmits(['add'])
const add = () => {
// 执行父组件传进来的add方法
emit('add')
}
</script>
使用上,和 Vue 2 的区别不大,主要区别在于如何获取上
defineExpose
在 Vue 3.2.x 版本出来前,我们使用 Vue3 开发项目都是用 setup 函数的方式,在这种方式下,父组件通过 ref 去获取子组件 return 出来的方法是比较方便的,我们修改 App.vue 和 Test.vue 组件如下:
// App.vue
<template>
<Test :count="count" ref="TestRef"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'
const count = ref(1)
const TestRef = ref()
console.log('TestRef', TestRef)
</script>
// Test.vue
<template>
<div>{{ props.count }}</div>
</template>
<script>
export default {
name: 'Test',
props: {
count: Number
},
setup(props) {
const testFn = () => {
console.log('我是测试方法')
}
return {
props,
testFn // 将这个测试方法 return 出去
}
}
}
</script>
上图显示 TestRef 的打印结果,也就意味着 App.vue 可以拿到 Test.vue 内部的方法。这个特性能帮我们实现很多有趣的组件。
但是到了 Vue 3.2.x 版本,使用 script setup 后,父组件就拿不到子组件的内部方法了,修改Test.vue 如下所示:
// Test.vue
<template>
<div>{{ props.count }}</div>
</template>
<script setup>
const props = defineProps({ count: Number })
const testFn = () => {
console.log('这是测试方法')
}
</script>
此时,defineExpose 的作用就得以体现了。在 Test.vue 中,将想要往外抛出的方法作为参数放到 defineExpose 中,如下所示:
defineExpose({ testFn })
reactive
reactive 是 Vue 3 中提供的实现响应式数据的方法。在 Vue 2 中实现响应式数据是通过 Object 的 defineProPerty 属性来实现的,而在 Vue 3 中的响应式是通过 ES2015 的 Proxy 来实现
reactive 参数必须是对象
reactive 方法接受一个对象(json 或 Array)。
<!--App.vue-->
<template>
<p>{{ state.title }}</p>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
title: 'json'
})
</script>
尝试着修改上面的 reactive 参数为:
<template>
<p>{{ state }}</p>
</template>
const state = reactive(['arr1', 'arr2', 'arr3'])
同学们可能会有疑问,为什么数组也可以直接渲染呢。这里你可以把数组理解为特殊的对象。我们平时常用的普通对象如下所示:
const obj = { a: ‘1’, b: ‘2’ }
数组作为特殊的对象,如下:
const arr = [‘a’, ‘b’]
此时你可以把它看作:
const arr = { 0: ‘a’, 1: ‘b’ }
所以我们同样可以使用键值对的形式获取值,如 arr[0]。所以这就解释了为什么 reactive 还可以接受数组的原因。
reactive 包裹的对象,已经通过 Proxy 进行响应式赋能,所以我们可以通过如下形式修改值,会直接体现在 template 模板上。
<template>
<p>{{ state.title }}</p>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
title: '十三'
})
setTimeout(() => {
state.title= '十六'
}, 2000)
</script>
2 秒后你将会在浏览器上看到“十三”变成了“十六”。
响应式转换是“深层的”,会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。
上述是官方文档描述 reactive 返回的代理后的对象,内部的二级三级属性,都会被赋予响应式的能力,所以官方建议,使用 reactive 返回的值,而不要去使用原始值。
ref
ref 和 reactive 一样,同样是实现响应式数据的方法。在业务开发中,我们可以使用它来定义一些简单数据,如下所示:
<template>
<p>{{ count }}</p>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
修改数据,可以通过 count.value = 1 类似这样的语法去修改。但是为什么它需要这样去修改变量,而 reactive 返回的对象可以直接修改如 state.count = 1 。
原因是 Vue 3 内部将 ref 悄悄的转化为 reactive,如上述代码会被这样转换:
ref(0) => reactive({ value: 0 })
所以 count 相当于 reactive 返回的一个值,根据 reactive 修改值的方式,就可以理解为什么 ref 返回的值是通过 .value 的形式修改值了。
还有一点需要注意,当 ref 作为渲染上下文的属性返回(即在 setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value。之所以会自动解套,是因为 template 模板在被解析的时候,Vue3 内部通过判断模板内的变量是否是 ref 类型。如果是,那就加上 .value,如果不是则为 reactive 创建的响应集代理数据。
我们不妨打印一下 ref 创建的对象 console.log(count),浏览器控制台如下图所示:
没错,就是通过上图 __v_isRef 变量去判断,模板内的变量是否为 ref 类型。判断类型也可以通过 isRef 方法,如下:
<template>
<p>{{ count }}</p>
</template>
<script setup>
import { ref, isRef } from 'vue'
const count = ref(0)
console.log(isRef(count)) // true
</script>
在 Vue 2 中,我们可以通过给元素添加 ref=“xxx” 属性,然后在逻辑代码中通过 this.$refs.xxx 获取到对应的元素。
到了 Vue 3 后,setup 函数内没有 this 上下文,因此我们可以通过 ref 方法去获取,并且还需要在页面挂载以后才能拿到元素。
<template>
<div ref='shisanRef'>十三</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const shisanRef = ref(null)
onMounted(() => {
console.log(shisanRef)
})
</script>
computed
Vue 2 时代,computed 作为选项出现在页面中,而到了 Vue 3 时代,它将会以钩子函数的形式出现。我们先来修改 App.vue 下的代码,如下所示:
<template>
<p>{{ text }}</p>
</template>
<script setup>
import { reactive, computed } from 'vue'
const state = reactive({
name: '十三',
desc: '你好'
})
const text = computed(() => {
console.log('11')
return state.name + state.desc
})
setTimeout(() => {
state.name = '十六'
}, 2000)
</script>
上述代码通过 computed 函数将 name 和 desc 变量拼接,返回 text 渲染在模板上。
这里要注意的是 2 秒后,name 变量将会被重新赋值,那么 computed 函数内带有 state.name,所以会被动态计算,重新返回 text 值,浏览器会有如下变化:
若是将 computed 方法内的函数做如下改动:
…
const text = computed(() => {
console.log(‘11’)
return state.desc
})
…
少了 state.name,2 秒后,你将不会看到控制台打印 11,因为函数内不会被检测执行。
上述形式 computed 返回的值是不可修改的,通过 get 和 set 的形式返回的值是可修改的,不过这种情况的使用场景不多,这里不作深究。
readonly
readonly 顾名思义,用于创建一个只读的数据,并且所有的内容都是只读,不可修改。我们看如下代码:
<template>
<p>{{ state.name }}</p>
<p>{{ state.desc }}</p>
<button @click="fn">修改</button>
</template>
<script setup>
import { reactive, computed, readonly } from 'vue'
const state = readonly({
name: '十三',
desc: '你好'
})
const fn = () => {
state.name = '十六'
state.desc = '他好'
console.log('state', state)
}
</script>
我们用 readonly 创建一个数据,将其渲染在 template 模板下,并且通过 fn 函数,修改这个数据,看看浏览器会有什么反馈。
点击按钮之后,如上图所示,控制台报警告了,并且 state 打印出来之后,内部数据也没有变化。
watchEffect
首先 watchEffect 会追踪响应式数据的变化,并且还会在第一次渲染的时候立即执行,我们来看看下面的例子:
<template>
<div>
<h1>{{ state.search }}</h1>
<button @click="handleSearch">改变查询字段</button>
</div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let state = reactive({
search: Date.now()
})
watchEffect(() => {
console.log(`监听查询字段${state.search}`)
})
const handleSearch = () => {
state.search = Date.now()
}
</script>
watchEffect 接受一个回调函数作为参数,并且该回调函数内如果有响应式变量,那么当我执行 handleSearch 方法改变 search 变量时,回调函数也会被执行,如下所示:
watchEffect 函数返回一个新的函数,我们可以通过执行这个函数或者当组件被卸载的时候,来停止监听行为。来看下面代码:
let timer = null
let state = reactive({
search: Date.now()
})
// 返回停止函数
const stop = watchEffect((onInvalidate) => {
console.log(`监听查询字段${state.search}`)
})
const handleSearch = () => {
state.search = Date.now()
}
setTimeout(() => {
console.log('执行 stop 停止监听')
stop() // 2 秒后停止监听行为
}, 2000)
我们一直点击按钮,控制台会一直打印改变的数据。当 2 秒是 stop 方法被执行,停止监听后,控制台不再打印数据。如下图所示:
watchEffect 的回调方法内有一个很重要的方法,用于清除副作用。它接受的回调函数也接受一个函数 onInvalidate。名字不重要,重要的是它将会在 watchEffect 监听的变量改变之前被调用一次,具体执行顺序我们通过代码来解释:
<template>
<div>
<h1>{{ state.search }}</h1>
<button @click="handleSearch">改变查询字段</button>
</div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let state = reactive({
search: Date.now()
})
const stop = watchEffect((onInvalidate) => {
console.log(`监听查询字段${state.search}`)
onInvalidate(() => {
console.log('执行 onInvalidate')
})
})
const handleSearch = () => {
state.search = Date.now()
}
</script>
每当我点击按钮,改变 search 值时,onInvalidate 会在监听打印之前被执行一次。
那么要它何用呢?用处非常大。举个例子,我们需要监听 search 的变化,去请求接口数据,此时接口是异步返回的,每当我改变 search 都会去请求一次接口,那么有可能 search 改变的很频繁,那就会频繁的去请求接口,导致服务端压力倍增。我们可以通过这个特性去降低服务端的压力,具体逻辑如下:
<template>
<div>
<h1>{{ state.search }}</h1>
<button @click="handleSearch">改变查询字段</button>
</div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let timer = null
let state = reactive({
search: Date.now()
})
watchEffect((onInvalidate) => {
console.log(`监听查询字段${state.search}`)
timer = setTimeout(() => {
console.log('模拟接口异步请求,3 秒之后返回详情信息')
}, 3000)
onInvalidate(() => {
console.log('清除');
clearInterval(timer);
})
})
const handleSearch = () => {
state.search = Date.now()
}
</script>
在 watchEffect 回调函数内,我用 setTimeout 的形式去模拟响应时间为 3 秒的异步请求,上面代码可以理解为 3 秒之内如果你不去改变 search 变量,那么页面就成功返回接口数据,如果在 3 秒之内你再次点击按钮改变了 search 变量,onInvalidate 将会被触发,从而清理掉上一次的接口请求,然后根据新的 search 变量去执行新的请求。我们来看浏览器的表现:
watch
watch 的功能和之前的 Vue 2 的 watch 是一样的。和 watchEffect 相比较,区别在 watch 必须指定一个特定的变量,并且不会默认执行回调函数,而是等到监听的变量改变了,才会执行。并且你可以拿到改变前和改变后的值,代码如下:
<template>
<div>
<h1>{{ state.search }}</h1>
<button @click="handleSearch">改变查询字段</button>
</div>
</template>
<script setup>
import { reactive, watch } from 'vue'
let timer = null
let state = reactive({
search: Date.now()
})
watch(() => {
return state.search
}, (nextData, preData) => {
console.log('preData', preData)
console.log('nextData', nextData)
})
const handleSearch = () => {
state.search = Date.now()
}
</script>
提供/注入(provide/inject)
祖先 想要传递数据给 儿子 的的话,正常情况下,需要先传递给 父亲 组件,然后 父亲 组件再将数据传给 儿子 组件。
现在我们有了 provide/inject,便可以在 祖先组件 声明 provide,然后在 儿子组件 通过 inject 拿到数据。下面我们用代码来诠释上面的分析。
Vue 2写法
清空上述 App.vue 的代码,将其作为 祖先组件,代码如下:
<template>
<div>
<h1>提供/注入</h1>
<Father />
</div>
</template>
<script>
import Father from './components/Father.vue'
export default {
components: {
Father
},
provide: {
name: '陈尼克'
}
}
</script>
在 src/components 文件夹新建两个文件 Father.vue 和 Son.vue 如下:
<!--Father.vue-->
<template>
<div>我是父亲</div>
<Son />
</template>
<script>
import Son from './Son.vue'
export default {
name: 'Father',
components: {
Son
}
}
</script>
<!--Son.vue-->
<template>
<div>我是儿子,{{ name }}</div>
</template>
<script>
export default {
name: 'Son',
inject: ['name']
}
</script>
Vue 3 写法
之前说过 Vue 3 作出最大的改动就是将 options 的书写形式改成了 hooks 的钩子函数形式。privide/inject 也不例外,我们使用它们需要通过 vue 去解构出来,下面我们修改上述代码如下:
<!--App.vue-->
<template>
<div>
<h1>提供/注入</h1>
<Father />
</div>
</template>
<script setup>
import { provide } from 'vue'
import Father from './components/Father.vue'
provide('name', '陈尼克') // 单个声明形式
provide('info', {
work: '前端开发',
age: '18'
}) // 多个声明形式
</script>
<!--Son.vue-->
<template>
<div>我是儿子,{{ name }}</div>
<div>职业:{{ info.work }}</div>
<div>年龄:{{ info.age }}</div>
</template>
<script setup>
import { inject } from 'vue'
const name = inject('name', '嘻嘻') // 第二个参数为默认值,可选
const info = inject('info')
</script>
当我们需要修改传入的数据时,Vue 不建议我们直接在接收数据的页面修改数据源,用上述的例子就是不建议在 Son.vue 组件内去修改数据源,我们可以在 App.vue 组件内通过 provide 传递一个修改数据的方法给 Son.vue,通过在 Son.vue 内调用该方法去改变值。我们将代码做如下修改:
<!--App.vue-->
<template>
<div>
<h1>提供/注入</h1>
<Father />
</div>
</template>
<script setup>
import { provide, ref } from 'vue'
import Father from './components/Father.vue'
const name = ref('陈尼克')
provide('name', name) // 单个声明形式
provide('info', {
work: '前端开发',
age: '18'
}) // 多个声明形式
const changeName = () => {
name.value = '李尼克'
}
provide('changeName', changeName)
</script>
<!--Son.vue-->
<template>
<div>我是儿子,{{ name }}</div>
<div>职业:{{ info.work }}</div>
<div>年龄:{{ info.age }}</div>
<button @click="changeName">修改名字</button>
</template>
<script setup>
import { inject } from 'vue'
const name = inject('name', '嘻嘻') // 第二个参数为默认值,可选
const info = inject('info')
const changeName = inject('changeName')
</script>
这里解释一下,在 Son.vue 组件中,你可以直接修改 inject 传进来的 name 值。但是你细想,数据源存在于 App.vue 中,你在 Son.vue 中私自修改了数据源传进来的值,那两边的值就会产生紊乱,上述业务逻辑属于简单的,当你在公司正式项目中这样做的时候,数据源就会变得杂乱无章,页面组件变得难以维护。综上所述,一定要控制好数据源,保持单一数据流。
vue3性能和业务层面上的提升
虚拟 DOM 性能优化
PatchFlag(静态标记)
Vue 2 中的虚拟 DOM 是全量对比的模式,而到了 Vue 3 开始,新增了静态标记(PatchFlag)**。在更新前的节点进行对比的时候,只会去对比带有静态标记的节点。**并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。下面我们通过图文实例分析这个对比的过程。假设我们有下面一段代码:
老八食堂
{{ message }}
通过上图,我们发现,Vue 2 的 diff 算法将每个标签都比较了一次,最后发现带有 {{ message }} 变量的标签是需要被更新的标签,显然这还有优化的空间。
在 Vue 3 中,对 diff 算法进行了优化,在创建虚拟 DOM 时,根据 DOM 内容是否会发生变化,而给予相对应类型的静态标记(PatchFlag)
观察上图,不难发现视图的更新只对带有 flag 标记的标签进行了对比(diff),所以只进行了 1 次比较,而相同情况下,Vue 2 则进行了 3 次比较。这便是 Vue 3 比 Vue 2 性能好的第一个原因。
我们再通过把模板代码转译成虚拟 DOM,来验证我们上述的分析是否正确。我们可以打开模板转化网站,对上述代码进行转译:
上图蓝色框内为转译后的虚拟 DOM 节点,第一个 p 标签为写死的静态文字,而第二个 p 标签则为绑定的变量,所以打上了 1 标签,代表的是 TEXT(文字),标记枚举类型如下:
诸如上述代码,如果将 PAGE_SIZE = 10 写在 getData 方法内,每次调用 getData 都会重新定义一次变量。
Vue 3 在这方面也做了同样的优化,继续用我们上一个例子写的代码,观察编译之后的虚拟 DOM 结构,如下所示。
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的 Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动态 solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算法
}
hoistStatic(静态提升)
我们平时在开发过程中写函数的时候,定义一些写死的变量时,都会将变量提升出去定义,如下所示:
const PAGE_SIZE = 10
function getData () {
$.get('/data', {
data: {
page: PAGE_SIZE
},
...
})
}
没有做静态提升前:
hoist.png
选择 Option 下的 hoistStatic:
optionh.png
静态提升后:
hoist2.png
细心的同学会发现, 老八食堂 被提到了 render 函数外,每次渲染的时候只要取 _hoisted_1 变量便可。认真看文章的同学又会发现一个细节, _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用作 diff。也就是说被打上 -1 标记的,将不在参与 diff 算法,这又提升了 Vue 的性能。
cacheHandler(事件监听缓存)
默认情况下 @click 事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3 对此作出了相应的优化叫 事件监听缓存,我们在上述代码中加一段:
giao.png
在未开启 事件监听缓存 的情况下,我们看到这串代码编译后被静态标记为 8,之前讲解过被静态标记的标签就会被拉去做比较,而静态标记 8 对应的是“动态属性,不包括类名和样式”。 @click 被认为是动态属性,所以我们需要开启 Options 下的 cacheHandler 属性,如下图所示:
giao1.png
细心的同学又会发现,开启 cacheHandler 之后,编译后的代码已经没有静态标记(PatchFlag),也就表明图中 p 标签不再被追踪比较变化,进而提升了 Vue 的性能。
SSR 服务端渲染
当你在开发中使用 SSR 开发时,Vue 3 会将静态标签直接转化为文本,相比 React 先将 jsx 转化为虚拟 DOM,再将虚拟 DOM 转化为 HTML,Vue 3 已经赢了。
ssr.jpg
StaticNode(静态节点)
上述 SSR 服务端渲染,会将静态标签直接转化为文本。在客户端渲染的时候,只要标签嵌套得足够多,编译时也会将其转化为 HTML 字符串,如下图所示:
staticnode.jpg
需要开启 Options 下的 hoistStatic
Tree-shaking
说到 Tree-shaking 这个属于,官方的解释用大白话说就是,没有被应用到的代码,编译后自动将其剔除。
我个人是这么记住 Tree-shaking 的,翻译成中文就是 摇树,树上有枯叶和绿叶,我摇动树干,枯叶掉了,新叶留着。这里的枯叶就是指没用到的代码,新叶指被应用到的代码,这么记就完全可以技术这个技术点。
在 Vue 2 中,无论有没有用到全部的功能,这些功能的代码都会被打包到生产环境中。究其原因,主要还是怪 Vue 2 生成实例是单例,这样打包的时候就无法检测到实例中的各个方法是否被引用到。如下:
import Vue from ‘vue’
Vue.nextTick(() => {})
而 Vue 3 经过改良之后,引入了 Tree-shaking 的特性,所有的方法通过模块化导入的形式。如下:
import { nextTick, onMounted } from ‘vue’
nextTick(() => {})
nextTick 方法会被打进生产包,而 onMounted 在代码里没有用到,最终不会出现在编译后的代码里。
Tree-shaking 做了两件事:
编译阶段利用 ES 的模块化判断有哪些模块已经加载。
判断哪些模块和变量,没有被使用或者引用到,从而删除对应的代码。
光看文字没有说服力,我们通过代码实例来演示一遍,通过 Vue CLI 启动一个 Vue 2 的项目,修改 App.vue 如下所示:
我们再加一个 Option,如下所示:
业务代码从 2.04 -> 2.10,而工具包还是 89.66。由此可见,增减代码,并不会影响工具包的打包大小。
我们再来看看 Vue 3 的表现,通过 Vue CLI 启动一个 Vue 3 的项目,App.vue 作如下修改:
我们加一个添加一个 computed 方法,如下所示:
<template>
<div>{{ state.test }}</div>
</template>
<script>
import { reactive, computed } from 'vue'
const state = reactive({
test: '十三'
})
const testc = computed(() => {
return state.test + '测试'
})
</script>
添加了 computed 之后,可以看到,工具包的大小从 87.35 -> 87.39,变大了。也就是说,之前没有用到 computed,它是不会被打包到生产环境的工具包里的。
综上所述,Vue 3 的 Tree-shaking 为我们带来了一下几个升级:
减少打包后的静态资源体积
程序执行更快