一.数据的双向绑定
Vue2.0使用Object.defineProperty
原理:整体思路是数据劫持+观察者模式 对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
Vue 3.0使用ES6的新特性porxy
原理:Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版,具体的文档可以查看此处;
总结:Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平
二.创建vue2和vue3项目的文件发生的变化
- main.js中
- vue2.0中是直接创建了一个vue实例
- vue3.0中按需导出了一个createApp
- vue3.0中的app单文件不再强制要求必须有根元素 也就是说 在vue2.0中必须要有一个根元素,在vue3中没这个要求,下面是vue3.0的写法:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
三.最具有颠覆意义的响应-组合 API
vue2.0 option API (配置式API)
vue3.0 composition 组合式API
这也是我们学习和转向 vue3 最有意义的一部分内容,可以这么说,如果没有掌握这一 部分内容,那么即使我们底层使用了vue3,也不过是写 vue2。就像我们拥有了一台铲车, 却还在坚持靠人力把重物装卸到车叉上,只把铲车当运输工具一样。
setup 函数
组件创建前执行的初始化函数,默认参数包含 props 和 context。context 可以理解为 组件实例挂载之前的上下文(虽然这么理解,但不是实例本身,这点要清楚),所以实例上 的 attrs,slots,emit 可以在 context 上访问到。
在setup中我们可以决定哪些数据用响应式哪些不用响应式
比较常用的是:ref reactive toRef toRefs下面分别来介绍一下:
1.ref
- 常用于包装基本类型值为响应式对象,
ref声明的变量,我们可以在html代码中直接使用变量,但在 js 代码中,我们需要通过.value来访问,因为ref定义的变量返回的是一个响应式的ref 对象,对象需要通过.的形式来访问,官网详情见[此处]www.javascriptc.com/vue3js/api/… ref举例 :
<div class="demo">
<h2>姓名: {{name}}</h2>
<h2>年龄: {{age}}</h2>
<h3>岗位: {{job.type}}</h3>
<h3>工龄: {{job.workingAge}}</h3>
<button @click="updateInfo()">更新</button>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup () {
// 以下方式定义的是普通数据,非响应式数据
// let name = '张三';
// let age = 18;
// let job = {
// type: 'web前端',
// workingAge: 8
// }
// function updateInfo() {
// name = '李四';
// age = 20;
// console.log(name, age);
// }
// 基本类型:ref生成一个引用对象,通过get/set做数据劫持,Ref(reference引用)Impl(implement实现)
let name = ref('张三');
let age = ref(18);
console.log('name', name, age)
// 对象类型,内部'求助'了vue3中的一个新函数reactive函数
let job = ref({
type: 'web前端',
workingAge: 8
})
function updateInfo() {
// 注:模板里面不需要使用.value,模板解析时遇到 ref对象会自动读取.value
console.log(name, age);
name.value = '李四';
age.value = 20;
job.value.type = 'JAVA';
job.value.workingAge = 10;
}
return {
name,
age,
updateInfo,
job
}
}
})
</script>
-
ref一般用来声明基本数据类型,当然也可以声明一个对象,但需要通过.value.xxx来访问,比较繁琐,并且它将被reactive函数处理为深层的响应式对象; -
ref也可以用于获取 DOM,具体用法如下:
2.reactive
用于创建响应式的对象(只能是对象),取值不用加.value 官网链接[www.javascriptc.com/vue3js/api/…] 具体细节如下:
<div class="demo">
<h2>姓名: {{name}}</h2>
<h3>岗位: {{job.type}}</h3>
<h3 v-show="job.workingAge">工龄: {{job.workingAge}}</h3>
<h3 v-show="job.age">年龄:{{job.age}}</h3>
<button @click="updateInfo()">更新</button>
</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
setup () {
let name = ref('张三');
let job = reactive({
type: 'web前端',
workingAge: 8
})
console.log('job', job)
function updateInfo() {
name.value = '李四';
job.type = 'JAVA';
delete job.workingAge;// 删除工龄
job.age = 18; // 增加年龄
}
// 返回一个对象(常用)
return {
name,
job,
updateInfo
}
}
})
</script>
3.toRef和toRefs toRef将对象中的某一个属性变成响应式,toRefs将对象中的所有属性转化为响应式
<div class="demo">
<div>{{person.name}}</div>
<div>{{name}}</div>
<div>{{sex}}</div>
<div>{{age}}</div>
</div>
</template>
<script>
import { defineComponent, ref, reactive,toRef, toRefs } from 'vue'
export default defineComponent({
setup () {
let person = reactive({
name: '张三',
sex: '男',
job: {
j1: {
age: 18
}
}
})
// 下面是几种方式的不同点描述
return {
person, // 直接return出去,模板中使用不能直接使用name 需要 person.name,比较麻烦
name1: person.name, // 解构后模板中直接使用name,但是属性不再具有响应性
name2: toRef(person, 'name'), //将对象中的某一个值name属性变为响应式可以通过toRef来转变成ref对象
...toRefs(person), // 如果想让person中的属性都变为响应式可以使用toRefs批量转换成ref对象
...toRefs(person.job.j1)
}
}
})
</script>
总结:
reactive对比ref
从定义数据角度对比:
-
ref用来定义基本类型数据
-
reactive用来定义对象(或数组)类型数据
-
备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过reactive转为代理对象;
从原理角度对比:
-
ref通过Object.defineProperty()的get与set来实现响应式(数据劫持)
-
reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。
从使用角度对比:
-
ref定义的数据:js操作数据需要.value,模板中读取时不需要.value
-
reactive定义的数据,操作与读取均不需要.value
toRef, toRefs:
作用: 创建一个ref对象,其value值指向另一个对象中的某个属性
语法: const name = toRef(person, 'name')
应用: 要将响应式对象中的某个属性单独提供给外部使用时
扩展: toRefs与toRef功能一致,但可以批量创建多个ref对象,语法: toRefs(person)
- readonly 防止更改响应式对象
- isRef 是用来检测ref类型的,如果是返回的是true,否则返回false .
- isReactive 可以判断对象是否为响应式的。是用来检测reactive类型的,如果是返回的是true,否者返回false
- isProxy 区分是哪 种方法创建的代理对象.
- isReadonly 判断对象是否为 readonly(不可被修改) 创建。
- shallowXXX 根据名称 可知,只将对象自身的浅层属性进行转换,深层属性保持不变。
setup语法糖简介
直接在script标签中添加setup属性就可以直接使用setup语法糖了。
使用setup语法糖后,不用写setup函数;组件只需要引入不需要注册;属性和方法也不需要再返回,可以直接在template模板中使用。
<template>
<my-component @click="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/component/myComponent.vue';
//此时注册的变量或方法可以直接在template中使用而不需要导出
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
setup语法糖中新增的api
defineProps:子组件接收父组件中传来的props
defineEmits:子组件调用父组件中的方法
defineExpose:子组件暴露属性,可以在父组件中拿到
- defineProps
父组件代码
<template>
<my-component @click="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/components/myComponent.vue';
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
子组件代码
<template>
<div>{{numb}}</div>
</template>
<script lang="ts" setup>
import {defineProps} from 'vue';
defineProps({
numb:{
type:Number,
default:NaN
}
})
</script>
- defineEmits
子组件代码
<template>
<div>{{numb}}</div>
<button @click="onClickButton">数值加1</button>
</template>
<script lang="ts" setup>
import {defineProps,defineEmits} from 'vue';
defineProps({
numb:{
type:Number,
default:NaN
}
})
const emit = defineEmits(['addNumb']);
const onClickButton = ()=>{
//emit(父组件中的自定义方法,参数一,参数二,...)
emit("addNumb");
}
</script>
父组件代码
<template>
<my-component @addNumb="func" :numb="numb"></my-component>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComponent from '@/components/myComponent.vue';
const numb = ref(0);
let func = ()=>{
numb.value++;
}
</script>
- defineExpose
子组件代码
<template>
<div>子组件中的值{{numb}}</div>
<button @click="onClickButton">数值加1</button>
</template>
<script lang="ts" setup>
import {ref,defineExpose} from 'vue';
let numb = ref(0);
function onClickButton(){
numb.value++;
}
//暴露出子组件中的属性
defineExpose({
numb
})
</script>
父组件代码
<template>
<my-comp ref="myComponent"></my-comp>
<button @click="onClickButton">获取子组件中暴露的值</button>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import myComp from '@/components/myComponent.vue';
//注册ref,获取组件
const myComponent = ref();
function onClickButton(){
//在组件的value属性中获取暴露的值
console.log(myComponent.value.numb) //0
}
//注意:在生命周期中使用或事件中使用都可以获取到值,
//但在setup中立即使用为undefined
console.log(myComponent.value.numb) //undefined
const init = ()=>{
console.log(myComponent.value.numb) //undefined
}
init()
onMounted(()=>{
console.log(myComponent.value.numb) //0
})
</script>
四.生命周期钩子
基于 setup 方法使用的生命周期钩子同样有对应更新,setup执行的顺序在beforeCreate之前
beforeCreate -> setup()
created -> setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeUnmount -> onBeforeUnmount
unmounted -> onUnmounted 原来的destroyed
errorCaptured -> onErrorCaptured // 错误上报钩子,仅做了解
renderTracked -> onRenderTracked // { key, target, type } 仅做了解 renderTriggered -> onRenderTriggered // { key, target, type } 仅做了解
五. 侦听器 watch
侦听器主要是用来监听页面数据或者是路由的变化,来执行相应的操作,在 vue3里面呢,也有侦听器的用法,功能基本一样,换汤不换药的东西。 侦听器是常用的 Vue API 之一,它用于监听一个数据并在数据变动时做一些自定义逻辑,下面将列举侦听器在 Vue 中的使用方式。
- watch 侦听器使用。
watch API 使用,至少需要指定两个参数:
source 和callback,其中 callback 被明确指定只能为函数,所以不同使用方式的差别其实只在 source 。
<div>
<h1>watch 侦听器</h1>
<input v-model="num" />
<br>
<br>
<button type="primary" @click="num++">num + 1</button>
</div>
</template>
<script>
import { watch, ref } from 'vue'
export default {
setup() {
const num = ref(1)
watch(num, (newVal, oldVal) => {
console.log("新值:", newVal, " 旧值:", oldVal)
})
return { num, }
}
}
</script>
<style scoped>
</style>
上面的代码是页面上有一个数字,点击按钮一下,数字加一,然后侦听器侦听数字的变化,打印出侦听的最新值和老值。
OK。上边的案例就是 vue3 侦听器的简单案例,侦听器和计算属性一样,可以创建多个侦听器,这个是没有问题的,案例就不写了,和上一节讲的声明多个计算属性是一致的。如果有不明白的可以看一下我的上一篇博客。
上边我们说过这么一句话,watch API 至少需要指定两个参数: source 和 callback。通过上边的案例我们看到了, 确实是两个,source 是监听的数据,callback 是监听回调,那为啥说是至少呢?
对的,因为他还有第三个参数 —— 配置对象。
在 vue2 里面,我们打开页面就像让侦听器立即执行,而不是在第一次数据改变的时候才开始执行,这时候有一个参数叫 immediate ,设置了这个参数,创建第一次就执行,所以说呢,vue3 同样可以使用。
上面的案例刷新执行的时候发现,在点击按钮之前,也就是 num 创建的时候,侦听器是没有执行的,所以说呢,加上 immediate 参数,就可以让侦听器立即执行操作。
<div>
<h1>watch 侦听器</h1>
<input v-model="num" />
<br>
<br>
<button type="primary" @click="num++">num + 1</button>
</div>
</template>
<script>
import { watch, ref } from 'vue'
export default {
setup() {
const num = ref(1)
watch(num, (newVal, oldVal) => {
console.log("新值:", newVal, " 旧值:", oldVal)
}, {
immediate: true
})
return { num, }
}
}
</script>
<style scoped>
</style>
我们看到,刷新完页面,还没有点击按钮让 num 加一的,控制台就有数据打印了,就是因为我们加了 immediate 为 true,让侦听器立即执行。控制台输出最新的值也就是我们初始化的值1,老的值没有,所以输出了 undefined。
- 侦听器监听 reactive 下面来说说用来侦听对象的变化。
<div>
<h1>watch 侦听器</h1>
<input v-model="num.age" />
<br>
<br>
<button type="primary" @click="num.age++">num + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是柠檬.',
age: 10
})
watch(num, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
return { num }
}
}
</script>
<style scoped>
</style>
比如说上面代码,我们侦听 num 这个对象的变化。
看效果我们发下,在监听整个 reactive 响应式对象的时候,确实当里面的属性值发生改变了之后可以被侦听器检测到,但是 newVal 和 oldVal 的值都是新的,默认是10,点击之后,新值是 11 很正常,但是老值不应该是 10 吗?为什么这里老值和新值一样也是 11 呢?
这个不需要疑问哈,如果监听整个 reactive 数据的话,只能回调到最新的值,获取不到老的值。
那问题来喽,我就修改 age 属性,我就要获取 age 老的值怎么办?其实我们只需要监听 num 下面的 age 就可以了,先看下面的代码哈。
<div>
<h1>watch 侦听器</h1>
<input v-model="num.age" />
<br>
<br>
<button type="primary" @click="num.age++">num + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是柠檬.',
age: 10
})
watch(num.age, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
return { num }
}
}
</script>
<style scoped>
</style>
监听对象直接是 num.age, 监听年龄属性值,看一下效果。
刷新结果我们可以看到哈,我们啥都没干,侦听器直接报了一个警告给我们,啥意思呢,其实不能直接这样监听。
当我们需要监听某个对象属性的时候,我们不能直接对象点属性的方式进行监听,需要传入一个 getter 方法,也就是箭头函数进行监听,下面的代码是正确方式哈。
<div>
<h1>watch 侦听器</h1>
<input v-model="num.age" />
<br>
<br>
<button type="primary" @click="num.age++">num + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是柠檬.',
age: 10
})
watch(() => num.age, (newVal, oldVal) => { console.log(newVal, oldVal) }) return { num }
}
}
</script>
<style scoped>
</style>
保存刷新,我们发现,侦听器已经不报错了,而且我们点击按钮让 age 加一的时候,可以顺利的监听到 age 的变化,并且回调出最新值和上一次的值。
通过箭头函数,我们就可以实现对象属性的监听。
很多人说,vue2 在监听对象的时候需要对侦听器设置深度侦听,为什么 vue3 这个不需要呢?因为他监听响应式对象,默认就是深度监听。但是,如果监听的是深度嵌套对象或数组中的 property 变化时,仍然需要 deep 选项设置为 true。
看下面的案例,我们监听深层嵌套的 time 属性值。其实我觉得没大必要,不使用箭头函数其实可以。但是还是写一下吧。
<div>
<h1>watch 侦听器</h1>
<input v-model="num.todo.time" />
<br>
<br>
<button type="primary" @click="num.todo.time ++">num.todo.time + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive, computed, } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是柠檬.',
age: 10,
todo: {
name: '弹吉他',
time: 1
}
})
watch(() => num, (newVal, oldVal) => {
console.log(newVal.todo.time, oldVal.todo.time)
})
return { num }
}
}
</script>
<style scoped>
</style>
这个时候就可以加上 deep 深度监听。
<div>
<h1>watch 侦听器</h1>
<input v-model="num.todo.time" />
<br>
<br>
<button type="primary" @click="num.todo.time ++">num.todo.time + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive, computed, } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是𝒆𝒅.',
age: 10,
todo: {
name: '弹吉他',
time: 1
}
})
watch(() => num, (newVal, oldVal) => {
console.log(newVal.todo.time, oldVal.todo.time)
}, { deep: true })
return { num }
}
}
</script>
<style scoped>
</style>
就可以打印出来了,这种场景也不常用
- 监听多个参数执行各自逻辑
// 第一个
watch(num, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// 第二个
watch(()=> boy.age, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
- 监听多个参数执行相同逻辑
<div>
<h1>watch 侦听器</h1>
<input v-model="num.name" />
<input v-model="num.age" />
<br>
<br>
<button @click="num.todo.time ++">num.todo.time + 1</button>
</div>
</template>
<script>
import { watch, ref, reactive, computed, } from 'vue'
export default {
setup() {
const num = reactive({
name: '我是柠檬.',
age: 10,
todo: {
name: '写代码',
time: 1
}
})
watch([() => num.name, () => num.age], (newVal, oldVal) => {
console.log(newVal, oldVal)
})
return { num }
}
}
</script>
<style scoped>
</style>
如果我们不想让他返回数组可以这样改一下,可以了解一下
watch([() => num.name, () => num.age], ([newName, newAge], [oldName, oldAge]) => {
console.log(newName, newAge, oldName, oldAge)
})
return { num }
}