组件和组件之间有时候会存在相同的代码逻辑,希望对相同的代码逻辑进行抽取
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成:
Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能;
一个Mixin对象可以包含任何组件选项;
当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中;
基本使用:
import { demoMixin } from './mixins/demoMixin';
export default {
mixins: [demoMixin],
}
// demoMixin.js
export const demoMixin = {
// 可以写data,可以写方法,可以写生命周期函数
}
合并规则;
如果Mixin对象中的选项和组件对象中的选项发生了冲突
这里分成不同的情况来进行处理;
情况一:data函数的返回值对象
返回值对象默认情况下会进行合并;
如果data返回值对象的属性发生了冲突,那么会保留组件自身的数据;
情况二:生命周期钩子函数
生命周期的钩子函数会被合并到数组中,都会被调用;
情况三:值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。
比如都有methods选项,并且都定义了方法,那么它们都会生效;
但是如果对象的key相同,那么会取组件对象的键值对;
全局混入Mixin:
全局的Mixin可以使用 应用app的方法 mixin 来完成注册;
一旦注册,那么全局混入的选项将会影响每一个组件;
// main.js
const app = createApp(App);
app.mixin({
created() {
console.log('1111');
}
})
app.mount("#app");
参数:
第一个参数:props
第二个参数:context
①父组件传递过来的属性会被放到props对象中,我们在setup中如果需要使用,那么就可以直接通过props参数获取
props: {
message: {
type: String,
required: true
}
},
setup(props) {
console.log(props.message);
}
②包含三个属性:
attrs:所有的非prop的attribute;
slots:父组件传递过来的插槽(在渲染函数返回时会有作用);
emit:当我们组件内部需要发出事件时会用到emit(因为我们不能访问this,所以不可以通过 this.$emit发出事件);
// setup(props, context) {
setup(props, {attrs, slots, emit}) {
console.log(props.message);
console.log(attrs.id, attrs.class);
console.log(slots);
console.log(emit);
}
返回值:
setup的返回值可以在模板template中被使用; 可以通过setup的返回值来替代data选项;
setup(props) {
return {
title: "hello!",
counter: 100
}
}
可以返回一个执行函数来代替在methods中定义的方法
无法实现界面的响应式
对于一个定义的变量来说,默认情况下,Vue并不会跟踪它的变化,来引起界面的响应式操作
setup() {
let counter = 100;
// 局部函数
const increment = () => {
counter++
}
return {
title: "hello!",
counter,
increment
}
}
// counter发生了变化,但界面没有变化
setup不可以使用this:
this并没有指向当前组件实例;
并且在setup被调用之前,data、computed、methods等都没有被解析;
所以无法在setup中获取this;
setup顶层编写方式:
<script setup> // 在script标签里使用setup
import { ref } from 'vue';
import HelloWorld from './HelloWorld.vue';
const counter = ref(0);
const increment = () => counter.value++;
const getCounter = (payload) => {
console.log(payload);
}
</script>
为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive函数
// 使用:{{state.counter}}
import { reactive } from 'vue';
setup() {
const state = reactive({
counter: 100
})
// 局部函数
const increment = () => {
state.counter++
}
return {
state,
increment
}
}
reactive API对传入的类型是有限制的,它要求我们必须传入的是一个对象或者数组类型
如果传入基本数据类型会报错
ref 会返回一个可变的响应式对象,该对象作为一个 响应式的引用 维护着它内部的值;
它内部的值是在ref的 value 属性中被维护的;
<h2>{{counter}}</h2>
<script>
import { ref } from 'vue';
setup() {
// counter变成一个ref的可响应式的引用
let counter = ref(100);
// 局部函数
const increment = () => {
// 注意调用方式
counter.value++
}
return {
counter,
increment
}
}
</script>
在模板中引入ref的值时,Vue会自动帮助我们进行解包操作,所以我们并不需要在模板中通过 ref.value 的方式来使用;
但是在 setup 函数内部,它依然是一个 ref引用, 所以对其进行操作时,我们依然需要使用 ref.value的方式;
ref自动解包:
模板中的解包是浅层的解包
将ref放到一个reactive的属性当中,那么在模板中使用时,它会自动解包
通过reactive或者ref可以获取到一个响应式的对象,但是某些情况下,我们传入给其他地方(组件)的这个响应式对象希望在另外一个地方(组件)被使用,但是不能被修改。
readonly会返回原生对象的只读代理(也就是它依然是一个Proxy,这是一个proxy的set方法被劫持,并且不能对其进行修改);
在开发中常见的readonly方法会传入三个类型的参数:
类型一:普通对象;
类型二:reactive返回的对象;
类型三:ref的对象;
import { reactive, ref, readonly } from 'vue';
setup() {
// 1.普通对象
const info1 = {name: 'wj'};
const state1 = readonly(info1)
// 2.reactive响应式对象
const state = reactive({
name: 'wj'
})
const state2 = readonly(state)
// 3.传入ref对象
const nameRef = ref('wj');
const state3 = readonly(nameRef);
const updateState = () => {
readonlyInfo1.name = "why"
}
return {
updateState
}
}
isProxy
检查对象是否是由 reactive 或 readonly创建的 proxy。
isReactive
检查对象是否是由 reactive创建的响应式代理:
如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;
isReadonly
检查对象是否是由 readonly 创建的只读代理。
toRaw
返回 reactive 或 readonly 代理的原始对象(不建议保留对原始对象的持久引用。请谨慎使用)。
shallowReactive
创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。
shallowReadonly
创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)
ES6的解构语法,对reactive返回的对象进行解构获取值,那么之后无论是修改结构后的变量,还是修改reactive返回的state对象,数据都不再是响应式的
将解构出来的属性是响应式的:
Vue提供了一个toRefs的函数,可以将reactive返回的对象中的属性都转成ref;
再次进行结构出来的 name 和 age 本身都是 ref的;
import { reactive, toRefs } from 'vue';
setup() {
const info = reactive({name: 'wj', age: 18})
const {name, age} = toRefs(info)
const changeAge = () => {
age.value++
}
return {
name,
age,
changeAge
}
}
转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法
const name = toRef(state, 'name');
const {age} = state;
const changeName = () => state.name = 'wj';
unref
如果我们想要获取一个ref引用中的value,那么也可以通过unref方法:
如果参数是一个 ref,则返回内部值,否则返回参数本身;
这是 val = isRef(val) ? val.value : val 的语法糖函数;
isRef
判断值是否是一个ref对象。
shallowRef
创建一个浅层的ref对象;
triggerRef
手动触发和 shallowRef 相关联的副作用:
创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制:
它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;
并且返回一个带有 get 和 set 的对象
使用computed
方式一:接收一个getter函数,并为 getter 函数返回的值,返回一个不变的 ref 对象;
方式二:接收一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;
import {computed} from 'vue'
// 传入getter函数
setup() {
const fullname = computed(() => firstname.value+" "+lastname.value);
}
// 传入一个对象
setup() {
const fullname = computed({
get: () => firstname.value + " " + lastname.value;
set(newValue) {
const names = newValue.split(" ");
firstname.value = names[0];
lastname.value = names[1];
}
});
}
在Composition API中,可以使用watchEffect和watch来完成响应式数据的侦听;
watchEffect用于自动收集响应式数据的依赖;
watch需要手动指定侦听的数据源;
侦听到某些响应式数据变化时,执行某些操作
watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
其次,只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
watchEffect(() => {
console.log("name:",name.value,"age:",age.value);
})
停止侦听:
获取watchEffect的返回值函数,调用该函数。
const stopWatch = watchEffect(() => {
console.log("name:",name.value,"age:",age.value);
})
const change = () => {
age.value++;
if(age.value > 20) {
stopWatch();
}
}
watchEffect清除副作用
在开发中需要在侦听函数中执行网络请求,但是在网络请求还没有达到的时候,我们停止了侦听器,或者侦听器侦听函数被再次执行了。
那么上一次的网络请求应该被取消掉,这个时候就可以清除上一次的副作用;
在给watchEffect传入的函数被回调时,其实可以获取到一个参数:onInvalidate
当副作用即将重新执行 或者 侦听器被停止 时会执行该函数传入的回调函数;
可以在传入的回调函数中,执行一些清楚工作;
const stopWatch = watchEffect((onInvalidate) => {
console.log("name:",name.value,"age:",age.value);
onInvalidate(() => {
// 在这个函数中清除额外的副作用,如清除定时器
})
})
const change = () => {
age.value++;
if(age.value > 20) {
stopWatch();
}
}
setup中使用ref
<h2 ref="title">hahaha</h2>
<script>
setup() {
// 默认绑定null
const title = ref(null);
return {
title
}
}
</script>
watchEffect的执行时机
默认情况下,组件的更新会在副作用函数执行之前
调整watchEffect的执行时机
如果在第一次的时候就打印出来对应的元素
这个时候需要改变副作用函数的执行时机;
它的默认值是pre,它会在元素 挂载 或者 更新 之前执行;
所以会先打印出来一个空的,当依赖的title发生改变时,就会再次执行一次,打印出元素;
可以设置副作用函数的执行时机:
flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
setup() {
const title = ref(null);
watchEffect(() => {
console.log(title.value);
},{
flush: 'post'
})
}
watch的API完全等同于组件watch选项的Property:
watch需要侦听特定的数据源,并在回调函数中执行副作用;
默认情况下它是惰性的,只有当被侦听的源发生变化时才会执行回调;
与watchEffect的比较,watch允许:
懒执行副作用(第一次不会直接执行);
更具体的说明当哪些状态发生变化时,触发侦听器的执行;
访问侦听状态变化前后的值;
侦听单个数据源:
watch侦听函数的数据源有两种类型:
一个getter函数:但是该getter函数必须引用可响应式的对象(比如reactive或者ref);
直接写入一个可响应式的对象,ref(如果是一个reactive的对象监听,需要进行转换);
setup() {
const info = reactive({name: "why", age: 18});
// 1.侦听watch时,传入一个getter函数
watch(() => info.name, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 2.传入一个可响应式对象: reactive对象/ref对象
// 情况一: reactive对象获取到的newValue和oldValue本身都是reactive对象
// 如果希望newValue和oldValue是一个普通的对象
watch(() => {
return {...info}
}, (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
// 情况二: ref对象获取newValue和oldValue是value值的本身
// const name = ref("why");
// watch(name, (newValue, oldValue) => {
// console.log("newValue:", newValue, "oldValue:", oldValue);
// })
侦听多个数据源
侦听器还可以使用数组同时侦听多个源
const name = ref('wj');
const age = ref(18);
const changeName = () => {
name.value = "ajg";
}
watch([name, age], (newValue, oldValue) => {
console.log("newValue:", newValue, "oldValue:", oldValue);
})
侦听响应式对象
如果侦听一个数组或者对象,那么可以使用一个getter函数,并且对可响应对象进行解构
const names = reactive(['abc','aaa','aas']);
watch(() => [...names], (newValue, oldValue) => {
console.log(newValue, oldValue);
})
watch选项
侦听一个深层的侦听,那么依然需要设置 deep 为true:
也可以传入 immediate 立即执行;
const state = reactive({
name: 'wj',
friend: {
name: 'ajg'
}
})
watch(() => state, (newValue, oldValue) => {
console.log(newValue, oldValue);
},{deep: true, immediate: true})
const changeName = () => {
state.friend.name = 'aaa';
}
setup中可以使用直接导入的 onX 函数注册生命周期钩子
import {onMounted} from 'vue';
onMounted(() => {
console.log('aaa')
})
通过 provide 方法来定义每个 Property;
provide可以传入两个参数:
name:提供的属性名称;
value:提供的属性值;
setup() {
const name = 'wj';
let counter = 100;
provide('name', name);
provide('counter', counter);
}
在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:
可以通过 inject 来注入需要的内容;
inject可以传入两个参数:
要 inject 的 property 的 name;
默认值;
setup() {
const name = inject('name');
const counter = inject('counter');
return {
name,
counter
}
}
为了增加 provide 值和 inject 值之间的响应性,可以在 provide 值时使用 ref 和 reactive。
let counter = ref(100);
let info = reactive({
name: 'wj',
age: 18
})
provide('counter', counter);
provide('info', info);
Vue在生成真实的DOM之前,会将节点转换成VNode,而VNode组合在一起形成一颗树结构,就是虚拟DOM(VDOM);
template 中的HTML 最终也是使用渲染函数生成对应的VNode;
想充分的利用JavaScript的编程能力,可以编写 createVNode 函数,生成对应的VNode;
使用 h()函数:
h() 函数是一个用于创建 vnode 的一个函数;
准确的命名是 createVNode() 函数,为了简便Vue将之简化为 h() 函数。
h函数接受三个参数
①tag 一个html标签名、一个组件、一个异步组件、或者一个函数式组件(必需)/ ‘div’
②props 与attribute、prop和事件相对应的对象,在模板中使用(可选) / { }
③children 子VNodes,使用’h()'构建,或使用字符串获取"文本Vnode",或者有插槽的对象(可选)
如果没有props,那么通常可以将children作为第二个参数传入;
如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
基本使用:
h函数可以在两个地方使用:
render函数选项中;
setup函数选项中(setup本身需要是一个函数类型,函数再返回h函数创建的VNode);
①
// 使用render函数可以将template模板省略不写
<script>
import { h } from 'vue';
export default {
render() {
return h("h2", {class: "title"}, "hello render")
}
}
</script>
②
setup () {
const counter = ref(0);
return () => {
return h("div", {class: "app"}, [
h("h2", null, `当前计数:${counter.value}`),
h("button", {
onClick: () => counter.value++
}, "+1"),
h("button", {
onClick: () => counter.value--
}, "-1"),
])
}
}
③组件的使用
直接使用组件名,不用注册
<script>
import { h } from 'vue';
import HelloWorld from './HelloWorld.vue'
export default {
render() {
return h(HelloWorld, {}, "")
}
}
</script>
④插槽的使用
// App.vue
render() {
return h("div", null, [
h(HelloWorld, null, {
default: props => h("span", null, `app传入到HelloWorld中的内容: ${props.name}`)
})
])
}
// HelloWorld.vue
render() {
return h("div", null, [
h("h2", null, "Hello World"),
this.$slots.default ? this.$slots.default({name: "wj"}): h("span", null, "HelloWorld的插槽默认值")
])
}
在项目中使用jsx,那么需要添加对jsx的支持:
jsx通常会通过Babel来进行转换(React编写的jsx就是通过babel转换的);
对于Vue来说,只需要在Babel中配置对应的插件即可;
安装Babel支持Vue的jsx插件:
npm install @vue/babel-plugin-jsx -D
vue3脚手架生成项目支持jsx,不用安装
在babel.config.js配置文件中配置插件:
目前不用配置插件(vue3脚手架生成)
// App.vue
render() {
const increment = () => this.counter++;
const decrement = () => this.counter--;
// 使用变量时用一个大括号包裹
return (
<div>
<h2>当前计数:{this.counter}</h2>
<button onclick={increment}>+1</button>
<button onclick={decrement}>-1</button>
<HelloWorld>
{{default:props => <button>我是按钮</button>}}
</HelloWorld>
</div>
)
}
// HelloWorld.vue
render() {
return (
<div>
<h2>hello wj</h2>
{this.$slots.default ? this.$slots.default(): <span>哈哈哈</span>}
</div>
)
}