赞
踩
代码逻辑复用(logic reuse)一直是开发中(任何开发)一个非常重要的功能。
对于当前组件化盛行的前端开发来说,如何可以更多的对组件中的代码逻辑进行复用是一致在探索的一个话题,无论是在React、Vue,还是在Angular中。
那么接下来,我们就一起来看一下目前比较常见的在Vue中可以实现组件代码复用的方式,并且会详细、深入的学习一下Vue3最新的Composition API。
其实在Vue2当中的Options API中已经有了一些代码复用的方式,当然这些API在Vue3中依然是保留的,所以我们一起来学习一下。
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成。
混合
进入该组件本身的选项中;const sayHelloMixin = {
created() {
this.sayHello();
},
methods: {
sayHello() {
console.log("Hello Page Component");
}
}
}
export default sayHelloMixin;
之后,在Home.vue中通过mixins的选项进行混入:
<template>
<div>
</div>
</template>
<script>
import sayHelloMixin from '../mixins/sayHello';
export default {
mixins: [sayHelloMixin]
}
</script>
<style scoped>
</style>
如果Mixin对象中的选项和组件对象中的选项发生了冲突,那么Vue会如何操作呢?
情况一:如果是data函数的返回值对象
const sayHelloMixin = {
data() {
return {
name: "mixin",
age: 18
}
}
}
export default sayHelloMixin;
Home.vue中的代码:
<script>
import sayHelloMixin from '../mixins/sayHello';
export default {
mixins: [sayHelloMixin],
data() {
return {
message: "Hello World",
// 冲突时会保留组件中的name
name: "home"
}
}
}
</script>
情况二:如何生命周期钩子函数
const sayHelloMixin = {
created() {
console.log("mixin created")
}
}
export default sayHelloMixin;
Home.vue中的代码:
<script>
import sayHelloMixin from '../mixins/sayHello';
export default {
mixins: [sayHelloMixin],
created() {
console.log("home created");
}
}
</script>
情况三:值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。
mixin中的代码:
const sayHelloMixin = {
methods: {
sayHello() {
console.log("Hello Page Component");
},
foo() {
console.log("mixin foo function");
}
}
}
export default sayHelloMixin;
Home.vue中的代码:
<script>
import sayHelloMixin from '../mixins/sayHello';
export default {
mixins: [sayHelloMixin],
methods: {
foo() {
console.log("mixin foo function");
},
bar() {
console.log("bar function");
}
}
}
</script>
如果组件中的某些选项,是所有的组件都需要拥有的,那么这个时候我们可以使用全局的mixin:
mixin
来完成注册;import { createApp } from "vue";
import App from "./14_Mixin混入/App.vue";
const app = createApp(App);
app.mixin({
created() {
console.log("global mixin created");
}
})
app.mount("#app");
另外一个类似于Mixin的方式是通过extends属性:
我们开发一个HomePage.vue的组件对象:
<script>
export default {
data() {
return {
message: "Hello Page"
}
}
}
</script>
在Home.vue中我们可以继承自HomePage.vue:
<script>
import BasePage from './BasePage.vue';
export default {
extends: BasePage
}
</script>
在开发中extends用的非常少,在Vue2中比较推荐大家使用Mixin,而在Vue3中推荐使用Composition API。
在Vue2中,我们编写组件的方式是Options API:
但是这种代码有一个很大的弊端:
下面我们来看一个非常大的组件,其中的逻辑功能按照颜色进行了划分:
跳转
到响应的代码块中;如果我们能将同一个逻辑关注点相关的代码收集在一起会更好,这就是Composition API想要做的事情,以及可以帮助我们完成的事情。
那么既然知道Composition API想要帮助我们做什么事情,接下来看一下到底是怎么做呢?
setup
函数;setup其实就是组件的另外一个选项:
我们先来研究一个setup函数的参数,它主要有两个参数:
我们来看一个ShowMessage.vue的组件:
this
去获取(后面我会讲到为什么);<template>
<div>
<h2>{{message}}</h2>
</div>
</template>
<script>
export default {
props: {
message: String
},
setup(props) {
console.log(props.message);
}
}
</script>
另外一个参数是context,我们也称之为是一个SetupContext,它里面包含三个属性:
this.$emit
发出事件);<template>
<div>
<show-message message="Hello World" id="why" class="kobe">
<template #default>
<span>哈哈哈</span>
</template>
<template #content>
<span>呵呵呵</span>
</template>
</show-message>
</div>
</template>
我们在ShowMessage.vue中获取传递过来的内容:
<script>
export default {
props: {
message: String
},
setup(props, context) {
console.log(props.message);
// 获取attrs
console.log(context.attrs.id, context.attrs.class);
console.log(context.slots.default);
console.log(context.slots.content);
console.log(context.emit);
}
}
</script>
当然,目前我们并没有具体演示slots和emit的用法:
setup既然是一个函数,那么它也可以有返回值,它的返回值用来做什么呢?
<template>
<div>
<h2>{{name}}</h2>
<h2>当前计数: {{counter}}</h2>
</div>
</template>
<script>
export default {
props: {
message: String
},
setup(props, context) {
const name = "coderwhy";
let counter = 100;
return {
name,
counter
}
}
}
</script>
甚至是我们可以返回一个执行函数来代替在methods中定义的方法:
<template>
<div>
<h2>{{name}}</h2>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
export default {
props: {
message: String
},
setup(props, context) {
const name = "coderwhy";
let counter = 100;
const increment = () => {
console.log("increment");
}
const decrement = () => {
console.log("decrement");
}
return {
name,
counter,
increment,
decrement
}
}
}
</script>
但是,如果我们将 counter
在 increment
或者 decrement
进行操作时,是否可以实现界面的响应式呢?
那么我们应该怎么做呢?接下来我们就学习一下setup中数据的响应式。
官方关于this有这样一段描述(这段描述是我给官方提交了PR之后的一段描述):
表达的含义是this并没有指向当前组件实例;
并且在setup被调用之前,data、computed、methods等都没有被解析;
所以无法在setup中获取this;
其实在之前的这段描述是和源码有出入的(我向官方提交了PR,做出了描述的修改):
之前的描述大概含义是不可以使用this是因为组件实例还没有被创建出来;
后来我的PR也有被合并到官方文档中;
我是如何发现官方文档的错误的呢?
在阅读源码的过程中,代码是按照如下顺序执行的:
createComponentInstance
创建组件实例;setupComponent
初始化component
内部的操作;setupStatefulComponent
初始化有状态的组件;setupStatefulComponent
取出了 setup
函数;callWithErrorHandling
的函数执行 setup
;setup
函数之前就创建出来的。如果想为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive的函数:
<template>
<div>
<h2>{{state.name}}</h2>
<h2>当前计数: {{state.counter}}</h2>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
const state = reactive({
name: "coderwhy",
counter: 100
})
const increment = () => state.counter++;
const decrement = () => state.counter--;
return {
state,
increment,
decrement
}
}
}
</script>
也就是我们按照如下的方式在setup中使用数据,就可以让数据变成响应式的了:
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
那么这是什么原因呢?为什么就可以变成响应式的呢?
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型:
这个时候Vue3给我们提供了另外一个API:ref API
value
的属性中被维护的;接下来我们看一下Ref的API是如何使用的:
<template>
<div>
<h2>{{message}}</h2>
<button @click="changeMessage">changeMessage</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref("Hello World");
const changeMessage = () => message.value = "你好啊, 李银河";
return {
message,
changeMessage
}
}
}
</script>
这里有两个注意事项:
ref.value
的方式来使用;setup
函数内部,它依然是一个 ref引用
, 所以对其进行操作时,我们依然需要使用 ref.value
的方式;但是,模板中的解包是浅层的解包,如果我们的代码是下面的方式:
但是,如果我们将ref放到一个reactive的属性当中,那么它会自动解包:
我们通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方的这个响应式对象希望在另外一个地方被使用,但是不能被修改,这个时候如何防止这种情况的出现呢 ?
在开发中常见的readonly方法会传入三个类型的参数:
在readonly的使用过程中,有如下规则:
<script>
export default {
setup() {
// readonly通常会传入三个类型的数据
// 1.传入一个普通对象
const info = {
name: "why",
age: 18
}
const state1 = readonly(info)
console.log(state1);
// 2.传入reactive对象
const state = reactive({
name: "why",
age: 18
})
const state2 = readonly(state);
// 3.传入ref对象
const nameRef = ref("why");
const state3 = readonly(nameRef);
return {
state2,
changeName
}
}
}
</script>
那么这个readonly有什么用呢?
这个时候我们可以传递给子组件时,使用一个readonly数据:
检查对象是否是由 reactive
或 readonly
创建的 proxy。
检查对象是否是由 reactive
创建的响应式代理:
import { reactive, isReactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
console.log(isReactive(state)) // -> true
}
}
如果该代理是readonly
创建的,但包裹了由 reactive
创建的另一个代理,它也会返回 true:
import { reactive, isReactive, readonly } from 'vue'
export default {
setup() {
const state = reactive({
name: 'John'
})
// 从普通对象创建的只读 proxy
const plain = readonly({
name: 'Mary'
})
console.log(isReactive(plain)) // -> false
// 从响应式 proxy 创建的只读 proxy
const stateCopy = readonly(state)
console.log(isReactive(stateCopy)) // -> true
}
}
检查对象是否是由 readonly 创建的只读代理。
返回 reactive 或 readonly 代理的原始对象。
const info = {name: "why"}
const reactiveInfo = reactive(info)
console.log(toRaw(reactiveInfo) === info) // true
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
const state = shallowReactive({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)。
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2
}
})
// 改变 state 本身的 property 将失败
state.foo++
// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 可用
如果我们使用ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改解构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的:
<script>
import { ref, reactive } from 'vue';
export default {
setup() {
const state = reactive({
name: "why",
age: 18
});
const { name, age } = state;
const changeName = () => state.name = "coderwhy";
return {
name,
age,
changeName
}
}
}
</script>
那么有没有办法让我们解构出来的属性是响应式的呢?
// 当我们这样来做的时候, 会返回两个ref对象, 它们是响应式的
const { name, age } = toRefs(state);
// 下面两种方式来修改name都是可以的
const changeName = () => name.value = "coderwhy";
const changeName = () => state.name = "coderwhy";
这种做法相当于已经在state.name和ref.value之间建立了 链接,任何一个修改都会引起另外一个变化;
如果我们只希望转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法:
const name = toRef(state, 'name');
const { age } = state;
const changeName = () => state.name = "coderwhy";
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
如果参数是一个 ref
,则返回内部值,否则返回参数本身;
这是 val = isRef(val) ? val.value : val
的语法糖函数;
import { ref, unref } from 'vue';
const name = ref("why");
console.log(unref(name)); // why
判断值是否是一个ref对象。
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
这里我们使用一个官方的案例:
import { customRef } from 'vue';
export function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
}
})
}
在组件界面中使用:
<template>
<div>
<input v-model="message">
<h2>{{message}}</h2>
</div>
</template>
<script>
import { useDebouncedRef } from '../hooks/useDebounceRef';
export default {
setup() {
const message = useDebouncedRef("Hello World");
return {
message
}
}
}
</script>
创建一个浅层的ref对象
const info = shallowRef({name: "why"});
// 下面的修改不是响应式的
const changeInfo = () => info.value.name = "coderwhy";
手动触发和shallowRef
相关联的副作用:
const info = shallowRef({name: "why"});
// 下面的修改不是响应式的
const changeInfo = () => {
info.value.name = "coderwhy"
// 手动触发
triggerRef(info);
};
原文:https://mp.weixin.qq.com/s/5S2Smn-Xb6bptMeFGscGcQ
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。