likes
comments
collection
share

vue3之Composition API详解

作者站长头像
站长
· 阅读数 15

今天开始要学习一下vue3,当然需要先从官方文档入手了,官方文档地址:[v3.vuejs.org/

这篇文章着重说一下V3里面的所有的compositionAPI,有什么作用以及如何使用,以下内容我将会按照官网的步骤逐渐展开,形成自己的理解,也是对官网一个较为细致的讲解。

首先来了解一下什么是Composition API,它不是针对用户层面的,而是针对vue这个框架的,把vue里面的功能那个拆分成一个个的hook,然后组合起来就形成了Composition API这种设计模式。

一、setup

vue3之Composition API详解

所有的Composition API都需要在setup函数中使用,相当于是一个入口,而setup函数是vue3中新增的一个组件选项,为什么说它是一个选项呢?我们知道vue2使用的是optionsAPI,vue3其实是组合式API和Composition API的结合,所以说vue3中也是由选项所组成的,vue3中依然需要保持这样的写法:

<script>
export default {
    name:"Home",
    components:{},
    setup(){},  // 新增option
}
</script>

像上面代码中的namecomponents属性都属于vue2中的options API,所以可以说setup是新增的一个option

调用时机

那这个函数在什么时候执行呢?

export default {
  setup() {
    console.log(123);
  },
};

运行上面代码会发现会直接打印出123,也就是说在启动后会自动执行该函数。具体的执行时机会在当前组件被创建并且属性初始化之后被调用, 从生命周期的角度上来说,会在beforeCreate之前被调用

vue3之Composition API详解

这张图告诉我们vue3中beforeCreate和created被setup函数替代了,并且其他的选项都变成了compositionAPI,一并需要在setup中使用。

与template之间的关系

vue3之Composition API详解

我们知道,在vue2.0中想要在页面上渲染一个数据需要先在data中声明,然后再绑定到template中,这句话告诉我们说,setup函数会返回一个对象,对象中是我们模板中需要用到的属性,并且这个对象里的属性将会被合并到render函数的执行上下文(因为render函数会渲染出模板)中,以提供给我们的模板使用,然后就可以直接在template中渲染出对应的数据,我们可以试一下

<template>
  <div>
    {{ count }}
  </div>
</template>

<script>
export default {
  setup() {
    const count = 0;
    return {
      count,
    };
  },
};
</script>

这时页面上就会显示出0,当然我们也可以声明响应式数据,这里需要注意的是,当我们声明了一个ref类型数据(后面会讲)并且在模板中进行访问时,它会自动浅层解包,因此我们不需要在模板中使用.valuesetup会自动帮我们拆解出来。当通过this访问时也会同样如此解包。

<template>
  <div>
    {{ count }}
  </div>
</template>

<script>
import { ref } from 'vue';
export default {
  setup() {
    const count = ref(0)
    // count一个RefImpl{value:0}对象
    console.log(count.value) // 先打印出0,1s后会自动打印1
    setTimeout(() => {
      count.value=1 // 在setup中无论是获取ref值还是给ref设置值都需要使用.value,而在template中则不需要
    }, 1000);
    return {
      count,
    };
  },
};
</script>

此外,setup函数返回的对象也会暴露给组件实例(this),其他选项也可以通过组件实例来获取setup暴露的属性:

<template>
  <div>
    {{ count }}
  </div>
</template>

<script>
import { ref } from "vue";
export default {
  setup() {
    const count = ref(0);
    // 返回值会暴露给模板和其他的选项式 API 钩子
    return {
      count,
    };
  },
  mounted() {
    console.log(this.count); // 0
  },
};
</script>

setup()自身并不包含对组件实例的访问权,即在setup()中访问this会是undefined。我们可以在选项式API中访问组合式API暴露的值,反过来则不行。

与渲染函数一起使用

setup也可以返回一个渲染函数(render),此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态:

import { h, ref } from "vue";
export default {
  setup() {
    const count = ref(0);

    return () => h("h1", [count.value]);
  },
};

返回一个渲染函数将会阻止我们返回其他东西。对于组件内部来说这样没有问题,但如果我们想通过模板引用将这个组件的方法暴露给父组件,那就有问题了。

我们可以通过expose()解决这个问题:

import {h,ref} from 'vue';

export default{
   setup(props,{expose}){
     const count=ref(0);
     const increment=()=>++count.value;
     
     expose({increment})
     
     return ()=>h("div",count.value)
   }
}

此时父组件可以通过模板引用来访问这个increment方法。

访问props

第一个参数是组件的props,并且也是响应式的,会在传入新的props时同步更新(可以通过watchEffectwatch来监测)

注意:如果对props对象进行了解构,解构出来的变量会丢失响应式, 如果确实需要解构props对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,那么可以用toReftoRefs这两个工具函数。这些后面都会提到。

//==================父组件===================
<template>
  <div >
    <Child :name="name" />
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
import {ref} from 'vue';

export default {
  components: { Child },
  setup() {
    const name=ref("张大");
    return {
      name,
    };
  },
};
</script>
//================子组件==============
<template>
  <div>
    {{name}}
  </div>
</template>

<script>
export default {
  props:{
    name:String
  },
  setup(props){
    console.log(props.name) // 张大
    // 注意不能在子组件中修改props中的属性,这个跟vue2里是一样的,改变的话需要注册自定义事件在父组件中修改
    props.name="瑶琴"  // 此时会提示'readonly'警告
  }
}
</script>

setup上下文

setup函数的第二个参数是setup上下文对象,该对象暴露了其他一些在setup中可能会用到的值:

vue3之Composition API详解

该上下文对象是非响应式的,可以安全的进行结构:

export default{
    setup(props,{attrs,slots,emit,expose}){
        ...
    }
}

attrsslots都是有状态的对象,它们总随着组件自身的更新而更新,也就是说总会拿到最新的值。这意味着我们应该避免去解构它们的属性,并始终通过attrs.xslots.x的形式使用其中的属性(因为属性都被Proxy包裹着)。此外需要注意,和props不同,attrsslots的属性都不是响应式的。

//==================父组件===================
<template>
  <div >
    <Child :name="name" />
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
import {ref} from 'vue';

export default {
  components: { Child },
  setup() {
    const name=ref("张大");
    return {
      name,
    };
  },
};
</script>
//================子组件==============
<template>
  <div>
    {{attrs.name}} //瑶琴
  </div>
</template>

<script>
export default {
  // √
  setup(props,ctx){
  return {
      attrs:ctx.attrs
    }
  }
  // √
  setup(props,{attrs}){
      return {
          name:attrs  
      }
  }
  // x 此时会name属性会丢失响应式 不会渲染出"瑶琴"
  setup(props,ctx){
      const {name}=ctx.attrs;
      return {
          name // 模板里需要直接改成{{name}}
      }
  }
}
</script>

为什么不把第一个参数props合并到第二个参数context中呢,是因为props相对于context中的属性使用起来更加频繁,所以单独把它拎了出来。

emit属性相当于vue2.0里面的自定义事件(this.$emit()),但是在setup函数中并没有this,所以我们需要这样做:

// ==========父组件===========
<template>
  <div>
    <h1>{{ name }}</h1>
    <Child :name="name" @handleChangeName="handleChangeName" />
  </div>
</template>

<script>
import Child from "./Child.vue";
import { ref } from "vue";

export default {
  components: {
    Child,
  },
  setup() {
    const name = ref("张大");
    const handleChangeName = (val) => {
      name.value = val;
    };

    return {
      name,
      handleChangeName,
    };
  },
};
</script>

// ===========子组件============
<template>
  <div>
    <button @click="changeName">change</button>
  </div>
</template>

<script>
export default {
  setup(props, ctx) {
    const changeName = () => {
      ctx.emit("handleChangeName", "瑶琴");
    };
    return {
      changeName,
    };
  },
};
</script>

expose函数用于显示地限制该组件暴露出的属性,当父组件通过模板引用访问该组件的实例时,将只能访问expose函数暴露出的内容:

export default{
    setup(props,(expose){
        // 让组件实例处于“关闭状态”
        // 即不向父组件暴露任何东西
        expose();
        
        const count=ref(0);
        const name=ref("张大")
        // 有选择地暴露局部状态
        expose({count:count})
    }
}

此时父组件只能获取到子组件中暴露出的count属性。

一、响应式核心

vue3之Composition API详解

ref

定义

接受一个内部值,返回一个响应的、可更改的ref对象,这个对象只有一个指向其内部值的属性 .value

如果将一个对象赋值给 ref,那么这个对象将通过 reactive() 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

用法

<script>
import {ref} from 'vue';
    export default {
        setup(){
            const count=ref(0);
            console.log(count);
        }
    }
</script>

vue3之Composition API详解

上图就是使用ref返回的ref对象,所以我们可以通过count.value的形式获取属性值,并且它是可以被修改的

<script>
import {ref} from 'vue';
    export default {
        setup(){
            const count=ref(0);
            console.log(count);
            count.value=1;
            console.log(count.vlaue)  //1
        }
    }
</script>

那么可不可以传入引用类型的值呢?是可以的,如果传入一个引用类型的话,会被reactive方法包装成一个深度的、响应式的数据,我们还用刚才的代码演示。

<script>
import {ref} from 'vue';
    export default {
        setup(){
            const count=ref(0);
            console.log(count);
            const state=reactive({
               a:1,
               b:2
           })
           console.log(state)  
        }
    }
</script>

vue3之Composition API详解

可以看到返回的还是一个ref对象,不同的是引用类型的value的值被reactive方法包装,reactive后面我们会讲到。

所以一般来说,我们声明一个数据时用ref,声明多个数据时用reactive,因为用ref声明多个数据时是无意义的,最终还是会通过reactive方法包装。

模板中的使用

<template>
  <div class="home">
    {{ count }}
  </div>
</template>

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

export default {
  name: "Home",
  components: {},
  setup() {
    const count = ref(0);
    console.log(count);
    return {
      count,
    };
  },
};
</script>

最终页面上会显示出0,为什么在setup函数里访问数据时需要.value,模板中可以不需要呢?这是因为在setup章节中我们讲道到要想在模板中使用自己声明的数据时,要显示返回出去一个对象,里面是模板中需要使用的数据,这个时候vue会自动的将返回对象里面的所有属性合并到render的执行上下文中去,并且自动展开里面的value,所以我们在模板中使用ref定义的数据时不需要再去使用.value

ref解包

如果在reactive里面有一个ref作为对象属性,当它被更改或者被访问时,会自动的将value提出来也就是解包,使我们可以当成一个正常的对象属性来访问或修改赋值。

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

export default {
  name: "Home",
  components: {},
  setup() {
    const count = ref(0);
    const state=reactive({
        count    // 会自动使用count:count.value这种写法
    })
    console.log(state.count)  // 0  这里不需要通过state.count.value来获取
    
    state.count=1
    console.log(state.count)
  },
};
</script>

注意:当修改ref值为一个新的ref时,新的ref将会替换老的ref 什么意思呢,我们还是用刚才的代码来解释:

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

export default {
  name: "Home",
  components: {},
  setup() {
    const count = ref(0);
    const otherCount=ref(1);  // 这里声明了一个新的ref
    const state=reactive({
        count   
    })
    state.count=otherCount;   // 将老的ref赋值为新的ref
    console.log(state.count); // 1
    console.log(count.value); // 0
  },
};
</script>

可以看到,state中的count已经被替换为了1,而count也就是老的ref还是0

注意:如果在reactive方法中传入了数组或者是一些原生的集合(Map),将不会自动展开value属性

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

export default {
  name: "Home",
  components: {},
  setup() {
    // 传入数组类型
    const state=reactive([ref(0)]);
    console.log(state[0])  // 包含vlaue属性的RefImpl对象
    console.log(state[0].value)  // 0
    
    // 传入Map集合
    const map=reactive(new Map([["foo",ref(0)]]))
    console.log(map,get("foo")); // 包含value属性的RefImpl对象
    console.log(map.get("foo").value) // 0
  },
};
</script>

模板引用

在某些情况下我们需要直接访问底层dom元素,在vue3中该如何使用ref获取dom元素呢?其实还是通过组合式API来获取模板引用。

访问模板引用

input框自动聚焦

<template>
  <div>
    <input type="text" ref="input" />
  </div>
</template>

<script>
import { onMounted, ref } from "vue";
export default {
  setup() {
    // 声明一个ref来存放该元素的引用
    // 必须和模板里定义的ref名称同名
    // 为什么要赋值为null?因为在赋值一个dom元素时初始值一般都是null,这是一种标准的写法
    const input = ref(null);
    onMounted(() => {
      // 获取时还是以.value的形式
      console.log(input.value); // <input type="text"/>
      input.value.focus();
    });
    return {
      input,  // 确保从setup里返回
    };
  },
};
</script>

注意,值可以在组件挂载后才能访问模板引用,在初次渲染时会是null,这是因为在初次渲染前这个元素还不存在!

组件上的ref

模板引用也可以被用在一个子组件上直接获取组件实例。

<template>
  <div>
    <Child ref="child" />
  </div>
</template>

<script>
import { onMounted, ref } from "vue";
import Child from "@/components/Child.vue";
export default {
  components: {
    Child,
  },
  setup() {
    const child = ref(null);
    onMounted(() => {
      console.log(child.value);
    });

    return {
      child,
    };
  },
};
</script>

如果一个子组件使用的是options API或没有使用<script setup>,被引用的组件实例和该子组件的this完全一致,这意味着父组件对子组件的每一个属性和方法都有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的propsemit接口来实现父子组件交互。

// ===========================child组件=============================
// composition API
<script>
import { ref } from 'vue'
export default {
  setup() {
    const name=ref("张三");
    return {
      name
    }
  },
}
</script>

// options API
<script>
export default {
  data(){
    return {
      name:"张三"
    }
  }
}
</script>
// ===========================父组件=============================
<template>
  <div>
    <Child ref="child" />
  </div>
</template>

<script>
import { onMounted, ref } from "vue";
import Child from "@/components/Child.vue";
export default {
  components: {
    Child,
  },
  setup() {
    const child = ref(null);
    onMounted(() => {
      console.log(child.value); // 张三
    });

    return {
      child,
    };
  },
};
</script>

有一个例外的情况,使用了<script setup>的组件是默认私有的:一个父组件无法访问到一个使用了<script setup>的子组件中的任何东西,除非子组件在其中通过defineExpose宏显示暴露:

// 如果child组件使用 <script setup>这样写法的话就需要显示暴露才行
<script setup>
import {ref} from 'vue';
  const name=ref("张三");
  // 不需要导入,并且导出的ref都会自动解包
  defineExpose({
    name
  })
</script>

v-for中的模板引用

v-for中使用模板引用时,对应的ref中包含的值是一个数组,它将在元素被挂载后包含对应整个列表的所有元素:

<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item" ref="itemRefs">{{ item }}</li>
    </ul>
  </div>
</template>

<script>
import { ref, onMounted } from "vue";
export default {
  components: {},
  setup() {
    const list = ref([1, 2, 3, 4]);
    const itemRefs = ref([]);
    onMounted(() => {
      console.log(itemRefs.value); // [li,li,li,li]
    });
    return {
      list,
      itemRefs,
    };
  },
};
</script>

需要注意的是,ref数组并不保证与源数组相同的顺序。

computed

定义

接受一个getter函数,返回一个只读的响应式ref对象,改ref通过。value暴露getter函数的返回值。它也可以接受一个带有get和set的对象来创建一个可写的ref对象。

用法

<script>
import { ref,computed } from "vue";

export default {
  setup() {
   const str=ref("computed")
   const strComputed=computed(()=>'hello,'+str.value)
   console.log(strComputed.value)  // 'hello,computed'
  },
};
</script>

注意此时computed方法返回的值是只读的,当你尝试修改这个值的时候,vue会提示Write operation failed: computed value is readonly的警告信息

那么当我们想要修改这个返回值的时候就用到定义中的第二种写法传入一个对象:

<script>
import { ref,computed } from "vue";

export default {
  setup() {
   const str=ref("computed")
   const strComputed=computed({
       get(){
           return 'hello,'+str.value
       },
       set(newVal){
           console.log('修改后的值,值为',newVal)
       }
   })
   console.log(strComputed.value);
   strComputed.value="hi,我是修改后的computed"
  },
};
</script>

语法其实跟vue2.0里差不多,不同的是里面使用了proxy对属性进行了拦截。

reactive

定义

返回一个对象的响应式代理。响应式转换也是“深层”的:它会影响到所有嵌套的属性,一个响应式对象也将深层的解包任何ref对象,同时保持响应性。

用Es6中的proxy包装传入的对象变成了一个被代理后的响应式的对象,功能上个vue2.0中的Vue.observable()是一样的。

用法

import {  reactive } from "vue";

export default {
  setup() {
    const proxyObj=reactive({
      a:1,
      b:2
    })
    console.log(proxyObj)  //Proxy对象
    
    // 深层响应式
    const state=reactive({
        a:1,
        b:{
            c:[1,2,3]
        }
    })
    setTimeout(()=>{
      state.b.c.push(6); // 视图中也会改变
    },1000)
  },
};

需要注意的是:reactive方法返回的对象与传入的对象不是同一个对象,建议只使用响应式代理,避免去使用原始对象

其他详细信息请看上面ref片段...

readonly

定义

接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。 只读代理是深层的, 对任何嵌套属性的访问都将是只读的。它的ref解包行为与reactive()相同,但解包得到的值是只读的。

用法

import { reactive, readonly } from "vue";

export default {
  setup() {
    const obj = reactive({
      a: 1,
      b: 2,
      c: {
        d: 3,
      },
    });
    const newObj = readonly(obj);
    obj.a = 11;
    console.log(obj.a);  // 11
    newObj.a = 11;
    console.log(newObj.a);  // 11 warning:target i readonly
  },
};

watchEffect

定义

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。

watchEffectwatch的区别:

  • 不需要手动传入依赖
  • 无法获取到原值,只能得到变化后的值
  • 每次初始化时会执行一次回调函数来自动获取依赖

用法

import { ref,watchEffect } from "vue";

export default {
  setup() {
    const count=ref(0);
    // 自动收集依赖并监听它们的变化
    setTimeout(()=>{
      count.value=1;
    },1000)
    // 接受一个回调函数作为参数,并且会立即执行
    watchEffect(()=>{
      console.log(count) // 打印两次第一次是0,第二次是1
    })
  },
};

watchEffect方法自动监听依赖的变化,并且会在组件卸载时取消监听

watchEffect方法会返回一个用于停止监听的函数

import { ref, watchEffect } from "vue";

export default {
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value = 1;
    }, 1000);
    setTimeout(() => {
      count.value = 2;
    }, 3000);
    const stop = watchEffect(() => {
      console.log(count.value); // 只会打印出0和1,因为在第2秒的时候停止了监听
    });
    setTimeout(() => {
      stop();
    }, 2000);
  },
};

参数

第一个参数就是要运行的副作用函数。这个副作用函数的参数也是一个函数,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求 (参见下面的示例)。

副作用可以理解为在执行数据请求时,频繁的发起请求,若前面的请求未完成则会造成重复请求,这个时候就可以用副作用函数中的onCleanUp函数参数。

副作用函数中的参数也是一个函数,用来注册清理回调

import { ref,watchEffect } from "vue";
export default {
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value=1;
    }, 1000);
    watchEffect((onCleanUp)=>{
      console.log(count.value)  // 其次是打印出1
      onCleanUp(()=>{
        console.log("onCleanUp is triggered")  // 会先打印出这个
      })
    })
  },
};

那么为什么onCleanUp会在每次状态改变之前先触发呢?是因为副作用函数很可能是一个async函数,里面会进行一些请求异步操作,在请求后面执行的话就达不到失效的效果了,相当于起到了一个拦截的作用。

所以再总结一下onCleanUp的执行时机:

  1. 在副作用即将执行之前调用(可以理解为总是优于watchEffect中的同步/异步代码执行)
import { ref,watchEffect } from "vue";
export default {
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value=1;
    }, 1000);
    watchEffect(async(onCleanUp)=>{
     // 这个叫副作用,async后面的函数叫副作用函数 
     const data = await getData();
      onCleanUp(()=>{
        console.log("onCleanUp is triggered")  
      })
    })
  },
};

2.取消监听之前(当组件被销毁的时候亦会取消监听)

import { ref, watchEffect } from "vue";
export default {
  components: {},
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value = 1;
    }, 1000);

    const stop = watchEffect((onCleanup) => {
      console.log(count.value);
      onCleanup(() => {
        console.log("oncleanup is triggered");
      });
    });
    setTimeout(() => {
      stop();
      console.log("watchEffect is stopped");
    }, 2000);
  },
};

// 0 
// oncleanup is triggered
// 1
// oncleanup is triggered
// watchEffect is stopped

第二个参数是一个可选的选项,可以用来调整副作用的刷新时机或调试副作用的依赖。

这里先说一下回调的触发时机,当我们更改了响应式状态后,它可能会同时触发Vue组件更新和侦听器回调。默认情况下,侦听器回调都会在Vue组件更新之前被调用,这意味着我们在侦听器回调中访问的DOM将是被Vue更新之前的状态,那如果想将侦听器延迟到组件更新渲染之后再执行(想在侦听器回调中能访问被Vue更新之后的DOM)可以添加flush:post选项:

// watchEffect(callback,{flush:'post'});
<template>
  <div>
    {{ count }}
  </div>
</template>

<script>
import { onBeforeUpdate, ref, watchEffect } from "vue";
export default {
  components: {},
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value = 1;
    }, 1000);

    onBeforeUpdate(() => {
      console.log("beforeUpdate");
    });

    watchEffect(() => {
      console.log(count.value);
    });
    // 0 
    // 1
    // befreoUpdate
    
    // 延迟到组件更新之后执行
     watchEffect(() => {
      console.log(count.value);
    },{flush:"post"}); // 默认是{flush:"pre"}
    // 0 
    // beforeUpdate 
    // 1
    return {
      count,
    };
  },
};
</script>

注意watchEffect第一次执行是在onMounted之前执行的,所以如果想要在watchEffect中获取dom元素需要在onMounted回调中执行。

<template>
  <div>
    <h1 ref="myRef"></h1>
  </div>
</template>

<script>
import { onMounted, ref, watchEffect } from "vue";
export default {
  components: {},
  setup() {
    const myRef = ref(null);

    onMounted(() => {
      console.log("onMounted");
    });

    watchEffect(() => {
      console.log(myRef.value);
    });
    // null
    // onMounted
    // <h1></h1>
    
    // 如果不想第一次出现null的话可以放到onMounted中
    onMounted(() => {
      watchEffect(() => {
        console.log(myRef.value);
      });
    });
    // <h1></h1>
    return {
      myRef,
    };
  },
};
</script>

Watcher Debugging

watchEffect第二个参数中还有onTrackonTrigger两个选项,主要在开发环境中用来调试

  • onTrack会在当依赖被收集到时调用(第一次会执行)
  • onTrigger会在当依赖被改变、watcher callback执行时调用(第一次不会执行)

watch

定义

侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

watch默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

第一个参数是侦听器的,这个来源可以是以下几种:

  • 一个函数,返回一个值
  • 一个ref
  • 一个响应式对象
  • ...或是由以上类型的值组成的数组

第二个参数是在发生变化时要调用的回调函数。这个回调函数接收三个参数:新值、旧值。以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行调用,可以用来清除无效的副作用,例如等待中的异步请求。

第三个参数可选参数是一个对象,支持一下这些选项:

  • immediate:在侦听器创建时立即触发回调,第一次调用时旧值是undefined
  • deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。

watchEffect相比,watch使我们可以:

  • 懒执行副作用;
  • 更加明确

用法

import { ref, watch } from "vue";

export default {
// 做demo时要看清楚再复制哦!!!
  setup() {
    // 参数为ref时
    const count = ref(0);
    setTimeout(() => {
      count.value = 1;
    }, 1000);
    watch(count, (newCount, oldCount) => {
      console.log(newCount, oldCount); // 1 0
    });
    
    // ============================================
    // 参数为多个ref时
     const count1=ref(1);
    setTimeout(() => {
      count.value = 1;
      count1.value=2
    }, 1000);
    watch([count,count1], (newCount, oldCount) => {
      console.log(newCount, oldCount); //[1,2] [0,1]
    });
    // 当侦听多个来源时,回调函数中也可这样写,相当于是一个解构,将不再返回数组会直接返回数据本身
    watch([count,count1], ([newCount,newCount1],[oldCount,oldCount1]) => {
      console.log(newCount, newCount1, oldCount,oldCount1); //[1,2] [0,1]
    });
  },
};

当要侦听的参数是一个对象形式时,回调只在此函数的返回值变化时才会触发,如果你想让回调在深层级变更时也能触发,需要使用{deep:true}强制侦听器进入深层模式。在深层模式时,如果回调函数由于深层级的变更而被触发,那么新值和旧值将是同一个对象。

import { reactive, watch } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
    });
    setTimeout(() => {
      state.count = 1;
    }, 1000);
   // 当使用getter函数作为源时,回调只在此函数的返回值变化时才会触发。
    watch(() => state.count,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);  // 1 0
      }
    );
    
    // ============================================
    // 需要开启深度侦听时(直接侦听一个对象/数组)
    const state = reactive({
      count: 0,
      obj: {
        name: "小明",
      },
    });
    setTimeout(() => {
      state.obj.name="小红"
    }, 1000);
    // 这时如果直接侦听一个对象是不会打印出值的
    watch(
      () => state.obj,
      (newVal, oldVal) => {
        console.log(newVal, oldVal); // 不会打印出值
      }
    );
   // 这时需要加上`watch`的第三个可选参数deep
   watch(
      () => state.obj,
      (newVal, oldVal) => {
        console.log(newVal, oldVal); //此时会打印出同一个对象(新值) Proxy {name: '小红'}
      },
      {deep:true}
    );
    // 此外,如果想要侦听多个getter函数形式源的话,也可以传递数组
 setup() {
    const state = reactive({
      count: 0,
      obj: {
        name: "xiaoming",
      },
      hobby: ["sing", "dance"],
    });
    setTimeout(() => {
      state.obj.name = "xiaohong";
      state.hobby[0] = "rap";
    }, 1000);
    watch(
      [() => state.obj, () => state.hobby],
      ([newVal, newVal1], [oldVal, oldVal1]) => {
        console.log(newVal, newVal1, oldVal, oldVal1);
      },
      { deep: true }
    );
  },

当直接侦听一个响应式对象时,侦听器会自动启用深层模式

import { reactive, watch } from "vue";

export default {
  setup() {
    const state = reactive({
      count: 0,
    });
    setTimeout(() => {
      state.count = 1;
    }, 1000);
    watch(state, (newVal, oldVal) => {
      console.log(newVal, oldVal); // Proxy {count: 1}  Proxy {count: 1} 
    });
  },
};

接下来再详细说一下第三个可选参数(deep属性不再重复,请仔细阅读上面的文章):

immediate:在侦听器创建时立即触发回调。第一次调用旧值为undefined

import { ref, watch } from "vue";

export default {
  setup() {
    const count = ref(0);
    setTimeout(() => {
      count.value = 1;
    }, 3000);
    // immediate属性会在侦听器创建时立即出发回调函数,所以第一次调用时旧值是undefined
    watch(
      count,
      (newVal, oldVal) => {
        console.log(newVal, oldVal); // 0 undefined
      },
      { immediate: true }
    );
  },
};

flush:调用回调函数的刷新时机。

回调的触发时机

当你改变了响应式状态,它可能会同时触发Vue组件更新和侦听器回调。默认情况下,用户创建的侦听器回调都会在Vue组件更新之前被调用。这意味着你在侦听器回调中访问的DOM将是被Vue更新之前的状态。

如果想在侦听器回调中能访问被Vue更新之后的DOM你需要指明flush:post选项:

<template>
  <div>
    <p>{{ state.a }}</p>
    <button @click="changestate">Click</button>
  </div>
</template>

<script>
import { watch, reactive, onBeforeUpdate } from "vue";
export default {
  setup() {
    const state = reactive({
      a: 1,
    });
    const changestate = () => {
      state.a = 2;
    };
    watch(
      () => state.a,
      (newVal, oldVal) => {
        console.log(newVal, oldVal); // 会先打印出2 1
      }
    );
    onBeforeUpdate(() => {
      console.log("onBeforeUpdate!"); // 其次打印出onBeforeUpdate!
    });

    return {
      state,
      changestate,
    };
    
    //========================
    watch(
      () => state.a,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);  // 加上flush属性后打印将延迟到onBeforeUpdate钩子函数之后打印
      },
      { flush: "post" }
    );
  },
};
</script>

onTrack/onTrigger:调试侦听器的依赖。(仅在开发模式下生效)

// onTrack会在某个响应式property或ref作为依赖被追踪时调用
// onTrigger会在侦听回调被某个依赖的修改触发时调用
// 所有的回调都会收到一个debugger事件,其中包含了一些依赖相关的信息,可以在这些回调内放置一个debugger语句以调试依赖
<template>
  <div>
    <p>{{ state.a }}</p>
    <button @click="changestate">Click</button>
  </div>
</template>

<script>
import { watch, reactive } from "vue";
export default {
  setup() {
    const state = reactive({
      a: 1,
    });
    const changestate = () => {
      state.a = 2;
    };
    watch(
      () => state.a,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      },
      {
        onTrack(e) {
          console.log("onTrack", e); // 默认会被触发
        },
        onTrigger(e) {
          console.log("onTrigger", e); // 点击事件触发后依赖发生改变会被触发
        },
      }
    );

    return {
      state,
      changestate,
    };
  },
};
</script>
<style  scoped>
</style>

侦听器的停止:当已不再需要该侦听器时,watchwatchEffect方法一样也会返回一个用来停止侦听的函数,调用即可停止:

import {  ref, watch } from "vue";

export default {
  setup() {
    const count = ref(0)
    setTimeout(() => {
      count.value = 1;
    }, 3000);
    let stop = watch(count, (newVal, oldVal) => {
      console.log(newVal, oldVal); // 3s后将不再打印
    });
    setTimeout(()=>{
      stop();
    },1000)
  },
};

二、响应式:工具篇

vue3之Composition API详解

isRef

定义

检查某个值是否为ref,返回一个布尔值

用法

import { isRef, reactive, ref } from 'vue';
export default {
  setup() {
    const name=ref("张大");
    console.log(isRef(name)) // true
    const state=reactive({
      name:"瑶琴"
    })
    console.log(isRef(state)) // false
  },
};

unRef

定义

如果参数是ref,则返回其内部值,否则返回参数本身。这是val=isRef(val)?val:value计算的一个语法糖

用法

import { unRef, reactive, ref } from 'vue';
export default {
  setup() {
    const name=ref("张大");
    console.log(unRef(name)) // 张大
    const state=reactive({
      name:"瑶琴"
    })
    console.log(unRef(state)) // Proxy{}
  },
};

toRef

定义

基于响应式对象上的一个属性,创建一个对应的ref。这样创建的ref与其源属性保持同步:改变源属性将更新ref的值,反之亦然

reactive对象取出的所有属性值都是非响应式的,而利用toRef可以将一个响应式reactive对象的所有原始属性转换为响应式的ref属性。

用法

import { reactive,toRef } from "vue";

export default {
  setup() 
   // 这里也可以声明一个普通的对象 const state={name:"张三"}
   const state=reactive({
     name:"张三",
     age:20
   })
    // toRef(obj,key)
    // 注意以下写法将不会和state.name保持同步,因为这个ref()接收到的是一个纯数值!!!
    // const nameRef=toRef(state.name)
    const nameRef=toRef(state,"name")
   console.log(nameRef); // 一个ref对象
   nameRef.value="李四"; // 注意ref语法
   console.log(state.name) // 李四 --- 源属性也发生了改变
  },
};

toRef这个函数在传递给一个组合式函数时会很有用:

import { reactive,toRef } from "vue";
//这里声明一个自定义方法
function doSth(name){
    return `My name is ${name.value}.`
}
export default {
  setup() 
   const state=reactive({
     name:"张三",
     age:20
   })
   // 想要传入一个响应式的属性
   const result=doSth(toRef(state,'name')); // My name is 张三.
  },
};

toRefs

定义

将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象响应属性的ref。每个单独的ref都是使用toRefs()创建的。

toRef是基于对象中的一个属性进行ref转换的,toRefs的作用是将响应式对象中的所有属性转换为单独的响应式数据,对象成为普通对象,并且值都是相互关联的。在这个过程中toRefs会做以下两件事:

  1. 把一个响应式对象转换成普通对象;
  2. 对该普通对象的每个属性都做一次ref操作,这样每个属性都将是响应式的;

reactive对象取出的所有属性都是非响应式的,而利用toRefs可以将一个响应式reactive对象的所有原始属性转换为响应式的ref属性。

用法

应用场景(展开运算符/解构)

import { reactive,toRefs } from "vue";

export default {
  setup() 
   const state=reactive({
     name:"张三",
     age:20
   })
   const stateRef=toRefs(state);
   console.log(stateRef); // 见下图
  },
};

vue3之Composition API详解

reactive方法是把响应式功能赋给一个对象,如果展开对象,会让数据丢失响应式的能力,使用toRefs可以保证对象展开的每个属性都是响应式的。这就是我们在使用reactive定义方法时,如果想要在模板中直接使用对象中的属性(而不是state.xxx)可以在setup函数返回的对象中对原来的对象进行展开/解构:

<template>
  <div>
    <!-- 可以直接使用 -->
    <h1>{{ name }}</h1>
    <h1>{{ age }}</h1>
  </div>
</template>

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

export default {
  components: {},
  setup() {
    const state = reactive({
      name: "张三",
      age: 20,
    });
    const stateRefs = toRefs(state);
    console.log(stateRefs);

    return {
      ...stateRefs,
    };
  },
};
</script>

注意:toRefs在调用时只会为源对象上可以枚举的属性创建ref。如果要为可能还不存在的属性创建ref,请使用toRef

六、依赖注入

下面来说一下最后两个composition API

vue3之Composition API详解

provide

定义

提供一个值,可以被后代组件注入

provide接受两个参数:第一个参数是要注入的key,可以是一个字符串或一个Symbol,第二个参数是要注入的值。

provide必须在组件的setup阶段同步调用。

<script>
import Father from "@/components/Father.vue";
import { provide, ref } from "vue";
export default {
  components: { Father },
  setup() {
    // 提供静态值
    provide("name", "张三");
    //提供响应式的值
    const count=ref(0);
    provide("count",count)
  },
};
</script>

inject

定义

注入一个由祖先组件或整个应用(通过app.provide())提供的值

inject也必须在组件的setup阶段同步调用。

第一个参数是注入的key,Vue会遍历父组件链,通过匹配key来确定所提供的值。如果父组件链上多个组件对同一个key提供了值,那么离得更近的组件将会“覆盖”链上更远的组件所提供的值。如果没有能通过key匹配到值,inject将返回undefined,除非提供了一个默认值。

//================App.vue================
<script>
import Father from "@/components/Father.vue";
import { provide } from "vue";
export default {
  components: { Father },
  setup() {
    // 祖先组件提供值
    provide("name", "张三");
  },
};
</script>
//================Father.vue================
<script>
import { provide } from 'vue';
import Child from "./Child.vue";

export default {
  name: "Father",
  components: {
    Child,
  },
  setup() {
    // 父组件提供值
    provide("name","father")
  },
};
</script>
//================Child.vue================
<script>
import { inject } from "vue";
export default {
  setup() {
    // 详情请看inject的第二个参数
    const name = inject("name", "无名");
    console.log(name) // 会打印出father,而不是App组件提供的“张三”
  },
};
</script>

第二个参数是可选的,即在没有匹配到key时使用的默认值。它也可以是一个函数,用来返回某些创建起来比较复杂的值。如果默认值本身就是一个函数,那么必须将false作为第三个参数传入,来表明这个函数就是默认值,而不是一个函数。

//================Father.vue================
<script>
import { provide } from 'vue';
import Child from "./Child.vue";

export default {
  name: "Father",
  components: {
    Child,
  },
  setup() {
    // 父组件这里没有提供值时
    // provide("name","father")
  },
};
</script>
//================Child.vue================
<script>
import { inject } from "vue";
export default {
  setup() {
    const name = inject("name", "无名");
    console.log(name) // 这时会打印出默认值“无名”
    
    // 返回某些创建起来比较复杂的值
    const name=inject("name",()=>new Map());
    
    // 默认值为函数需要加上第三个参数
    const name=inject("name",()=>{},false);
  },
};
</script>

Add Reactivity

为了使我们提供的值具有响应式,我们可以使用refreactive

// ====================App.vue====================
<template>
  <div>
    <Child />
    <button @click="changeName">Click!</button>
  </div>
</template>

<script>
import { ref, provide } from "vue";
import Child from "@/components/Child.vue";

export default {
  components: { Child },
  setup() {
    const name = ref("张三");
    provide("name", name);

    const changeName = () => {
      name.value = "李四";
    };
    return {
      changeName,
    };
  },
};
</script>
// ====================Child.vue====================
<script>
import { inject } from "vue";

export default {
  setup() {
    const name = inject("name");
    console.log(name) //张三  点击按钮之后打印出李四
  },
};
</script>

此外,官方并不推荐我们想要修改数据时直接在inject组件中修改,如果这样做的话也可以实现,但是会造成数据的混乱,提供方只是提供数据,依赖方只是使用数据,因此不推荐这样的做法,正确的做法应该是这样:

// ====================App.vue====================
<template>
  <div>
    <Child />
    <button @click="changeName">Click!</button>
  </div>
</template>

<script>
import { ref, provide } from "vue";
import Child from "@/components/Child.vue";

export default {
  components: { Child },
  setup() {
    const name = ref("张三");
    const changeName = () => {
      name.value = "李四";
    };
    provide("name", name);
    // 提供修改数据的方法
    provide("changeName",changeName)
    return {
      changeName,
    };
  },
};
</script>
// ====================Child.vue====================
<script>
<template>
  <div>{{ name }}</div>
  <button @click="handleClick">changeName</button>
</template>
import { inject } from "vue";

export default {
  setup() {
    const name = inject("name");
    const changeName=inject("changeName");// 此时页面上就会是最新修改的数据
    return {
        changeName
    }
  },
};
</script>