这是我参与【第四届青训营】笔记创作活动的第7天。
1、setup
理解:Vue3.0 中一个新的配置项,值为一个函数。 setup 是所有 Composition API(组合API)“表演的舞台”。 组件中所用到的:数据、方法等等,均要配置在 setup 中。
setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。
- 若返回一个渲染函数:则可以自定义渲染内容。(了解)
注意点:
-
尽量不要与 Vue2.x 配置混用
- Vue2.x 配置(data、methos、computed...)中可以访问到 setup 中的属性、方法。
- 但在 setup 中不能访问到 Vue2.x 配置(data、methos、computed...)。
- 如果有重名, setup 优先。
-
setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 promise,模板看不到 return 对象中的属性。(后期也可返回一个 promise 的实例,但需要Suspense和异步组件的配合)
举例: main.js
// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
// 创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更轻量
// cons app = createApp(App)
// 挂载
// apply.mount('#app')
createApp(App).mount('#app')
App.vue
<template>
<!-- Vue3组件中的模板结构可以没有根标签 -->
<div>
<h1>我是App组件</h1>
<h2>姓名:{{ name }}</h2>
<h2>性别:{{ age }}</h2>
<button @click="sayHello">点击按钮</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld,
},
setup() {
// 数据
let name = "张三";
let age = 18;
// 方法
function sayHello() {
console.log(`我叫${name},今年${age}岁`);
}
return {
name,
age,
sayHello,
};
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
2、ref 函数
作用:定义一个响应式的数据。
语法:const xxx = ref(initvalue)
- 创建一个包含响应式数据的引用对象(reference 对象,简称 ref 对象)
- JS 中操作数据:xxx.value
- 模板中读取数据:不需要
.value,直接:<div>{{xxxx}}</div>
备注:
- 接收的数据可以是:基本类型、也可以是对象类型。
- 基本类型的数据:响应式依然是靠 object.defineProperty() 的 get 与 set 完成的。
- 对象类型的数据:内部“求助”了 Vue3.0 中的一个新函数 —— reactive 函数。
<template>
<!-- Vue3组件中的模板结构可以没有根标签 -->
<div>
<!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->
<h1>我是App组件</h1>
<h2>姓名:{{ name }}</h2>
<h2>性别:{{ age }}</h2>
<h2>工作:{{ job.type }}</h2>
<h2>工资:{{ job.salary }}</h2>
<button @click="change">点击按钮</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import { ref } from "vue";
export default {
name: "App",
components: {
HelloWorld,
},
setup() {
// 数据
// 使用引用实现对象RefImpl
let name = ref("张三");
let age = ref(18);
let job = ref({
type: "后端工程师",
salary: "30K",
});
// 方法
function change() {
// ref——修改基本类型的数据:
// 使用的是defineProperty,利用getter、setter实现响应
age.value = 55;
// ref——修改对象类型的数据:
// 使用的是ES6中的proxy实现响应
job.value.type = "前端工程师";
console.log(`我叫${name.value},今年${age.value}岁`);
}
return {
name,
age,
change,
job,
};
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
3、reactive 函数
作用:定义一个对象类型的响应式数据(基本类型别用它,用ref函数)
语法:const 代理对象 = reactive(源对象)
接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象,简称为:proxy对象)
- reactive 定义的响应式数据是“深层次的”。
- 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据,都是响应式的
举例:
<template>
<!-- Vue3组件中的模板结构可以没有根标签 -->
<div>
<!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->
<h1>我是App组件</h1>
<h2>姓名:{{ name }}</h2>
<h2>性别:{{ age }}</h2>
<h2>工作:{{ job.type }}</h2>
<h2>工资:{{ job.salary }}</h2>
<h2>爱好:{{ hobby }}</h2>
<h2>测试数据:{{ job.a.b.c }}</h2>
<button @click="change">点击按钮</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import { ref, reactive } from "vue";
export default {
name: "App",
components: {
HelloWorld,
},
setup() {
// 数据
// 使用引用实现对象RefImpl
let name = ref("张三");
let age = ref(18);
let job = reactive({
type: "后端工程师",
salary: "30K",
a: {
b: {
c: 10,
},
},
});
let hobby = reactive(["吃饭", "睡觉", "打豆豆"]);
// 方法
function change() {
// ref——修改基本类型的数据:
// 使用的是defineProperty,利用getter、setter实现响应
age.value = 55;
// ref——修改对象类型的数据:
// ref会去求助reactive,reactive使用的是ES6中的proxy实现响应
// job.value.type = "前端工程师";
// console.log(`我叫${name.value},今年${age.value}岁`);
// 直接使用reactive修改对象类型的数据:
job.type = "前端工程师";
// reactive可以对深层次的数据进行监视
job.a.b.c = 20;
// reactive可以也可以代理数组
// vue3中可以通过reactive代理直接使用下标对数组进行修改
hobby[0] = "喝可乐";
}
return {
name,
age,
change,
job,
hobby,
};
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
<template>
<!-- Vue3组件中的模板结构可以没有根标签 -->
<div>
<!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->
<h1>我是App组件</h1>
<h2>姓名:{{ person.name }}</h2>
<h2>性别:{{ person.age }}</h2>
<h2>工作:{{ person.job.type }}</h2>
<h2>工资:{{ person.job.salary }}</h2>
<h2>爱好:{{ person.hobby }}</h2>
<button @click="change">点击按钮</button>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
import { ref, reactive } from "vue";
export default {
name: "App",
components: {
HelloWorld,
},
setup() {
let person = reactive({
name: "张三",
age: 18,
job: {
type: "后端工程师",
salary: "30K",
},
hobby: ["吃饭", "睡觉", "打豆豆"],
});
// 方法
function change() {
console.log(121);
person.age = 55;
person.job.type = "前端工程师";
person.hobby[0] = "喝可乐";
}
return {
person,
change,
};
},
};
</script>
4、Vue3.0 中的响应式原理
4.1、vue2.x 的响应式
实现原理:
- 对象类型:通过 object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。
- 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
Object.defineProperty(data,'count ', {
get() {},
set() {}
})
存在问题:
- 新增属性、删除属性,界面不会更新。
- 直接通过下标修改数组,界面不会自动更新。
Vue2响应式中存在问题的解决方案:
补充:修改数组还可以使用 splice 方法变更数组来达到修改的目的。
模拟源码:
4.2、Vue3.0 的响应式
实现原理:
- 通过 Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等。
- 通过 Reflect(反射):对源对象的属性进行操作。
- MDN文档中描述的 Proxy 与 Reflect:
new Proxy (data,{
//拦截读取属性值
get(target,prop){
return Reflect.get(target,prop)
},
//拦截设置属性值或添加新属性
set(target,prop, value){
return Reflect.set(target,prop, value)
},
//拦截删除属性
deleteProperty(target, prop){
return Reflect.deleteProperty(target,prop)
}
})
proxy .name = 'tom"
模拟源码:
① 使用 Proxy 代理:
② 配合 Reflect 反射:
Reflect的作用:JS 是单线程,如果一旦发生错误则会程序挂起,对于封装框架来说有很大的隐患,为了框架的健壮性,使用 reflect 可以保证后面的代码正确执行,不至于全部挂起。当然,使用 try...catch 也是可以的。
- 使用Object.defineProperty
- 使用Reflect.defineProperty
5、reactive 对比 ref
5.1、从定义数据角度对比
- ref 用来定义:基本类型数据。
- reactive 用来定义:对象(或数组)类型数据。
备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过 reactive 转为代理对象。
5.2、从原理角度对比
- ref 通过 0bject.defineProperty() 的 get 与 set 来实现响应式(数据劫持)。
- reactive 通过使用 Proxy 来实现响应式(数据劫持),并通过 Reflect 操作源对象内部的数据。
5.3、从使用角度对比
- ref 定义的数据:操作数据需要 .value,读取数据时,模板中直接读取,不需要 .value。
- reactive 定义的数据:操作数据与读取数据——均不需要 .value。
Vue2: attrs 中,如果被接受了就会将其挂载到组件上,被挂载的就不会出现在 slot:当在父组件中使用插槽,但是子组件无论有没有使用 slot 标签接受时,插槽的内容会被放在 $slots 中。
6、setup 的两个注意点
6.1、setup 执行的时机
在beforeCreate之前执行一次,this是undefined
6.2、setup 的参数
① props:值为对象,包含组件外部传递过来,且组件内部声明接收了的属性
② context:上下文对象
- attrs:值为对象,包含:组件外部传递过来,但没有在 props 配置中声明的属性,相当于 this.$attrs
- slots:收到的插槽内容,相当于 this.$slots
- emit:分发自定义事件的函数,相当于 this.$emit
注意:
- slots 里面保存着接收到的插槽内容,是一些虚拟 DOM
- Vue3 中要想在 slots 里面显示插槽名,得用 v-slot 进行命名
7、计算属性与监视
7.1、computed函数
与 Vue2.x 中 computed 配置功能一致。
举例:
简写形式
完整形式:
7.2、watch 函数
与 Vue2.x 中 watch 配置功能一致。
两个小“坑”:
- 监视 reactive 定义的响应式数据时:oldValue 无法正确获取、强制开启了深度监视(deep配置失效)
- 监视 reactive 定义的响应式数据中某个属性时:deep 配置有效
7.3、watchEffect 函数
watch 的套路是:既要指明监视的属性,也要指明监视的回调。
watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
watchEffect 有点像 computed,区别如下:
- computed 注重计算出来的值(回调函数的返回值),所以必须要写返回值
- watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
console.log('watchEffect配置的回调执行了')
})
8、生命周期
Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有两个被更名:
- beforeDestroy 改名为 beforeUnmount
- destroyed 改名为 unmounted
可以直接以配置项的形式使用生命周期钩子,也可以使用组合式API的形式使用,尽量统一
一般来说组合式API里的钩子会比配置项的钩子先执行,组合式API的钩子名字有变化。
Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
- beforeCreate===>setup()
- created=======>setup()
- beforeMount ===>onBeforeMount
- mounted=======>onMounted
- beforeUpdate===>onBeforeUpdate
- updated =======>onUpdated
- beforeUnmount ==>onBeforeUnmount
- unmounted =====>onUnmounted
9、自定义 hook 函数(重点)
什么是hook?
本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。
类似于 vue2.x 中的 mixin。
自定义 hook 的优势:复用代码,让 setup 中的逻辑更清楚易懂。
创建一个 hooks 文件夹,里面创建文件 userPoint.js
import{ reactive, onMounted, onBeforeUnmount } from "vue";
export default function() {
//实现鼠标“打点”相关的数据
let point = reactive({
x: 0,
y: 0,
});
//实现鼠标“打点”相关的方法
function savePoint(event) {
point.x = event.pageX;
point.y = event.pageY;
console.log(event.pageX, event.pageY);
}
//实现鼠标“打点”相关的生命周期钩子 onMounted(() => {
window.addEventListener("click", savePoint); });
onBeforeUnmount(() => {
window.removeEventListener("click", savePoint);
});
return point;
}
}
在组件中使用:
<template>
<h2>我是HelloWorld组件</h2>
<h2>当前点击时鼠标的坐标为:x:{{point.x}},y:{{point.y}}</h2>
</template>
<script>
import usePoint from '../hooks/usePoint'
export default {
name:'HelloWorld',
setup(){
const point = usePoint()
return {point}
}
}
</script>
演示效果:
10、toRef
作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。
语法:const name = toRef(person,'name')
应用: 要将响应式对象中的某个属性单独提供给外部使用时。
扩展:toRefs 与 toRef 功能一致,但可以批量创建多个 ref 对象,
语法:toRefs(person)
<template>
<h4>{{person}}</h4>
<h2>姓名:{{name}}</h2>
<h2>年龄:{{age}}</h2>
<h2>薪资:{{job.j1.salary}}K</h2>
<button @click="name+='~'">修改姓名</button>
<button @click="age++">增长年龄</button>
<button @click="job.j1.salary++">涨薪</button>
</template>
<script>
import {ref,reactive,toRef,toRefs} from 'vue'
export default {
name: 'Demo',
setup(){
//数据
let person = reactive({
name:'张三',
age:18,
job:{
j1:{ salary:20 }
}
})
// const name1 = person.name
// console.log('%%%',name1)
// const name2 = toRef(person,'name')
// console.log('####',name2)
const x = toRefs(person)
console.log('******',x)
return {
person,
// name:toRef(person,'name'),
// age:toRef(person,'age'),
// salary:toRef(person.job.j1,'salary'),
...toRefs(person)
}
}
}
</script>