vue3系统入门与项目实战学习笔记(下)

220 阅读8分钟

六、Composition API

本章中,将会给大家全面介绍 Vue3 新提供的 Composition API 语法 ,帮助大家学会优雅合理的使用 新版本 Vue 语法 。在本章中,还通过小例子的形式帮助大家进行实操联系,确保大家能够学以致用,为之后的实战课程做好准备。

1.Setup 函数的使用

什么是 Composition API ?  当我们在 项目开发中 的时会遇到这种问题, data 中很多 变量 , methods 中很多 方法 ,当我们 想看一些代码的逻辑 时,需要上下文的来回查看 ,当 项目代码上百 / 千行的时,想看一些代码逻辑定位非常麻烦 ,下图 颜色相同 的 代码块 , 代表同一个功能的代码 ,可以看到 Options API(传统vue写法)一个功能代码分布在上、中、下,代码很零散,而 Composition API(vue3 新写法) 每个功能的代码都汇聚在一块 ,这样 查阅就会很方便可读性强 。

image.png

要想使用 Composition API ,首先要学习一个 函数 ,就是 Setup 函数 , Setup 函数 会在 created 实例被完全初始化之前 执行,它有 2 个参数 , 第1个参数参数 是 当前组件的 props , 第2个参数 是 上下文, Setup函数 必须要 return 返回一个值 , Setup 函数返回的值可以直接在 template 模板中使用 如下代码:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 函数的使用</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div @click="handleClick">{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            return { 
                name: 'dell', // Options API 中data中的变量
                handleClick(){ // Options API 中methods中的方法
                    alert(123)
                }
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

我们发现 Setup 这个 函数 ,它 return 的 所有内容 会 暴露在外部 ,我们在 template 模板 中可以直接 使用暴露出来的变量或方法 。

1.Setup 函数内调用外部方法与变量

Setup 函数 是 在实例被创建之前执行的一个函数 ,我们可能会想到,我们如果在 Setup 函数 中 调用 methods 中定义的方法 ,会怎样呢?

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 函数内调用外部方法与变量</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div @click="handleClick">{{name}}</div>
        `,
        methods: {
            test() {
                alert('test')
            }
        },
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            this.test() 
            return { 
                name: 'dell', // data中的变量
                handleClick(){ // methods中的方法
                    alert(123)
                }
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

会直接报错 this.test is not a function ,这是因为 Setup 函数 是 在实例被初始化之前就执行的一个函数 , 实例初始化之前 methods 并没有被放到 this 上面, 所以通过 this.test() 根本就无法调用这个 methods , 所以要记住, 在Setup 函数里面,不能使用 this 这样的关键词, this 里面实际上什么也没有 ,所以我们不要去这样写,用了 Setup 函数 之后,我们就 不会去用this这样语法,再去写任何的代码 了,后面这种 methods 的定义,我们都会换成新的 composition API 的语法来定义 。

2.外部方法(methods)、生命周期(mounted)中调用 Setup 函数

在 Setup 函数 中 我们无法调用外部的一些方法 ,或者一些模板,或者mounted这样的生命周期函数 ,但是 在外部的方法或者生命周期中,我们可以调用 Setup 这个方法 ,原因是 Setup 执行时,实例并没有被创建 ,但是 在实例创建后,Setup 已经被挂载到实例上了,所以在 mounted 或者 methods 中可以获取到 Setup ,如下代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>外部方法(methods)、生命周期(mounted)中调用 Setup 函数</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div @click="handleClick">{{name}}</div>
        `,
        methods: {
            test() {
                console.log(this.$options.setup())
            }
        },
        mounted(){
            this.test()
        },
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            return { 
                name: 'dell', // data中的变量
                handleClick(){ // methods中的方法
                    alert(123)
                }
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

2.ref,reactive 响应式引用的用法和原理

ref 与 reactive 是 vue3 提供给我们 解决数据响应式问题的方法,下面我们将通过 2 个例子来讲解 ref 与 reactive 以及 readonly 、toRefs 具体用法。

  1. ref :解决 基本数据类型数据(基本类型:Boolean、String等等)  的 数据响应式问题
  2. reactive :解决 非基本数据类型数据(引用类型:Array、Object等等)  的 数据响应式问题
  3. readonly :数据 只读 。
  4. toRefs : 解决 解构赋值 的 数据响应式问题

ref

需求 :我们在 setup 中定义了变量 name , 并且将 name 返回 return 出去,在页面的 template 模板 中使用 name, 2s 之后更改了变量 name , 我们 希望 template 模板上的 name 也随之更新 , 代码如下:

1.非响应式数据案例

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ref 响应式引用的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div>{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 声明定义变量name
            let name = 'dell'
            // 2s 后更改 name
            setTimeout(() => {
                name = 'lee'
            }, 2000);

            return { 
                name // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

2.ref 响应式数据案例

上面的数据,无法更新,这是 因为 name 只是一个普通的变量并不会响应式的更新 ,如果想 响应式的更新 name ,使用 ref 可以解决 基本数据类型数据 的 数据响应式问题 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ref 响应式引用的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // ref 响应式的引用
    // 原理, 通过 proxy 对数据进行封装, 当数据变化时, 触发模板等内容的更新
    // ref 处理基础类型的数据
    const app = Vue.createApp({
        template: `
            <div>{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 ref
            const { ref } = Vue
            
            // 调用 ref 后,proxy, 'dell' 变成 proxy({value: 'dell'}) 这样的一个响应式引用
            let name = ref('dell') // 值会存入到 name.value 中

            // 2s 后更改 name
            setTimeout(() => {
                name.value = 'lee' // 修改 name.value 的值
            }, 2000);

            return { 
                name // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

template 模板 中不需要写 name.value ,直接写 name 就行,因为 vue 在 做模板处理时,它会做一个转换,它如果知道 name 是通过 ref 返回的一个响应式引用,会自动的在底层帮我们调用 name.value

reactive

下面的代码是 非基本数据类型数据 的案例,也是 无法做到数据的响应式 。

1.非响应式数据案例

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>reactive 响应式引用的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div>{{nameObj.name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 定义声明对象
            const nameObj = { name: 'dell' }

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj.name = 'lee'
            }, 2000);

            return { 
                nameObj // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

2.reactive 响应式数据案例

reactive 可以解决 非基本数据类型数据 的 数据响应式问题 。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>reactive 响应式引用的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // reactive 响应式的引用
    // 原理, 通过 proxy 对数据进行封装, 当数据变化时, 触发模板等内容的更新
    // reactive 处理非基础类型的数据
    const app = Vue.createApp({
        template: `
            <div>{{nameObj.name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive } = Vue

            // 调用 reactive 后,proxy, { name: 'dell' } 变成 proxy({ name: 'dell' }) 这样的一个响应式引用
            const nameObj = reactive({ name: 'dell' })

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj.name = 'lee'
            }, 2000);

            return { 
                nameObj // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

上面是数据是 对象 ,也可以使用 数组 ,如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>reactive 响应式引用的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // reactive 响应式的引用
    // 原理, 通过 proxy 对数据进行封装, 当数据变化时, 触发模板等内容的更新
    // reactive 处理非基础类型的数据
    const app = Vue.createApp({
        template: `
            <div>{{nameObj[0]}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive } = Vue

            const nameObj = reactive([123])

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj[0] = 456
            }, 2000);

            return { 
                nameObj // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

readonly

假如我想让部分数据 不希望,数据被变更 ,变成 只读状态 , 我们可以使用 vue3 提供的 readonly 实现这个功能,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>readonly 的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // ref, reactive 响应式的引用
    // 原理, 通过 proxy 对数据进行封装, 当数据变化时, 触发模板等内容的更新
    // reactive 处理非基础类型的数据
    const app = Vue.createApp({
        template: `
            <div>{{nameObj[0]}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive, readonly } = Vue
            const nameObj = reactive([123])
            // 复制一份数据
            const copyNameObj = readonly(nameObj)

            // 2s 后更改 nameObj 与copyNameObj
            setTimeout(() => {
                nameObj[0] = 456
                copyNameObj[0] = 456
            }, 2000);

            return { 
                nameObj, // data中的变量
                copyNameObj
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

这样 2s 过后,浏览器控制台就会 抛出警告 。

toRefs

下面代码中在 setup 函数 中 return 一个对象

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRefs 的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div>{{nameObj.name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive } = Vue
             // 调用 reactive 后,proxy, { name: 'dell' } 变成 proxy({ name: 'dell' }) 这样的一个响应式引用
            const nameObj = reactive({ name: 'dell' })

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj.name = 'lee'
            }, 2000);

            return { 
                nameObj // data中的变量
            }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

有些人就会觉得这么写很麻烦,用 ES6 解构赋值 直接 return 一个 name ,像下面这样写:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRefs 的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div>{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive } = Vue
             // 调用 reactive 后,proxy, { name: 'dell' } 变成 proxy({ name: 'dell' }) 这样的一个响应式引用
            const nameObj = reactive({ name: 'dell' })

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj.name = 'lee'
            }, 2000);

            // 解构赋值
            const { name } = nameObj

            // data中的变量
            return { name }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>     

2s 后就会发现不生效 , 解构赋值 并没有生效,因为 解构只是把这个值导出去,这个值并不是响应式的 ,如果想做解构赋值 ,解决这个问题,需要使用 toRefs 才可以解决这个问题,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRefs 的用法和原理</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        template: `
            <div>{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入 composition API 的 reactive
            const { reactive, toRefs } = Vue
             // 调用 reactive 后,proxy, { name: 'dell' } 变成 proxy({ name: 'dell' }) 这样的一个响应式引用
            const nameObj = reactive({ name: 'dell' })

            // 2s 后更改 nameObj
            setTimeout(() => {
                nameObj.name = 'lee'
            }, 2000);

            // 解构赋值
            // 调用 toRefs 后,它会把 proxy({ name: 'dell' }) 转换成 { name: proxy({value:'dell'}) }
            // 实际上最终返回的是 { name: proxy({value:'dell'}) }
            const { name } = toRefs(nameObj)

            // data中的变量
            return { name }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

总结

当我们学了 ref 、 reactive 这种新的 composition API 之后,我们完全 可以替代老的语法中 data 这种写法,比如说以前 定义 都在 data 中定义变量 ,如下:

data() {
  return {
      name: 'dell',
      nameObj: {
          name: 'dell'
      } 
  }
}

用新的 composition API 语法来定义的话,完全没必要用 data ,用 ref 、 reactive 这种形式来写即可。

3.toRef 以及 context 参数

这章讲解一下 2 个新的知识点,一个是 composition API 中的 toRef API,另一个是我们之前讲过的 setup 函数 的 第2个参数 context 。

toRef

1.解构赋值响应式数据正确写法

我们之前通过 toRefs 来解决 解构赋值数据响应式问题 ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRef</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // toRef
    const app = Vue.createApp({
        template: `
            <div>{{name}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            const { reactive, toRefs } = Vue
            const data = reactive({ name: 'dell' })

            // 解构赋值
            const { name } = toRefs(data)

            // 2s 后修改name变量
            setTimeout(() => {
                name.value = 'lee'
            }, 2000)

            return { name }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

2.解构赋值响应式数据错误写法

如果在 解构赋值时 ,我们把 name 改成 age ,这时候我们就会发现,在一开始 data 中没有 age , 这时候 解构赋值时有 age ,就会发生错误 ,这是 因为 toRefs 这种语法,它从 data 这样一个响应式对象里去找数据时,如果找不到,它不会给 age 一个默认的响应式的引用,而是给 age 赋值为 undefined,这样的话, age 最开始不存在 data 中,它后面就不具备响应式,再改它,也无效果 ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRef</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // toRef
    const app = Vue.createApp({
        template: `
            <div>{{age}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            const { reactive, toRefs } = Vue
            const data = reactive({ name: 'dell' })

            // 解构赋值
            const { age } = toRefs(data)

            // 2s 后修改name变量
            setTimeout(() => {
                age.value = 'lee'
            }, 2000)

            return { age }
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

3.toRef

如果想解决上面的问题,我们可以用 toRef ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRef</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // toRef
    const app = Vue.createApp({
        template: `
            <div>{{age}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            const { reactive, toRef } = Vue
            const data = reactive({ name: 'dell' })
            // 意思是从 data 中取 age ,如果能取到就用取到的值,
            // 取不到就声明一个空的响应式数据
            const age = toRef(data, 'age')

            // 2s 后给 age 赋值
            setTimeout(() => {
                age.value = 'lee'
            }, 2000)

            // 导出 age ,这里必须是对象的形式,就像在 vue2.x 时 return {} 一样
            return { age } 
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

建议一般情况不要用 toRef ,一般来说,如果后面代码中要用到 age ,不妨直接在 data 中提前定义一个空的 age ,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>toRef</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // toRef
    const app = Vue.createApp({
        template: `
            <div>{{age}}</div>
        `,
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            const { reactive, toRefs } = Vue
            const data = reactive({ name: 'dell', age: '' })

            // 2s 后给 age 赋值
            setTimeout(() => {
                data.age = 'lee'
            }, 2000)

            // 解构赋值
            const { age } = toRefs(data)

            // 导出 age ,这里必须是对象的形式,就像在 vue2.x 时 return {} 一样
            return { age } 
        }
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

Setup 的第二个参数 context

接下来我们讲一下 context , context 是个 对象, 它有 3 个 属性值

1.attrs

context 的第 1 个参数是 attrs(None-Props属性)  ,下面用它写一个小例子:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 的第二个参数 context </title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `<child style="color:red" />`
    })   
  
    // 子组件
    app.component('child', {
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入h函数
            const { h } = Vue
            const { attrs, slots, emit } = contex

            // 使用render函数,并且使用 attrs
            return () => {
                return h('div', {
                    style: attrs.style
                }, 'child')
            } 

        }
    })

    
    const vm = app.mount('#root')
</script>

</html>   

2.slots

context 的第 2 个参数是 slots , slots 是 父组件传递过来的插槽 ,在 vue2.x 版本中,我们使用 slots 都需要 this.$slots ,在新版 composition API 中可以使用 slots ,例子如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 的第二个参数 context </title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `<child>parent</child>`
    })   
  
    // 子组件
    app.component('child', {
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            // 引入h函数
            const { h } = Vue
            const { attrs, slots, emit } = contex

            // 使用render函数,并且使用 slots
            return () => {
                return h('div', {}, slots.default())
            } 
        }
    })

    
    const vm = app.mount('#root')
</script>

</html>   

3.emit

在旧版 vue2.x 版本中, 子组件 给 父组件传值 通过 this.$emit ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 的第二个参数 context </title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        template: `<child @change="handleChange">parent</child>`,
        methods: {
            handleChange(){
                alert('handleChange')
            }
        }
    })   
  
    // 子组件
    app.component('child', {
        template: '<div @click="handleClick">123123</div>',
        methods: {
            handleClick(){
                this.$emit('change')
            }
        }
    })

    
    const vm = app.mount('#root')
</script>

</html>   

在新版 composition API 中这样写即可,代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Setup 的第二个参数 context </title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        methods: {
            handleChange(){
                alert('handleChange')
            }
        },
        template: `<child @change="handleChange">parent</child>`
    })   
  
    // 子组件
    app.component('child', {
        template: '<div @click="handleClick">123123</div>',
        /**
        * created 实例被完全初始化之前
        * @param {object} props - 当前组件的props
        * @param {number} contex - 上下文
        */
        setup(props, contex) {
            const { attrs, slots, emit } = contex
            // 定义方法
            function handleClick(){
                emit('change')
            }
            // 导出方法
            return { handleClick }
        }
    })

    const vm = app.mount('#root')
</script>

</html>   

4.使用 Composition API 开发TodoList

下面用 composition API 实现了一个 TodoList 效果

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 Composition API 开发TodoList</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        setup(){
            const { ref, reactive } = Vue
            const inputValue = ref('')
            const list = reactive([])
            const  handleInputValueChange = (e) => {
                inputValue.value = e.target.value
            }
            const handleSubmit = () => {
                list.push(inputValue.value)
            }
            return {
                list,
                inputValue,
                handleSubmit,
                handleInputValueChange
            }
        },
        template: `
        <div>
            <input :value="inputValue" @input="handleInputValueChange"/>  
            <button @click="handleSubmit">提交</button>
            <ul>
                <li v-for="(item, index) in list" :key="index">{{item}}</li>
            </ul>
        </div>

        `,
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

封装TodoList

上面的写法虽然是完成了功能,但是 代码零散,其实我们可以做一个 封装 ,把 逻辑相同的代码进行归类封装

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 Composition API 开发TodoList</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 关于 list 操作的内容进行了封装
    const listRelativeEffect = () => {
        const { reactive } = Vue
        const list = reactive([])
        const addItemToList = (item) => {
            list.push(item)
        }  
        return { list, addItemToList }      
    }
    
    // 关于 inputValue 操作的内容进行了封装
    const inputRelativeEffect = () => {
        const { ref } = Vue
        const inputValue = ref('')
        const  handleInputValueChange = (e) => {
            inputValue.value = e.target.value
        }
        return { inputValue, handleInputValueChange }
    }


    const app = Vue.createApp({
        setup(){
            // 流程调度中转
            const { list, addItemToList } = listRelativeEffect()
            const { inputValue, handleInputValueChange } = inputRelativeEffect()
            return {
                list, addItemToList,
                inputValue, handleInputValueChange
            }
        },
        template: `
        <div>
            <input :value="inputValue" @input="handleInputValueChange"/>  
            <button @click="() => addItemToList(inputValue)">提交</button>
            <ul>
                <li v-for="(item, index) in list" :key="index">{{item}}</li>
            </ul>
        </div>

        `,
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

上面的代码中,我们把对 list 操作的相关逻辑进行了封装 , input 相关的逻辑也进行了封装 ,代码中的 addItemToList 用 箭头函数处理逻辑 ,如果觉得不太好懂,也可以像下面这样封装一个函数

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用 Composition API 开发TodoList</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 关于 list 操作的内容进行了封装
    const listRelativeEffect = () => {
        const { reactive } = Vue
        const list = reactive([])
        const addItemToList = (item) => {
            list.push(item)
        }  
        return { list, addItemToList }      
    }
    
    // 关于 inputValue 操作的内容进行了封装
    const inputRelativeEffect = () => {
        const { ref } = Vue
        const inputValue = ref('')
        const  handleInputValueChange = (e) => {
            inputValue.value = e.target.value
        }
        return { inputValue, handleInputValueChange }
    }


    const app = Vue.createApp({
        setup(){
            // 流程调度中转
            const { list, addItemToList } = listRelativeEffect()
            const { inputValue, handleInputValueChange } = inputRelativeEffect()

            const handleSubmit = () => {
                addItemToList(inputValue.value)
            }
            return {
                list, addItemToList,
                inputValue, handleInputValueChange,
                handleSubmit
            }
        },
        template: `
        <div>
            <input :value="inputValue" @input="handleInputValueChange"/>  
            <button @click="handleSubmit">提交</button>
            <ul>
                <li v-for="(item, index) in list" :key="index">{{item}}</li>
            </ul>
        </div>
        `,
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

但是还是 建议使用箭头函数的方式,没必要单独封装一个函数 。

5.computed方法生成计算属性

下面讲解一下 composition API 的新版 computed计算属性 如何使用。

computed 普通用法

1.Vue2.x 版本 computed 使用方法

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>computed方法生成计算属性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // vue2.x 版本 computed 计算属性
    const app = Vue.createApp({
        data(){
            return{
                cd: 'cd'
            }
        },
        computed: {
            abc(){
                return this.cd + 'efg'
            }
        },
        template: `
            <div>
                {{ abc }}
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

2.Vue 3 版本 computed 使用方法

在 composition API 中使用 computed 计算属性 ,只需要引入一个 computed 方法 ,在使用时候返回一个计算后的值即可。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>computed方法生成计算属性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // computed 计算属性
    const app = Vue.createApp({
        setup(){
            const { ref, computed } = Vue
            const count = ref(0)
            // 点击事件
            const handleClick = () => {
                count.value += 1
            }
            // 计算属性
            const countAddFive = computed(() => {
                return count.value + 5
            })
            return { count, countAddFive, handleClick }
        },
        template: `
            <div>
                <span @click="handleClick">{{ count }}</span> -- {{ countAddFive }}
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>   

或者可以写的复杂一些,用 get 方法来写。

computed 的 get set方法

1.get

get 实现的效果, 与上面的普通写法实现的效果是一样的 , get 表示获取返回的这个值。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>computed方法生成计算属性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // computed 计算属性
    const app = Vue.createApp({
        setup(){
            const { ref, computed } = Vue
            const count = ref(0)
            // 点击事件
            const handleClick = () => {
                count.value += 1
            }
            // 计算属性
            const countAddFive = computed({
                get: () => {
                  return count.value + 5
                }
            })
            return { count, countAddFive, handleClick }
        },
        template: `
            <div>
                <span @click="handleClick">{{ count }}</span> -- {{ countAddFive }}
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

2.set

当计算属性被修改时,会触发 set 这个方法,如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>computed方法生成计算属性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // computed 计算属性
    const app = Vue.createApp({
        setup(){
            const { ref, computed } = Vue
            const count = ref(0)
            // 点击事件
            const handleClick = () => {
                count.value += 1
            }
            // 计算属性
            let countAddFive = computed({
                get: () => {
                  return count.value + 5
                },
                set: () => {
                    count.value = 10
                }
            })
            // 2s 后修改 countAddFive 计算属性
            setTimeout(() => {
                countAddFive.value = 100
            }, 3000)
            return { count, countAddFive, handleClick }
        },
        template: `
            <div>
                <span @click="handleClick">{{ count }}</span> -- {{ countAddFive }}
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

computed 的 get set使用实例

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>computed方法生成计算属性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // computed 计算属性
    const app = Vue.createApp({
        setup(){
            const { ref, computed } = Vue
            const count = ref(0)
            // 点击事件
            const handleClick = () => {
                count.value += 1
            }
            // 计算属性
            let countAddFive = computed({
                get: () => {
                  return count.value + 5
                },
                set: (param) => {
                    count.value = param - 5
                }
            })
            // 2s 后修改 countAddFive 计算属性
            setTimeout(() => {
                countAddFive.value = 100
            }, 3000)
            return { count, countAddFive, handleClick }
        },
        template: `
            <div>
                <span @click="handleClick">{{ count }}</span> -- {{ countAddFive }}
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

6.watch 和 watchEffect 的使用和差异性

我们讲一下在 composition API 中如何来使用 watch 和 watchEffect

watch 使用方法

1.监听基本数据

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        setup(){
            const { ref, watch } = Vue
            // 定义name属性
            const name = ref('dell')
            // 监听name属性
            // 具备一定的惰性 lazy
            // 参数可以拿到原始和当前值
            watch(name, (currentValue, prevValue) => {
                console.log(currentValue, prevValue)
            })
            
            return { name }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

2.监听对象或数组

  • 1.错误写法示范

  • 如果 监听 的是一个 reactive 这样的数据, watch 的 第 1 个参数 就 不可以 像下面这样写:

  • index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, toRefs } = Vue
            const nameObj = reactive({ name: 'dell' })
            // 具备一定的惰性 lazy
            // 参数可以拿到原始和当前值
            watch(nameObj.name, (currentValue, prevValue) => {
                console.log(currentValue, prevValue)
            })
            
            const { name } = toRefs(nameObj)

            return { name }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    
  • 2.正确写法示范

  • 监听 reactive 这样的数据,watch 的第一个参数,要用 箭头函数 的形式

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, toRefs } = Vue
            const nameObj = reactive({ name: 'dell' })
            // 具备一定的惰性 lazy
            // 参数可以拿到原始和当前值
            watch(() => nameObj.name, (currentValue, prevValue) => {
                console.log(currentValue, prevValue)
            })
            
            const { name } = toRefs(nameObj)

            return { name }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

3.监听多个数据

像下面这个代码中,想要 同时监听 2 个数据 就要写 2 次 watch

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
  </head>

  <body>
    <div id="root"></div>
  </body>

  <script>
    // watch 侦听器
    const app = Vue.createApp({
      setup() {
        const { reactive, watch, toRefs } = Vue

        // 定义变量
        const nameObj = reactive({
          name: 'dell',
          englishName: 'lee',
        })

        // 监听 nameObj.name
        watch(
          () => nameObj.name,
          (currentValue, prevValue) => {
            console.log(currentValue, prevValue)
          },
        )

        // 监听 nameObj.englishName
        watch(
          () => nameObj.englishName,
          (currentValue, prevValue) => {
            console.log(currentValue, prevValue)
          },
        )

        // 解构取值
        const { name, englishName } = toRefs(nameObj)

        return { name, englishName }
      },
      template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
                <div>
                    EnglishName: <input v-model="englishName">
                </div>
                <div>
                    EnglishName is {{ englishName }}
                </div>
            </div>  
        `,
    })

    const vm = app.mount('#root')
  </script>
</html>

上面的代码就会 显得特别麻烦,重复性的代码写了多次 ,那我们可以这样写,把 它们俩写在一起监听 ,在 watch 里可以接收一个数组 ,它的意思就是 这个数组中不管哪个值发生变化,我都会执行后面的函数,同样后面函数的参数也必须是数组的形式

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, toRefs } = Vue

            // 定义变量
            const nameObj = reactive({ 
                name: 'dell', englishName: 'lee' 
            })
            
            // 监听 nameObj.name
            watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng],[preName, preEng]) => {
                console.log(curName, preName, '---', curEng, preEng)
            })
            
            // 解构取值
            const { name, englishName } = toRefs(nameObj)

            return { name, englishName }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
                <div>
                    EnglishName: <input v-model="englishName">
                </div>
                <div>
                    EnglishName is {{ englishName }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

4.初始化执行一次监听

watch 也可以 默认执行一次监听 , watch 的 第 2 个参数 是个 对象 ,可以在里面进行 配置 immediate: true 即可,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, watchEffect, toRefs } = Vue

            // 定义变量
            const nameObj = reactive({ 
                name: 'dell', englishName: 'lee' 
            })
            
            // 监听 nameObj.name
            watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng],[preName, preEng]) => {
                console.log('watch', curName, preName, '---', curEng, preEng)
            }, {
                immediate: true 
            })
            
            // 解构取值
            const { name, englishName } = toRefs(nameObj)

            return { name, englishName }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
                <div>
                    EnglishName: <input v-model="englishName">
                </div>
                <div>
                    EnglishName is {{ englishName }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>        

watchEffect 使用方法

传统的 vue2.x 版本 watch 想实现 默认执行一次监听 ,就要写 immediate: true ,如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    const app = Vue.createApp({
        data(){
            return {
                nameObj: {
                    name: 'dell',
                    englishName: 'lee'
                }
            }
        },
        watch: {
            'nameObj.name' : {
                immediate: true,
                handler: function (oldValue, newValue) {
                    console.log(oldValue,)
                }
            }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="nameObj.name">
                </div>
                <div>
                    Name is {{ nameObj.name }}
                </div>
                <div>
                    EnglishName: <input v-model="nameObj.englishName">
                </div>
                <div>
                    EnglishName is {{ nameObj.englishName }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

而新版 composition API 的 watchEffect 方法,初始化就会执行一次 , 它会 自动检测方法内部使用的代码是否有变化 ,而且 不需要传递你要侦听的内容,它会自动感知内容变化,缺点:无法获取之前或当前的数据

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    // watchEffect 侦听器,偏向于 effect
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, watchEffect, toRefs } = Vue

            // 定义变量
            const nameObj = reactive({ 
                name: 'dell', englishName: 'lee' 
            })
            
            // 监听 nameObj.name
            // watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng],[preName, preEng]) => {
            //     console.log('watch', curName, preName, '---', curEng, preEng)
            // })


            // 立即执行,没有惰性 immediate
            // 不需要传递你要侦听的内容,自动会感知代码依赖,不需要传递很多参数,只要传递一个回调函数
            // 不能获取之前数据的值
            watchEffect(() => { 
                console.log(nameObj.name)
            })
            
            // 解构取值
            const { name, englishName } = toRefs(nameObj)

            return { name, englishName }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
                <div>
                    EnglishName: <input v-model="englishName">
                </div>
                <div>
                    EnglishName is {{ englishName }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

结束 watch 或 watchEffect 监听

假如我有这样一个需求想 2s 后 结束监听 ,可以用 变量或者常量 把 watch 或 watchEffect 储存起来,然后 2s 后执行一下这个变量的方法即可结束监听,具体代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>watch 和 watchEffect 的使用和差异性</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // watch 侦听器
    // watchEffect 侦听器,偏向于 effect
    const app = Vue.createApp({
        setup(){
            const { reactive, watch, watchEffect, toRefs } = Vue

            // 定义变量
            const nameObj = reactive({ 
                name: 'dell', englishName: 'lee' 
            })
            
            // 监听 nameObj.name
            const stop1 = watch([() => nameObj.name, () => nameObj.englishName], ([curName, curEng],[preName, preEng]) => {
                console.log('watch', curName, preName, '---', curEng, preEng)
                setTimeout(() => {
                    stop1() // 结束监听
                }, 2000)                
            })


            // 立即执行,没有惰性 immediate
            // 不需要传递你要侦听的内容,自动会感知代码依赖,不需要传递很多参数,只要传递一个回调函数
            // 不能获取之前数据的值
            const stop = watchEffect(() => { 
                console.log(nameObj.name)
                setTimeout(() => {
                    stop() // 结束监听
                }, 2000)
            })
            
            // 解构取值
            const { name, englishName } = toRefs(nameObj)

            return { name, englishName }
        },
        template: `
            <div>
                <div>
                    Name: <input v-model="name">
                </div>
                <div>
                    Name is {{ name }}
                </div>
                <div>
                    EnglishName: <input v-model="englishName">
                </div>
                <div>
                    EnglishName is {{ englishName }}
                </div>
            </div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

7.生命周期函数的新写法

vue2.x 版本中 生命周期写法,目前新的 composition API 中的 生命周期 都需要写在 setup函数 中,用 按需引入 的方式来使用,具体代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>生命周期函数的新写法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        setup(){
            const { 
                ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
                onBeforeUnmount, onUnmounted
            } = Vue
            const name = ref('dell')

            // beforeMount 等同于 onBeforeMount
            onBeforeMount(() => {
                console.log('onBeforeMount')
            })
            
            // mounted 等同于 onMounted
            onMounted(() => {
                console.log('onMounted')
            })
            
            // beforeUpdate 等同于 onBeforeUpdate
            onBeforeUpdate(() => {
                console.log('onBeforeUpdate')
            })
            
            // updated 等同于 onUpdated
            onUpdated(() => {
                console.log('onUpdated')
            })
            
            // beforeUnmount 等同于 onBeforeUnmount(当组件从页面v-if隐藏移除时会执行)
            onBeforeUnmount(() => {
                console.log('onBeforeUnmount')
            })
            
            // unmounted 等同于 onUnmounted(组件从页面移除后执行)
            onUnmounted(() => {
                console.log('onUnmounted')
            })

            // 定义方法
            const handleClick = () => {
                name.value = 'lee'
            }

            return { name, handleClick }
        },
        template: `
            <div @click="handleClick">{{name}}</div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

composition API 中很多的 生命周期 与原来一样,不同的就是 名字有所改变而已

弃用的生命周期

在 composition API 中有 beforeCreate 与 created 这 2 个生命周期函数我们是不能使用的 ,因为 setup 函数执行的时间点,在 beforeCreate 与 created 之间 ,所以这 2 个 生命周期 中要写的东西,直接写在 setup 中就可以了。

新的的生命周期

composition API 出了 2 个新的 生命周期函数 ,onRenderTracked 与 onRenderTriggered ,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>生命周期函数的新写法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        setup(){
            const { ref, onRenderTracked, onRenderTriggered } = Vue
            const name = ref('小明')

            // 每次渲染后重新收集响应式依赖
            onRenderTracked(() => {
                console.log('onRenderTracked')
            })
            
            // 每次触发页面重新渲染时自动执行
            onRenderTriggered(() => {
                console.log('onRenderTriggered')
            })

            // 定义方法
            const handleClick = () => {
                name.value = 'lee'
            }

            return { name, handleClick }
        },
        template: `
            <div @click="handleClick">{{name}}</div>  
        `
    })   
  
    const vm = app.mount('#root')
</script>

</html>    

8.Provide,Inject,模版 Ref 的用法

本章将讲解 Provide、Inject、Ref 在 composition API 中如何使用,但是我们说的这个Ref ,是 获取dom元素 的那个Ref ,不是 定义基本类型响应式数据 时用的那个 Ref 。

Provide Inject 的用法

composition API 解决了 Provide Inject 数据响应式的问题,下面看一下在 composition API 中如何使用 Provide Inject ,如下代码:

1.inject 默认值

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Provide、Inject的用法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        setup(){
            // 引入 provide
            const { provide } = Vue;
            // 向【后代】传递数据(格式:provide(属性名,值))
            provide('name', 'dell')
            return { }
        },
        template: `
            <div>
                <child />
            </div>
        `
    })   

    // 子组件
    app.component('child', {
        setup(){
            // 引入 inject
            const { inject } = Vue;
            // 获取【祖先】组件传的数据(格式:inject('属性名', '默认值'),如果 provide 没有 name 就使用默认值小明)
            const name = inject('name', '小明')
            // 返回数据
            return { name }
        },
        template: '<div>{{ name }}</div>'
    })
  
    // 挂载
    const vm = app.mount('#root')
</script>

</html>   

上面代码中 inject 如果获取不到值,默认赋值为 inject 的第 2 个参数(默认值 )。

2.后代修改祖先值

Vue 是 单向数据流 的概念,子组件 不可以 修改 父组件 传入的 数据 。那使用 Provide、Inject 时,后代组件 想修改 祖先组件 Provide 传过来的值 ,该怎么修改呢?

2.1 错误示范

下面这种写法,虽然也可以修改数据,但是 不符合单向数据流的要求

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Provide、Inject的用法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        setup(){
            // 引入 provide
            const { provide, ref } = Vue;
            // 向【后代】传递数据
            provide('name', ref('dell'))
            return { }
        },
        template: `
            <div>
                <child />
            </div>
        `
    })   

    // 子组件
    app.component('child', {
        setup(){
            // 引入 inject
            const { inject } = Vue;
            // 获取【祖先】组件传的数据
            const name = inject('name')
            const handleClick = () => {
                name.value = '小明'
            }
            // 返回数据
            return { name, handleClick }
        },
        template: '<div @click="handleClick">{{ name }}</div>'
    })

    // 挂载
    const vm = app.mount('#root')
</script>

</html>   

2.2 正确写法

后代组件需要调用祖先组件的方法,然后在祖先组件中去修改数据

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Provide、Inject的用法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        setup(){
            // 引入 provide
            const { provide, ref } = Vue;
            const name = ref('dell')
            // 向【后代】传递数据
            provide('name', name)
            // 向【后代】传递 changeName 方法
            provide('changeName', (value) => {
                name.value = value
            })
            return { }
        },
        template: `
            <div>
                <child />
            </div>
        `
    })   

    // 子组件
    app.component('child', {
        setup(){
            // 引入 inject
            const { inject } = Vue;
            // 获取【祖先】组件传的数据
            const name = inject('name')
            const changeName = inject('changeName')
            const handleClick = () => {
                changeName('小明')
            }
            // 返回数据
            return { name, handleClick }
        },
        template: '<div @click="handleClick">{{ name }}</div>'
    })

    // 挂载
    const vm = app.mount('#root')
</script>

</html>   

3.搭配使用 readonly ,防止单向数据流被后代组件篡改

有许多时候,许多人不懂得 单向数据流的概念 ,还是会在 后代组件 中去修改 祖先组件 传入的值,为了解决这个问题,可以使用 readonly 把参数变成只读,来解决这个问题,代码如下:

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Provide、Inject的用法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    // 父组件
    const app = Vue.createApp({
        setup(){
            // 引入 provide
            const { provide, ref, readonly } = Vue;
            const name = ref('dell')
            // 向【后代】传递数据
            provide('name', readonly(name))
            return { }
        },
        template: `
            <div>
                <child />
            </div>
        `
    })   

    // 子组件
    app.component('child', {
        setup(){
            // 引入 inject
            const { inject } = Vue;
            // 获取【祖先】组件传的数据
            const name = inject('name')
            const handleClick = () => {
                name.value = '小明'
            }
            // 返回数据
            return { name, handleClick }
        },
        template: '<div @click="handleClick">{{ name }}</div>'
    })

    // 挂载
    const vm = app.mount('#root')
</script>

</html>   

Ref 的用法

在 composition API 中,我们如何通过 ref 获取 dom 元素呢?

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ref 的用法</title>
    <!-- 通过cdn方式引入vue -->
    <script src="https://unpkg.com/vue@next"></script>
</head>

<body>
    <div id="root"></div> 
</body>

<script>
    const app = Vue.createApp({
        setup(){
            const { onMounted, ref } = Vue;

            // 01. 名字与标签 ref 的名字相等,这是固定写法
            const hello = ref(null)

            onMounted(() => {
                // 获取 ref 有 hello 的 dom 元素
                console.log(hello.value)
            })

            // 02. 导出 hello
            return { hello }
        },
        // 03. ref 起名为 hello
        template: `
            <div ref="hello">小明</div>
        `
    })   

    // 挂载
    const vm = app.mount('#root')
</script>

</html>