Vue3的composition-API

易宣
2023-12-01

1.Mixin

组件和组件之间有时候会存在相同的代码逻辑,希望对相同的代码逻辑进行抽取

在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");

2.setup函数

参数:

​ 第一个参数: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>

3.Reactive API

为在setup中定义的数据提供响应式的特性,那么我们可以使用reactive函数

// 使用:{{state.counter}}

import { reactive } from 'vue';
setup() {
      const state = reactive({
        counter: 100
      })
      // 局部函数
      const increment = () => {
        state.counter++
      }
      return {
        state,
        increment
      }
    }

4.Ref API

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的属性当中,那么在模板中使用时,它会自动解包

5.readonly

通过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
      }
    }

6.Reactive判断的API

isProxy

检查对象是否是由 reactive 或 readonly创建的 proxy。

isReactive

检查对象是否是由 reactive创建的响应式代理:

如果该代理是 readonly 建的,但包裹了由 reactive 创建的另一个代理,它也会返回 true;

isReadonly

检查对象是否是由 readonly 创建的只读代理。

toRaw

返回 reactive 或 readonly 代理的原始对象(建议保留对原始对象的持久引用。请谨慎使用)。

shallowReactive

创建一个响应式代理,它跟踪其自身 property 的响应性,但不执行嵌套对象的深层响应式转换 (深层还是原生对象)。

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换(深层还是可读、可写的)

7.toRefs

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
      }
    }

8.toRef

转换一个reactive对象中的属性为ref, 那么可以使用toRef的方法

const name = toRef(state, 'name');
const {age} = state;
const changeName = () => state.name = 'wj';

9.ref其他的API

unref

如果我们想要获取一个ref引用中的value,那么也可以通过unref方法

​ 如果参数是一个 ref,则返回内部值,否则返回参数本身;

​ 这是 val = isRef(val) ? val.value : val 的语法糖函数;

isRef

判断值是否是一个ref对象。

shallowRef

创建一个浅层的ref对象;

triggerRef

手动触发和 shallowRef 相关联的副作用:

10.customRef

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制

​ 它需要一个工厂函数,该函数接受 track 和 trigger 函数作为参数;

​ 并且返回一个带有 get 和 set 的对象

11.computed

使用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];
  }
  });
}

12.侦听数据的变化

在Composition API中,可以使用watchEffect和watch来完成响应式数据的侦听;

​ watchEffect用于自动收集响应式数据的依赖;

​ watch需要手动指定侦听的数据源;

1.watchEffect

侦听到某些响应式数据变化时,执行某些操作

​ 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'
  })
}

2.Watch

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';
}

13.生命周期钩子

setup中可以使用直接导入的 onX 函数注册生命周期钩子

import {onMounted} from 'vue';

onMounted(() => {
  console.log('aaa')
})

14.Provide函数和Inject函数

1.Provide函数

通过 provide 方法来定义每个 Property;

provide可以传入两个参数:

​ name:提供的属性名称;

​ value:提供的属性值;

setup() {
      const name = 'wj';
      let counter = 100;

      provide('name', name);
      provide('counter', counter);
}

2.Inject函数

在 后代组件 中可以通过 inject 来注入需要的属性和对应的值:

可以通过 inject 来注入需要的内容;

inject可以传入两个参数:

​ 要 inject 的 property 的 name;

​ 默认值;

setup() {
      const name = inject('name');
      const counter = inject('counter');
      return {
        name,
        counter
      }
    }

3.数据的响应式

为了增加 provide 值和 inject 值之间的响应性,可以在 provide 值时使用 ref 和 reactive。

let counter = ref(100);
let info = reactive({
  name: 'wj',
  age: 18
})
provide('counter', counter);
provide('info', info);

15.h函数

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的插槽默认值")
      ])
    }

16.jsx

1.bable设置

在项目中使用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>
      )
}

 类似资料: