前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3 | Composition API 包括setup、ref等新特性详解 与 实战

Vue3 | Composition API 包括setup、ref等新特性详解 与 实战

作者头像
凌川江雪
发布2022-01-20 11:32:39
1.1K0
发布2022-01-20 11:32:39
举报
文章被收录于专栏:李蔚蓬的专栏李蔚蓬的专栏

完整原文地址见简书 更多完整Vue笔记目录敬请见《前端 Web 笔记 汇总目录(Updating)》

本文内容提要

  • Composition API 的作用
  • setup函数
  • 例程,打印查看setup内容
  • 非响应引用的案例
  • ref()**概念、原理 与 实战**
  • reactive()**概念、原理 与 实战**
  • 使用**readonly**限制对象的访问权限
  • 使用**toRefs()**对**reactive**对象进一步封装
  • 多个属性进行解构
  • 多个属性 配合toRefs() 进行解构
  • toRefs()无法处理 undefined的键值
  • 使用toRef()应对上述问题
  • 关于setup函数的三个参数【attrs、slots、emit】
  • 回顾 没有 CompositionAPI时,emit的用法
  • 使用setup的 context.emit 替代 this.$emit
  • 使用Composition API开发 todoList
  • 完善toDoList案例
  • 优化上例的逻辑结构!
  • setup的 computed 计算属性
  • 当然以上是computed 的默认用法,实际上它可以接收一个对象
  • 将上例的处理值换成 Object类型,再例
  • setup 中的 watch 监听
  • setup 中的 watch 监听:监听Object类型
  • setup 中的 watch 监听:监听Object类型的 多个属性
  • setup 中的 watchEffect监听 以及 与 watch 的异同比较
  • 两者都可以用以下的方式,在一个设定的时延之后,停止监听
  • 为 watch 配置 immediate属性,可使具备同watchEffect的 即时性
  • setup 中的 生命周期
  • setup中的provide、inject用法
  • 配合上ref实现 响应特性 以及 readonly实现 单向数据流
  • setup结合ref指令

Composition API 的作用

使得相同的、相关的功能代码 可以比较 完整地聚合起来,

提高可维护性、可读性,提高开发效率;

规避 同一个功能的代码,

却散落在 组件定义中的**data、methods、computed、directives、template、mixin**等各处 的问题;

setup函数

--- Composition API 所有代码编写之前, 都要 建立在setup函数 之上; --- 在created 组件实例 被完全初始化之前 回调; (所以注意在**setup**函数中, 使用与**this**相关的调用是没有用的) --- setup函数中的内容, 可以在 该组件的 模板**template** 中直接使用; (如下例程)

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div @click="handleClick">{{name}}</div>    
        `,

        setup(props, context) {
            return {
                name: 'zhao',
                handleClick: () => {
                    alert(666);
                }
            }
        }
    });

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

运行效果:

例程,打印查看setup内容
代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div @click="handleClick">{{name}}</div>    
        `,
        methods: {
            test() {
                console.log(this.$options);
            }
        },
        mounted() {
            this.test();
        },
        setup(props, context) {
            return {
                name: 'zhao',
                handleClick: () => {
                    alert(666);
                }
            }
        }
    });

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

运行效果:

由于调用时序的关系,setup中 无法调用this等相关 如变量、methods中 等 其他内容,但是其他内容 却可以调用 setup函数!!【setup生时众为生,众生时setup已生】
代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div @click="handleClick">{{name}}</div>    
        `,
        methods: {
            test() {
                console.log(this.$options.setup());
            }
        },
        mounted() {
            this.test();
        },
        setup(props, context) {
            return {
                name: 'zhao',
                handleClick: () => {
                    alert(666);
                }
            }
        }
    });

    const vm = app.mount('#heheApp');
</script>
非响应引用的案例

如下,这样没有使用**ref**/**reactive**的写法,是不会有响应的:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            let name = 'guan';
            setTimeout(() => {
                name = 'zhao';
            }, 2000);
            return { name }
        }
    });

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

如下,运行之后,两秒延时之后,DOM文本展示并不会自动改成zhao,而是一直展示初始化的guan

ref()概念、原理 与 实战

使用**ref**可以 用于处理 基础类型的数据**,赋能**响应式**;** 原理:通过 proxy 将 数据 封装成 类似 proxy({value: '【变量值】'})**这样的一个**响应式引用**,** 当**数据**变化时,就会 触发**template**等相关UI的更新 【赋予 非data中定义的变量 以**响应式**的能力 —— 原先,我们是借助Vue的**data函数**,完成**响应式变量**的定义的; 有了**ref**之后,我们可以不借助**data**中的定义, 而直接在**普通的函数**中对**js变量**做**proxy**封装, 就可以对 普通的js引用 赋能**响应式**了】;

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            const { ref } = Vue;
            let name = ref('guan');
            setTimeout(() => {
                name.value = 'zhao';
            }, 2000);
            return { name }
        }
    });

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

运行效果:

两秒后自动变化:

reactive()概念、原理 与 实战

使用**reactive()**用于处理 非基础类型的数据(如Object、Array)**,赋能**响应式**;** 原理类似**ref()**,只是处理的数据格式不同而已;

如下,普通的Object类型是没有响应式的效果的:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div>{{nameObj.name}}</div>    
        `,
        setup(props, context) {
            // const { ref } = Vue;
            const nameObj = { name: 'guan'};
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);
            return { nameObj }
        }
    });

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

运行效果,两秒后无反应:

使用**reactive()**处理**Object类型**后,具备**响应式**的能力:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div>{{nameObj.name}}</div>    
        `,
        setup(props, context) {
            const { reactive } = Vue;
            const nameObj = reactive({ name: 'guan'});
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);
            return { nameObj }
        }
    });

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

运行效果:

两秒后自动变化:

使用**reactive()**处理**Array类型**数据

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({

        template: `
            <div>{{nameObj[0]}}</div>    
        `,
        setup(props, context) {
            const { reactive } = Vue;
            const nameObj = reactive([123, 99, 567]);
            setTimeout(() => {
                nameObj[0] = 666;
            }, 2000);
            return { nameObj }
        }
    });

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

运行效果:

两秒后自动变化:

使用readonly限制对象的访问权限

使用**readonly()**封装对象,可以限制对象为**只读权限**;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{nameObj[0]}}</div>    
            <div>{{copyNameObj[0]}}</div> 
        `,
        setup(props, context) {
            const { reactive, readonly } = Vue;
            const nameObj = reactive([123, 99, 567]);
            const copyNameObj = readonly(nameObj);//----
            setTimeout(() => {
                nameObj[0] = 666;
                copyNameObj[0] = 666;//----
            }, 2000);
            return { nameObj, copyNameObj }//----
        }
    });

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

运行两秒之后会有相应的报错:

错误的解构案例

如下的解构是行不通的,

const { name } = nameObj;**只能拿到**nameObj**的值,拿不到**proxy**对象;**

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            const { reactive } = Vue;
            const nameObj = reactive({ name: 'guan'});
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);

            const { name } = nameObj;
            return { name }
        }
    });

    const vm = app.mount('#heheApp');
</script>
注意解构技巧

赋值时 加上**{}**,会 直取 其 赋值右侧 Object的值;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            const { reactive } = Vue;
            const nameObj = reactive({ name: 'guan'});
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);

            const { name } = nameObj;
            console.log(name);
            return { name }
        }
    });

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

直接赋值,便是直接赋值一份引用;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            const { reactive } = Vue;
            const nameObj = reactive({ name: 'guan'});
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);

            const name  = nameObj;
            console.log(name);
            return { name }
        }
    });

    const vm = app.mount('#heheApp');
</script>
使用toRefs()reactive对象进一步封装

--- toRefs() expects a reactive object**;** 首先,**toRefs()**需要接受一个**reactive**对象; 即,传给**toRefs()**之前,需要先用**reactive()**进行封装; --- 使用**toRefs()**对**reactive**封装的对象 进一步封装, 便可以顺利解构; --- 原理:**toRefs()**将类似**proxy({name:'guan'})**的结构, 转化成类似**{ name: proxy( {value:'guan'}) }**的结构, 这里可以看到**name**键的值,其实就类似于**ref**的处理结果; 然后使用**const { name }**对**{ name: proxy( {value:'guan'}) }**进行解构赋值, 左侧**name**变量 拿到的就是**proxy( {value:'guan'})**这一部分的值, 所以放入DOM节点展示时候, 直接使用**name**即可; --- !!!注意: 类似**reactive()**的处理结果, 即**proxy({name:'guan'})**的结构, 放入DOM节点展示时候,需要使用**nameObj.name**的格式; 而类似**ref()**的处理结果, 即**proxy( {value:'guan'})**的结构, 放入DOM节点展示时候,直接使用**nameObj**的格式即可;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
        `,
        setup(props, context) {
            const { reactive, toRefs } = Vue;
            const nameObj = reactive({ name: 'guan'});
            setTimeout(() => {
                nameObj.name = 'zhao';
            }, 2000);

            const { name } = toRefs(nameObj);
            console.log(name);
            return { name }
        }
    });

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

运行两秒后,自动更新UI:

多个属性进行解构

注意多个属性解构时的写法 以及 return时的写法;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
            <div>{{age}}</div>    
            <div>{{address}}</div>    
        `,
        setup(props, context) {
            const { reactive, toRefs } = Vue;
            const nameObj = reactive({ name: 'guan', age: 22, address:'chaozhou'});
            setTimeout(() => {
                nameObj.name = 'zhao';
                nameObj.age = 66;
                nameObj.address = 'guangzhou';
            }, 2000);

            const { name, age, address } = (nameObj);
            console.log(name, age, address);
            return { name, age, address }
        }
    });

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

运行结果(当然不会自动更新):

多个属性 配合toRefs() 进行解构
代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{name}}</div>    
            <div>{{age}}</div>    
            <div>{{address}}</div>    
        `,
        setup(props, context) {
            const { reactive, toRefs } = Vue;
            const nameObj = reactive({ name: 'guan', age: 22, address:'chaozhou'});
            setTimeout(() => {
                nameObj.name = 'zhao';
                nameObj.age = 66;
                nameObj.address = 'guangzhou';
            }, 2000);

            const { name, age, address } = toRefs(nameObj);
            console.log(name, age, address);
            return { name, age, address }
        }
    });

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

运行结果:

两秒后自动刷新:

toRefs()无法处理 undefined的键值

如果意图解构的键,

不存在于**toRefs()**封装的对象中,

使用时会报错:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{age}}</div>    
        `,
        setup(props, context) {
            const { reactive, toRefs } = Vue;
            const data = reactive({ name: 'guan'});
            const { age } = toRefs(data);
            setTimeout(() => {
                age.value = 'zhao';
            }, 2000);

            return { age }
        }
    });

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

运行结果:

使用toRef()应对上述问题

toRef(data, key)会尝试从data中读取key对应的键值,

如果读得到,就直接取值,

如果读不到,会赋值undefined,后续可以为之赋实值:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({

        template: `
            <div>{{age}}</div>    
        `,
        setup(props, context) {
            const { reactive, toRef } = Vue;
            const data = reactive({ name: 'guan'});
            const age = toRef(data, 'age');
            setTimeout(() => {
                age.value = 'zhao';
            }, 2000);

            return { age }
        }
    });

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

运行两秒后自动更新:

关于setup函数的三个参数

setup函数的context参数中的三个属性,即**attrs, slots, emit**; 获取方法(解构**context参数**): setup(props, context) { const { attrs, slots, emit } = context; return { }; }

attrs

-- 是一个**Proxy**对象; -- 子组件的**none-props属性**都存进**attrs**中;

如下,父组件调用子组件,传递**myfield**属性,

子组件没有用**props**接收,则**myfield**作为**none-props属性**被 子组件承接,

这时候会存进**setup**函数的**attrs**中:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({
        template: `
            <child myfield='heheda' />    
        `
    });

    app.component('child', {
        template:`
            <div>child</div>
        `,
        setup(props, context) {
            const { attrs, slots, emit } = context;
            console.log(attrs);
            return { };
        }
    })

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

运行效果:可以看到**attrs**也是一个**Proxy**对象

(attrs.myfield)**打印取值:**

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        template: `
            <child myfield='heheda' />    
        `
    });

    app.component('child', {
        template:`
            <div>child</div>
        `,
        setup(props, context) {
            const { attrs, slots, emit } = context;
            console.log(attrs.myfield);
            return { };
        }
    })

    const vm = app.mount('#heheApp');
</script>
slots

-- 是一个**Proxy**对象; -- 其中有一个 以为**default**为键的函数, 这个函数会以 虚拟DOM**的形式,** 返回 传给 子组件的**slot插槽** 的**组件**;

打印slots属性:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        template: `
            <child>hehehe</child>    
        `
    });

    app.component('child', {
        template:`
            <div>child</div>
            <div><slot/></div>
        `,
        setup(props, context) {
            const { attrs, slots, emit } = context;
            console.log(slots);
            return { };
        }
    })

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

打印slots属性中的 default函数, 可见其返回的是虚拟DOM:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        template: `
            <child>hehehe</child>    
        `
    });

    app.component('child', {
        template:`
            <div>child</div>
            <div><slot/></div>
        `,
        setup(props, context) {
            const { attrs, slots, emit } = context;
            console.log(slots.default());
            return { };
        }
    })

    const vm = app.mount('#heheApp');
</script>
使用setup中 context参数的 slots属性中的 default方法所返回的 虚拟DOM,通过render函数的形式,在setup函数返回,可以覆盖template的内容,渲染UI
代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        template: `
            <child>hehehe</child>    
        `
    });

    app.component('child', {
        template:`
            <div>child Text</div>
            <div><slot/></div>
        `,
        setup(props, context) {
            const { h } = Vue;
            const { attrs, slots, emit } = context;
            return () => h('div', {}, slots.default());
        }
    })

    const vm = app.mount('#heheApp');
</script>
补充:不使用 Vue3 的这个compositionAPI,子组件只能这样去获取slots内容

通过在其他函数中,使用**this.$slots**的方式调用到**slots**内容

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        template: `
            <child>hehehe</child>    
        `
    });

    app.component('child', {
        mounted () {
            console.log("this.$slots --- ",this.$slots);
        },
        setup(props, context) {
            console.log("context.slots --- ", context.slots);

            const { h } = Vue;
            const { attrs, slots, emit } = context;
            return () => h('div', {}, slots.default());
        }
    })

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

运行结果如下,可以看到跟**setup**函数的**context.slots**是一样的:

回顾 没有 CompositionAPI时,emit的用法
代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        methods: {
            handleChange() {
                alert('hehehe');
            }
        },
        template: `
            <child @change="handleChange">hehehe</child>    
        `
    });

    app.component('child', {
        template: `
            <div  >hehehe</div>    
        `,
        mounted () {
            this.$emit('change');
        }
    })

    const vm = app.mount('#heheApp');
</script>
使用setup的 context.emit 替代 this.$emit
代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        methods: {
            handleChange() {
                alert('hehehe');
            }
        },
        template: `
            <child @change="handleChange">hehehe</child>    
        `
    });

    app.component('child', {
        template: `
            <div @click="handleClick">6666</div>    
        `,
        setup(props, context) {
            const { h } = Vue;
            const { attrs, slots, emit } = context;
            function handleClick() {emit('change');}
            return { handleClick };
        }
    })

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

运行,点击文本:

使用Composition API开发 todoList
调测input框事件

setup中,

--- const inputValue = ref('6666');**定义到一个**proxy**对象,**

可以将此对象 用于setup外的 template中,

完成**双向绑定**中的一环——**数据字段映射到 UI**!!!;

--- handleInputValueChange**定义一个方法;**

运行时,将对应**触发组件**的 内容**,**

赋值给 inputValue**,**

完成**双向绑定**中的另一环——**UI 映射到数据**!!!;

template中,

:value="inputValue"**使得对象的内容 初始化显示**inputValue**的内容;**

--- @input="handleInputValueChange"**使得输入框被用户输入新的内容时,**

会调用对应的方法,这里调用:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { ref } = Vue;
            const inputValue = ref('6666');
            const handleInputValueChange = (e) => {
                console.log("e --- ",e);
                console.log("e.target.value", e.target.value);

                inputValue.value = e.target.value;
            }
            return {
                inputValue,
                handleInputValueChange
            }
        },
        template: `
            <div>
                <div>
                    <input :value="inputValue" @input="handleInputValueChange"/>
                    <div>{{inputValue}}</div>
                    <button>提交</button>
                </div>
                <ul>
                    <li>1</li>
                    <li>2</li>
                </ul>                        
            </div>    
        `
    });

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

运行效果:

完善toDoList案例

--- setup 中定义 数组list,以及 handleSubmit处理函数,

并都在return中返回;

--- template中,

button添加click事件回调;

li 标签,使用v-for,完成列表渲染:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

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

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

运行效果:

优化上例的逻辑结构!

如下,将 setup()中, --- 与 list 相关的定义和操作函数, 封装到 listHandleAction**中,然后return;** --- 与 inputValue 相关的定义和操作函数, 封装到 inputHandleAction**中,然后return;** --- 最后 setup() 调用以上两个**业务模块封装函数**, 解构**返回的内容**,进行**中转调用**; 【分模块 聚合**业务逻辑**,只留一个**核心方法**进行**统筹调度**,有**MVP**那味了】 【这样设计,业务分明,方便定位问题,可读性、可维护性高!!

代码语言:javascript
复制
<script>

    const listHandleAction = () => {
        const { reactive } = Vue;
        const list = reactive([]);
        const addItemToList = (item) => {
            console.log("now, item --- ", item);
            list.push(item);
        }
        return { list, addItemToList }
    }

    const inputHandleAction = () => {
        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 } = listHandleAction();
            const { inputValue, handleInputValueChange } = inputHandleAction();
            return {
                list, addItemToList,
                inputValue, handleInputValueChange
            }
        },
        template: `
            <div>
                <div>
                    <input :value="inputValue" @input="handleInputValueChange"/>
                    <button @click="addItemToList(inputValue)">提交</button>
                </div>
                <ul>
                    <li v-for="(item, index) in list" :key="index">{{item}}</li>
                </ul>                        
            </div>    
        `
    });

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

运行效果:

setup的 computed 计算属性
代码语言:javascript
复制
<script>

    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>
                <div>
                    <span @click="handleClick">{{count}}</span> --- {{countAddFive}}
                </div>
            </div>    
        `
    });

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

运行结果:

当然以上是computed 的默认用法,实际上它可以接收一个对象

这个对象包含两个函数属性,

第一个是**get**函数,其内容即取值时候返回的内容,同默认用法;

第二个是**set**函数,当试图修改**computed**变量的值时,就会回调这个方法,

接收的参数,即**试图修改的值**:

如下,试图在3秒后修改computed变量countAddFive的值,

这时回调set方法:

代码语言:javascript
复制
<script>

    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;
                },
                set: (param) => {
                    count.value = param -5;
                }
            })

            setTimeout(() => {
                countAddFive.value = 1000;
            }, 2000);

            return { count, countAddFive, handleClick }
        },
        template: `
            <div>
                <div>
                    <span @click="handleClick">{{count}}</span> --- {{countAddFive}}
                </div>
            </div>    
        `
    });

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

运行3s后:

将上例的处理值换成 Object类型,再例
代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, computed } = Vue;
            const countObj = reactive({ count: 0 });
            const handleClick = () => {
                countObj.count += 1;
            }
            const countAddFive = computed({
                get: () => {
                    return countObj.count + 5;
                },
                set: (param) => {
                    countObj.count = param -5;
                }
            })

            setTimeout(() => {
                countAddFive.value = 1000;
            }, 2000);

            return { countObj, countAddFive, handleClick }
        },
        template: `
            <div>
                <div>
                    <span @click="handleClick">{{countObj.count}}</span> --- {{countAddFive}}
                </div>
            </div>    
        `
    });

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

运行效果同上例;

setup 中的 watch 监听

如下,

---**watch**一个参数为要监听的引用,

第二个参数为函数类型,当监听的引用发生变化时会回调,

其有两个参数,一个是当前(变化后的)值,一个是变化前的值;

--- input**组件中,**v-model**完成**双向绑定**!!!**

--- input**输入内容时,触发** 双向绑定**的特性,**

内容映射到**name**引用上,

由**ref**的**响应特性**,**name**的内容又映射到**{{name}}**这**DOM节点**上:

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({
        setup() {
            const { ref, watch } = Vue;
            const name = ref('heheda')
            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('#heheApp');
</script>
</html>

运行效果:

setup 中的 watch 监听:监听Object类型
注意setup的watch的 可监听数据类型

所以,这里主要是

---将watch的 第一个参数改成 函数;

---使用toRefs,简化传递过程;

(不然template要接收和处理的就是 nameObject.name了,而不是这里的name)

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, watch, toRefs } = Vue;
            const nameObj = reactive({name: 'heheda'});
            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('#heheApp');
</script>

运行效果同上例;

setup 中的 watch 监听:监听Object类型的 多个属性

注意watch的参数写法,

一参写成,以**函数类型**为**元素**的**数组**;

二参,参数列表写成两个数组,

第一个为current值数组,第二个为prev值数组;

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, watch, toRefs } = Vue;
            const nameObj = reactive({name: 'heheda', englishName:"lueluelue"});
            watch([() => nameObj.name, () => nameObj.englishName],
                 ([curName, curEngN], [prevName, prevEngN])  => {
                console.log(curName, prevName, curEngN, prevEngN);
            });

            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('#heheApp');
</script>
</html>

运行,先在Name输入框输入,后再EnglishName框输入,效果:

setup 中的 watchEffect监听 以及 与 watch 的异同比较

函数中,使得纯函数 变成 非纯函数的 异步处理等部分逻辑块, 称之为**effect块**;

  • --- watch**是**惰性**的,只有**watch**监听的字段发生变化时,** watch**的处理逻辑才会被回调;** --- watchEffect**是**即时性**的,也就是除了**watch**的**回调特性**,** watchEffect**的**处理逻辑**还会在**页面渲染完成时**立马先执行一次,** 即**watchEffect**监听的字段未曾改变, watchEffect**就已经执行了一次;** (实例可以见下例运行效果)
  • watch**需要写明**监听字段**,** watchEffect**不需要,直接写**处理逻辑**即可,** 底层封装**会**!自动监听!**所写**处理逻辑**中用到的**!所有字段!**;**

如下例子中,

watchEffect的处理逻辑——console.log(nameObj.name, nameObj.englishName);

仅一行代码,

完成对nameObj.namenameObj.englishName两个字段的监听,

并完成了处理逻辑;

  • watch**可以直接从**参数列表**中获取到**之前(变化前)的值**和**当前(变化后)的值**,** watchEffect**不行,处理逻辑中拿到的直接就是**当前(变化后)的值**;**

  • 两者都可以用以下的方式,在一个设定的时延之后,停止监听
  • watch 配置 immediate属性,可使具备同**watchEffect**的 即时性
代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, watch, watchEffect, toRefs } = Vue;
            const nameObj = reactive({name: 'heheda', englishName:"lueluelue"});

            watchEffect(() => {
                console.log(nameObj.name, nameObj.englishName);
            })

            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('#heheApp');
</script>

跟紧**console.log(nameObj.name, nameObj.englishName);**,

先在Name输入框输入123,后再EnglishName框输入456,运行效果:

注意第一行打印,第一行是页面渲染完成时立马执行, 用户未曾输入内容,**watchEffect**监听的字段未曾改变, watchEffect**就已经执行了一次,体现**watchEffect**的**即时性**!!!**

两者都可以用以下的方式,在一个设定的时延之后,停止监听

将**watch / watchEffect**的**函数返回值** 赋给一个字段(如下**stopWatch / stopWatchEffect**); 接着在**watch / watchEffect**的**处理逻辑**中, 编写类似**setTimeout**的异步函数, 并在其中调用 与 刚刚定义的**字段** 同名的 函数(如下**stopWatch() / stopWatchEffect()**), 即可停止**对应的监听**;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, watch, watchEffect, toRefs } = Vue;
            const nameObj = reactive({name: 'heheda', englishName:"lueluelue"});
            const stopWatch = watch([() => nameObj.name, () => nameObj.englishName],
                 ([curName, curEngN], [prevName, prevEngN])  => {
                console.log(curName, prevName, curEngN, prevEngN);
                setTimeout(() => {
                    stopWatch();
                }, 3000);
            });

            const stopWatchEffect = watchEffect(() => {
                console.log(nameObj.name, nameObj.englishName);
                setTimeout(() => {
                    stopWatchEffect();
                }, 3000);
            })

            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('#heheApp');
</script>

运行,可以看到,

前3s(我们设定的时延)两个输入框是可以响应监听的,

但是3s之后,无论怎么输入内容,console也不会打印log,

因为这时两个监听效果已经取消了:

watch 配置 immediate属性,可使具备同watchEffect的 即时性

如下,使用**{ immediate: true}**为 watch 配置 immediate属性,

可使具备同**watchEffect**的 即时性:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { reactive, watch, toRefs } = Vue;
            const nameObj = reactive({name: 'heheda', englishName:"lueluelue"});
            const stopWatch = watch([() => nameObj.name, () => nameObj.englishName],
                 ([curName, curEngN], [prevName, prevEngN])  => {
                console.log(curName, prevName, curEngN, prevEngN);
            }, { 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('#heheApp');
</script>

运行效果如下,**undefined**是因第一次执行时,

监听的变量还没有变化,

所以就没有**先前值(prevValue)**的说法,只有**当前值(currentValue)**:

setup 中的 生命周期

--- Vue3.0提供了一些对应生命周期的,可以写在**setup**函数中的回调方法;

(具体请看 下方例程)

--- setup**函数的**执行时间点 在于**beforeCreate**和**Created**之间,

所以**CompositionAPI**里边是没有类似**onBeforeCreate**和**onCreated**的方法的,

要写在这两个周期中的逻辑,

直接写在**setup**中即可;

下面是两个Vue3.0引入的新钩子:

--- onRenderTracked

渲染跟踪,

跟踪 收集响应式依赖的时机,

每次准备开始渲染时(onBeforeMount后,onMounted前)回调;

--- onRenderTriggered

渲染触发,

每次**触发**页面重新渲染时回调,

回调后,下一轮的 onBeforeMount紧跟其后;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
                 onRenderTracked, onRenderTriggered } = Vue;
            const heheda = ref('heheda');
            onBeforeMount(() => {
                console.log('onBeforeMount');
            }),
            onMounted(() => {
                console.log('onMounted');
            }),
            onBeforeUpdate(() => {
                console.log('onBeforeUpdate');
            }),
            onUpdated(() => {
                console.log('onUpdated');
            }),
            onRenderTracked((event) => {
                console.log('onRenderTracked', event);
            }),
            onRenderTriggered((event) => {
                console.log('onRenderTriggered', event);
            })
            const handleClick = () => {
                heheda.value = 'lululu';
                console.log("you had clicked the text!");
            }
            return { heheda, handleClick }
        },
        template: `
            <div @click="handleClick">
                {{heheda}}
            </div>    
        `
    });

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

运行效果:

setup中的provide、inject用法

--- 父组件拿出**provide**,提供键值对;

--- 子组件拿出**inject**,一参为接收键,二参为默认值;

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World! heheheheheheda</title>
    <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
    <div id="heheApp"></div>
</body>
<script>

    const app = Vue.createApp({
        setup() {
            const { provide } = Vue;
            provide('name', 'li');
            return { }
        },
        template: `
            <div>
                <child />
            </div>    
        `
    });

    app.component('child', {
        setup() {
            const { inject } = Vue;
            const name = inject('name', 'default');
            return {name}
        },
        template: `
            <div>{{name}}</div>    
        `
    })

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

运行结果:

配合上ref实现 响应特性 以及 readonly实现 单向数据流

--- setup中, 借**provide**传输的 数据字段 配合上**ref**,

使之具备**响应**的特性;

--- 父组件提供 更改数据的接口,

在使用**provide**传递 数据字段时,加上 readonly**包裹,**

使得子组件 需要更改 父组件传递过来的数据字段 时,

无法直接 修改字段,

需调用 父组件的接口方法 更改,

按 单向数据流 规范编程;

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { provide, ref, readonly } = Vue;
            const name = ref('li');
            provide('name', readonly(name));
            provide('changeName', (value) => {
                name.value = value;
            })
            return { }
        },
        template: `
            <div>
                <child />
            </div>    
        `
    });

    app.component('child', {
        setup() {
            const { inject } = Vue;
            const name = inject('name', 'default');
            const changeName = inject('changeName');
            const handleClick = () => {
                changeName('lin');
            }
            return { name, handleClick }
        },
        template: `
            <div @click="handleClick">{{name}}</div>    
        `
    })

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

运行效果:

点击:

setup结合ref指令

前面笔记《Vue3 | Mixin、自定义指令、Teleport传送门、Render函数、插件 详解 及 案例分析》有写到普通场景的ref指定;

--- setup中的ref是前面说的生成响应式字段的意思;

--- template中的ref是获取对应的dom节点;

--- 而当 template中的ref**指定的字段名,**

跟setup中的ref生成的响应式字段名一样的时候,两者就会关联起来;

如下,

template中和setup中的字段**heheda**关联起来,

在setup的onMounted中,使用这个DOM节点字段,

打印DOM代码:

代码语言:javascript
复制
<script>

    const app = Vue.createApp({
        setup() {
            const { ref, onMounted } = Vue;
            const heheda = ref(null);
            onMounted(() => {
                console.log(heheda);
                console.log(heheda.value);
            })
            return { heheda }
        },
        template: `
            <div>
                <div ref="heheda">lueluelue</div>
            </div>    
        `
    });


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

运行结果:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021.11.25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 本文内容提要
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档