常用Composition API | 青训营笔记

134 阅读7分钟

这是我参与【第四届青训营】笔记创作活动的第7天。

官方文档:v3.cn.vuejs.org/guidelcompo…

1、setup

理解:Vue3.0 中一个新的配置项,值为一个函数。 setup 是所有 Composition API(组合API)“表演的舞台”。 组件中所用到的:数据、方法等等,均要配置在 setup 中。

setup函数的两种返回值:

  1. 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。
  2. 若返回一个渲染函数:则可以自定义渲染内容。(了解)

注意点:

  1. 尽量不要与 Vue2.x 配置混用

    • Vue2.x 配置(data、methos、computed...)中可以访问到 setup 中的属性、方法。
    • 但在 setup 中不能访问到 Vue2.x 配置(data、methos、computed...)。
    • 如果有重名, setup 优先。
  2. setup 不能是一个 async 函数,因为返回值不再是 return 的对象,而是 promise,模板看不到 return 对象中的属性。(后期也可返回一个 promise 的实例,但需要Suspense和异步组件的配合)

举例: main.js

// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数 
import { createApp } from 'vue' 
import App from './App.vue' 

// 创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更轻量 
// cons app = createApp(App) 

// 挂载 
// apply.mount('#app') 

createApp(App).mount('#app')

App.vue

<template>   
    <!-- Vue3组件中的模板结构可以没有根标签 -->   
    <div>     
        <h1>我是App组件</h1>     
        <h2>姓名:{{ name }}</h2>     
        <h2>性别:{{ age }}</h2>     
        <button @click="sayHello">点击按钮</button>   
    </div>
</template>

<script> 
    import HelloWorld from "./components/HelloWorld.vue"; 
    export default {  
        name: "App",   
        components: {     
            HelloWorld,   
        },   
        setup() {     
            // 数据     
            let name = "张三";     
            let age = 18;     
            // 方法     
            function sayHello() {       
                console.log(`我叫${name},今年${age}岁`);     
            }     
            return {      
                name,       
                age,       
                sayHello,    
            };   
        },
    }; 
</script> 

<style> 
    #app {   
        font-family: Avenir, Helvetica, Arial, sans-serif;   
        -webkit-font-smoothing: antialiased;   
        -moz-osx-font-smoothing: grayscale;   
        text-align: center;  
        color: #2c3e50;  
        margin-top: 60px; 
    } 
</style>

2、ref 函数

作用:定义一个响应式的数据。

语法:const xxx = ref(initvalue)

  • 创建一个包含响应式数据的引用对象(reference 对象,简称 ref 对象)
  • JS 中操作数据:xxx.value
  • 模板中读取数据:不需要 .value,直接:<div>{{xxxx}}</div>

备注:

  • 接收的数据可以是:基本类型、也可以是对象类型。
  • 基本类型的数据:响应式依然是靠 object.defineProperty() 的 get 与 set 完成的。
  • 对象类型的数据:内部“求助”了 Vue3.0 中的一个新函数 —— reactive 函数。
<template>   
    <!-- Vue3组件中的模板结构可以没有根标签 -->   
    <div>     
        <!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->    
        <h1>我是App组件</h1>     
        <h2>姓名:{{ name }}</h2>  
        <h2>性别:{{ age }}</h2>    
        <h2>工作:{{ job.type }}</h2> 
        <h2>工资:{{ job.salary }}</h2>   
        <button @click="change">点击按钮</button>   
    </div> 
</template> 

<script> 
    import HelloWorld from "./components/HelloWorld.vue"; 
    import { ref } from "vue"; 
    export default {   
        name: "App",  
        components: {    
            HelloWorld,  
        },   
        setup() {     
            // 数据     
            // 使用引用实现对象RefImpl   
            let name = ref("张三");    
            let age = ref(18);   
            let job = ref({      
                type: "后端工程师",      
                salary: "30K",     
            });     
            // 方法
            function change() {      
                // ref——修改基本类型的数据:       
                // 使用的是defineProperty,利用getter、setter实现响应
                age.value = 55;    
                // ref——修改对象类型的数据:       
                // 使用的是ES6中的proxy实现响应      
                job.value.type = "前端工程师";       
                console.log(`我叫${name.value},今年${age.value}岁`);    
            } 
            return {
                name,
                age,
                change, 
                job,    
            }; 
        },
    };
</script>

<style> 
    #app {   
        font-family: Avenir, Helvetica, Arial, sans-serif;   
        -webkit-font-smoothing: antialiased;   
        -moz-osx-font-smoothing: grayscale;  
        text-align: center;   
        color: #2c3e50;  
        margin-top: 60px; 
    }
</style>

3、reactive 函数

作用:定义一个对象类型的响应式数据(基本类型别用它,用ref函数)

语法:const 代理对象 = reactive(源对象)

接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象,简称为:proxy对象)

  • reactive 定义的响应式数据是“深层次的”。
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据,都是响应式的

举例

<template>   
    <!-- Vue3组件中的模板结构可以没有根标签 -->  
    <div>     
        <!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->    
        <h1>我是App组件</h1>  
        <h2>姓名:{{ name }}</h2>    
        <h2>性别:{{ age }}</h2>    
        <h2>工作:{{ job.type }}</h2>  
        <h2>工资:{{ job.salary }}</h2>    
        <h2>爱好:{{ hobby }}</h2>    
        <h2>测试数据:{{ job.a.b.c }}</h2>    
        <button @click="change">点击按钮</button>   
    </div>
</template> 

<script> 
    import HelloWorld from "./components/HelloWorld.vue"; 
    import { ref, reactive } from "vue";
    export default {   
        name: "App",  
        components: {    
            HelloWorld,   
        },   
        setup() {    
            // 数据    
            // 使用引用实现对象RefImpl   
            let name = ref("张三");   
            let age = ref(18);    
            let job = reactive({       
                type: "后端工程师",     
                salary: "30K",      
                a: {         
                    b: {         
                        c: 10,         
                    },      
                },     
            });   
            let hobby = reactive(["吃饭", "睡觉", "打豆豆"]);     
            // 方法    
            function change() {      
                // ref——修改基本类型的数据:       
                // 使用的是defineProperty,利用getter、setter实现响应       
                age.value = 55;      
                // ref——修改对象类型的数据:    
                // ref会去求助reactive,reactive使用的是ES6中的proxy实现响应  
                // job.value.type = "前端工程师";      
                // console.log(`我叫${name.value},今年${age.value}岁`);     
                // 直接使用reactive修改对象类型的数据:     
                job.type = "前端工程师";      
                // reactive可以对深层次的数据进行监视     
                job.a.b.c = 20;      
                // reactive可以也可以代理数组    
                // vue3中可以通过reactive代理直接使用下标对数组进行修改  
                hobby[0] = "喝可乐";    
            }    
            return {     
                name,       
                age,       
                change,      
                job,       
                hobby,     
            };  
        }, 
    }; 
</script> 

<style>
    #app {  
        font-family: Avenir, Helvetica, Arial, sans-serif; 
        -webkit-font-smoothing: antialiased;   
        -moz-osx-font-smoothing: grayscale;  
        text-align: center; 
        color: #2c3e50;   
        margin-top: 60px;
    }
</style>
<template>   
    <!-- Vue3组件中的模板结构可以没有根标签 -->   
    <div>    
        <!-- 不需要自己手动写name.value,Vue3的模板会自动将RefImpl实例对象解析为value值 -->   
        <h1>我是App组件</h1>     
        <h2>姓名:{{ person.name }}</h2>    
        <h2>性别:{{ person.age }}</h2>    
        <h2>工作:{{ person.job.type }}</h2> 
        <h2>工资:{{ person.job.salary }}</h2>  
        <h2>爱好:{{ person.hobby }}</h2>  
        <button @click="change">点击按钮</button>  
    </div> 
</template> 

<script> 
    import HelloWorld from "./components/HelloWorld.vue"; 
    import { ref, reactive } from "vue";
    export default {   
        name: "App",   
        components: {     
            HelloWorld,   
        },   
        setup() {   
            let person = reactive({      
                name: "张三",      
                age: 18,     
                job: {         
                    type: "后端工程师",        
                    salary: "30K",      
                },       
                hobby: ["吃饭", "睡觉", "打豆豆"],     
            });    
            // 方法     
            function change() {       
                console.log(121);       
                person.age = 55;     
                person.job.type = "前端工程师";   
                person.hobby[0] = "喝可乐";   
            }     
            return {      
                person,    
                change,     
            };   
        }, 
    }; 
</script>

4、Vue3.0 中的响应式原理

4.1、vue2.x 的响应式

实现原理:

  • 对象类型:通过 object.defineProperty() 对属性的读取、修改进行拦截(数据劫持)。
  • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)
Object.defineProperty(data,'count ', { 
    get() {}, 
    set() {} 
})

存在问题:

  • 新增属性、删除属性,界面不会更新。
  • 直接通过下标修改数组,界面不会自动更新。

Vue2响应式中存在问题的解决方案: image.png

补充:修改数组还可以使用 splice 方法变更数组来达到修改的目的。

模拟源码image.png

4.2、Vue3.0 的响应式

实现原理

new Proxy (data,{ 
    //拦截读取属性值 
    get(target,prop){ 
        return Reflect.get(target,prop) 
    }, 
    
    //拦截设置属性值或添加新属性
    set(target,prop, value){ 
        return Reflect.set(target,prop, value) 
    }, 
    
    //拦截删除属性
    deleteProperty(target, prop){ 
        return Reflect.deleteProperty(target,prop)
    } 
})

proxy .name = 'tom"

模拟源码

① 使用 Proxy 代理: image.png

② 配合 Reflect 反射: image.png

Reflect的作用:JS 是单线程,如果一旦发生错误则会程序挂起,对于封装框架来说有很大的隐患,为了框架的健壮性,使用 reflect 可以保证后面的代码正确执行,不至于全部挂起。当然,使用 try...catch 也是可以的。

  • 使用Object.defineProperty image.png
  • 使用Reflect.defineProperty image.png

5、reactive 对比 ref

5.1、从定义数据角度对比

  • ref 用来定义:基本类型数据。
  • reactive 用来定义:对象(或数组)类型数据。

备注:ref也可以用来定义对象(或数组)类型数据,它内部会自动通过 reactive 转为代理对象。

5.2、从原理角度对比

  • ref 通过 0bject.defineProperty() 的 get 与 set 来实现响应式(数据劫持)。
  • reactive 通过使用 Proxy 来实现响应式(数据劫持),并通过 Reflect 操作源对象内部的数据。

5.3、从使用角度对比

  • ref 定义的数据:操作数据需要 .value,读取数据时,模板中直接读取,不需要 .value。
  • reactive 定义的数据:操作数据与读取数据——均不需要 .value。

Vue2: attrs:当props被传送数据但是没有被接受时,会把数据放在attrs:当 props 被传送数据但是没有被接受时,会把数据放在 attrs 中,如果被接受了就会将其挂载到组件上,被挂载的就不会出现在 attrsattrs 中 slot:当在父组件中使用插槽,但是子组件无论有没有使用 slot 标签接受时,插槽的内容会被放在 $slots 中。

6、setup 的两个注意点

6.1、setup 执行的时机

在beforeCreate之前执行一次,this是undefined

6.2、setup 的参数

① props:值为对象,包含组件外部传递过来,且组件内部声明接收了的属性

② context:上下文对象

  • attrs:值为对象,包含:组件外部传递过来,但没有在 props 配置中声明的属性,相当于 this.$attrs
  • slots:收到的插槽内容,相当于 this.$slots
  • emit:分发自定义事件的函数,相当于 this.$emit

注意

  • slots 里面保存着接收到的插槽内容,是一些虚拟 DOM
  • Vue3 中要想在 slots 里面显示插槽名,得用 v-slot 进行命名

7、计算属性与监视

7.1、computed函数

与 Vue2.x 中 computed 配置功能一致。

举例

简写形式 image.png

完整形式: image.png

7.2、watch 函数

与 Vue2.x 中 watch 配置功能一致。

两个小“坑”:

  • 监视 reactive 定义的响应式数据时:oldValue 无法正确获取、强制开启了深度监视(deep配置失效)
  • 监视 reactive 定义的响应式数据中某个属性时:deep 配置有效

image.png

image.png

image.png

image.png

image.png

image.png

7.3、watchEffect 函数

watch 的套路是:既要指明监视的属性,也要指明监视的回调。

watchEffect 的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

watchEffect 有点像 computed,区别如下:

  1. computed 注重计算出来的值(回调函数的返回值),所以必须要写返回值
  2. watchEffect 更注重的是过程(回调函数的函数体),所以不用写返回值
//watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。 
watchEffect(()=>{ 
    const x1 = sum.value
    const x2 = person.age
    console.log('watchEffect配置的回调执行了') 
})

8、生命周期

image.png

Vue3.0 中可以继续使用 Vue2.x 中的生命周期钩子,但有两个被更名:

  • beforeDestroy 改名为 beforeUnmount
  • destroyed 改名为 unmounted

可以直接以配置项的形式使用生命周期钩子,也可以使用组合式API的形式使用,尽量统一 一般来说组合式API里的钩子会比配置项的钩子先执行,组合式API的钩子名字有变化。 image.png

Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:

  • beforeCreate===>setup()
  • created=======>setup()
  • beforeMount ===>onBeforeMount
  • mounted=======>onMounted
  • beforeUpdate===>onBeforeUpdate
  • updated =======>onUpdated
  • beforeUnmount ==>onBeforeUnmount
  • unmounted =====>onUnmounted

9、自定义 hook 函数(重点)

什么是hook?

本质是一个函数,把 setup 函数中使用的 Composition API 进行了封装。

类似于 vue2.x 中的 mixin。

自定义 hook 的优势:复用代码,让 setup 中的逻辑更清楚易懂。

创建一个 hooks 文件夹,里面创建文件 userPoint.js

import{ reactive, onMounted, onBeforeUnmount } from "vue"; 
export default function() { 
    //实现鼠标“打点”相关的数据 
    let point = reactive({ 
        x: 0, 
        y: 0, 
    }); 
    
    //实现鼠标“打点”相关的方法 
    function savePoint(event) {
        point.x = event.pageX;
        point.y = event.pageY; 
        console.log(event.pageX, event.pageY); 
    } 
        
    //实现鼠标“打点”相关的生命周期钩子 onMounted(() => { 
        window.addEventListener("click", savePoint); }); 
        onBeforeUnmount(() => { 
            window.removeEventListener("click", savePoint); 
        }); 
        return point; 
    }
}

在组件中使用:

<template>
    <h2>我是HelloWorld组件</h2> 
    <h2>当前点击时鼠标的坐标为:x:{{point.x}},y:{{point.y}}</h2> 
</template>

<script> 
    import usePoint from '../hooks/usePoint' 
    export default { 
        name:'HelloWorld', 
        setup(){ 
            const point = usePoint() 
            return {point}
        } 
    } 
</script>

演示效果: image.png

10、toRef

作用:创建一个 ref 对象,其 value 值指向另一个对象中的某个属性。

语法:const name = toRef(person,'name')

应用: 要将响应式对象中的某个属性单独提供给外部使用时。

扩展:toRefs 与 toRef 功能一致,但可以批量创建多个 ref 对象,

语法:toRefs(person)

<template> 
    <h4>{{person}}</h4> 
    <h2>姓名:{{name}}</h2> 
    <h2>年龄:{{age}}</h2> 
    <h2>薪资:{{job.j1.salary}}K</h2>
    <button @click="name+='~'">修改姓名</button> 
    <button @click="age++">增长年龄</button> 
    <button @click="job.j1.salary++">涨薪</button>
</template>

<script>
    import {ref,reactive,toRef,toRefs} from 'vue' 
    export default {
        name: 'Demo', 
        setup(){ 
            //数据
            let person = reactive({ 
                name:'张三',
                age:18, 
                job:{ 
                    j1:{ salary:20 } 
                } 
            }) 
            // const name1 = person.name 
            // console.log('%%%',name1) 
            // const name2 = toRef(person,'name') 
            // console.log('####',name2)
            const x = toRefs(person) 
            console.log('******',x) 
            return { 
                person, 
                // name:toRef(person,'name'), 
                // age:toRef(person,'age'), 
                // salary:toRef(person.job.j1,'salary'),
                ...toRefs(person)
            } 
        } 
    } 
</script>