我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!
一. 前言
技术更新很快,时代在推你进步,你却停滞不前,只会被时代淘汰。迫于内卷压力,再不学
vue3 真的感觉在跟时代划一道鸿沟。接下来让我们以最快的脚步认识vue3。
二. Vue3优势和新特性
vue3相对于vue2有哪些性能提升?
源码体积的优化
- 重写了虚拟 dom。
响应式系统的升级
- 用 Proxy 和 Reflect 来代替 vue2 中的 Object.definepeoperty()方法来重写响应式。
- vue3 中可以监听动态新增的属性。
- vue3 中可以监听删除的属性。
- vue3 中可以监听数组的索引和 length 属性。
代码编译优化
- 使用了 Composition API 来代替 vue2 中的 Options API。
- 组件内不需要根节点了,使用 fragment(代码片段)代替了,fragment(代码片段)不会在页面显示。
- vue3 中标记和提升所有的静态根节点,diff 的时候只需要对比动态节点内容。
Composition API 与 Options API 对比
vue2结构是这样的,当代码量少的时候,逻辑结构看起来挺清晰的。当功能越来越多时,代码分散在不同的地方,反复横跳翻滚,显得非常臃肿,难以维护。
Vue3的思路就是根据逻辑功能,对代码进行组织划分,把同一个功能的相关代码全都放在一起,或者把它们单独拿出来放在一个函数中,从而解决上述代码臃肿的问题。
演变过程
Proxy 相对于 Object.defineProperty 有哪些优点?
- 代码的执行效率更快。
- Proxy 可以直接监听对象而非属性。
- Proxy 可以直接监听数组的变化。
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的。
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改。
- Proxy 不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一级的属性。
vue3 响应式系统的实现原理
通过 2 个响应式 API 函数的调用,一个是 reactive() , 一个是 ref()。
reactive()
- reactive 函数是用来把普通对象创建成为响应式对象的,函数内通过执行 proxy 创建的 get、set、deleteProperty 方法来实现。
- get 方法获取响应式数据,同时调用 track 方法去收集依赖。
- set 方法设置响应式数据,同时调用 trigger 方法是触发响应式数据的更新。
- deleteProperty 方法删除响应式数据,同时用 trigger 方法是触发响应式数据的更新。
ref()
- ref 函数是用来把一般类型的数据或者普通对象创建成为响应式对象的,函数内返回的是一个对象。
- 对象内的 get 方法获取响应式数据,同时调用 track 方法去收集依赖。
- 对象内的 set 方法设置响应式数据,同时调用 trigger 方法是触发响应式数据的更新。
- tarck 函数内通过 targetMap 找到 depsMap 通过 depsMap 找到 dep,最后向 dep 内添加 effect()函数。
- trigger 函数内通过 targetMap 找到 depsMap 通过 depsMap 找到 dep,最后遍历 dep 数组,执行里面的 effect()函数来更新响应式数据。
vue2.x 与 vue3.x 生命周期对比
由下图可见:
- beforeCreate、created由
setup()代替。 - beforeDestroy ->
onBeforeUnmount。 - destroyed ->
onUnmounted。 - errorCaptured ->
onErrorCaptured。 - 新增#
onRenderTracked()和onRenderTriggered()。 - 其余的在原基础上增加
on。
三. Fragment
<template>
<img alt="Vue logo" src="../assets/logo.png" />
<div>home页面</div>
</template>
<script>
export default {
name: 'HomeView',
}
</script>
在 template 中不再需要一个根元素包裹。实际上内部会将多个标签包含在一个Fragment虚拟元素中。
好处:减少标签层级, 减小内存占用。
四. setup函数
setup 返回的是一个对象,这个对象的属性会与组件中 data 函数返回的对象进行合并,返回的方法和 methods 合并,合并之后直接可以在模板中使用。
setup在 beforeCreate 之前执行,所以this在setup里面无效。
code.juejin.cn/pen/7145283…
<template>
<div>学习课程:{{ message }}</div>
<button @click="changeMessage">changeMessage</button>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'HomeView',
setup () {
let message = ref("极速入门Vue3");
console.log(this)
function changeMessage () {
message.value = "我被改变了" //注意使用ref重新赋值要在.value上操作
}
return {
message,
changeMessage
}
}
})
五. ref方法
Demo
<template>
<p>基础类型:{{ message }}</p>
<p>引用类型:{{ obj.name }}-{{ obj.sex }}</p>
<button @click="changeObjName">修改名字</button>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: "App",
setup () {
let message = ref("学习Vue3");
let obj = ref({
name: "小明",
sex: "男"
})
function changeObjName () {
obj.value.name = "小风"
}
return {
message,
obj,
changeObjName
}
}
})
</script>
使用ref获取dom元素
<template>
<div ref="myRef">myRef元素</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
name: "HomeView",
setup () {
let myRef = ref(null);
onMounted(() => {
setTimeout(() => {
myRef.value.style.color = "red"; //1秒后变颜色
}, 1000)
})
return {
myRef
}
}
})
</script>
六. reactive / toRefs方法
<template>
<p>基础类型:{{ message }}</p>
<p>引用类型:{{ name }}-{{ sex }}</p>
<button @click="changeObjName">修改名字</button>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent({
name: "HomeView",
setup () {
let message = reactive("学习Vue3");
let obj = reactive({
name: "小明",
sex: "男"
});
function changeObjName () {
obj.name = "小风"; //reactive可以直接修
}
return {
message,
...toRefs(obj), //拓展运算符,使用toRefs在模板上不需要通过obj.name取值
changeObjName,
};
}
})
</script>
reactive参数必须是对象 (json / arr),reactive定义基本数据类型的话,我们需要在reactive中将数据包装一下。
七. 判断响应式
isRef
检查值是否为一个 ref 对象。
<script>
import { defineComponent, ref, isRef } from 'vue'
export default defineComponent({
name: "HomeView",
setup () {
const ref1 = ref(1)
const ref2 = 2;
console.log(isRef(ref1)) // true
console.log(isRef(ref2)) // false
}
})
</script>
isReactive
检查对象是否是由 reactive 创建的响应式代理。
let ref2 = reactive({name: '小浪'})
console.log(isReactive(ref2)) // true
isReadonly 只读
检查对象是否是由 readonly 创建的只读代理。
let ref3 = readonly({name: '小浪'})
console.log(isReadonly(ref3)) // true
isProxy
检查对象是否是由 reactive 或 readonly 创建的 proxy。
let ref4 = reactive({name: '小浪'})
console.log(isProxy(ref4)) // true
let ref5 = readonly({name: '小浪'})
console.log(isProxy(ref5)) // true
八. customRef 自定义 ref
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,
该函数接收 track 和 trigger 函数作为参数,并且应该返回一个带有 get 和 set 的对象。
官方Demo实现自定义hook防抖函数【传送门】 code.juejin.cn/pen/7145285…
<template>
<div>{{ message }}</div>
<input v-model="text" />
</template>
<script>
import { defineComponent, ref, customRef } from 'vue'
function useDebouncedRef (value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {
get () {
// 告诉Vue追踪数据
track()
return value
},
set (newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 告诉Vue更新界面
trigger()
}, delay)
}
}
})
}
export default defineComponent({
name: "HomeView",
setup () {
const message = ref("学习Vue3");
const text = useDebouncedRef('hello');
console.log(message)
console.log(text)
return {
message,
text
}
}
})
</script>
九. 浅响应式
shallowRef / shallowReactive
shallowRef只处理基本数据类型的响应式, 不进行对象的响应式处理。
shallowReactive只处理对象最外层属性的响应式(浅响应式),一旦发生改变,则更新视图。其它深层,虽然值发生了改变,但是视图不会进行更新。
什么时候使用?
- 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
- 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef
//shallowRef
<template>
<div>
<h1>数值:{{ number }}</h1>
<button @click="number++">number增加+1</button>
<h1>年龄:{{ person.age }}</h1>
<button @click="person.age++">年龄增加+1</button>
</div>
</template>
<script>
import { shallowRef } from 'vue'
export default {
name: "HomeView",
setup () {
let number = shallowRef(0); // 基础类型做了响应式处理
let person = shallowRef({ //引用类型不做响应式处理
age: 18
})
return {
number,
person
}
}
};
</script>
//shallowReactive
<template>
<div>
<h1>姓名:{{ name }}</h1>
<h2>年龄:{{ age }}</h2>
<h3>喜欢的水果:{{ likeFood.fruits.apple }}</h3>
<button @click="name += '仔'">修改姓名</button>
<button @click="age++">修改年龄</button>
<button @click="likeFood.fruits.apple += '香'">修改水果</button>
</div>
</template>
<script>
import { toRefs, shallowReactive } from 'vue'
export default {
name: "HomeView",
setup () {
let person = shallowReactive({ // 只将第一层数据做了响应式处理
name: '小明',
age: 18,
likeFood: {
fruits: {
apple: '草莓' // 深层次的数据将会是一个普通的对象
}
}
})
// 将数据返回出去
return {
...toRefs(person)
}
}
};
</script>
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。通俗点就是只有第一层是只读,深层为可更改。
//shallowReadonly
<template>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>薪水:{{ job.j1.salary }}</h2>
<button @click="name += '~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">增长薪水</button>
</template>
<script>
import { reactive, toRefs, shallowReadonly } from 'vue'
export default {
name: 'HomeView',
setup () {
// 数据
let person = reactive({
name: '张三',
age: 18,
job: {
j1: {
salary: 20
}
}
})
person = shallowReadonly(person)
return {
...toRefs(person)
}
},
}
</script>
十. toRaw / markRaw
toRaw
返回 reactive 或 readonly 代理的原始对象。这是一个“逃生舱”,可用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改。不建议保留对原始对象的持久引用。请谨慎使用。
const foo = {};
const reactiveFoo = reactive(foo);
console.log(toRaw(reactiveFoo) === foo) // true
<script>
import { reactive, toRaw, isReactive } from 'vue'
export default {
name: 'HomeView',
setup () {
const person = reactive({
name: '小明',
age: 18,
likeFood: {
fruits: {
apple: '草莓'
}
}
});
const person2 = toRaw(person);
console.log(person)
console.log(person2)
console.log(isReactive(person))
console.log(isReactive(person2))
return {
}
},
}
</script>
markRaw
标记一个对象,使其永远不会转换为 proxy。返回对象本身。
<script>
import { reactive, markRaw, isReactive } from 'vue'
export default {
name: 'HomeView',
setup () {
let person = {
name: "小明",
age: 18
};
const markRawObj = markRaw(person); //标记person
const reactiveObj = reactive(markRawObj); //尝试转为响应式
console.log(isReactive(reactiveObj)) // false,说明未转成响应式
console.log(reactiveObj)
const obj = reactive({
name: "小风",
age: 19
})
console.log(obj)
return {};
}
}
</script>
十一. computed 计算属性
接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。 code.juejin.cn/pen/7145286…
<template>
<div>姓名:{{ name }}</div>
<div>年龄:{{ age }}</div>
<div>合并信息:{{ getInfo }}</div>
</template>
<script>
import { ref, computed } from "vue";
export default {
name: 'HomeView',
setup () {
let name = ref('小明')
let age = ref(21)
//计算属性
let getInfo = computed(() => {
return `${name.value},${age.value}`
})
getInfo.value += "尝试新增点东西"; //不允许修改,这里是非法的,修改不成功的
return {
name,
age,
getInfo,
}
}
}
</script>
或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
十二. watch 监听
watch用法与vue2.x的差不多,watch(data,handler,object)。
- data:可以是返回值的getter函数,也可以是 ref。
- hander: 回调函数
- object: 可选配置项{ immediate : true, deep : true } //立即执行和深度监测
<template>
<ul>
<li
v-for="(item, index) in courseList"
:key="index"
@click="selectCourseFun(index)"
>
{{ item }}
</li>
</ul>
<div>默认课程:{{ defaultCourse }}</div>
<div>选中的课程:{{ selectCourse }}</div>
</template>
<script>
import { ref, reactive, toRefs, watch } from "vue";
export default {
name: 'HomeView',
setup () {
const defaultCourse = ref("javaScript进阶");
const data = reactive({
courseList: ["vue3初识", "Node.js进阶", "css世界"],
selectCourse: "",
selectCourseFun: (index) => {
data.selectCourse = data.courseList[index];
defaultCourse.value = data.courseList[index];
}
})
//监听单个
// watch(defaultCourse, (newValue, oldValue) => {
// console.log(`new--->${newValue}`);
// console.log(`old--->${oldValue}`);
// })
//监听多个
watch([defaultCourse, () => data.selectCourse], (newValue, oldValue) => {
console.log(`new--->${newValue}`);
console.log(`old--->${oldValue}`);
})
return {
defaultCourse,
...toRefs(data)
}
}
}
</script>
十三. watchEffect
立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
- 不需要手动传入依赖
- 每次初始化时会执行一次回调函数来自动获取依赖
- 无法获取到原值,只能得到变化后的值
<template>
<input type="text" v-model="name" />
</template>
<script>
import { reactive, toRefs, watchEffect } from "vue";
export default {
name: 'HomeView',
setup () {
let data = reactive({
name: "小明",
sex: "男",
});
watchEffect(() => {
console.log(data.name); //需要监听哪个就写哪个
});
return {
...toRefs(data),
};
}
}
</script>
十四. 注意事项
Vue.prototype 替换为 config.globalProperties
//vue2.x
//main.js
Vue.prototype.$test = "测试"
//页面xx.vue
let value = this.$test;
//vu3.x
//main.js
const app = createApp({});
app.config.globalProperties.$test = "测试"
//页面使用
<template>
<h1>{{ $test }}</h1> //直接使用
</template>
import { getCurrentInstance } from "vue";
export default {
setup(){
const { appContext } = getCurrentInstance();
console.log(appContext.config.globalProperties.$test) //可以通过这样获取
}
}
废弃 v-bind:title.sync 写法
//父组件
<ChildComponent :title.sync="pageTitle" /> //vue2.x写法
<ChildComponent v-model:title="pageTitle" /> //vue3.x写法
//子组件
<template>
<h1>{{ title }}</h1>
<button @click="changeTitle('newTitle')">Change</button>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
props: {
title: String
},
setup () {
function changeTitle (val) {
this.$emit("update:title", val)
}
return {
changeTitle
}
}
})
</script>
废弃 filter 过滤器
在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。
//父页组件
<template>
<child v-model:name="name" v-model:value="value"></child>
</template>
<script>
import { defineComponent, reactive, toRefs } from 'vue'
import child from "@/components/child";
export default defineComponent({
components: {
child
},
setup () {
let params = reactive({
name: "测试过滤功能",
value: 12.0124525721
})
return {
...toRefs(params)
}
}
})
</script>
//子组件
<template>
<h1>{{ name }}</h1>
<h2>{{ newValue }}</h2>
</template>
<script>
import { defineComponent, computed } from "vue";
export default defineComponent({
props: {
name: String,
value: Number,
},
setup (props) {
let newValue = computed(() => {
return Number(props.value.toFixed(2))
})
return {
newValue
}
}
})
</script>
使用全局方法实现过滤器
//main.js
const app = createApp(App);
app.config.globalProperties.$filters = {
exactTwoDecimal(val){
return Number(val.toFixed(2))
}
}
//页面
<template>
<h2>{{ $filters.exactTwoDecimal(value)}}</h2>
</template>