在Vue2
中,我们编写组件的方式是Options API
在对应的属性中编写对应的功能模块
但是这种代码有一个很大的弊端
实现某一个功能
时,这个功能对应的代码逻辑
会被拆分
到各个属性中组件变得更大、更复杂时
,逻辑关注点的列表就会增长,那么同一个功能的逻辑就会被拆分的很分散
难以阅读和理解
的如果我们能将同一个逻辑关注点相关的代码收集在一起会更好
Composition API想要做的事情
,以及可以帮助我们完成的事情VCA
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
实际使用它(编写代码)的地方
setup 函数
setup其实就是组件的另外一个选项
可以用它来替代之前所编写的大部分其他选项
我们先来研究一个setup函数的参数,它主要有两个参数
props
context
props非常好理解,它其实就是父组件传递过来的属性会被放到props对象中
,我们在setup中如果需要使用,那么就可以直接通过props参数获取
定义props的类型
,我们还是和之前的规则是一样的,在props选项中定义
在setup函数中想要使用props
,那么不可以通过 this 去获取
(后面我会讲到为什么)直接通过参数来使用
即可另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性
attrs
:所有的非prop的attributeslots
:父组件传递过来的插槽(这个在以渲染函数返回时会有作用,后面会讲到)emit
:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件)下面通过一个例子来说明怎样使用这两个参数
假设我们现在有一个home组件,里面接收一个props
假设app.vue为父组件,传一个message的prop属性,和id、class两个非prop属性
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
模板template中被使用
替代data选项
还是上面的那个例子
甚至是我们可以返回一个执行函数
来代替在methods中定义的方法
但是,如果我们将 counter 在 increment 或者 decrement进行操作时,是否可以实现界面的响应式呢?
不可以
因为对于一个定义的变量
来说,默认
情况下,Vue并不会跟踪它的变化
,来引起界面的响应式
操作上面的示例,通过控制台可以发现,counter的值确实发生了改变,但在页面上并不会看到跟着一起改变
如果想为在setup中定义的数据提供响应式
的特性,那么我们可以使用reactive
的函数
还是上面的例子,按照下面这样写就可以响应式了,counter会在页面随着点击发生改变
那么这是什么原因呢?为什么就可以变成响应式的呢?
使用reactive函数处理我们的数据之后
,数据再次被使用时
就会进行依赖收集
数据发生改变时
,所有收集到的依赖
都是进行对应的响应式操作
(比如更新界面)data选项
,也是在内部交给了reactive函数
将其编程响应式对象的Reactive判断的API
isProxy
是否是由 reactive 或 readonly创建的 proxy
。isReactive
是否是由 reactive创建的响应式代理
:该代理是 readonly 建的
,但包裹了由 reactive 创建的另一个代理
,它也会返回 true;是否是由 readonly 创建的只读代理
。reactive 或 readonly 代理的原始对象
(不建议保留对原始对象的持久引用。请谨慎使用)。不执行嵌套对象的深层响应式转换
(深层还是原生对象)。不执行嵌套对象的深层只读转换
(深层还是可读、可写的)。reactive API
对传入的类型
是有限制
的,它要求我们必须传入
的是一个对象
或者数组
类型
传入一个基本数据类型
(String、Number、Boolean)会报一个警告ref API
返回一个可变的响应式对象
,该对象作为一个 响应式的引用
维护着它内部的值
,这就是ref名称的来源
在ref的 value 属性中被维护
的模板中引入ref的值时
,Vue会自动帮助
我们进行解包操作
,所以我们并不需要在模板中通过 ref.value 的方式来使用
在 setup 函数内部
,它依然是一个 ref引用
, 所以对其进行操作时,我们依然需要使用 ref.value的方式
unref
如果参数是一个 ref,则返回内部值,否则返回参数本身
val = isRef(val) ? val.value : val
的语法糖函数isRef
是否是一个ref对象
。shallowRef
浅层的ref对象
triggerRef
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用
,但是不能被修改
,这个时候如何防止这种情况的出现呢?
readonly的方法
readonly会返回原生对象的只读代理
(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持
,并且不能对其进行修改)普通对象
reactive返回的对象
ref的对象
在readonly的使用过程中,有如下规则
返回的对象都是不允许修改的
原来的对象是允许被修改
的info对象是不允许被修改的
obj被修改时
,readonly返回的info对象也会被修改
不能去修改readonly返回的对象info
;readonly返回的对象的setter方法
被劫持了而已那么这个readonly有什么用呢?
在我们传递给其他组件数据时,往往希望其他组件使用我们传递的内容,但是不允许它们修改时,就可以使用readonly了 |
---|
如果我们使用ES6的解构语法 ,对reactive返回的对象进行解构获取值 ,那么之后无论是修改结构后的变量 ,还是修改reactive返回的state对象 ,数据都不再是响应式的 |
---|
toRefs
的函数,可以将reactive返回的对象中的属性都转成ref
name 和 age 本身都是 ref的
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法 |
---|
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制
工厂函数
,该函数接受 track
和 trigger
函数
作为参数带有 get 和 set 的对象
这里我们使用一个案例:
在前面我们讲解过计算属性computed:当我们的某些属性是依赖其他状态时,我们可以使用计算属性来处理
在 setup 函数中使用 computed 方法
来编写一个计算属性如何使用computed呢?
方式一
:接收一个getter函数
,并为 getter 函数返回的值,返回一个不变的 ref 对象方式二
:接收一个具有 get 和 set 的对象
,返回一个可变的(可读写)ref 对象在前面的Options API
中,我们可以通过watch
选项来侦听data或者props的数据变化
,当数据变化时执行某一些操作
在Composition API
中,我们可以使用watchEffect和watch
来完成响应式数据的侦听
watchEffect
用于自动收集响应式数据
的依赖watch
需要手动指定侦听的数据源
当侦听到某些响应式数据变化时,我们希望执行某些操作,这个时候可以使用 watchEffect
我们来看一个案例
首先,watchEffect传入的函数
会被立即执行一次
,并且在执行的过程中会收集依赖
如果在发生某些情况下,我们希望停止侦听,这个时候我们可以获取watchEffect的返回值函数,调用该函数即可
什么是清除副作用呢?
比如在开发中我们需要在侦听函数中执行网络请求
,但是在网络请求还没有达到
的时候,我们停止了侦听器
,或者侦听器侦听函数被再次执行了
那么上一次的网络请求应该被取消掉
,这个时候我们就可以清除上一次的副作用
在我们给watchEffect传入的函数被回调时,其实可以获取到一个参数
:onInvalidate
当副作用即将重新执行
或者 侦听器被停止
时会执行该函数传入的回调函数
我们可以在传入的回调函数中
,执行一些清除工作
setup中使用ref获取标签元素或者组件
watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前
如果我们希望在副作用函数中获取到元素,是否可行呢?
上面的案例的结果描述
我们会发现打印结果打印了两次:
这是因为setup函数在执行时就会立即执行传入的副作用函数,这个时候DOM并没有挂载
,所以打印为null
而当DOM挂载时
,会给title的ref对象赋值新的值
,副作用函数会再次执行
,打印出来对应的元素
调整watchEffect的执行时机
如果我们希望在第一次的时候就打印出来对应的元素
呢?
这个时候我们需要改变副作用函数的执行时机
它的默认值是pre
,它会在元素 挂载
或者 更新
之前
执行
所以我们会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素
flush 选项还接受 sync
,这将强制效果始终同步触发
。然而,这是低效
的,应该很少需要
watch的API完全等同于组件watch选项
的Property
需要侦听特定的数据源
,并在回调函数中执行副作用
惰性
的,只有当被侦听的源发生变化时
才会执行回调
与watchEffect的比较,watch允许我们
第一次不会直接执行
)更具体的说明当前哪些状态发生变化时
,触发侦听器的执行状态变化前后的值
侦听单个数据源
getter函数
:但是该getter函数必须
引用可响应式的对象
(比如reactive或者ref)直接写入一个可响应式的对象
,reactive或者ref(比较常用的是ref)数组
同时侦听多个源
深层
的侦听,那么依然需要设置 deep 为true
immediate 立即执行
我们前面说过 setup 可以用来替代 data 、 methods 、 computed 、watch 等等这些选项,也可以替代生命周期钩子 |
---|
事实上我们之前还学习过Provide和Inject,Composition API也可以替代之前的 Provide 和 Inject 的选项。
Provide函数
Inject函数
其实vue中的代码抽取很像react的hooks,下面来体验一下hooks怎么使用
目录结构
App.vue
<template>
<div>
<h2>当前计数: {{counter}}</h2>
<h2>计数*2: {{doubleCounter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<h2>{{data}}</h2>
<button @click="changeData">修改data</button>
<p class="content"></p>
<div class="scroll">
<div class="scroll-x">scrollX: {{scrollX}}</div>
<div class="scroll-y">scrollY: {{scrollY}}</div>
</div>
<div class="mouse">
<div class="mouse-x">mouseX: {{mouseX}}</div>
<div class="mouse-y">mouseY: {{mouseY}}</div>
</div>
</div>
</template>
<script>
import { ref, computed } from 'vue';
import {
useCounter,
useLocalStorage,
useMousePosition,
useScrollPosition,
useTitle
} from './hooks';
export default {
setup () {
// counter
const { counter, doubleCounter, increment, decrement } = useCounter();
// title
const titleRef = useTitle("coderwhy");
setTimeout(() => {
titleRef.value = "kobe"
}, 3000);
// 滚动位置
const { scrollX, scrollY } = useScrollPosition();
// 鼠标位置
const { mouseX, mouseY } = useMousePosition();
// localStorage
const data = useLocalStorage("info");
const changeData = () => data.value = "哈哈哈哈"
return {
counter,
doubleCounter,
increment,
decrement,
scrollX,
scrollY,
mouseX,
mouseY,
data,
changeData
}
}
}
</script>
<style scoped>
.content {
width: 3000px;
height: 5000px;
}
.scroll {
position: fixed;
right: 30px;
bottom: 30px;
}
.mouse {
position: fixed;
right: 30px;
bottom: 80px;
}
</style>
useCounter.js
import { ref, computed } from 'vue';
export default function() {
const counter = ref(0);
const doubleCounter = computed(() => counter.value * 2);
const increment = () => counter.value++;
const decrement = () => counter.value--;
return {
counter,
doubleCounter,
increment,
decrement
}
}
useTitle.js
import { ref, watch } from 'vue';
export default function(title = "默认的title") {
const titleRef = ref(title);
watch(titleRef, (newValue) => {
document.title = newValue
}, {
immediate: true
})
return titleRef
}
useScrollPosition.js
import { ref } from 'vue';
export default function() {
const scrollX = ref(0);
const scrollY = ref(0);
document.addEventListener("scroll", () => {
scrollX.value = window.scrollX;
scrollY.value = window.scrollY;
});
return {
scrollX,
scrollY
}
}
useMousePosition.js
import { ref } from 'vue';
export default function() {
const mouseX = ref(0);
const mouseY = ref(0);
window.addEventListener("mousemove", (event) => {
mouseX.value = event.pageX;
mouseY.value = event.pageY;
});
return {
mouseX,
mouseY
}
}
useLocalStorage.js
import { ref, watch } from 'vue';
export default function(key, value) {
const data = ref(value);
if (value) {
window.localStorage.setItem(key, JSON.stringify(value));
} else {
data.value = JSON.parse(window.localStorage.getItem(key));
}
watch(data, (newValue) => {
window.localStorage.setItem(key, JSON.stringify(newValue));
})
return data;
}
// 一个参数: 取值
// const data = useLocalStorage("name");
// // 二个参数: 保存值
// const data = useLocalStorage("name", "coderwhy");
// data.value = "kobe";
index.js
import useCounter from './useCounter';
import useTitle from './useTitle';
import useScrollPosition from './useScrollPosition';
import useMousePosition from './useMousePosition';
import useLocalStorage from './useLocalStorage';
export {
useCounter,
useTitle,
useScrollPosition,
useMousePosition,
useLocalStorage
}
这样的代码结构一目了然,便于他人理解。
本篇文章主体内容来自coderwhy老师的讲解,很喜欢这个老师,干货满满