6. vue3-composition-api

166 阅读2分钟

一、vue3中setup函数的参数

# 有两个参数 props 和 context

## setup和props
1. props依然需要注册使用 props:['xx']
2. setup中用props接收传递进来的响应式属性 setup(props){}
3. 在执行setup函数的时候没有绑定this,所以在setup中没有this
4. setup中返回(return)的内容可以直接在视图中渲染
5. setup函数只有在组件第一次加载的时候才执行一次,当组件重新渲染不会再次执行
    - 销毁后再重新加载属于第一次加载逻辑
6. 我们会把vue2中 `data /mehods /computed/watch/filters...`这些optionsAPI
    - 全部聚合在setup函数中处理,且基于vue3中提供的各种api,实现函数式编程!!

## context有三个属性:
attrs: 所有的非prop的attribute
slots: 父组件传递进来的插槽
emit: 组件内部需要发出事件时用emit

二、vue3的各种响应式api

1.使用ref实现计数器功能

image.png

<template>
  <div class="app">
    <h2>当前计数: {{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import { ref } from 'vue';


export default {
  setup() {
    // 默认定义的数据不是响应式数据
    // let counter = 100;

    let counter = ref(100);

    // 普通方法直接return即可
    const increment = () => {
      counter.value++;
    };
    const decrement = () => {
      counter.value--;
    };

    return {
      counter,
      increment,
      decrement
    }
  }
}
</script>

2.setup定义数据

<template>
  <div class="app">
    <h2>message: {{ message }}</h2>
    <button @click="changeMessage">修改message</button>

    <p>--------------------------</p>
    <h2>账号:{{ username }}</h2>
    <h2>密码:{{ password }}</h2>
    <button @click="changeAccount">修改账号</button>

    <p>--------------------------</p>
    <!-- <h2>当前计数reactive写法:{{ counter.counter }}</h2> -->
    <h2>当前计数ref写法:{{ counter }}</h2>
    <button @click="increment">+1</button>

    <p>--------------------------</p>
    <h2>当前计数-template表达式中修改counter:{{ counter }}</h2>
    <!-- counter已经解包,不需要写.value -->
    <button @click="increment">+1</button>
    <button @click="counter++">+1</button>

    <p>--------------------------</p>
    <!-- 使用时不需要加.value,修改时需要加.value 才可以 -->
    <h2>当前计数-浅层解包:{{ info.counter }}</h2>
    <button @click="info.counter.value++">+1</button>
  </div>
</template>

<script>
import { reactive, ref, toRefs } from 'vue';

export default {
  setup() {
    // 1.定义普通的数据,可以在template中正常使用
    // 缺点:不是响应式的
    let message = 'Hello World';
    const changeMessage = () => {
      // message不是响应式的,点击按钮,数据改变,视图不会渲染
      message = '你好啊,Hezi';
    }

    // 2.定义响应式数据
    /**
     * 2.1 reactive函数:定义复杂的数据
     *  + 返回的是Proxy代理对象
     *  + 基于account.xxx去操作对应的状态
     *  + 基于ES6中的Proxy实现数据劫持
     *  + 尝试把其解构返回 return {...account} => return { username: 'wx',password: '123456'}这样不可以,里面没有劫持
     *  + 解决办法:可以基于 toRefs 函数,把account状态中的每一项都变为单独的 RefImpl 对象,在视图中可以直接用对象里面的属性值不用account.xxx了
     *    + toRef(account.password) 只处理单个的数据
     *    + toRefs(account) 多个非响应式变为响应式对象
     */
    const account = reactive({
      username: 'wx',
      password: '123456'
    });
    
    // 点击会修改数据并渲染
    // 比Object.defineProperty好用在于:对于数据或者并未初始化的对象成员,都可以随意修改值,而且具备响应式的效果
    const changeAccount = () => {
      account.username = 'Hezi';
    }

    // 2.2 ref函数:定义简单类型的数据,也可以定义复杂类型的数据
    // 用reactive函数,视图中需要counter.counter才可以显示
    // 如果传入一个基本值,会报一个警告
    // const counter = reactive({
    //   counter: 0
    // });
    /**
     * 基于ref(初始值)构建响应式数据(状态)
     *  + 返回 RefImpl对象
     *  + 具备value属性可以获取和设置状态值 RefImpl.value
     *  + 而且value属性进行了set和get数据劫持(Object.defineProperty)
     *  + 在模板视图中渲染的时候,我们无需 RefImpl.value,因为自动渲染的就是它的value值
     */

    const counter = ref(0);
    const increment = () => {
      counter.value++;
    }

    // 3.ref是浅层解包
    const info = {
      counter
    }

    return {
      message,
      changeMessage,
      // account, 使用的时候account.xxx
      ...toRefs(account),
      changeAccount,
      counter,
      increment,
      info
    }
  }
}
</script>

ref 和 reactive的应用场景

<template>
  <div class="app">
    <form>
      账号:<input type="text" v-model="account.username"><br />
      密码:<input type="password" v-model="account.password">
    </form>
    <form>
      账号:<input type="text" v-model="username"><br />
      密码:<input type="password" v-model="password">
    </form>
  </div>
</template>

<script>// @ts-nocheck

import { ref, reactive, onMounted } from 'vue'

export default {
  setup() {
    // 定义响应式数据 reactive / ref

    // 强调:ref也可以定义复杂的数据
    const info = ref({});

    // 1.reactive的应用场景
    // 1.1 条件一: reactive应用于本地的数据
    // 1.2 条件二:多个数据之间是有关系的(聚合的数据,组合在一起有特定的作用)
    const account = reactive({
      username: "wx",
      password: "123456"
    });

    const username = ref('wx');
    const password = ref('123456');

    // 2. ref的应用场景,其他的基本都用ref
    // 2.1 定义本地的 一些简单的数据
    const message = ref("Hello world");

    // 2.2 定义从网络中获取的数据也是使用ref
    const musics = ref([]);
    onMounted(() => {
      const serverMusics = ["海阔天空", "小苹果", "月光"];

      musics.value = serverMusics;
    })

    return {
      account,
      username,
      password,
      musics
    }
  }
}
</script>

3.setup其他函数

单向数据流

<template>
  <div class="app">
    <h2>app:{{ info }}</h2>
    <show-info :info="info" :roInfo="roInfo" @changeInfoName="changeInfoName"
      @changeRoInfoName="changeRoInfoName"></show-info>
  </div>
</template>

<script>
import { reactive, readonly, isProxy, isReactive } from 'vue';
import ShowInfo from './ShowInfo.vue';

export default {
  components: {
    ShowInfo
  },
  setup() {
    // 本地定义多个数据,都需要传递给子组件
    const info = reactive({
      name: 'wx',
      age: 18,
      height: 1.88
    });

    const changeInfoName = (payload) => {
      info.name = payload;
    }

    // 1.readonly
    /**
     * readonly创建的数据也是响应式的
     * readonly会返回的Proxy代理对象不允许修改
     *    + roInfo对象是不允许被修改的
     *    + 当info被修改时,readonly返回的roInfo也会被修改
     *    + 但是不能去修改readonly返回的RoInfo
     * 原理:readonly返回的对象的setter方法被劫持
     */
    const roInfo = readonly(info);
    const changeRoInfoName = (payload) => {
      info.name = payload;
    }

    // 2.isProxy:检测对象是否由reactive 或 readonly创建的 Proxy
    console.log(isProxy(roInfo));// true

    // 3.isReactive:检测对象是否由reactive创建的响应式代理
    //   + 如果代理是readonly创建的,但包裹了由reactive创建的另一个代理,它也会返回true
    const obj1 = reactive({ name: 'wx' });
    const obj2 = readonly({ name: 'Hezi' });
    const obj3 = readonly(obj1);
    console.log(isReactive(obj1), isReactive(obj2)); //true false
    console.log(isReactive(obj3));// true

    // 4.isReadonly:检查对象是否由 readonly创建的只读代理
    console.log(obj2);// true

    // 5.toRaw 返回 reactive 或 readonly代理的原始对象(谨慎使用),调试
    // const info = Proxy({});
    // import { toRaw } from 'vue'
    // toRaw(info);

    // 6.shallowReactive:创建一个响应式代理,不对属性进行深层响应式转换
    // 7.shallowReadonly:创建一个Proxy,深层属性可读,可写

    return {
      info,
      changeInfoName,
      roInfo,
      changeRoInfoName
    }
  }
}
</script>

子组件:ShowInfo.vue

<template>
  <div class="app">
    <h2>showInfo:{{ info }}</h2>
    <!-- 
      子组件直接修改props
        + 可行的,但是不规范
        + 单向数据流(规范)
          + 子组件拿到的数据只能使用,不能修改
          + 如果要修改应该将事件传递出去让父组件修改
          + 因为这个数据在父组件中可能被多个子组件使用
     -->
    <!-- <button @click="info.name = 'kobe'">showInfo按钮:</button> -->
    <!-- 正确的做法:符合单向数据流-->
    <button @click="showInfoClick">showInfo按钮:</button>
    
    <p>---------------------------------</p>
    <h2>使用reandonly数据:{{ roInfo }}</h2>
    <!-- 报警告: target is readonly -->
    <!-- <button @click="roInfo.name = 'kobe'">showInfo按钮-readonly:</button>  -->
    <button @click="showInfoRoClick">showInfo按钮-readonly:</button> 

  </div>
</template>

<script>

export default {
  props: {
    // reactive data
    info: {
      type: Object,
      default: () => {}
    },
    // readOnly data
    roInfo: {
      type: Object,
      default: () => {}
    },
  },
  emits:["changeInfoName", "changeRoInfoName"],
  setup(props, { emit }){

    const showInfoClick = () => {
      emit('changeInfoName', 'kobe');
    }

    const showInfoRoClick = () =>{
      emit('changeRoInfoName', 'Hezi');
    }

    return {
      showInfoClick,
      showInfoRoClick
    }
  }
}
</script>

4.ref其他函数补充

<template>
  <div class="app">
    <h2>info:{{ info.name }} - {{ info.age }}</h2>
    <h2>单独显示:{{ name }}-{{ age }}-{{ height }}</h2>
    <!-- age是响应式的 -->
    <button @click="age++">修改age</button>
    <!-- height是响应式的 -->
    <button @click="height = 1.70">修改height</button>
  </div>
</template>

<script>
import { reactive, toRefs, toRef, shallowRef, triggerRef } from 'vue';

export default {
  setup() {
    const info1 = reactive({
      name: 'wx',
      age: 18,
      height: 1.80
    });

    // reactive被解构后会失去响应式
    // toRefs:多个非响应式变为响应式对象
    const { name, age } = toRefs(info1);

    // toRef:只处理单个的数据
    const height = toRef(info1, "height");

    // unref:获取一个ref引用中的value
    // => val = isRef(val) ? val.value : val

    // isRef:是否是一个ref对象
    // shallowRef:创建一个浅层的ref对象
    // triggerRef:手动触发和shallowRef相关联的副作用
    const info2 = shallowRef({ name: "Hezi" });
    const changeInfo = () => {
      info2.value.name = "wx"
      // 手动触发
      triggerRef(info2);
    }

    return {
      info: info1,
      name,
      age,
      height
    }
  }
}
</script>

5.setup中的computed

<template>
  <div class="app">
    <h2>{{ names.firstname + " " + names.lastName }}</h2>
    <h2>{{ fullName }}</h2>
    <h2>{{ firstname + " " + lastName }}</h2>
    <button @click="setFullname">设置fullname</button>
  </div>
</template>

<script>
import { ref, reactive, computed, toRefs } from 'vue';

export default {
  setup() {
    /**
      计算属性
        + 语法:computed([getter函数])
        + 返回ComputedRefImpl对象,和RefImpl对象类似,都是操作其value属性...
        + 这样获取的计算属性值是只读的,当我们尝试去修改的时候,报警告:computed value is readonly
     */

    // 1.定义数据
    const names = reactive({
      firstname: 'kobe',
      lastName: 'bryant'
    })

    // const fullName = computed(() => {
    //   return names.firstname + " " + names.lastName;
    // })
    // fullName.value= "He zi" computed value is readonly

    const fullName = computed({
      set: function (newValue) {
        const tempNames = newValue.split(" ")
        names.firstname = tempNames[0];
        names.lastName = tempNames[1];
      },
      get: function () {
        return names.firstname + " " + names.lastName;
      }
    })
    const setFullname = () => {
      fullName.value = "He zi"
    }

    const { firstname, lastName } = toRefs(names)

    return {
      names,
      fullName,
      firstname,
      lastName,
      setFullname
    }
  }
}
</script>

6.setup中使用ref引入元素组件

父组件:App.vue

<template>
  <div class="app">
    <h2 ref="titleRef">我是标题</h2>
    <button ref="btnRef">按钮</button>
    
    <p>---------------------</p>
    <show-info ref="showInfoRef"></show-info>
    <button @click="getElement">获取元素</button>
  </div>
</template>

<script>
import { onMounted, ref } from 'vue';
import ShowInfo from './ShowInfo.vue';

export default {
  components: {
    ShowInfo
  },
  setup() {
    const titleRef = ref()
    const btnRef = ref()
    const showInfoRef = ref()
    // console.log(titleRef.value); undefined

    const getElement = () => {
      console.log(titleRef.value);//<h2>我是标题</h2>
      console.log(btnRef.value);//<button>按钮</button>
      console.log(showInfoRef.value);//Proxy {…} 组件实例
      console.log(showInfoRef.value.showInfoFoo());//showInfo foo function 可以获取组件实例中的方法
    }

    onMounted(()=>{
      console.log(titleRef.value);
    })

    return {
      titleRef,
      btnRef,
      getElement,
      showInfoRef
    }
  }
}
</script>

子组件:ShowInfo.vue

<template>
  <div class="showInfo">
    <h2>showInfo</h2>
  </div>
</template>

<script>

export default {
  setup() {
    function showInfoFoo() {
      console.log("showInfo foo function");
    }

    return {
      showInfoFoo
    }
  }
}
</script>

7.setup中的Provide 和 inject

父组件

<template>
  <div class="app">
    <show-info></show-info>
  </div>
</template>

<script>
import { ref, provide } from 'vue';
import ShowInfo from './ShowInfo.vue';

export default {
  components: {
    ShowInfo
  },
  setup() {
    const name = ref('wx')

    // 共享的是响应式数据时,父组件改变,子组件也会改变,反之则不行
    provide("name", name)
    provide("age", "18")

  }
}
</script>

子组件

<template>
  <div class="app">
    showInfo:{{ name }}-{{ age }}-{{ height }}
  </div>
</template>

<script>
import { inject } from 'vue';

export default {
  setup() {
    // 如果用的是options api,需要手动解包,加.value
    // composition api 自动解包
    const name = inject("name")
    const age = inject("age")

    // 没有共享的时候给一个默认值
    const height = inject("height", 1.80)

    return {
      name,
      age,
      height
    }
  }
}
</script>

8.setup中的侦听器

watch

<template>
  <div class="app">
    {{ message }}
    <button @click="changeMessage">修改message</button>
    <button @click="info.friend.name = 'Hezi'">修改info</button>
  </div>
</template>

<script>
import { ref, watch, reactive } from 'vue';


export default {
  setup() {
    // 定义数据
    const message = ref("Hello world")
    const info = reactive({
      name: 'wx',
      age: 18,
      friend: {
        name: 'kobe'
      }
    })

    // watch监听器:监听现有状态改变,触发对应函数执行,第一次不会执行

    //监听ref某一个状态直接监听
    watch(message, (newValue, oldValue) => {
      console.log(newValue, oldValue);//Hello App Hello world
    })
    const changeMessage = () => {
      message.value = 'Hello App'
    }

    // 监听reactive某一个状态要写成一个函数,监听整个reactive状态新值和老值是同一个引用 
    watch(info, (newValue, oldValue) => {
      console.log(newValue, oldValue);
      console.log(newValue === oldValue);//true
    })
    // watch默认深度侦听
    watch(() => info.friend.name, (newValue, oldValue) => {
      console.log(newValue, oldValue);// Hezi kobe
    })
    // 如果结构成一个普通的对象就要手动深度监听,返回的也是普通对象
    watch(() => ({ ...info }), (newValue, oldValue) => {
      console.log(newValue, oldValue);
    }, {
      immediate: true,
      deep: true
    })

    // 第一次立即执行,监听单个源
    watch(() => info.friend.name, (newValue, oldValue) => {
      console.log(newValue, oldValue);// kobe undefined => Hezi kobe
    }, {
      immediate: true
    })


    return {
      message,
      info,
      changeMessage
    }
  }
}
</script>

watchEffect

<template>
  <div class="app">
    <h2>当前计数:{{ counter }}-{{ name }}</h2>
    <button @click="counter++">修改counter</button>
    <button @click="name = 'Hezi'">修改name</button>
  </div>
</template>

<script>
import { ref, watch, watchEffect } from 'vue'

export default {
  setup() {
    const counter = ref(0)
    const name = ref('wx')
    /**
     * watchEffect传入的函数默认会执行
     * 在执行过程中,会自动收集依赖(依赖哪些响应式的数据)
     * 只有在收集的依赖发生改变时,watchEffect传入的函数才会再次执行
     */
    const stopWatch = watchEffect(() => {
      // 修改counter或者name都会使得watchEffect函数执行
      console.log('------', counter.value, name.value);

      if (counter.value >= 10) {
        // 停止监听
        stopWatch();
      }
    })

    return {
      counter,
      name
    }
  }
}
</script>

三、hooks练习

1.useCounter抽取

image.png app页面:

<template>
  <div class="app">
    <div>app</div>
    <p>-----------------</p>
    <!-- 计数器 -->
    <home></home>
    <p>-----------------</p>
    <about></about>
  </div>
</template>

<script>
import Home from './views/Home.vue'
import About from './views/About.vue'

export default {
  components: {
    Home,
    About
  },
  setup() {

  }
}
</script>

about页面 和 home页面:

<template>
  <div class="about">
    <h2>about计数:{{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
  </div>
</template>

<script>
import useCounter from '../hooks/useCounter.js'
import useTitle from '../hooks/useTitle';

export default {
  setup() {
    // 修改标题
    useTitle('关于')
    
    return {
      ...useCounter()
    }
  }
}
</script>

useCounter.js

import { ref } from 'vue'

export default function useCounter() {
  const counter = ref(0)

  function increment() {
    counter.value++;
  }

  function decrement() {
    counter.value--;
  }

  return {
    counter,
    increment,
    decrement
  }
}

2.useTitle

image.png

app页面:

<template>
  <div class="app">
    <div>
      app:
      <button @click="changeTitle">修改title</button>
    </div>
    <p>----点击按钮显示 home or about 页面----</p>
    <button @click="currentPage = 'home'">home</button>
    <button @click="currentPage = 'about'">about</button>
    <p>----显示页面----</p>
    <component :is="currentPage"></component>
  </div>
</template>

<script>
import { ref } from 'vue'
import Home from './views/Home.vue'
import About from './views/About.vue'
import useTitle from './hooks/useTitle'

export default {
  components: {
    Home,
    About
  },
  setup() {
    const currentPage = ref('home')

    function changeTitle() {
      useTitle("app title")
    }

    return {
      currentPage,
      changeTitle
    }
  }
}
</script>

home页面

<template>
  <div class="home">
    <h2>home计数:{{ counter }}</h2>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <p>---在home页面点击按钮修改标题---</p>
    <button @click="popularClick">首页-流行</button>
    <button @click="hotClick">首页-热门</button>
    <button @click="songClick">首页-歌单</button>
  </div>
</template>

<script>
import useCounter from '../hooks/useCounter';
import useTitle from '../hooks/useTitle';


export default {
  setup() {
    // 修改标题
    const title = useTitle('首页')

    function popularClick(){
      title.value="首页-流行"
    }

    function hotClick(){
      title.value="首页-热门"
    }

    function songClick(){
      title.value="首页-歌单"
    }
    
    return {
      ...useCounter(),
      popularClick,
      hotClick,
      songClick
    }
  }
}
</script>

useTitle.js

import { watch, ref } from "vue"

export default function useTitle(titleValue) {
  // document.title = title;

  // 定义ref的引用数据
  const title = ref(titleValue)

  watch(title, (newValue, oldValue)=>{
    document.title = newValue
  }, {
    immediate: true
  })

  // 返回ref的值
  return title
}

四、setup语法糖

优点: image.png

image.png

app页面

<script setup>
// 1.所有编写在顶层的代码和引入的对象或组件都是暴露给template使用
import { ref, onMounted } from 'vue'
import ShowMessage from './ShowMessage.vue'

// 2.定义响应式数据
const message = ref("hello World")

// 3.定义绑定的函数
function changeMessage() {
  message.value = "你好,Hezi"
}

function messageBtnClick(payload) {
  console.log(payload);//showMessage内部发生了点击
}

// 4.获取组件实例
const showMessageRef = ref()
onMounted(() => {
  showMessageRef.value.foo() //foo function
})
</script>

<template>
  <div class="app">
    <div>message:{{ message }}</div>
    <button @click="changeMessage">修改message</button>
    <show-message name="wx" :age="18" @messageBtnClick="messageBtnClick" ref="showMessageRef"></show-message>
  </div>
</template>


ShowMessage页面

<template>
  <div class="showMessage">
    <div>showMessage:{{ name }}-{{ age }}</div>
    <button @click="showMessageBtnClick">showMessageBtn</button>
  </div>
</template>

<script setup>
defineProps({
  name: {
    type: String,
    default: ""
  },
  age: {
    type: Number,
    default: 0
  }
})

const emits = defineEmits(["messageBtnClick"])
function showMessageBtnClick() {
  emits("messageBtnClick", "showMessage内部发生了点击")
}

function foo() {
  console.log('foo function');
}

// 暴露函数
defineExpose({
  foo
})
</script>