Vue快速上手:三、初识组件

145 阅读11分钟

在上一节曾简单的介绍了一下组件,却是以一个.vue文件的视角来介绍的,这一节我们以JS的视角的介绍一下组件。

既然vue是一个组件化开发的框架,那么布局、样式、事件交互、数据以及数据处理等功能肯定是一个组件必须具备的。对于组件的的编码风格有两种,一种是老版本vue就开始存在的选项式风格,另一种是从vue3才开始有的组合式风格。

刚开始学习Vue可直接在app.vue文件清空,测试单组件的用法,多组件的引用放在后续再讲。

预览项目工程

安装依赖和预览项目

前面已经讲过了创建vue项目工程,接下来讲讲怎么预览这个项目。

第一步:使用终端命令行定位到当前项目的位置;

第二步:安装项目的依赖,刚开始创建的工程是没有安装项目所需的依赖,使用npm install命令即可安装项目所需的所有依赖,如果已经安装过了则可跳过。

第三步:使用命令:npm run dev命令来预览这个项目,当命令运行成功后,会提示一个预览地址,直接在浏览器打开即可。

package.json简要说明

项目的依赖和项目的预览、打包等命令都是定义在项目中的package.json文件中。scripts中定义了项目程中的预览、打包等命令。dependencies中定义了一些项目运行中所需的依赖,devDependencies中定义了一些项目开发过程中所需的依赖。

使用npm install命令可安装package.json中定义的所需依赖,使用npm run scripts中的命令名(如:npm run dev),可运行在scripts中定义的命令。

具体npm的学习可以 www.npmjs.cn/ 中学习或百度即可,刚开始只需了解什么是npm,安装、卸载依赖等即可。

选项式风格

选项式风格编码就是将组件的数据、逻辑处理等都以对象的属性、方法形式呈现。

1、响应式数据

选项式风格定义模板所使用的响应式数据都在data方法中,该方法返回一个数据对象,这个数据对象就是这个组件的响应式数据。

<template>    
    <p>姓名:{{info.name}}</p>
    <p>性别:{{info.sex}}</p>
    {{introduction}}
</template>

<script>
    export default { // 导出这个组件以便其它组件使用
        data() {
            return {
                introduction: "韩国女歌手、演员、主持人,女子演唱团体少女时代成员。",
                info: {
                    name: "林允儿",
                    sex: "女" 
                }
            }
        }
    }
</script>

2、事件

根据HTML和JS的知识想要一个事件产生交互效果,有两个要素:定义事件、绑定事件。

  • 定义事件

定义事件是在methods属性中实现的,methods属性的值是一个对象,当前组件的事件方法都定义在这个对象中,同时在事件方法中,可以通过使用this关键字获取组件的中data部分的响应式数据。

<script>
    export default {
        data() {
            return {
                n: 0
            }
        },
        methods: {
            up() {
                this.n++;
            },
            down() {
                this.n--;
            }
        }
    }
</script>
  • 绑定事件

绑定事件的语法格式为:@事件名="事件方法名", 如将上面的定义的两个方法绑定中在如下的两个button元素中,当点击这两个button按钮将会触发对应的事件修改n的值,当n的值发生变化时,页面也会随着n的更新而更新。

<template>
    <div>当前计数:{{n}}</div>
    <button @click="up">加1</button>
    <button @click="down">减1</button>
</template>

@后面的事件名,这个是和原生JS的事件名和事件作用一一应对的(change、keyup、keydown等事件),同时也可自定义事件名,这个会在后续组件传值的章节中讲到。

3、计算属性

模板中的表达式虽然方便,但如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。就可以将一些模板中的计算逻辑放在计算属性中。

比如这种场景:有一批订单要给出订单的总金额、批次交易内最大订单金额、最小订单金额。如果按以上知识使用单纯的模板语法写出来的维护性和可读性就不好了,如:(如以下map、reduce等方法不知道可以去学习一下JS的ES6+语法和API)

<template>
    <div>最小:{{ goods.map(item => item.sum).reduce((prev, cur) => prev > cur? cur: prev) }}</div>
    <div>最大:{{ goods.map(item => item.sum).reduce((prev, cur) => prev < cur? cur: prev) }}</div>
</template>

<script>
export default {
    data() {
        return {
            goods: [
                { id: 1, sum: 102 },
                { id: 2, sum: 30 },
                { id: 3, sum: 200 },
                { id: 4, sum: 46 }
            ]
        }
    }
}
</script>

模板上本来就应该干净一些做一些插值即可,如以上这么费劲的在模板上做这么复杂的计算工作实在没必要(为能在模板中使用,多做了不必要的运算,先是用map重组数组然后又用reduce来计算,虽然看上去代码少但是运算量却多了,数据少还好数据一多就有性能问题了)。当然这种情况可以封装一个方法放在methods中,然后在模板中调用这个方法,但是不推荐,如以下:

<template>
    <div>最小:{{ min() }}</div>
    <div>最大:{{ max() }}</div>
</template>

<script>
export default {
    data() {
        return {
            goods: [
                { id: 1, sum: 102 },
                { id: 2, sum: 30 },
                { id: 3, sum: 200 },
                { id: 4, sum: 46 }
            ]
        }
    },
    methods: {       
        min() {
            let min = this.goods[0].sum;
            for(let i = 1; i < this.goods.length; i++) {
                if(min > this.goods[i].sum) {
                    min = this.goods[i].sum;
                }
            }
            return min;
        },
        max() {
            let max = this.goods[0].sum;
            for(let i = 1; i < this.goods.length; i++) {
                if(max < this.goods[i].sum) {
                    max = this.goods[i].sum;
                }
            }
            return max;
        }
    }
}
</script>

但是前面我们就说过methods中主要还是以定义事件方法为主,如果其它方法也定义在这一块,就会使得methods也会变得臃肿混乱同样不利于维护。所以最好的办法是像这种情况放在计算属性中,计算属性放在computer中,如以下:

<template>
    <div>最小:{{ min }}</div>
    <div>最大:{{ max }}</div>
</template>

<script>
export default {
    data() {
        return {
            goods: [
                { id: 1, sum: 102 },
                { id: 2, sum: 30 },
                { id: 3, sum: 200 },
                { id: 4, sum: 46 }
            ]
        }
    },
    computed: {        
        min() {
            let min = this.goods[0].sum;
            for(let i = 1; i < this.goods.length; i++) {
                if(min > this.goods[i].sum) {
                    min = this.goods[i].sum;
                }
            }
            return min;
        },
        max() {
            let max = this.goods[0].sum;
            for(let i = 1; i < this.goods.length; i++) {
                if(max < this.goods[i].sum) {
                    max = this.goods[i].sum;
                }
            }
            return max;
        }
    }
}
</script>

请注意计算属性它是一个属性,虽然在computed中定义的是一个个的方法,但是每一个方法名都会转换成属性名,所以在模板中使用的时候{{ sum() }}这种就是错的,正确的使用方式为{{ sum }}

当计算属性中计算过程有依赖到响应式数据时,计算属性的值会随着响应式数据的变化自动更新计算结果到页面中。

4、侦听器

计算属性允许我们声明性地计算衍生值,并且随着响应式数据变化而变化,但是它毕竟是个值,能力有限。同样还有一些场景需要我们去侦听响应式数据的状态变化去执行一些操作,这时候就需要侦听器登场了,侦听器放在watch中,假如我们要侦听到n的值在小于0时就会弹出一个提示框出来,如下:

<template>
    <div>{{ n }}</div>
    <button @click="handleN">点击</button>
</template>

<script>
export default {
    data() {
        return {
            n: 10
        }
    },
    watch: {
        n(newValue, oldValue) {
            if(newValue < 0) {
                newValue = oldValue;
                alert("n的值不能小于0");
            }
        }
    },
    methods: {
        handleN() {
            this.n--;
        }
    },
}
</script>

使用watch侦听状态时,watch的方法名一定要和被侦听的响应式数据名要一致。如上例中被侦听的数据的名为n,所以在侦听器中要侦听到n就要使用n方法来侦听。

侦听器方法有两个参数newValueoldValue,一个是侦听到属性变化后的新值,另一个是变化前的老值。

watch 默认是深层的,当被侦听的属性是一个对象时,对象所有属性以及嵌套属性的改变都能侦听到。如果只想侦听第一层属性的变化时,则要用到浅侦听,需要加一个deep: false,这个属性默认是为true如:

watch() {
    myObj(newValue, oldValue) {
        // 执行的操作
    },
    deep: false // 开启浅层侦听
}

关于选项式风格不会讲得太详细,大多数情况还是用的组合式开发,当然有些特殊情况下和vue2版本的时候还是要用选项式,只是那时vue基本已经入门了,可以自行去看文档了。当然了基础打得好的建议直接看文档上手更快,而且理解更深。

组合式风格

组合式风格的写法和上面的选项式不一样,是使用一系列的组合式API(就是一系列公共函数方法),来开发。需要哪个就导入哪个使用,所有的组合式API方法在一个setup方法使用。如以下:

<script>
    // 导入vue提供的API
    import { ref, reactive } from "vue"; 
    
    export default {
        setup() {
            // 在此处进行组合式开发
            
            return {} // 返回要在模板中使用的响应式数据
        }
    }
</script>

总结:将要使用的内置API或自定义的API导入,然后在合适的位置使用,最后将需要在模板中使用的数据返回出去。

同时为了方便大家书写更方便,vue后续又推出了一个组合式开发的语法糖写法script setup形式,如以下:

<script setup> // 这里的setup属性标记不能丢
    // 导入vue提供的API
    import { ref, reactive } from "vue";      
    // 其它组合式开发代码    
</script>

这个语法糖写是不是方便了很多,在开发阶段了省去了export 导出setupreturn这几步,将其放在编译阶段自动补上,然后会将里面所声明的数据和方法都会自动返回出去。

1、响应式数据

  • 声明

声明响应式数据时,需要用到两个vue提供的API方法:refreactiveref是用来声明普通类型的数据(如字符串、数值、布尔类型等),而reactive则用来声明引用类型的数据(对象、数组等)。如:

<template>
    <div>{{ msg }}</div>
    <p>{{ studys.name }} --- {{ studys.age }} --- {{ studys.lang }}</p>
</template>

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

    const msg = ref("欢迎学习前端!");
    const studys = reactive({name: '林允儿', age: 18, lang: "kr"});    
   
</script>

<!-- style 部分中的scoped表示这些样式只对当前组件有效-->
<style lang="scss" scoped>

</style>
  • 修改

要修改响应式数据的值可分为两种情况:refreactive

1、修改ref声明的值

通过使用变量名.value = 新值这种格式来修改使用ref声明的响应式数据。如:

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

    const msg = ref("欢迎学习前端!");
    
    // 1秒后修改msg的值
    setInterval(() => {
        msg.value="前端学习从入门到入土!";
    }, 1000);
</script>

2、修改reactive声明的数据

如果是一个对象数据,那可以就直接修改其属性期即可,如:

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

    const studys = reactive({name: '林允儿', age: 18, lang: "kr"}); 
    
    setInterval(() => {
        studys.name = "刘亦菲";
    }, 1000);
</script>

2、事件

在组合式风格中定义事件非常简单,通过以下一个例子就全明白了:

<template>
    <div>{{ n }}</div>
    <button @click="add">加1</button>
</template>

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

    const n = ref(0);
	
    // 定义方法:可以是一个事件方法,也可是一个内部逻辑方法
    const add = () => n.value++; // 如果不明白箭头函数语法的,建议去了解JS ES6箭头函数
</script>

3、计算属性

组合式的计算属性需用到computedAPI,它接一个方法作为参数,在这个方法中执行并返回所需的值。

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

    const goods = reactive([
        {id:1, name:"手机", num: 100},
        {id:2, name:"电脑", num: 200},
        {id:3, name:"电视", num: 300},
        {id:4, name:"耳机", num: 400}
    ]);

    // 声明一个计算属性count
    const count = computed(() => {
        return goods.reduce((pre, cur) => pre + cur.num, 0);
    });
</script>

一般情况下计算属性是只读的,不能直接修改,可能在少数场景下会有修改的需要,那么就需要对计算属性定义setget方法,那么这个方法的参数就不能是一个函数了,而一个对象了,如:

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

    const goods = reactive([
        {id:1, name:"手机", num: 100},
        {id:2, name:"电脑", num: 200},
        {id:3, name:"电视", num: 300},
        {id:4, name:"耳机", num: 400}
    ]);

    // 声明一个计算属性count
    const count = computed({
        get() {
            return goods.reduce((pre, cur) => pre + cur.num, 0)
        },
        set(v) {
            const fIndex = goods.findIndex(item => item.id === v); 
            goods.splice(fIndex, 1);
        }
    });
    
    count.value = 1; // 删除goods中下标为1的的数据项
</script>

上述声明了一个计算可读写的计算属性,当在模板中直接插值时就是读,会将get方法的结果返回。要修改时请使用.value,因为计算属性也是一个响应式数据。按上面的例子如果在下面执行count.value = 1,则会将goods数据中删除下标为1的那项数据。

4、侦听器

侦听器所使用的API是watch,它的使用格式为:watch(侦听源, 变化回调函数),使用方式如下:

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

    const n = ref(1);
    // 开始侦听n
    watch(n, (newValue, oldValue) => {
        alert(`n的值更新了,新值为:${newValue}, 老值为:${oldValue}`);
    })
</script>

1、侦听器的侦听源

侦听源数据可以有多种形式:

  • 直接使用响应式数据名
watch(x, (newV, oldV) => { console.log(`x is ${newX}`) })
  • 使用函数/getter方法

当我们要侦听一个响应式对象本身,直接使用响应式数据名就可以,但是如果要监听这个响应式对象中的某个属性的时候,你如果直接使用响应式对象.属性名的方式作为侦听源是没有效果的。这时就需要一个返回该属性的 getter 函数作为侦听源。

const obj = reactive({
    name: "允儿",
    age: 18
});

watch(() => obj.name, (newV, oldV) => {
    alert(` obj.name的值更新了,新值为:${newValue}, 老值为:${oldValue}`);
})
  • 使用数组

当需要同时要侦听多个数据源变化执行同样的逻辑时,虽然我们可以写多个watch,但是那没必要,可以将这些要侦听的数据放在数组里一起侦听,此时watch中的oldValue和newValue同样也是一个数组的形式记录新值与旧值。

const n = ref(1);
const obj = reactive({
    name: "允儿",
    age: 18
});

watch( [n, () => obj.name], (newV, oldV) => {
    console.log(newV, oldV); // newV,oldV的值也是一个数组,根据顺序记录[n的值, obj.name的值]
})

2、深层监听

响应式对象数据默认情况是深层侦听的能检测到内部嵌套属性的变化,如果要只要想侦听第一层属性的变化 则需要开户浅侦听,在watch方法的再加一个参数对象{deep: false},如:

const obj = reactive({
    name: "允儿",
    age: 18,
    other: {
        lang: "kr",
        phone: "123456789"
    }
});

watch(obj, (nV, oV) => {
    console.log(nV, oV);
}, {deep: false});

setTimeout(() => {
    obj.other.lang = "韩"; 
}, 1000)

在上述例子中obj.other.name因为是第二屋的属性了,所以改了值并不会被侦听到数据变化。

3、即时回调的侦听器

watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调,使用immediate: true 选项来强制侦听器的回调立即执行:

watch(obj, (nV, oV) => {
    console.log(nV, oV);
}, {immediate: true})

4、只执行一次的侦听器

每当被侦听源发生变化时,侦听器的回调就会执行。如果希望回调只在源变化时触发一次,请使用 once: true 选项。

watch(obj, (nV, oV) => {
    console.log(nV, oV);
}, {once: true})

5、侦听执行程序过程的所需的数据依赖

对于有多个侦听项的侦听器来说,有需要维护侦听依赖列表的负担,此外如果需要侦听一个嵌套数据结构中的几个属性,会跟踪所有的属性,这会有效能上的负担,此时就可以使用watchEffect

它会自动追踪回调执行函数中所使用的响应式数据的变化,同时它的回调函数也会立即执行,是一个即时回调的侦听器。

import { ref, watch, reactive, watchEffect } from "vue";
const n = ref(1);
const obj = reactive({
    name: "允儿",
    age: 18,
    other: {
        lang: "kr",
        phone: "123456789"
    }
});

watchEffect(() => {
    const str = `姓名:${obj.name} -- 使用语言:${obj.other.name}`;
    console.log(str);
});

7、停止侦听

有时候会需要侦听器不一直侦听下去,当要停止这个侦听时就可使用以下方式:

// 获取这个侦听器的返回值,这个值是一个函数,运行可停止侦听
const unWatch = watch(x, (newV, oldV) => {
    console.log(newV, oldV);
})

// 3秒后停止这个侦听器
setTimeout(() => {
    unWatch();
}, 3000);