前端学习日记 # Vue2.x 入门

625 阅读12分钟

简介

Vue是一套构建用户界面渐进式 javascritp 框架

  • 框架 vs 库

库:方法的集合,避免相同功能重复定义,还具有一定兼容性(js高级封装)

框架:规范开发者(按照框架的设计进行编码),还提供了很多方法

  • 渐进式

指物品具有核心功能和扩展功能的,核心功能是必备,而扩展功能是选配,例如:手机的核心功能是打电话,扩展功能是上网、看视频、听音乐、遥控等

vue的核心功能:声明式渲染
vue的扩展功能:组件、路由、统一状态管理、脚手架

  • 构建用户界面

框架本身只关注视图层,也是最后展示的结果

MVC vs MVVM

  • MVC: 基于事件驱动来控制界面的显示

  • MVVC: 基于数据驱动来控制界面的显示,是MVC的优化升级版

特点

1、视图与数据解耦
2、响应式数据绑定
3、可复用的组件
4、前端路由技术
5、状态管理
6、虚拟Dom

模板(声明式)

插值

绑定内容( {{}} )

使用 “Mustache” 语法 (双大括号) 在模板中声明

<div id="app">
    <p>{{name}}</p>  // 与 v-text 一样
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            name: 'xxx'
        }
    })
</script>

上面这种方式,只能给标签添加文本,如果需要给标签添加HTML,要使用指令v-html,代码如下

<div id="app">
    <p v-html="content"></p>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            content: '<span>1234</span>'
        }
    })
</script>

注意点

  1. 插值可以理解为一个迷你的JS运行环境,它内部只支持变量方法调用计算属性表达式,不能是语句流程控制
  2. 使用插值表达式去访问Vue实例对象不存在的属性数据时,将会报错
  3. 使用插值表达式去访问Vue实例对象存在的属性数据下不存的属性(undefined)时,不会报错
  4. 如果同时存在v-textv-html)和插值表达式{{}}v-text优先级高于插值表达式

指令

绑定内容(v-html、v-text)

HTML内容
<div id="app">
    <p v-html="content"></p>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            content: '<span>1234</span>'
        }
    })
</script>

v-html指令慎用,只能用于绑定自己确定安全的数据,千万别绑定第三方请求数据(代码注入攻击)

文本内容
<div id="app">
    <p v-text="content"></p>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            content: '123456'
        }
    })
</script>

v-text指令一般很少使用,都是直接使用插值取代

绑定属性(v-bind)

使用v-bind指令,简写

<div id="app">
    <a :href="url">跳转百度</a>
    <a v-bind:href="qq">qq</a>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            url: 'http://www.baidu.com',
            qq: 'http://www.qq.com'
        }
    })
</script>

绑定事件(v-on)

使用v-on指令,简写@。虽然v-on中可以写JS代码,但由于事件处理逻辑比较复杂,直接写JS代码在v-on指令中不可行,所以v-on指令支持绑定事件处理函数,以下有两种事件绑定方式:

绑定函数名
<div id="app">
    <button @click="openMsg">点击</button>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            url: 'http://www.baidu.com',
            qq: 'http://www.qq.com'
        },
        methods: {
            openMsg: function(e){
                console.log(e);  // Dom事件对象
            }
        }
    })
</script>
绑定函数调用 (推荐)

与上面方式相比,区别如下
1、函数名方式无法传递参数,函数调用方式可以传递参数
2、函数名方式默认传递原始Dom事件,函数调用方式必须显式指定$event
3、@click="openMsg"@click="openMsg($event)"效果一样

<div id="app">
    <button @click="openMsg('str', $event)">点击</button>
</div>
		
<script>
    const app = new Vue({
        el: '#app',
        data: {
            url: 'http://www.baidu.com',
            qq: 'http://www.qq.com'
        },
        methods: {
            openMsg: function(p,e){
                console.log(p,e);
            }
        }
    })
</script>

总结:
1、事件回调函数需要编写在methods对象中,最终会在Vue实例上
2、methods中编写的函数,不要使用箭头函数,否则this指向Vue实例
3、methods中编写的函数,都是被Vue管理的,所以this指向Vue实例组件实例

条件渲染(v-if)

有两个指令 v-ifv-show

语法

// v-if
<div v-if="condition">111</div>
<div v-else-if="type === 'username'">222</div>
<div v-else>333</div>

// v-show
<div v-show="type === 'password'">xxxx</div>

v-show 与 v-if 区别

1、v-show是通过CSS来控制显示和隐藏,v-if是通过是否渲染来控制的
2、v-if可以使用在<template>元素上,v-show不可以使用在<template>元素上

经常需要频繁地切换,则使用v-show较好;
很少切换,则使用v-if较好

v-if 元素复用

默认会自动复用元素,如果不需要复用,必须在元素上加上 key 属性

注意事项

1、v-if/v-else/v-else-if 必须连着使用,不能中间有起来元素
2、v-else/v-else-if 不能单独使用,必须前面要有v-if元素

列表渲染(v-for)

使用指令 v-for,遍历的数据类型 数组对象,页面的代码结构一致都可以使用 v-for 指令,例如:ul>lli

语法

// 遍历数组
<ul id="example-2">
    <li v-for="(item, index) in arr">
        {{ index }} - {{ item }}
    </li>
</ul>

// 遍历对象
<div v-for="(v, k, index) in obj">
    {{ index }}. {{ k }}: {{ v }}
</div>

// 一般写法
<div v-for="(v, k) in obj">
    {{ k }}: {{ v }}
</div>

* key

提高更新效率,减少不必要DOM操作,( key更像列表项唯一标识 )

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

<script>
    const app = new Vue({
        el: "#app",
        data: {
            list: ['vue', 'html', 'css']
        }
    });
</script>

总结:
1、如果用 v-for 指令,必须配合 :key 属性一起使用
2、:key 的值是浏览器判断是否需要更新DOM的依据,如果这个依据发生变化,浏览器就会对这个值进行更新,否则不会更新(DOM更新就是先删除后重新创建)
3、:key不要使用数组索引作为值

双向数据绑定(v-model)

使用指令 v-model 对表单<input><select><textarea>元素进行双向数据绑定,并该指令只能与前面三种表单元素和自定义输入组件配合使用,其他元素无法使用

什么是双向数据绑定

用户操作视图层(view)导致模型层(model)数据发生变化,反过来模型层(model)数据发生变化又影响视图层(view)数据的展示

简单应用

<div id="app">
    <input type="text" v-model="desc" />
    <p>{{desc}}</p>
    
    <select v-model="selVal">
        <option value="bj">北京</option>
        <option value="nj">南京</option>
        <option value="hz">杭州</option>
    </select>
    <p>{{selVal}}</p>
</div>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            desc: '',
            selVal: ''
        }
    });
</script>

原样输出(v-pre)

跳过这个元素和它的子元素的解析,直接输出(说白就是不解析插值语法) 很少使用

<div v-pre>{{content}}</div>  // {{content}}

延时隐藏(v-cloak)

这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }一起使用,一般配合webpack开发单页应用时,动态生成HTML结构,不需要用到此指令

[v-cloak] {
  display: none;
}

<div v-cloak>
  {{ message }}
</div>

一次渲染(v-once)

只渲染元素、组件及其子元素一次,随后的重新渲染将跳过

<!-- 单个元素 --> 
<span v-once>This will never change: {{msg}}</span>
<!-- 有子元素 -->
<div v-once> 
    <h1>comment</h1>
    <p>{{msg}}</p>
</div>

自定义指令

对原生Dom操作进行一次封装

应用场景

动态样式绑定

动态绑定样式是每个项目必备的技能类名style属性是唯一的两种绑定方式。

通过类名

静态就直接写,动态绑定必须在属性前加冒号(v-bind缩写)

字符串写法

适用:类个数确定,类名字不确定,需要动态绑定

// 推荐
<div class="base" :class="color"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="'main'"></div>

<script>
    new Vue({
        el: "#app",
        data: {
            color: "main"
        }
    });
</script>
数组写法

适用:类个数不确定,类名字也不确定,需要动态绑定

// 推荐
<div class="base" :class="classArr"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="['main', 'box', 'mt-10']"></div>

<script>
    new Vue({
        el: "#app",
        data: {
            classArr: ["main", "box", "mt-10"]
        }
    });
</script>
对象写法

适用:类个数确定,类名字也确定,需要动态j决定用不用

// 推荐
<div class="base" :class="classObj"></div>
// 直接在模板绑定(写死,Vue无法管理)
<div class="base" :class="{main:true,box:false}"></div>

<script>
    new Vue({
        el: "#app",
        data: {
            classObj: {
                main: true,
                box: false
            }
        }
    });
</script>

通过style属性

对象写法
// 不推荐
<p style="color:red" :style="{fontSize: fontMin+'px'}">测试<p>
// 推荐
<p style="color:red" :style="styleObj">测试<p>

new Vue({
    el: '#app',
    data: {
        fontMin: 16,
        styleObj: {
            fontSize: '36px',
            backgroundColor: 'red'
        }
    }
})
数组写法(不推荐
// 不推荐
<p style="color:red" :style="[styleObj]">测试<p>
// 推荐
<p style="color:red" :style="styleObj">测试<p>

new Vue({
    el: '#app',
    data: {
        fontMin: 16,
        styleObj: {
            fontSize: '36px',
            backgroundColor: 'red'
        },
        styleArr: [
            {
                 fontSize: '36px',
                 backgroundColor: 'red'
            }
        ]
    }
})

样式对象的Key要和样式名是一致,遇到样式名带横杠连接的,需要把横杠去掉后面单词首字母大写(例如:font-size => fontSize

事件修饰符

有些HTML元素有默认行为,例如:<a>会自动跳转;为了阻止默认行为,我们需要使用到Vue提供的事件修饰符

语法

// 以前做法
<div id="app">
    <a href="http://www.baidu.com" @click="send">发送</a>
</div>

<script>
    new Vue({
        el: '#app',
        methods: {
            send(e){
                e.preventDefault()
                alert('123');
            }
        }
    });
</script>

// 事件修饰符做法
<div id="app">
    <a href="http://www.baidu.com" @click.prevent="send">发送</a>
</div>

<script>
    new Vue({
        el: '#app',
        methods: {
            send(e){
                alert('123');
            }
        }
    });
</script>

种类

.stop // 阻止单击事件冒泡 (常用
.prevent // 阻止默认行为 (常用
.once // 事件只触发一次 (常用
.capture // 捕获阶段触发事件
.self // 只有当event.target是当前元素时,才会触发事件处理函数
.passive // 元素默认行为立刻执行,不用等待事件处理函数执行完成触发 (移动端使用

.stop.self 的区别

1、.stop是阻止事件冒泡,.self并没有阻止冒泡
2、.stop与.self看上去好像是相反的功能

串联顺序问题(有些串联顺序不同效果也一样,没区别

<div @click="show(3)">
    <div @click.self.stop="show(2)">   
        <a href="http://www.baidu.com" @click.prevent="show(1)">百度</a>
    </div>
</div>

methods: {
    show(i){
        console.log(i);
    }
}

// 当前元素的点击事件如果是自身触发(非冒泡)就会阻止事件冒泡
 @click.self.stop   
 
 // 当前元素的点击事件将阻止冒泡,如果是自身触发,将执行后面的事件处理函数 
 (无论是否是自身触发的点击事件,都会阻止事件冒泡)
 @click.stop.self   

键盘事件修饰符

键盘事件常用的有两个:keyupkeydownkeyup更为常用

用法

// 焦点在这个输入框时,按enter键时触发test函数
<input type="text" @keyup.enter="test" />  

motheds:{
    test(){
        alert('已按');
    }
}

为了兼容旧浏览器,Vue 提供了绝大多数常用的按键码的别名

.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right

.tab 比较特殊,只能配合keydown,不能配合keyup

Vue未提供别名的按键,可以使用按键原始的key去绑定(小写)

注意事项

1、不是所有元素都有键盘事件的,可绑键盘事件的元素如下:
<a>、<button>、<input>、<textarea>、<select>

2、键盘事件修饰符为按键名,用小写,遇到多个单词的按键名时,需要转换为kebab-case形式
例如:PageDown 转为 page-down   @keyup.page-down="xxx"

3、每个按键都有键名和键值
键值: $event.keyCode
键名:$event.key

系统修饰键

.ctrl
.alt`
.shift
.meta

1、配合keyup使用,按下修饰键的同时,再按下其他键,然后释放其他键,事件才被触发 2、配合keydown使用,正常触发

数据绑定

是一种响应式的数据操作,摆脱传统的Dom操作

单向数据绑定

通过v-bind指令绑定,数据只能从data流向页面

双向数据绑定

通过v-model指令绑定,修改数据来让页面发生变化,还可以通过页面输入来让数据发生变化

限制:v-model只支持表单类(输入类)元素,例如<input><textarea><select>

收集表单数据

单行与多行文本输入

使用 value 属性 和 input 事件;v-model收集的是value值,用户输入的也是value值

<input type="text" v-model="formData.username" />
<textarea v-model="formData.desc" />

new Vue({
    el: "#app",
    data: {
        formData: {
            username: '',
            desc: ''
        }
    }
})

单选

使用 checked 属性 和 change 事件;v-model收集的是value值,并且必须给标签配置value值

<input type="radio" value="man" v-model="formData.sex" />男
<input type="radio" value="woman" v-model="formData.sex" />女


new Vue({
    el: "#app",
    data: {
        formData: {
            sex: ''
        }
    }
})

多选

使用 checked 属性 和 change 事件

单个复选框

当应用一个复选框时,无须指定value值,v-model绑定的是布尔值类型,选中就返回true,未选中就返回false

<input type="checkbox" v-model="formData.isAgree" /> 是否同意协议

new Vue({
    el: "#app",
    data: {
        formData: {
            isAgree: false
        }
    }
})

多个复选框

当应用一个复选框时,必须指定value值,v-model绑定的必须是数组类型,而且是同一个数据

<input type="checkbox" value="tw" v-model="formData.hobby" /> 跳舞
<input type="checkbox" value="cg" v-model="formData.hobby" /> 唱歌

new Vue({
    el: "#app",
    data: {
        formData: {
            hobby: []
        }
    }
})

总结

1、没有配置value属性,则v-model收集的是checked(选中就是true,未选中就是false,是布尔类型)
2、配置value属性
    (1)如果v-model的初始值是非数组,则v-model收集的是checked(选中就是true,未选中就是false,是布尔类型)
    (2)如果v-model的初始值是数组,则v-model收集的是value组成的数组

三大修饰符

  1. .lazy 失去焦点再收集数据
  2. .number 把输入字符转成数字类型
  3. .trim 去掉输入的首尾空格

下拉选择

使用 value 属性 和 change 事件

<select v-model="formData.birthplace">
    <option disabled value="">请选择</option>
    <option value="gz">广州</option>
    <option value="zj">湛江</option>
    <option value="sz">深圳</option>
</select>

new Vue({
    el: "#app",
    data: {
        formData: {
            birthplace: ''
        }
    }
})

生命周期

是什么

又名生命周期回调函数,Vue在特定时刻会自动调用一些特殊名称的函数,这些特殊名称的函数是Vue规定好的,我们不能更改,但函数内容是让我们自己编写的

流程图讲解

生命周期.png

注意

1、生命周期函数必须使用function定义,不能使用箭头函数定义
2、生命周期函数体内this指向Vue实例对象组件实例对象
3、destroyed函数体内,还是可以访问到销毁的Vue实例,哪怕beforeDestroy函数体内修改过Vue实例中的data的数据,都可以访问到
4、【重要】生命周期函数中,最重要的两个函数分别是:

计算属性与监听器

computed

什么叫计算属性?

拿着已有的属性进行加工和计算,然后生成全新的属性,这就是所谓计算属性

语法

computed是对象类型,而computed对象中的属性可以是函数(简化写法)或者对象(完整写法)

const app = new Vue({
    el: "#app",
    data: {
        firstName: "张",
        lastName: "三"
    },
    computed: {
        // 简化写法(其实是get方法)
        fullName(){
            // 必须要有返回值,并且返回值就是fullName的值
            return this.firstName + '-' + this.lastName
        },
        // 完整写法
        fullName:{
            get(){
                // 必须要有返回值,并且返回值就是fullName的值
                return this.firstName + '-' + this.lastName
            },
            set(val){
                // 没有返回值
            }
        }
    }
});

1、在计算属性的get和set方法中,this也是指向Vue实例
2、get方法什么时候调用?
(1) 初始读取计算属性时
(2) 所依赖的数据发生变化时
3、set方法什么时候调用?
(1)当计算属性给修改时
4、set方法作用
用于修改计算属性的依赖,并非修改计算属性

缓存

计算属性方法的唯一区别就是:缓存,计算属性性能优于方法

<div id="app">
    <p>{{test()}}</p>
    <p>{{res}}</p>
    <p>{{test()}}</p>
    <p>{{res}}</p>
    <p>{{test()}}</p>
    <p>{{res}}</p>
    <button @click="go">123</button>
</div>
		
<script>
    const app = new Vue({
        el: "#app",
        data: {
            num1: 1,
            num2: 2
        },
        computed:{
            res(){
                console.log(1);
                return this.num1 + this.num2;
            }
        },
        methods:{
            test(){
                console.log(2);
                return this.num1 + this.num2;
            },
            go(){
                this.num1 = 12;
            }
        }
    });
</script>

计算属性方法 都具有响应式,依赖一旦发生变化,两者都会自动重新调用,计算属性调用一次,而方法就会调用N次

watch

对已存在的数据进行监听

写法(2种)

第一种,写在Vue实例初始化配置中

<script>
    const app = new Vue({
        el: "#app",
        data: {
            num1: 1,
            num2: 2
        },
        computed:{
            res(){
                console.log(1);
                return this.num1 + this.num2;
            }
        },
        methods:{},
        watch:{
            res: {
                handler(val, oldVal){
                    alert(123);
                }
            }
        }
    });
</script>

第二种,调用Vue实例的$watch方法进行监听

<script>
    const vm = new Vue({
        el: "#app",
        data: {
            num1: 1,
            num2: 2
        },
        computed:{
            res(){
                console.log(1);
                return this.num1 + this.num2;
            }
        },
        methods:{}
    });
    
    vm.$watch('res', {
        handler(val, oldVal){
            alert(123);
        }
    })
</script>

简写

如果不需要配置deepimmediate,可以使用简写

// 使用Vue初始配置
<script>
    const app = new Vue({
        el: "#app",
        data: {
            num1: 1,
            num2: 2
        },
        computed:{
            res(){
                console.log(1);
                return this.num1 + this.num2;
            }
        },
        methods:{},
        watch:{
            res(val, oldVal){
               alert(123);
            }
           
        }
    });
</script>

// 使用Vue实例方法$watch
<script>
    const vm = new Vue({
        el: "#app",
        data: {
            num1: 1,
            num2: 2
        },
        computed:{
            res(){
                console.log(1);
                return this.num1 + this.num2;
            }
        },
        methods:{}
    });
    
    vm.$watch('res', function(val, oldVal){
        alert(123);
    })
</script>

深度监听

// 监听多层级结构对象(数据属性)的某个属性的变化
const vm = new Vue({
    el: "#app",
    data: {
        a: {
            b: {
                c: 12
            }
        }
    },
    watch: {
        'a.b.c': {
            handler(newVal,oldVal){
                console.log('修改')
            }
        }
    }
})

// 监听多层级结构对象的所有属性的变化
const vm = new Vue({
    el: "#app",
    data: {
        a: {
            b: {
                c: 12
            }
        }
    },
    watch: {
        'a': {
            deep: true, // 不论其被嵌套多深,只要a对象里的属性值给改变就会触发监听函数
            handler(newVal,oldVal){
                console.log('修改')
            }
        }
    }
})

1、第一种监听多层级数据的某个属性时,监听函数中的newValoldVal的值,如果是基本数据类型就正常,否则不正常
2、第二种监听多层级数据的所有属性时,监听函数中的newValoldVal的值都是指向引用地址,所以oldVal失效了

watch与computed区别

1、computed能实现的功能,watch也能实现
2、watch可以实现的功能,computed不一定能实现,例如:watch能进行异步任务
3、computed的每个计算属性的get函数必须带有returnwatch不需要带有return

总结

1、可监听的属性包含:数据属性计算属性
2、当被监听的属性发生变化时,处理函数会自动调用,进行相关操作
3、监听的属性必须存在,才能够监听
4、如果监听属性的处理函数有修改本属性值的代码,将出现死循环,所以不推荐处理器中修改本属性
5、Vue默认监听对象内部属性值的变化,而Vue的watch默认不监听对象内部属性值的变化,要watch监听需要配置deep配置为true
6、使用watch监听数据时,要根据数据的具体结构来决定是否采取深度监听

过滤器

过滤器其实就是函数,用于对要显示的数据进行特定格式化后再显示(适合一些简单逻辑的处理),但计算属性方法也同样可以实现类似功能

局部过滤器

局部过滤器只能使用在当前Vue实例中,不能使用在其他Vue实例中

// 注册
new Vue({
    el: "#app",
    data: {
        nowday: 1222334444
    },
    filters: { 
        formatDay(value){
            return value+"天";
        }
    }
});

// 使用
<p>{{ nowday | formatDay }}</p>
<div :title="nowday | formatDay">测试</div>

在多组件单页应用中,组件间的局部过滤器不能相互使用

全局过滤器

// 注册
Vue.filter('formatDay', function(value){
     return value+"天";
});

new Vue({...})

注意:
必须在Vue实例创建前定义全局过滤器,否则无效

过滤器传参

// 使用
<p>{{ nowday | formatDay('yyds') }}</p>

// 注册
new Vue({
    el: "#app",
    data: {
        nowday: 1222334444
    },
    filters: { 
        formatDay(value, str){
            return value+"天"+str;
        }
    }
});

注意:
过滤器接收的第一个参数永远都是管道符前面的数据

过滤器串联

使用多个过滤器格式化数据时,后面过滤器接收的参数是前面过滤器返回的值

// 模板
<p>{{ nowday|formatDay('yyds')|formatNum }}</p>

// 语法
new Vue({
    el: "#app",
    data: {
        nowday: 1222334444
    },
    filters: { 
        formatDay(value, str){
            return value+"天"+str;
        },
        formatNum(value){
            return "$"+value;
        }
    }
});

总结

1、注册过滤器2种方式:局部全局
2、使用过滤器2种方式:插值表达式{{ xxx | 过滤器名称 }}:属性名="xxx | 过滤器名称"
3、过滤器是无法改变原数据的
4、过滤器也能接收额外的参数,过滤器可以串联使用

组件

组件化诞生历史

传统开发方式

一个页面就是:一个html文档+N个JS文件+N个CSS文件的集合

缺点:
1、依赖关系混乱(js模块化缺失),不好维护
2、代码复用率不高 (每次复用都要复制黏贴html结构,引入JS和CSS,复用很麻烦)
3、容易造成命名污染(JS还好,可以使用自执行的函数作用域来解决,但CSS就没有,只能通过定制命名规范来避免)
4、html缺少模块化
5、css缺少模块化

模块化出现

模块化最主要想体现的就是代码拆分,便于代码维护复用

1、JS模块化出现,标志着依赖引入代码更合理,更好维护(无需顾虑引入依赖的顺序问题命名污染问题
2、html模块化依然没有
3、css模块化虽然语法有提供@import语法,但由于性能问题,不建议使用

组件化出现

模块化最主要想体现的就是代码封装,把页面上局部功能的代码(结构、样式、交互)和资源封装成集合,便于代码维护复用

模块化的基础上,组件化更进一步解决以上2个问题,把页面拆分成一个一个组件,每个组件都相互独立,互不影响,每个组件包含其独自的htmlcssjs代码,其中cssjs都在组件私有作用域内,并不会命名污染全局

解决
1、出现css模块化(通过前端工程化工具解决)
2、出现html模块化(通过前端工程化工具解决)
3、3种语言史无前例出现大融合,出现了一个完整的整体,复用率更高,更好维护
4、引入了前端工程化的概念

第一步:创建组件

Vue组件分为2种:单文件组件非单文件组件

非单文件组件

组件template选项的结构,最外层元素只能是一个

const school = Vue.extend({
    template: `
        <div>
            <h1>{{name}}</h1>
        </div>
    `,
    data(){
        return {
            name: "张三"
        }
    }
});

// 简写方式
const school = {
    template: `
        <div>
            <h1>{{name}}</h1>
        </div>
    `,
    data(){
        return {
            name: "张三"
        }
    }
};

单文件组件 [推荐]

创建一个以.vue为后缀的文件,该文件就是单文件组件,文件内部结构如下:

<template>
    // 组件html结构,最外层元素只能是一个
</template>

<script>
    // 组件数据交换js
</script>

<style>
    // 组件样式
</style>

命名规范
1、单个单词使用小写(例如:school),多个单词使用横杠'-'连接小写单词(例如:my-school)
2、单个单词使用首字母大写(例如:School),多个单词使用大驼峰(例如:MySchool)
更推荐使用第二种

组件与Vue实例的区别

组件和Vue实例在创建时,都需要需要传一个配置对象,配置项基本相同,但存在以下区别:

1、组件没有el配置项,Vue实例有 (所有组件一切听从Vue实例管理与分配)
2、组件的data配置项必须是函数,Vue实例的data配置项可以是对象也可以是函数

单文件组件 vs 非单文件组件

非单文件组件缺点:
1、封装不完整,无法把css封装在组件当中
2、组件样式没有私有作用域,会产生污染

总结:实际开发必须使用单文件组件

第二步:注册组件

注册组件有2种方式:局部注册全局注册

局部注册

// 非单文件组件创建
const a = Vue.extend({
    template: `
        <div>
            <h1>{{name}}</h1>
        </div>
    `,
    data(){
        return {
            name: "张三"
        }
    }
});

// 创建Vue实例来管理组件
new Vue({
    el: "#app",
    data: {},
    // 注册组件
    components: {
        a // 对象属性简写,全写:a:a
    }
})

全局注册

全局注册使用比较少,因为一般我们开发单页应用时,都是一个Vue实例管理所有组件的,哪怕组件再嵌套组件

// 非单文件组件创建
const a = Vue.extend({
    template: `
        <div>
            <h1>{{name}}</h1>
        </div>
    `,
    data(){
        return {
            name: "张三"
        }
    }
});

// 全局组件注册
Vue.component('组件名',a)

第三步:使用

和普通html标签一样写法

<div>
    <student></student>
</div>

// 非单文件组件创建
const student = Vue.extend({
    template: `
        <div>
            <h1>{{name}}</h1>
        </div>
    `,
    data(){
        return {
            name: "张三"
        }
    }
});

// 创建Vue实例来管理组件
new Vue({
    el: "#app",
    data: {},
    // 注册组件
    components: {
        student // 对象属性简写
    }
})

Vue实例的模板有2种:
1、直接写在el指定的容器里
2、直接使用template配置项

Vue实例的模板只有一种:
1、单文件组件写在<template>标签内
2、非单文件组件写在template配置项

组件样式问题

命名冲突

Vue脚手架项目中,组件与组件之间存在着class命名冲突问题(后面导入的组件会覆盖前面导入的组件的相同class名的样式),所以vue-cli脚手架提供了scoped属性让样式只在组件内生效(组件局部样式)

<style scoped>
    .test{
        color: red;
    }
</style>

CSS 预处理器

默认<style></style>标签内只能写css代码,如果需要用到Sass/Scss、Less等预编译语言,需要先安装sass-loader、less-loader等,然后书写代码如下:

<style scoped lang="scss">

</style>

插槽

让组件使用者可以向组件指定的位置插入自定义的html结构,让组件灵活性、复用性更强大,也是一种组件间的通信的方式,只适合 父组件 ==> 子组件

作用:不仅可以传数据,还可以传结构

组件插槽有以下3种:默认插槽、具名插槽、作用域插槽

默认插槽

最简单的插槽

// 父组件
<Category>
    <div>xxxx</div>
</Category>

// 子组件
<template>
    <div>
        <slot>如果使用者没有定义插槽内容,默认就显示这句话</slot>
    </div>
</template>

具名插槽

当一个组件有多个插槽时,需要使用具名插槽

// 父组件(使用者)
<Category>
    <!-- 这种方式已废弃 -->
    <template slot="header">
        <div>xxxxx</div>
    </template>
    
    <!-- 推荐这种方式 -->
    <template v-slot:footer>
         <div>sssss</div>
    </template>
</Category>

// 子组件
<template>
    <div>
        <slot name="header">如果使用者没有定义插槽内容,默认就显示这句话</slot>
        <slot name="footer">如果使用者没有定义插槽内容,默认就显示这句话</slot>
    </div>
</template>

推荐写法:v-slot

作用域插槽

数据在组件(定义插槽的组件)的自身,但根据数据生成的结构需要组件的使用者来决定。

// 父组件(使用者)
<Category>
     <!-- 第一种方式已废弃 -->
     <template scope="scopeData">
         <ul>
             <li v-for="item in scopeData.games" :key="item.id">{{item.name}}</li>
         </ul>
     </template>
     
     <!-- 第二种方式已废弃 -->
     <template slot-scope="scopeData">
         <h2 v-for="item in scopeData.games" :key="item.id">{{item.title}}</h2>
     </template>
     
     <!-- 推荐这种方式 -->
     <template v-slot="scopeData">
         <h2 v-for="item in scopeData.games" :key="item.id">{{item.title}}</h2>
     </template>
     
      <!-- 推荐这种方式(指定插槽名称,缺省时默认是default) -->
     <template v-slot:main="scopeData">
         <h2 v-for="item in scopeData.books" :key="item.id">{{item.title}}</h2>
     </template>
</Category>

// 子组件
<template>
    <div>
        <slot name="main" :books="books">如果使用者没有定义插槽内容,默认就显示这句话</slot>
        <slot :games="games">如果使用者没有定义插槽内容,默认就显示这句话</slot>
    </div>
</template>

作用域插槽 scopeslot-scope、'v-slot' 支持解构赋值
scope="{books}"
slot-scope="{books}"
v-slot:default="{books}"

特殊属性

$root

用于子组件获取根组件对象,可以使用$root属性,很少用,Vue单页应用根组件对象就是Vue对象(上面没数据和方法)

new Vue({
    el: "#app",
    data: {},
    components: {
        Book: {
            name: 'Book',
            methods: {
                test(){
                    console.log(this.$root) // Vue对象
                }
            }
        }
    }
})

$parent

用于子组件获取父组件对象,可以使用$parent属性,很少用,因为这样操作组件间耦合度很高

new Vue({
    el: "#app",
    data: {},
    components: {
        Book: {
            name: 'Book',
            methods: {
                test(){
                    console.log(this.$parent) // Vue对象
                }
            }
        }
    }
})

$children

用于父组件获取子组件对象,可以使用$children属性,很少用,因为这样操作组件间耦合度很高

(获取子组件的属性或调用子组件的方法)

数组类型,默认是空数组

new Vue({
    el: "#app",
    data: {},
    methods: {
        getSubComp(){
            console.log(this.$children[0].name)
        }
    }
})

$refs

用于父组件获取子组件对象,使用比较多,比$children好

对象类型,默认是空对象

<div id="app">
    <Header ref="aaa"></Header>
</div>

new Vue({
    el: "#app",
    data: {},
    methods: {
        getSubComp(){
            console.log(this.$refs.aaa)
        }
    }
})

组件间通信 (重要)

组件间数据事件的传递

父子组件

父 ==> 子
  • [推荐]通过props向子组件传递数据

给子组件配置props选项,有2种方式:数组方式对象方式

// 第一种:数组方式
<script>
    export default {
        name: 'Test',
        props: ['books']
    }
</script>

/*
 * 注意:
 * 使用数组方式,编码简洁方便,但缺省:
 * 1、数据类型校验
 * 2、默认值指定
 * 3、必填校验
 */

// 第二种:对象方式
<script>
    export default {
        name: 'Test',
        props: {
            // 仅限制类型写法
            books: Arraycontent: String,
            
            // 完整限制写法(数据类型、默认值、是否必填),其中默认值和必填为互斥的,只能有一个
            books: {
                type: Array,
                default: () => []
            },
            content: {
                type: String,
                required: true
            },
            user: {
                type: Array,
                default: () => ({})
            }
        }
    }
</script>

父组件使用子组件并传值

<div>
    <MyComp :books="books"></MyComp>
</div>

<script>
import MyComp from './components/MyComp.vue'

new Vue({
    el: '#app',
    data(){
        return {
            books: ['柯南','火影']
        }
    },
    components: {
        MyComp
    }
})
</script>

注意
1、如果子组件props中定义的参数名是小驼峰命名的,那么父组件传值要使用短横线分隔命名,例如 studentName ==> student-name
2、子组件不能直接修改props中父组件传过来的参数值,只能通知父组件,让父组件修改传过来的值

  • 通过$attrs向子组件传递数据

虽然能够实现,但$attrs更适合用于多层嵌套的祖孙组件数据通信(祖先传给子孙)

// 祖先组件
<template>
  <div class="home">
    <h1>{{msg}}</h1>
    <HelloWorld :msg="msg" class="wt-container" style="font-size: 30px;"/>
  </div>
</template>

// 中间组件
<template>
  <div class="hello">
    <Sub v-bind="$attrs"></Sub>
  </div>
</template>

// 目标子孙组件
this.$attrs

注意:
1、父组件传过来,不作为props给识别的数据,(除开class与style) 2、v-bind="$attrs"v-bind不能缩写

  • 通过$refs或者$children向子组件传递数据

原理:获取子组件对象,然后直接操作子组件对象上的data属性或直接调用motheds上的方法

这种方式实际开发中使用相对少点

子 ==> 父
  • [推荐]通过自定义事件向父组件传递数据

子组件定义并发射自定义事件

// 调用组件对象的$emit方法来发射自定义事件
this.$emit('custem-event', args)

父组件监听并处理自定义事件

<ChildComp @custem-event="add"></ChildComp>

import ChildComp from './components/ChildComp.vue'
<script>
    new Vue({
        methods: {
            add(params){
                console.log(params); // 子组件传过来的参数
            }
        },
        components: {
            ChildComp
        }
    })
</script>

注意
1、自定义事件名称,推荐使用"短横线分隔命名"进行命名
2、自定义事件是否会冒泡??? 答案:Vue 自定义事件没有冒泡机制,也无需使用事件修饰符.stop,只有在父组件中子组件标签上能监听自定义事件,因此也不会有命名冲突问题

  • 通过props向父组件传递数据

原理:利用props能传函数类型的特性

把父组件定义的函数传给子组件,父组件代码如下

<template>
  <div class="home">
    <h1>{{msg}}</h1>
    <HelloWorld :goChange="goChange" />
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'Home',
  data(){
    return {
      msg: '你好'
    }
  },
  components: {
    HelloWorld
  },
  methods:{
    goChange(msg){
      this.msg = msg.title;
    }
  }
}
</script>

子组件直接调用传过来的函数并带上数据

<template>
  <div class="hello-world">
    <button @click="send">调用父函数传参</button>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    goChange: Function
  },
  methods:{
    send(){
      this.goChange({
        title: '我是标题'
      })
    }
  }
}
</script>
  • 通过$parent向父组件传递数据

简单粗暴,直接修改父组件data中属性或者直接调用父组件mothed中方法

兄弟组件

  • [推荐]通过Vuex实现

Vuex是Vue的一个官方插件,专门用于任意组件间通信。

好处:使用简单,便于后期维护,几乎所有Vue单页应用必备

学习请看链接:前端学习日记 # Vuex

  • 通过EventBus(全局事件总线)实现

原理:借助Vue根实例的API:$on$once$off$emit,通过自定义事件进行任意组件通信

不适合中大型项目中使用,造成难以维护

注意:
1、$on、$once在哪个生命周期都可以使用,但推荐在mounted()中使用
2、记得在beforeDestroy或destroyed生命周期函数中使用$off解绑自定义事件

组件A

mounted() {
    this.$root.$on('go-come', function(d) {
        console.log(d);
    });
}

组件B

methods: {
    send() {
        this.$root.$emit('go-come', '我调用了')
    }
}

待解决问题:
1、多人协助时,事件名命名冲突问题
2、当同一个组件在同一个界面被多次复用,那该组件上使用$on监听自定义事件重复注册同一个事件问题

  • [不推荐]通过消息订阅与发布实现

需要数据的人订阅消息,提供数据的人发布消息

原理:大致上和全局事件总线的实现原理大同小异,更推荐直接使用全局事件总线,因为vuedevTools能监控触发的自定义事件

现在已经有很多实现消息订阅与发送的第三方库,推荐 **pubsub-js **库(可用于Vue、react、anglerJs)

// 安装
npm i pubsub-js

祖先后代组件

  • [推荐]通过Vuex实现

可追溯,可调试,但只适合应用用,不适合开发组件库使用

  • 通过$attrs/$listeners实现

上面"父子组件"中,已经介绍过$attrs属性的用法,它可以把数据从父-->子-->孙

下面介绍$listeners属性的用法,它可以把事件从孙-->子-->父,顺序和$attrs刚好相反

由于自定义事件没有冒泡机制,所有要通过$listeners属性来实现

// 孙组件
<template>
  <div class="sub">
    <p>我是孙子</p>
    <button @click="getAttrs">获取$attrs</button>
  </div>
</template>

<script>
  export default {
    name: 'Sub',
    methods: {
      getAttrs() {
        this.$emit('top-event', '来自Sub的事件');
      }
    }
  }
</script>

// 子组件
<template>
  <div class="hello">
    <p>
      For a guide and recipes on how to configure / customize this project,<br>
      check out the
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <Sub v-on="$listeners"></Sub>
  </div>
</template>

<script>
  import Sub from './Sub.vue'
  export default {
    components:{ Sub },
    name: 'HelloWorld'
  }
</script>

// 父组件
<template>
  <div class="home">
    <h1>{{msg}}</h1>
    <HelloWorld :msg="msg" class="wt-container" style="font-size: 30px;" @top-event="test"/>
  </div>
</template>

<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'Home',
  data(){
    return {
      msg: '你好'
    }
  },
  components: {
    HelloWorld
  },
  methods:{
    test(msg){
      this.msg = msg;
    }
  }
}
</script>

总结:
1、v-on="$listeners"v-on不能缩写
2、与props相比,写法简化了些,但如果组件层级比较多,还是很麻烦,所以不太适合组件层级过多的使用

  • 通过provide/inject实现

provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。

// 父组件或祖先组件
<script>
import HelloWorld from '@/components/HelloWorld.vue'

export default {
  name: 'Home',
  data(){
    return {}
  },
  provide(){
    return {
      bookName: "我来自Home的provide"
    }
  },
  components: {
    HelloWorld
  }
}
</script>

// 子组件或后代组件
<script>
  export default {
    name: 'Sub',
    inject: ['bookName'],
    methods: {
      getAttrs() {
        console.log(this,this.bookName);
      }
    }
  }
</script>

注意:
1、provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
2、编码方便优雅,只要在祖先组件上声明提供的数据,在任意后代组件上都可以获取,无需逐层传递的代码

使用 provide/inject 做全局状态管理

// 根组件提供将自身提供给后代组件
export default { 
    provide() { 
        return { 
            app: this 
        } 
    }, 
    data() { 
        return { 
            text: 'bar' 
        } 
    }
}

// 后代组件注入 'app'
<template> 
    <div>{{this.app.text}}</div>
</template> 

<script> 
export default { 
    inject: ['app'], 
    created() { 
        this.app.text = 'baz' // 在模板中,显示 'baz' 
    } 
}
</script>

也许有的同学会问:使用 $root 依然能够取到根节点,那么我们何必使用 provide/inject 呢?

在实际开发中,一个项目常常有多人开发,每个人有可能需要不同的全局变量,如果所有人的全局变量都统一定义在根组件,势必会引起变量冲突等问题。

使用 provide/inject 在不同模块的入口组件传给各自的后代组件可以完美的解决该问题。以最近的组件优先级最高

虽然provide/inject能做全局状态管理,但慎用,因为最好做全局状态管理的是Vuex

provide/inject不适合做全局状态管理

provide/inject 最适合编写组件

使用 provide/inject 做组件开发,是 Vue 官方文档中提倡的一种做法。

以我比较熟悉的 elementUI 来举例:

在 elementUI 中有 Button(按钮)组件,当在 Form(表单)组件中使用时,它的尺寸会同时受到外层的 FormItem 组件以及更外层的 Form 组件中的 size 属性的影响。

如果是常规方案,我们可以通过 props 从 Form 开始,一层层往下传递属性值。看起来只需要传递传递两层即可,还可以接受。但是,Form 的下一层组件不一定是 FormItem,FormItem 的下一层组件不一定是 Button,它们之间还可以嵌套其他组件,也就是说,层级关系不确定。如果使用 props,我们写的组件会出现强耦合的情况。

provide/inject 可以完美的解决这个问题,只需要向后代注入组件本身(上下文),后代组件中可以无视层级任意访问祖先组件中的状态。

跨层级组件

  • [推荐]通过Vuex实现

  • 通过EventBus(全局事件总线)实现

  • 通过消息订阅与发布实现

  • 通过provide/inject实现

注意点

1、组件命名

1、一个单词
第一种写法(首字母小写):school
第二种写法(首字母大写):School (推荐)

2、多个单词
第一种写法(kebab-case命名法):my-school
第二种写法(大驼峰命名法):MySchool (推荐,需要vue脚手架支持)

注意:
1、组件名必须避开html标签名称
2、组件可以使用name配置项指定组件在调试工具上显示的名称

2、组件标签

1、第一写法:<school></school>
2、第二写法(自闭合):<school/> (需要vue脚手架支持)

特殊API

nextTick

用途:回调函数延迟到下次Dom更新之后执行

vm.$nextTick(fn) 与 Vue.nextTick(fn)区别
1、$nextTick是局部的(使用较多),nextTick是全局的(使用较小)
2、$nextTick的回调函数中this指向当前vm/vc,而nextTick的回调函数中this指向window

<div id="app">
    <p ref="title">{{ msg }}</p>
    <button @click="changeMsg">更改</button>
    </div>
		
<script>	
    new Vue({
        el: '#app',
        data: {
            msg: '你好呀'
        },
        methods:{
            changeMsg(){
                this.msg = "我改过自身";
                alert(this.$refs.title.innerHTML)  // '你好呀'

                this.$nextTick(function(){
                    alert(this.$refs.title.innerHTML)  // '我改过自身'
                })
            }
        }
    });

    Vue.nextTick(function(){
        console.log(this);   // window
    })
</script>

其实无论是否有Dom更新,最后都会延迟执行nextTick中的回调函数,所以nextTick实现原理因该是setTimeout

过渡与动画

实现原理:Vue 在插入、更新或者移除 DOM 时,在合适的时机给元素添加样式类名

无论是v-if还是v-show,都可以使用Vue过渡与动画

过渡与动画的区别

动画是过渡的超集,动画更加强大,能实现更加炫酷的效果,而过渡相对动画就简单点,因为过渡只要指定2端的效果就可以了

图示

image.png

总共有6个Class类名

// 元素进入的类名

v-enter:进入的起点
v-enter-active:进入的过程
v-enter-to:进入的终点

// 元素离开的类名

v-leave:进入的起点
v-leave-active:进入的过程
v-leave-to:进入的终点

2个内置组件

transition

使用<transition>包裹要过渡或动画的元素,元素只能有一个,可以配置name属性

transition-group

使用<transition-group>包裹要过渡或动画的元素,元素可以是多个,但每个元素都要指定key

注意事项

1、如果数据对象data是对象时,数据对象data中的属性,如果使用thisthis指代是window

{{title}}  // 'undefined'
{{testThis}}   // '123'

var title = "123";

data: {
    title: 'undefined',
    testThis: this.title  // 这里的this指代window
}

2、所有Vue管理的函数都不能用箭头函数定义,必须用function定义,否则this不是指向Vue实例,而是window,下面是Vue管理的函数

1、methods中定义的函数
2、事件处理函数
3、计算属性中get和set函数(包括简写)
4、watch中的监听函数(包括简写)

3、data、computed中的数据发生变化,只要有用到,Vue将重新渲染模板

4、开发单页应用时,需要修改第三方UI组件(例如:ElementUI组件)或自定义组件的 默认样式 时,何时直接修改,何时需要用到::v-deep深度作用选择器呢?

深度作用选择器有3种
1、>>>
2、/deep/
3、::v-deep (推荐)

当父组件中用到子组件,并且我要在作用域样式(<style scoped lang="scss">)中修改子元素的默认样式时,
子元素的跟元素会有2种属性标识,一种是父组件实例唯一标识,一种是本组件实例唯一标识

如果只需要修改子组件中根元素的样式,直接修改就可以
<style scoped lang="scss">
.parentClass{
    .childClass{}
}
</style>
编译后
<style scoped lang="scss">
.parentClass[data-v-0945424]{}
.parentClass .childClass[data-v-0945424]{}
</style>

如果需要修改子组件中非根元素的样式,那必须使用深度选择器::v-deep
<style scoped lang="scss">
.parentClass{
    ::v-deep .childNotRootClass{}
}
</style>
编译后
<style scoped lang="scss">
.parentClass[data-v-0945424]{}
.parentClass[data-v-0945424] .childNotRootClass{}
</style>

原因:子组件的非根元素只有本组件唯一标识的属性,而没有携带父组件唯一标识的属性,所以要使用深度作用选择器