Vue

文章目录

Vue.js

是一个渐进式框架,意味着你可以把 Vue 作为你应用的一部分嵌入到其中。通俗点举例,比如公司正在用 jQuery,但这时候需要项目重构,你可以从新项目开始使用 Vue,然后再一点一点地替换掉其他的项目,一点点地替换,就是渐进。

基本使用

(注意,操作与指令都必须在 Vue 挂载的元素内),全文代码基本是伪代码

插值表达式 mustache

mustache 翻译过来就是胡子, 为什么 {{}} 叫 mustache, 看这个图 :{ 就懂了吧。

可以获取到 Vue 实例的 data 属性中的 msg 值,只能在标签内使用,如 <div></div>

当 data 中的 msg 值被改变时,对应的 渲染出来的值也会发生改变,即数据是响应式的。

{{}} 内部可以写表达式,如三元表达式,但不能写语句,如 if 、 for 语句

题外: 与 mustache 命名类似的还有 toast(吐司),它经常在各种 UI 库中见到,它表示轻弹窗,就像烤面包时候的烟一样,冒出来一会儿就消失,所以得名 toast。

基本指令

v-once

v-once 元素只在第一次加载的时候去获取 data 中的数据,如 <div v-once></div> 只会在第一次渲染的时候把 data.msg 的值渲染到页面上,之后再修改 data.msg 是不会发生改变的。

v-for

v-for 列表渲染指令,迭代 data 中的值,如

1
2
3
4
5
6
7
8
9
10
<ul>
<li v-for="(item, index) in list" :key="index"></li>
</ul>
<!--
如果 v-for 遍历的是对象
<li v-for="value in obj" :key="key"></li>
<li v-for="(value, key) in obj" :key="key"></li>
注意,对象确实能获取 index, 按它的顺序,但基本不用,可能是无法保持每次的顺序都一样?因为 JS 对象的遍历顺序不一定是自己定义属性的顺序。不知道 Vue 是否有做优化。
<li v-for="(value, key, index) in obj" :key="key"></li>
-->

这个操作会把 data.list 遍历一次,可以得到值和它对应的索引,之后再这个标签内部其他属性都可以访问到这两个变量。使用这个指令之后,一定要记得加上:key="index" key 的值应该是唯一的,不应该是 index,这样可能会使 diff 优化失效,这里只是演示一下,可以绑定数据请求之后返回的 id, 它一般都是唯一且不变的,在整个生命周期中都保持稳定。

有 key 在内存在实际就是链表了,所以插入效率变高,key 的作用主要是为了高效的更新 DOM.

注意,重复的 key 将会使 Vue 出现警告, 可能会错误渲染 DOM。

v-html

v-html 类似于 innerHTML,但是绑定在标签属性上,如

1
2
3
4
5
6
7
8
9
<!-- html -->
<div v-html="html">
</div>
<!-- js -->
// ..
data: {
html: '<p> innerHTML </p>'
}
// ..

v-text

v-text 基本与 mustache 一致,但是它会覆盖标签内的全部内容,如 <div v-text="msg">被覆盖的内容</div> div 标签内只会显示 data.msg 的值

v-pre

v-pre 原封不动地显示标签内的内容,不做任何解析,无视插值表达式,其实与 <pre></pre> 标签有点相似,如 <div v-pre></div> ,div 内将会显示 ,不做任何解析

v-cloak

v-cloak 在 Vue 实例未挂载到页面上时,元素上会有 v-cloak 属性,当 Vue 渲染好了之后这个属性就会被删除。常用于解决请求时间过长而导致的闪现问题,某些情况下,页面会直接显示未编译的 Mustache 标签(如 用户可能看到 ,然后才会变成 data.msg )。实际上只是为元素指定了一个属性名,然后可以在 css 中选中这个元素,在页面渲染完成的时候删除这个属性。 cloak (斗篷)

1
2
3
4
5
6
<!-- css -->
[v-cloak] {
display: none;
}
<!-- html -->
<div v-clock>{{ msg }}</div>

这样设置之后,用户就不会再网络请求速度较慢的情况下,看到 一闪而过的画面了。

v-bind

v-bind 相当于属性的 mustache ,例如,在标签的属性中,我们不能直接写 <img src=""></img>,而是要写成 <img v-bind:src="imgUrl"></img> ,这样就会去 data 里找到 imgUrl 这个变量并将值赋给 img 的 src 属性。同时,如果修改了 imgUrl 的值,也会同步反应到 src 属性上。 相当于在 Vue 实例上做了一个中转。 简写为 :,同样,双引号内能写表达式,如三元表达式,但不能写语句,如 if 、 for 语句。注意,被v-bind 绑定的 "" 内的内容,是一个表达式,里面的不带 '' 的值都不再是字符串了。如 <div :class="[class1, class2]"></div>,class1 和 class2 都是变量,会去 data 中找到之后传入数组,数组中的值都会被传到 class 上。但如果加上 '' 就会变成字符串。如 :class=['class1',class]"

下面是用 v-bind 通过对象的方式动态绑定 class 的例子

1
2
3
4
5
6
7
8
9
10
<!-- html -->
<div class="title" :class="{ active: isActive, line: isLine }"></div>
<!-- :class="{ className: Boolean }" -->
<!-- js -->
// ..
data: {
isActive: true,
isLine: false
}
// ..

根据 data 中 isActive 的布尔值决定是否有 active 的类名, 根据 isLine 的布尔值决定是否有 line 的类名,而原本的 class 不会被覆盖,上面将会显示 <div class="title active"></div>,而此时如果想删除原本的 title,就会比较困难,还是要获取 DOM,所以,以后把不会修改的 class 可以定死,而可能需要修改的 class 用 v-bind 绑定。如果 class 过于复杂,可以放到计算属性中,仍然用 v-bind 绑定。

如果 class 过于复杂,可以放到 computed (推荐) 或 methods 中,仍然用 v-bind 绑定。但这时候注意要写 this,同时注意 computed 不需要加 (),并且不要把名字写的像个函数比如带个 getXXXByXXX,而 methods 需要 ()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- html -->
<div class="title" :class="{ active: isActive, line: isLine }"></div>
<div class="title" :class="getClasses()"></div>
<div class="title" :class="classes2"></div>
<!-- js -->
// ..
data: {
isActive: true,
isLine: false
},
methods: {
getClasses: function () {
return { active: this.isActive, line: this.isLine }
}
},
computed: {
classes2: function () {
return { active: this.isActive, line: this.isLine }
}
}
// ..

为了看得更清楚,再举个简单的例子 v-bind 绑定 style,以对象的形式,以后会经常这样写,当然,如果写的过于复杂,可以放到计算属性中,返回一个对象即可。

1
2
3
4
5
6
7
8
<!-- html -->
<div class="title" :style="{ fontSize: yourSize, width: '500px' }"></div>
<!-- js -->
// ..
data: {
yourSize: 50px
}
// ..

可以看到 font-size 值与 data 中的 yourSize 绑定,注意驼峰命名,而 width 是 500px,注意单引号内的内容才表示字符串,表示写死。如果 500px 不加 '' ,将会被当作变量去解析,然后 data 中找不到 50px 这个变量就会报错,而且对象的属性也不能以数字开头。

初始值的定义

如果 v-bind 一个对象(对象 a)中的对象(对象 b)的某个属性(属性 c),并且对象 a 的初始值只是定义了 {},对象 b 是异步获取的情况下,需要把对象 b 定义一下。同理,如果有一个嵌套数组,也应该定义一下初始值,如 arr[0][0] ,arr[0] 是一个数组,如果没定义,那么它将返回 undefined,undefined[0] 将会报错。如果在 data 中某个值将会被异步赋值成一个函数,并在页面中调用,也需要注意这个问题,应该初始化成一个函数。

其他时候,并不需要定义初始值。注意引用类型的初始值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<!--
这种情况下, 刚进入页面时将会报错,因为 a.b 为 undefined, a.b.c 相当于 undefined['c'],直接报错 TypeError,当然页面还是能正常运行,因为异步获取数值之后,页面会重新渲染,但控制台会飘红,难受。
-->
{{a.b.c}}
<!--
这种情况下,刚进入页面不会报错,因为 a.b 会取得一个空对象 {},所以 a.b.c 将会得到 undefined,不会报错了。
-->
{{anotherA.b.c}}
</div>
</template>
<script>
export default {
data() {
return {
a: {},
anotherA: {
b: {}
}
}
},
created() {
axios.get('url').then(res => { // 假如 res.data 为 { b: { c: 1 } }
this.a = res.data
this.anotherA = res.data
})
}
}
</script>

v-show

v-show 决定元素是否显示在页面上,当值为 false 时,仅仅是将元素行内样式的 display 属性设置为 none,从而控制显示与否。切换频繁时用该指令控制。

v-if

v-if 是条件渲染,按 v-if 绑定的值为 true 或 false 来决定是否渲染在页面中, 当值为 false 时,将不会有该元素存在于 DOM 中,这也是与 v-show 的区别,如果没有出现在 DOM 中,它的 ref 属性是无效的,无法在 $ref 中找到元素。只有少数切换时用该指令控制。

<div v-if="isShow"></div> data: { isShow: true }

由于 v-if 渲染的时候也会经过 diff 算法,所以可能 v-if 前后同位置的元素可能是同一个,而不是替换了新元素。例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<span v-if="isUser">
<label for="username">用户账号</label>
<input type="text" id="username" placeholder="用户账号">
</span>
<span v-else>
<label for="email">用户邮箱</label>
<input type="text" id="email" placeholder="用户邮箱">
</span>

<!--
页面中有两个 span 标签, 用 v-if 控制它们的显示,当在 input 框中输入内容后,改变 isUser 的布尔值, 将会显示另一个 input 框, 但它框内有值,而且是切换之前输入的值。
当切换 isUser 的值时, 将会显示不用的 span 标签内容, 而 span 标签中 都是 label input 两个标签, input 的 type 也都是 text, 这时候 diff 算法分析两组元素都没有 key 值, 之后比较位置发现都是同一位置同一结构,就会将原 input 的属性改变, 而不是替换一个新的 input, 也就是说, 在DOM 上, 它只是改变了 input 的 id 属性 和 placehloder 属性, 而不是 document.createElement('input') 创建了新的 input 标签
如果我们不希望 Vue 出现这种重复利用的问题, 给 input 绑定唯一的 key 值就可以解决
-->

<script>
// ...
data() {
return {
isUser: true
}
}
</script>
<!--
题外:
<button :disable="isDisable" \> button 标签有 disable 属性, 可以为 true 或 false 来控制是否可以点击

<input type="text" @input="valueChanged" /> input 标签有 input 事件,当输入框的内容发生改变时触发

// v-model 它是 v-on 和 v-bind 的语法糖,双向绑定实现 注意 value 要用 $event.target.value 取
<input type="text" @input="message = $event.target.value" :value="message">
-->

v-model

v-model 实现表单元素的数据双向绑定。例举比较少用但可能用到的, text、password 这些常见的不再举例。

本质是用 v-bind:value + v-on:input 实现

radio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 正常原生写法,两个 radio 框的 name 需要相同才能让这两个 radio 只能选中一个,否则两个框都可以点 -->
<input type="radio" value="男" name="sex">
</br>
<input type="radio" value="女" name="sex">


<!-- v-model 写法,如果 v-model 绑定同一个值, 可以省去 name, 点击单选框的时候,自动把 value 的值赋值给 sex -->
<input type="radio" value="男" v-model="sex">
</br>
<input type="radio" value="女" v-model="sex">
<!--
相当于 <input type="radio" value="男" name="sex" :checked="sex === '男'" @input="sex = $event.target.value">
<input type="radio" value="女" name="sex" :checked="sex === '女'" @input="sex = $event.target.value">
-->
<script>
// ...
data() {
return {
sex: '男' // 定义默认值,选择男
}
}
</script>
checkbox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<input type="checkbox" value="多选框1" v-model="CB1">
{{CB1}}
<script>
// ...
data() {
return {
CB1: '' // 如果绑定的值不是数组, v-model 会根据这个 checkbox 的选中与否 将这个值赋值为布尔值,如选中后将会变成 CB1: true, 不选中时 CB1: false
// , CB1: [], // 如果绑定的是数组, v-model 会根据这个 checkbox 的选中与否 往这个数组中传递 checkbox 的 value 值, 选中后 CB1: ['多选框1'](如果该 checkbox 上没有 value 值,会变成 CB1: [null]), 不选中时 CB1: []
}
}
</script>

<input type="checkbox" value="多选框2" v-model="CB2">
</br>
<input type="checkbox" value="多选框3" v-model="CB2">
{{CB2}}

<script>
// ...
data() {
return {
CB2: '' // 如果绑定的值不是数组, v-model 会根据 checkbox 的选中与否 将这个值赋值为布尔值,如选中后将会变成 CB2: true, 不选中时 CB2: false, 所以如果是两个 checkbox 同时绑定了这个值,那么它们就会被同时选中或同时不被选中,因为绑定的是同一个布尔值
// , CB2: [], // 如果绑定的是数组, v-model 会根据 checkbox 的选中与否 往这个数组中传递 checkbox 的 value 值, 如第一个 checkbox 选中后 CB2: ['多选框2'], 不选中时 CB2: []
/* 如果选中第一个 checkbox 后再选择第二个 checkbox, CB2: ['多选框2', '多选框3'](里面的项的顺序与点击顺序一致),注意,如果它们都没有 value 值,就会传入 [null],而且由于两个 checkbox 的值都是 null,将会只有 [null] 和 [] 两种状态,也就是说,它们也会变成只能同时选中或同时不选中。所以多个选择框的时候一定要带 value.
checkbox 没 value 时,默认 value 为 null
*/
}
}
</script>

<!--
总结,单选框绑定的值不要是数组,一般绑定一个值为 false 的变量
多选框要求绑定数组,一般绑定一个值为 [] 的变量


正式使用写法, value 也是从 data 中取值。建立两个数组,一个存放可选值,一个存放选中值
label 的 :for 和 input 的 :id 这两者的值应该保持一致才能让 label 生效,即点击文字也能选中选择框

<label v-for="item in originList" :for="item">
<input type="checkbox" :value="item" :id="item" v-model="selectedList">{{item}}
</label>
-->
select
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!-- select 也分成单选和多选的情况 -->
<!-- 单选, 即不带 multiple 属性时 -->
<select v-model="fruit">
<option value="苹果"></option>
<option value="香蕉"></option>
<option value="西瓜"></option>
</select>
<script>
// ...
data() {
return {
fruit: '' // select 不带 multiple 属性时, 不管这个值定义成数组还是对象还是布尔值什么的,这个值都会变成字符串,变成被选中项的 value 值,如选中第一项,就会变成 fruit: '苹果'
}
}
</script>


<!-- 多选, 即带 multiple 属性时 -->
<select v-model="fruits" multiple>
<option value="苹果"></option>
<option value="香蕉"></option>
<option value="西瓜"></option>
</select>
<script>
// ...
data() {
return {
fruits: '' // select 带 multiple 属性时, 不管这个值定义成字符串还是数组还是对象还是布尔值什么的,这个值都会变成数组,数组中存有被选中项的 value 值,如选中第一项,就会变成 fruits: ['苹果'],此时用 Ctrl + 左键 选择第二项,就会变成 fruits: ['苹果', '香蕉'],即使先选中第二项再选中第一项,数组中的顺序也是这样。这一点和 checkbox 不一样
}
}
</script>
v-model 的拆解

v-model 实际是一个语法糖,可以拆解为 props: value 和 events: input。也就是说,只要某个组件能提供一个名为 value 的 prop,以及名为 input 的自定义事件,就能在该组件上使用 v-model。

vue2.2 之后,可以定制 v-model 指令的 prop 和 event 名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<template>
<div>
<cp v-model="value"></cp>
<button @click="changePVal">点击只改变父组件的 value</button>
</div>
</template>

<script>
export default {
data() {
return {
value: 321
}
},
methods: {
changePVal() {
this.value++
}
}
}
</script>

<template>
<div>
from parent: {{value}} <br/>
current: {{currentValue}} <br/>
<button @click="changeVal">点击两个都加1</button>
</div>
</template>

<script>
export default {
props: {
// 定义 value
value: Number
},
data() {
return {
// 这里写两个 value,只是想说明 data 里的值可以这样写,this.xxx 获取 props 的值
currentValue: this.value
}
},
methods: {
changeVal() {
this.currentValue++
// 定义 input 事件
this.$emit("input", this.currentValue)
}
}
}
</script>
v-model 修饰符

lazy 由于默认情况下每次修改 input 的值,data 中的数据都会同步改变,改变频率非常高,我们希望只在 input 失焦或按下回车的时候才修改 data 中的值,就可以加上 lazy 修饰符,写法 <input type="text" v-model.lazy="lazyData">

number 由于 v-model 默认赋给 data 中绑定的数据的值为 String 类型,通过 number 修饰符,可以变为数字 Number 类型。实际上是判断输入框的值第一个字符是否为数值,如果是数值,就调用 parseFloat(val) 方法,把这个值传入,data 中数据为这个方法的返回值,如果第一个字符不为数值,则无事发生,返回正常的字符串,所以不能保证为 Number 类型,除非写在 type="number" 的 input 中,因为这种类型的 input 只允许输入数字,但它也会把绑定的值赋值为 String 类型。

text input 写法 <input type="text" v-model.number="numberData">

number input 写法 <input type="number" v-model.number="numberData">

trim 去掉要传入到 data 中的值首尾的空格

v-on

v-on 相当于事件绑定,可以绑定已经设定好的事件或者自定义事件,如 <button v-on:click="btnClick"></button> 简写为 @

当通过 methods 中定义方法,以供 @click 调用时,需要注意参数问题:

  • 情况一,如果该方法不需要额外参数,那么方法之后的 () 可以不添加。但是如果方法本身中有参数,这种情况下会默认将原生事件 event 对象当作参数传递过去 @click=clickHandle

    methods:{ clickHandle(e){ /* 方法需要参数,但 @ 绑定的事件回调函数不带括号时,默认第一个参数接收到 event 对象 ... */ } }

  • 情况二,如果需要传入某个参数,同时需要 event 时,可以通过 $event 传入事件对象。@click="clickHandle(val, $event)" methods:{ clickHandle(val, e){ /* ... */ } }

事件修饰符

stop 阻止冒泡 @click.stop="clickHandle" 相当于调用了 event.stopPropagation()

prevent 阻止默认事件,如 a 标签的默认点击跳转,相当于调用了 event.preventDefault().

1
2
3
4
5
6
7
<form action="baidu">
<input type="submit" value="提交"></input>
</form>
<!-- 不添加 event.preventDefault() 点击这个 input, 会默认在地址栏之后加上 /baidu?, 自动提交,发送一次请求,Vue 中改写成以下形式 -->
<input type="submit" value="提交" @click.prevent="submitHandle">
<!-- 也可以后面不跟回调函数,单纯组织默认事件 -->
<form @submit.prevent></form>

enter 当按下 enter 键时才触发事件 @keyup.enter="enterHandle" , 还有一种是写成键代码的形式@keyup.13="enterHandle"

native 监听组件的点击事件 <cpn @click.native="cpnClick"></cpn><cpn> 是自定义的组件,直接写 @click 是无法触发组件的点击事件的

once 只触发一次事件之后就不触发了 @click.once

串联修饰符: 修饰符可以串起来用,如 @click.stop.prevent="clickHandle"

自定义指令

Vue.directiveVue2.0 中,代码复用和抽象的主要形式是组件。有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

基本写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 全局注册自定义指令 v-focus
// 如果传入的第二个参数是函数而不是对象,将会默认把 bind 和 update 属性定义为该函数
Vue.directive('focus', {
// 当被绑定元素插入到父节点中,仅保证父节点存在,但不一定已被插入到文档中
inserted: function(el) {
// 原生聚焦方法
el.focus()
}
})

// 局部注册
export default {
directives: {
'focus': {
inserted(el) {
el.focus()
}
}
}
}

// 好了,仔细看一下
let directObj = {}
// 参数项,除了 el 外, 其他属性都应该是只读的,不要进行修改
let args = {
el: '指定绑定的元素,可以用来操作 DOM',
binding: { // 一个对象,包含以下属性
name: '指令名',
value: '指令的绑定值,如 v-mydirective="1+1", 绑定值为2',
oldValue: '指令绑定的前一个值,仅在 update 和componentUpdated 钩子可用',
expression: '字符串形式的指令表达式。如 v-mydirective="1+1", 表达式为 "1+1"',
arg: '传给指令的参数,例如 v-mydirective:foo 中, 参数为 "foo"',
modifiers: '一个包含修饰符的对象,如 v-mydirective:foo.bar 中,值为{foo: true, bar: true}'
},
vnode: '虚拟节点',
oldVnode: '上一个虚拟节点'
}
// 只调用一次,指定第一次绑定到元素时调用,在这里可以进行一些初始化的东西
directObj.bind = function ({...args}) {}
// 当被绑定元素插入父节点时调用,仅保证父节点存在,但不一定已插入到文档中
directObj.inserted = function ({...args}) {}
// 所在组件的 vnode 更新时调用,但是可能发生在其子 vnode 更新前,指令的值可能发生了改变,也可能没有,但你可以比较更新前后的值来忽略不必要的模板更新
directObj.update = function ({...args}) {}
// 指令所在组件的 vnode 及其子组件的 vnode 全部更新完成后调用
directObj.componentUpdate = function ({...args}) {}
// 只调用一次,指令与元素解绑时调用
directObj.unbind = function ({...args}) {}

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="root">
<input v-focus>
</div>

<script>
const vm = new Vue({
directives: {
focus: {
// 插入到 DOM 中的狗子
inserted(el) {
// 自动聚焦
el.focus()
}
}
}
})
</script>

可以利用它来实现图片懒加载指令,v-lazyload,具体代码不展示了。

计算属性

computed 对象中的属性,都要写成函数形式,并且有一个返回值。而在页面中用 mustache 时,不需要像 methods 中定义的方法那样加上 () ,就当作 data 中的值来用即可。计算属性与方法的区别是计算属性是有缓存的,而方法每次都需要重新执行。

它的本质是 set 和 get,如 fullName: { set(), get() } ,只是我们一般不定义 set(),所以默认指定 get()

下面是个简单的例子,写出了 computed 内属性的简写形式与完整形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<!-- html -->
<div>{{ fullName }}</div>
<!-- js -->
// ..
data: {
firstName: 'Sansan',
lastName: 'Zhang'
},
computed: {
// 简写
fullName: function () {
return this.firstName + ' ' + this.lastName
}
// 完整
fullName: {
set: function (newValue) {
// set 会传入新修改的值
/* 我们一般希望计算属性没有 set 方法,让它成为一个只读属性,因此都不写 set, 之后发现每次只写一个 get, 外面也还要包一层对象太麻烦了,直接把 get 和外面包的那层对象也省略了,所以最后写出来就是上面的简写形式 */
},
// 每次获取 fullName 这个属性的时候,都会自动执行 fullName.get(),所以计算属性不加 (),而它本身是一个对象的属性,所以会有缓存。这是原生 JS 对象部分的内容。
get: function () {
return this.firstName + ' ' + this.lastName
}
}
}
// ..
/* 补充,Vue 源码片段,可以看出,其实没有什么完整版不完整的写法,只是它设定了根据 computed 内的属性类型是函数还是对象来决定怎么获取这个属性,大家这样叫而已。
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
// ...
}
同样地,贴上 Vue 源码片段,可以看到它把计算属性保存在了 target 的 get 属性描述符上。
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
// ...
Object.defineProperty(target, key, sharedPropertyDefinition);
*/

其实计算属性也能实现类似 methods 的效果,它可以返回一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
{{numPower(3)}} <!-- 最终显示 9 -->
</div>

<script>
new Vue({
computed: {
numPower() {
return function(num) {
return num * num
}
}
}
}).$mount('#app')
</script>

过滤器

filter Vue3.0 移除了,鸽了。

留坑

watch

watch 可以

响应式

Vue 内部重写了部分数组方法,将会监听数组数据的变化。从而达到响应式的目的。只有通过这些方法修改数组,数据才会是响应式的。 注意:直接修改对象的属性是响应式的,而直接修改数组的属性不是。准确地说,直接修改已在 data 中声明的对象属性是响应式的,而直接修改数组的属性或 data 中未声明的对象属性不是响应式的。

直接通过索引,如 this.arr[o] = 'no render' 这样通过索引值来修改数组中的元素,页面绑定的该值(arr[o])是不会发生改变的。如果想要响应式修改数据,可以这样写

1
2
3
4
5
6
7
8
9
10
this.arr.splice(0, 1, 'render')
// 或者 Vue 中提供 set 方法
// Vue.set(obj, key, newVal), 这是 Vue 官方提供的,与我们直接修改的区别就是,它是响应式的,因为通过这种方式添加或修改的数据上面通知 Observer 对象,再通知它的 Dep 对象(用于监听数组、对象这种复杂对象),这个对象会检测值是否发生改变,如果发生改变,查找一个数组(Dep 上的 subs 属性),这个数组的内容是页面中所有用到这个值的地方,类似这样 [Watcher, Watcher, ... , Watcher],里面是一个个 Watcher 对象,一旦 Dep 监听的属性发生了变化,就会通知这些 Watcher,告诉它们这些地方要重新渲染了。
Vue.set(this.arr, 0, 'render') // 数组中键名 key 为它们的索引

Vue.set(this.obj, 'num', 0) // 给 obj 添加一个 num 属性,它的值为 0.

// 删除
Vue.delete(this.arr, 0)
Vue.delete(this.obj, 'num')
  • push() 将元素添加到最后 可以 push 多个元素,如 this.arr.push('aa', 'bb', 'cc')
  • pop() 删除最后一个元素
  • shift() 删除第一个
  • unshift() 将元素添加到最前面 同样可以 unshift 多个元素,如 this.arr.unshift('aa', 'bb', 'cc'),源码 lib.es5.d.ts (d 就是 define 定义的意思) 文件中是这样定义的 unshift(...items: T[]):number 看到参数是 ...,所以可以传多个参数。 push 同理。 T[] 是 ts 语法,泛型,先不用管这个。
  • splice(start, num, ele, ele, ..., ele) 在指定位置开始,替换指定个数元素,变成新传入的元素。写成 splice(start) 将会 删除索引为 start 之后的所有元素.
  • sort()
  • reverse()

Vue.set(obj, key, newVal)

Vue.set(obj, key, newVal), 这是 Vue 官方提供的,与我们直接修改的区别就是,它是响应式的,因为通过这种方式添加或修改的数据上面会增加一个 Dep 对象,这个对象会检测值是否发生改变,如果发生改变,查找一个数组,这个数组的内容是页面中所有用到这个值的地方,类似这样 [Watcher, Watcher, … , Watcher],里面是一个个 Watcher 对象,一旦 Dep 监听的属性发生了变化,就会通知这些 Watcher,告诉它们要重新渲染了。

Vue.delete(obj, key)

Vue.delete(obj, key),同样也是 Vue 官方提供的,与我们直接使用 delete 删除对象的方法(如 delete this.obj.num)的区别是,它是响应式的,如果我们直接 delete,页面中通过这个属性被渲染地方仍然存在,不会因为这个属性消失了而消失。

混入

在生命周期钩子中调用时,如果有同名方法,优先执行混入对象的方法再调用它自己的方法。

合并策略

如果在 methods 中有和混入对象同名的方法,默认执行组件自己的方法,而不是混入对象的方法。

可以对 Vue.config.optionMergeStrategies 对象进行配置自定义选项合并策略。详情见官方文档。

1
2
3
Vue.config.optionMergeStrategies.myOptions = function (toVal, fromVal) {
// return mergedVal
}

使用

慎用全局混入方法 Vue.mixin ,它将会使所有新建的 Vue 实例对象上都有 mixin 的内容,包括第三方模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// common/mixin.js
export const itemMixin = {
data() {
return {
msg: 'mixin'
}
},
components: {},
methods: {},
computed: {},
mounted: {
console.log(this.msg)
}
}

export const itemMixin2 = {
created() {
console.log('mixin2')
}
}

// 组件中使用
import { itemMixin, itemMixin2 } from 'common/mixin.js'

export default {
data() {
return {}
},
created() {},
mixins: [itemMixin, itemMixin2] // 局部使用,接收一个数组,会把数组中的每项都混入到当前组件中
}

// 全局使用
Vue.mixin(itemMixin)
Vue.mixin(itemMixin2)

组件

组件化是 Vue.js 中的重要思想,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。当我们开始新项目的时候,可以直接使用之前封装好的组件来快速开发。任何的应用都会被抽象成一颗组件树。

组件使用步骤

  • 创建组件构造器 Vue.extend() 传入一个对象,里面有 template 这个属性,包含了 html 模板字符串。 但这种方式已经非常少见了,一般使用语法糖(主要是省去了 Vue.extend() 这一步,由 Vue 内部来执行)
  • 注册全局组件 Vue.component() 传入两个参数,组件名 和 组件构造器对象,如果要局部注册,则在 Vue 实例对象中的 components 属性 以 '组件名': 组件构造器对象 形式传入要注册的组件。注意,组件名应该加上引号!否则将被识别为一个变量。当然,局部注册的时候,可以不加,因为是对象的属性名,但如果这个属性名里带有特殊字符如 - 这种,也必须加引号。
  • 使用组件 在 Vue 实例的作用范围内使用组件

基本使用

简单举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<!-- 假如我们的页面中有这样几个重复的部分 -->
<div id="app">
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>

<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>

<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
</div>

<!-- 使用组件。 这是一种思想! -->

<!--
伪代码
<cpn>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</cpn>
-->

<div id="app">
<!-- 使用组件 -->
<my-cpn></my-cpn>
<!-- 以下写法均有效,效果同上 -->
<my-cpN></My-cpn>
<mY-cpn></my-cpn>
<my-Cpn></my-cpn>
<!-- 以下为错误写法 -->
<myCpn></myCpn>
</div>

<script>
// 创建组件构造器对象
const cpnConstructor = Vue.extend({
// 写 template 的时候记得外面必须包一层,如这里包的是 <div></div>
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
})

// 注册组件, 注意:注册组件名为 my-cpn 和 myCpn 在 html 中都只能使用 <my-cpn> 来使用这个组件,因为 Vue 会自动把 html 标签转为小写,如 C 变成 c. 之后再根据是否有连字符 -, 来决定是否转为大写,还会把组件名首字母大写,如 <MY-cpn> --> <my-cpn> --> <myCpn> --> <MyCpn>
// 因此 <MYCPN><mycpn><myCpn> 被 vue 解析出来的都是 <Mycpn>. 不符合 <my-cpn> 所以不能用,只能用 <my-cpn></my-cpn> 这样的形式,不区分大小写,所以这样写也有效 <MY-cpn></my-CPN>
// 而当组件名写成 'my-Cpn'时(虽然一般人也不可能这样写,但举个例子),html 就得写成 <my--cpn> 大小写随意, 反正记得在 Vue 中 html 标签不区分大小写
Vue.component('my-cpn', cpnConstructor)

const vm = new Vue({
el: '#app'
})


/*
局部注册
const vm = new Vue({
el: '#app',
components: {
'my-cpn': cpnConstructor
}
})
*/
</script>
把上面的例子用父子组件来改写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<div id="app">
<cp></cp>
</div>

<script>
// 定义子组件
const childCp = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
})

// 定义父组件
const parentCp = Vue.extend({
template: `
<div>
<cp></cp>
<cp></cp>
<cp></cp>
</div>
`,
components: {
'cp': childCp // 在父组件中注册子组件并取名为 cp, 这里特地和父组件中的组件重名,来说明这样注册是局部注册,这个 cp 只会作用在父组件自身的 template 中,并且是 childCp 的 template 内容,全局中并没有这个 cp 组件
}
})

const vm = new Vue({
el: '#app',
components: {
'cp': parentCp // 在 Vue 实例中注册父组件并取名为 cp, 这里特地和父组件中的组件重名,来说明这样上面父组件的注册是局部注册,这个 cp 只会作用在 Vue 实例范围内,并且是 parentCp 的 template 内容,而且传进来的 parentCp 的 template 内容中,已经将 parentCp 中注册的组件 cp 编译为了 childCp 中的 template 内容。
}
})
</script>
语法糖形式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!-- 古老写法 全局注册 -->
<script>
// 定义组件
const cp = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
})

// 注册组件
Vue.component('cp', cp)

</script>

<!-- 语法糖形式 全局注册 -->
<script>
Vue.component('cp', {
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
})
// 可以看到,语法糖形式只是把 Vue.extend 给省去了,Vue.component 中的第二个参数直接传入一个对象,而不是一个组件构造器对象。 实际上,Vue 内部仍然会使用 Vue.extend(),对第二个参数进行转化,变成组件构造器对象,再进行 Vue.component, 只是 Vue 在内部帮我们做了这一步,所以是语法糖。
</script>

<!-- 古老写法,局部注册 -->
<script>
const cp = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
})
const vm = new Vue({
el: '#app',
component: {
'cp': cp
}
})
</script>

<!-- 语法糖形式,局部注册,由于现在不是用 vue-cli 开发,所以看上去这就像全局组件,但如果页面里你写上两个 Vue 实例,那就不一样了。 -->
<script>
const vm = new Vue({
el: '#app',
components: {
'cp': {
template: `
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
`
}
}
})
</script>
抽离 template 的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- html 的抽离写法 -->
<template id="cp">
<!-- 同样外面也要包着一层,不能把这 div 去了,但可以换成别的,不过一般都是 div -->
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
</template>
<!-- script 标签的写法, type="text/x-template" 会保持里面的内容,不会被浏览器解析,也不会被执行,不写 type 默认 type="text/javascript" -->
<script type="text/x-template" id="cp">
<div>
<h2>我是标题</h2>
<p>我是内容一</p>
<p>我是内容二</p>
</div>
</script>

<script>
Vue.component('cp', {
template: '#cp'
})
</script>


<!-- 局部组件可能这样写 -->
<script>
const cp = {
template: '#cp'
}
const vm = new Vue({
el: '#app',
components: {
cp // ES6 对象的增强写法,如果有 键名与键值同名,可以直接这样写。
}
})
</script>>

每个组件可以有自己的 data 和 methods, 而且这个 data 必须写成函数的形式,原因见下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<div id="app">
<cp></cp>
<cp></cp>
<cp></cp>
</div>

<template id="cp">
<div>
{{ count }}
<button @click="increment">
点击 count 加一
</button>
</div>
</template>
<script>
// ...
Vue.component('cp', {
template: '#cp',
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
})
/*
此时我们是以函数的形式定义了组件实例的 data(当然,不这样写就会报错,不过这里想说的不是这个)。
假设一下,如果我们在这里写
data: {
count: 0
}
并且假设它是合法的,那么,初始化完成时上面的三个 cp 组件应该都显示 0,此时点击任何一个按钮,将会发现,三个 cp 组件中的 count 都发生了改变,原因在于它们共用了同一个对象。它们获取到的 data 对象的地址相同,每次创建组件实例的时候,都使用了同一个对象(每次都会去获取这个 data,为什么能每次都获取同一个 data 呢,因为它有名字叫做 data,所以就能通过名字访问它的地址。)。
而当我们修改为函数返回形式时,每个组件的 data 对象都将成为它们自己独有的 data 对象,此时点击按钮,只会把它们自己所对应的 count 值改变。

用代码来模拟,写成对象形式就相当于
const obj = { count: 0 }
function badData() {
return obj
}

写成函数形式就是
function goodData() {
return {
count: 0
}
}
函数形式相当于
function goodData() {
const obj = new Object()
obj.count = 0
return obj
}

const a = badData()
const b = badData()
a === b // true

const c = goodData()
const d = goodData()
c == d // false
*/
</script>

父子组件传值(组件通信)

父传子

使用场景:页面加载进来时,可能会有很多请求,一般只在最外层组件中进行一次请求,之后把数据下发到各个子组件中,子组件再下发到更下层的组件,而不是每个子组件独立请求,这样服务器压力较大,而且性能也不好。

子组件通过 props 接收来自父组件传递的值,父组件用 v-bind:propname="ParentData" 来向子组件传递值,由于要用 v-bind 来绑定 props 中的变量名,而 v-bind 不支持驼峰,会默认转为小写,所以 props 中的变量名不要大写,如 props: ['aBc'] 之后,html 中写成 <cp :aBc="pData">,由于 html 会转小写,就变成了 :abc 因 此 aBc 无法接收到来自父组件的值,但如果写成 <cp :a-bc="pData"> 这样是可以的,这时候使用这个值还是写 ,而不是NaN,这是无法识别的,它会将a 和 bc 当成两个不同的变量,中间用连字符隔开。所以 props 中的变量名就写小写。但是父组件传递来的值可以是大写的,如果还带有连字符,那么只能用 this 来获取了。如 <cp :abc="this['pDa-ta']">

主要用到 props 属性接收来自父组件的值,它是单词 properties 的简写,推荐写成对象形式而不是数组形式,props 如果验证不通过(type、validator), Vue 会报错,但不会中止运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<div id="app">
<cp1 :receivedata1="transmitData1" :receivedata2="transmitData2"></cp1>
<cp2
:data1="pData1"
:data2="pData2"
:data3="pData3"
:data4="pData4"
:data5="pData5"
:data6="pData6"
:data7="pData7"
:data8="pData8"
>
</cp2>
</div>

<template id="cp1">
<div>
{{ receivedata1 }}--{{ receivedata1 }}
</div>
</template>

<template id="cp2">
<div>
{{ data1 }}--{{ data2 }}--{{ data3 }}--{{ data4 }}</br>
{{ data5 }}--{{ data6 }}--{{ data7 }}--{{ data8 }}
</div>
</template>

<script>
const cp1 = {
template: '#cp1',
props: ['receivedata1', 'receivedata2'] // 注意这里用来接收的属性名是字符串格式
}

const cp2 = {
template: '#cp2',
props: { // 这里的 String Number Object Array 都是 JS 原生对象!不是字符串,不需要引号
data1: String, // 表示接受 String 类型
data2: Number, // 表示接受 Number 类型
data3: [String, Number], // 表示接受 String 和 Number 类型
data4: {
type: String,
default: '111', // 设定当父组件不向子组件传入 data4 这个值时的默认值
required: true // 设定父组件是否必须向子组件传入 data4 这个值
},
data5: {
type: Object, // 表示接受 Object 类型
// 设置的默认值为引用类型时,必须这样写成函数返回值形式,理由和上面组件里的 data 为什么要写成函数形式一样,防止多个属性使用同一个变量
default() {
return { message: 'hello' }
}
},
data6: {
type: Array, // 表示接受 Array 类型
// 设置的默认值为引用类型时,必须这样写成函数返回值形式,理由和上面组件里的 data 为什么要写成函数形式一样,防止多个属性使用同一个变量
default() {
return [5, 6, 7]
}
},
data7: { // validator 定义一个函数,可以验证传入的值是否合法,返回一个布尔值。
validator: function (value) {
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
},
data8: Person // 表示接受 Person 对象的实例,就和上面值那些接受 String Number Array Object 的实例对象意思一致
}
}

function Person(name) {
this.name = name
}

const vm = new Vue({
el: '#app',
data: {
transmitData1: 'MyData1',
transmitData2: 'MyData2',
pData1: 'str',
pData2: 1,
pData3: 3,
pData4: 'str4',
pData5: {
message: 'hi'
},
pData6: [1, 2, 3],
pData7: 'success',
pData8: new Person('fullName')
},
components: {
cp1, cp2
}
})
</script>
子传父

子组件使用 $emit('eventName', val) 来向父组件传值,父组件用 @eventName="eventHandle" 来接受子组件的值,捕获到子组件传值事件 eventName,并定义处理的回调函数 eventHandle 。 eventName 是自定义事件名。自定义事件名在非 vue-cli 的项目中不能写成大写,回调函数允许驼峰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<div id="app">
<cp @itemclick="cpnClick"></cp> <!-- 捕获到子组件传值事件,并定义处理的回调函数,默认传递进来的参数是子组件传递来的 val,而不是 $event。 -->
</div>

<template id="cp1">
<div>
<button @click="btnClick('Cpn Click')">Click Here</button>
</div>
</template>

<script>
const cp = {
template: '#cp1',
methods: {
btnClick(val) {
this.$emit('itemclick', val) // 定义自定义事件名,供父组件捕获
}
}
}

const vm = new Vue({
el: '#app',
data: {},
methods: {
cpnClick(item) { // 默认接收到来自子组件的值,而不是 event 对象
console.log(item)
}
},
components: {
cp
}
})
// 点击 btn 将打印 'Cpn Click'
</script>
综合小例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
<div id="app">
<cp2 :c-num-temp1="num1" :c-num-temp2="num2"></cp2>
</div>

<template id="cp2">
<div>
<!-- 此时,修改 input 内容,可以控制 cum 的值,不会影响到父组件的值,如果需要修改,见下 -->
<!-- 注意,如果直接 v-model cNumTemp,Vue 会报错,要求避免这种写法,这样写 cNmuTemp 的值会被子组件和父组件中各自的一个值绑定,它的值就会受到两个变量的控制,不好确定来源,但也是可以运行的,将会同步改变 cNumTemp,不会改变父组件的 num,错误中写到:需要一个变量来接收这个值之后再 v-model 这个变量,就是这里的写法,用 data 里的变量接收一下 -->
{{ cNumTemp1 }}
{{ cnum1 }}
<input type="text" v-model="cnum1"> </br>
{{ cNumTemp2 }}
{{ cnum2 }}
<input type="text" v-model="cnum2">
</div>
</template>

<script>
const vm = new Vue({
el: '#app',
data: {
num1: 88,
num2: 99
},
methods: {

},
components: {
cp2: {
template: '#cp2',
props: {
cNumTemp1: Number, // 接收来自父组件的值
cNumTemp2: Number // 接收来自父组件的值
},
data() {
return {
cnum1: this.cNumTemp1, // 把父组件的值变成自己的变量,这样使用 v-model 不会污染父组件的值。
cnum2: this.cNumTemp2 // 把父组件的值变成自己的变量,这样使用 v-model 不会污染父组件的值。
}
},
methods: {
}
}
}
})
</script>


<!-- 修改父组件的值,方法一: 拆分 v-model -->

<div id="app">
{{num1}}
{{num2}}
<cp2 :c-num-temp1="num1" :c-num-temp2="num2" @cnum1-changed="cnum1Changed" @cnum2-changed="cnum2Changed"></cp2>
</div>

<template id="cp2">
<div>
<!-- 为了在修改 input 的值时修改父组件的值,此时不再能写 v-model,需要把它拆分,在自定义 input 事件中进行 $emit (发射) -->
{{ cNumTemp1 }}
{{ cnum1 }}
<input type="text" :value="cnum1" @input="cnum1Changed"> </br>
{{ cNumTemp2 }}
{{ cnum2 }}
<input type="text" :value="cnum2" @input="cnum2Changed">
</div>
</template>

<script>
const vm = new Vue({
el: '#app',
data: {
num1: 88,
num2: 99
},
methods: {
cnum1Changed(val) {
this.num1 = val
},
cnum2Changed(val) {
this.num2 = val
}
},
components: {
cp2: {
template: '#cp2',
props: {
cNumTemp1: Number,
cNumTemp2: Number
},
data() {
return {
cnum1: this.cNumTemp1,
cnum2: this.cNumTemp2
}
},
methods: {
cnum1Changed(e) {
this.cnum1 = e.target.value // 先修改 cum 的值,完成 v-model 做的事
this.$emit('cnum1-changed', +this.cnum1) // 添加自己想要做的事,这里是传递修改事件给父组件,这里有个 + 号,是因为这时候传递到父组件的值是字符串,为了把它修改为数值而写的忍者代码。如果不修改为数值,这样发射到父组件,父组件修改完自己的 num 后,将会把 num 传递到子组件的 cNumTemp 中,而cNumTemp 的验证规则是 Number, 所以会出现报错。
},
cnum2Changed(e) {
this.cnum2 = e.target.value
this.$emit('cnum2-changed', this.cnum2) // 这里特地没加 + 号,所以会出现报错,但能运行。
}
}
}
}
})
</script>



<!-- 修改父组件的值,方法二: watch 监听 -->

<div id="app">
{{num1}}
{{num2}}
<cp2 :c-num-temp1="num1" :c-num-temp2="num2" @cnum1-changed="cnum1Changed" @cnum2-changed="cnum2Changed"></cp2>
</div>

<template id="cp2">
<div>
<!-- 为了在修改 input 的值时修改父组件的值,此时不再能写 v-model,需要把它拆分,在自定义 input 事件中进行 $emit (发射) -->
{{ cNumTemp1 }}
{{ cnum1 }}
<input type="text" v-model="cnum1"> </br>
{{ cNumTemp2 }}
{{ cnum2 }}
<input type="text" v-model="cnum2">
</div>
</template>

<script>
// ...
components: {
cp2: {
template: '#cp2',
props: {
cNumTemp1: Number,
cNumTemp2: Number
},
data() {
return {
cnum1: this.cNumTemp1,
cnum2: this.cNumTemp2
}
},
watch: {
cnum1(newVal, oldVal) { // 写了参数不用是为了说明传进来的是啥
this.$emit('cnum1-changed', +this.cnum1)
},
cnum2(newVal, oldVal) { // 写了参数不用是为了说明传进来的是啥
this.$emit('cnum1-changed', newVal)
}
}
}
</script>

父子组件的访问方式(组件访问)$refs

$parent 子组件可以用 $parent 来获取父组件的引用,返回父组件的实例对象,可能是 Vue 实例, 也可能是 VueComponent 实例。由于可能同一个子组件会被注册在不同的父组件下,所以获取的 $parent 也会不一致,可能会出现一些问题,所以不太方便。比如在该组件的某个方法中,用到了 $parent.name,可能这个组件的父组件有 name 属性,另一个组件的父组件没有这个属性,就出问题了。

$children 父组件可以通过 $children 来取得包含了所有子组件的引用的数组,由于每次获取的是数组,所以需要通过子组件在数组中的索引来取得子组件,而且索引又不太好确定,所以这个方法不太方便。

以上两种获取引用方式都不太常用。一般使用 $refs

$refs 给组件加上 ref="refName" 属性,之后通过 $refs.refName 来及获取指定的组件对象,这是最常用的方法。注意,不要用 $refs 做数据绑定和计算属性,因为 ref 本身是作为渲染结果拿到的,初始渲染时不能访问。此时再通过 $el 可以拿到组件的 DOM ,写法 $refs.refName.$el

$refs 绑定给一个 html 元素时,this.$refs.refName 获取的是该 DOM 元素,在 Vue 中,如果真的想用 DOM 元素,不应该使用原生的获取 DOM 的方法,而是通过这种方式获取,因为如果在组件外部有一个同名的(比如 class),通过原生方法获取,将会拿到外部元素,而不是真正想拿到的 DOM 元素,通过 this.$refs.refName 可以避免这一问题。

$root 获取根组件 Vue 实例,也不太用。

注意,在 created 生命周期函数中,拿不到 $refs 里的对象。必须要到 mounted 挂载 DOM 之后再获取 $refs

组件补充

.native

父级组件想对子组件绑定父组件的方法需要加上修饰符 .native

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div id="app">
<cp @click.native="pToC"></cp>
</div>

<script>
// 父组件
export default {
methods: {
pToC() {
console.log('pToC')
}
}
}
</script>
父组件向子组件传递处理函数

可以把父组件的处理函数直接传入子组件处理,这样就不用每次都让子组件 $emit 出事件让父组件监听并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!-- 父组件 -->
<template>
<cp :on-plus="plusVal" :value="value"></cp>
</template>

<script>
import cp from './childComps/cp'
export default {
data() {
return {
value: 0
}
},
methods: {
plusVal() {
this.val++
}
}
}
</script>

<!-- 子组件 ./childComps/cp.vue -->
<template>
<div>
{{val}}
<button @click="onPlus">增加数量</button>
</div>
</template>

<script>
export default {
props: {
value: Number,
onPlus: Function
}
}
</script>

事件总线($bus)

new 一个 Vue 实例对象,并将它挂载到真正使用的 Vue 实例上,新 new 出来的实例对象充当中转站的作用,因为每个 Vue 实例对象上都有 $emit$on 方法。

1
2
3
4
5
6
7
8
// 挂载
Vue.prototype.$bus = new Vue()

// a 组件 传递事件到 bus
this.$bus.$emit('eventA', params) // 参数可选

// b 组件从 bus 监听事件,如果监听到事件,就执行回调函数
this.$bus.$on('eventA', params => { console.log(params) })

需要注意的是,如果某个组件中使用了 $bus.$emit,并且在主页监听 $bus.$on 这个事件。当在 App.vue 中用 <keep-alive> 包裹 <router-view> ,并切换到其他页面时,主页仍然被缓存,若此时这个页面中也引入了这个组件,并且触发了它的 $bus.$emit,那么被 <keep-alive> 保留的主页中,仍然会用 $bus.$on 监听到 这个事件,并调用指定的回调函数。也就是说,在其他页面中的操作,可能会导致主页的修改,这一点需要注意。

所以解决方法可以是用 route, 判断是否为主页决定是否 $bus.$emit

1
2
3
4
5
6
7
8
9
10
11
12
/* 某组件中 */
{
methods: {
imgLoad() {
if (this.$route.path.indexOf('/home')) {
this.$bus.$emit('homeImgLoad')
} else if (this.$route.path.indexOf('/detail')) {
this.$bus.$emit('detailImgLoad')
}
}
}
}

依赖注入(provide/inject)

injectprovide 是为了解决组件间通信过于复杂的问题, 比如 父组件向孙组件传递的时候, 需要传两层 prop, 再更多层次的时, 如果不用 Vuex 烦不胜烦, 但我们必然希望我们的组件有高复用性而不依赖于 Vuex, 通过这组属性, 可以直接向孙组件中注入数据, 但用的很少, 官方也并不推荐直接应用于程序代码。因为开发中必然使用 Vuex, 这个方法可能常见于 UI 库, 如 ELEMENT-UI.

provide/inject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

它们要求的值类型应该是

  • provide : 返回一个对象的函数或一个对象(就是 data(){return {}} 这样的写法, 不推荐直接写一个对象, 除非传递不在当前实例上的静态数据,即不需要 this.data 获取的数据)
  • inject : 一个字符串数组(数组里每项都是字符串, 并对应 provide 的名字),或 一个对象,对象的 key 是本地的绑定名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- 只演示一下父子组件, 展示一下功能 -->
<!-- 父组件 -->
<template>
<div>
<cp></cp>
</div>
</template>

<script>
import Cp from './Cp'
export default {
componets: {
Cp
}
data() {
return {
msg: 'parent2child msg'
}
},
provide() {
// return 写法, 这样写安全靠谱!
return {
pMsg: this.msg
}
}
}
</script>

<!-- 子组件 -->
<template>
<div>
{{pMsg}} <!-- 将显示 parent2child msg, 即注入成功 -->
</div>
</template>

<script>
export default {
inject: ['pMsg']
}
</script>

组件传值注意点

组件传值是异步的,并且子组件渲染 DOM 可能比父组件慢,所以有时候需要在父组件中设置延迟。

比如,封装轮播图的时候,可能 SwiperItem 的 DOM 还未生成,而 Swiper 已经生成 DOM,并进入 mounted阶段,但此时无法获取到 SwiperItem 这个组件的元素,也就无法确定数量,因此会导致轮播小点的数量为 0,无法加载。

或者在子组件 DOM 渲染完毕时,给父组件一个标志,让它去执行初始化。

插槽

基本使用
匿名插槽

当插槽不指定 name 属性的时候,是匿名插槽,如果组件标签没有包裹 DOM 内容的话,默认自动显示 <slot>中间包裹的值</slot> 中间包裹的内容,如果 slot 标签中间没有内容,那就不显示。如果在组件标签中包裹了 DOM 内容,那么<slot></slot>就显示包裹的 DOM 内容,它自己中间包裹的内容不再显示。相当于把 <slot> 标签所在的位置替换为传入的 DOM 内容。插槽也是会被 整个替换 的内容,就像 <div id="app" />App 组件整个替换一样,所以不要在插槽上写 什么属性(如 class)或是什么指令(如 v-if ,如果想要对它使用 v-if 或是 @click,在它外面包裹一层 div 标签,对这个 div 标签使用 v-if 或 @click 吧)。

注意,可以写多个匿名插槽,只是组件标签中传入的 DOM 内容会被反应在所有的匿名插槽上,所以一般不写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div id="app">
<cp></cp> <!-- 插槽位置显示默认值 <div>你好</div> -->
<cp>Hi</cp> <!-- 插槽位置显示 Hi -->
<cp> <!-- 插槽显示 <div>Hello</div> <p>World</p> -->
<div>Hello</div>
<p>World</p>
</cp>
</div>

<template id="cp">
<div>
<p>下面是插槽</p>
<slot><div>你好</div></slot>
</div>
</template>

<script>
const cp = {
template: '#cp'
}

const vm = new Vue({
el: '#app',
components: {
cp
}
})
</script>
具名插槽

指定了插槽的 name 属性后,它就是具名插槽。同样地,可以指定两个同名的插槽,都会显示,被选中的时候也都会被替换,所以一般不会出现同名插槽。匿名插槽其实是有 name 属性的,它的值默认是 default

我们插入到匿名插槽中的内容,如果没有指定 slot 属性,也默认会带上 slot="default" 属性。你也可以插入多个内容都修改同一个插槽位置。

新版中使用 v-slot:slotName 指定来指定要替换的插槽,简写形式是 #slotNmae与旧版非常大的区别是,必须要在 <template> 标签上使用这个指令,这一点和旧版的 slot 属性不同,slot 在任何标签都可以使用。而且如果定义了多个 <template> 标签指向同一个插槽,只会显示最后一个 template 标签的内容

注意 slot 属性和 slot-scoped 属性在 vue3.0 以后将无法使用,目前已被官方废弃。

注意, v-slot 只有在 <template> 元素上才有效, slot 则都有效。

template 的直接子元素允许多个。之前一直以为只能写一个 div,这是错误的认知,但这样写好不好就是另外一回事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<!-- HTML 内容 -->

<!-- 新版写法 -->

<div id="app">
<!-- 显示 left right 匿名插槽位 -->
<cp></cp>

<!-- 显示 left right 修改了匿名插槽内容 -->
<cp>
<template> <!-- 默认加上了 v-slot:default -->
修改了匿名插槽内容
</template>
</cp>

<!-- 显示 修改了 name 为 left 的插槽内容 right 匿名插槽位 -->
<cp>
<template v-slot:left>
<span>修改了 name 为 left 的插槽内容</span>
</template>
</cp>

<!-- 显示 left right 修改了匿名插槽 default -->
<cp>
<template v-slot:default>
<span>修改了匿名插槽</span>
<span> default</span>
</template>
</cp>

<!-- 显示 left 修改了 right 一次 修改了 right 二次 匿名插槽位 -->
<cp>
<template #right> <!-- v-slot:right 的语法糖 -->
<span>修改了 right 一次</span>
<span>修改了 right 二次</span>
</template>
</cp>
</div>



<!-- 即将淘汰的旧版写法 -->

<div id="app">
<!-- 显示 left right 匿名插槽位 -->
<cp></cp>

<!-- 显示 left right 修改了匿名插槽内容 -->
<cp>修改了匿名插槽内容</cp>

<!-- 显示 修改了 name 为 left 的插槽内容 right 匿名插槽位 -->
<cp><span slot="left">修改了 name 为 left 的插槽内容</span></cp>

<!-- 显示 left right 修改了匿名插槽 default -->
<cp><span slot="default">修改了匿名插槽</span><span slot="default"> default</span></cp>

<!-- 显示 left 修改了 right 一次 修改了 right 二次 匿名插槽位 -->
<cp><span slot="right">修改了 right 一次</span><span slot="right">修改了 right 二次</span></cp>
</div>


<!-- 模板 -->

<template id="cp">
<div>
<slot name="left">left</slot>
<slot name="right">right</slot>
<slot>匿名插槽位</slot>
</div>
</template>

<script>
const cp = {
template: '#cp'
}

const vm = new Vue({
el: '#app',
components: {
cp
}
})
</script>
编译作用域

官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译,子组件模板的所有东西都会在子级作用域内编译。(原文:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="app">
<!-- cp 组件将会从父组件上获取 isShow, 值为 true,组件将被渲染 -->
<cp v-show="isShow"></cp>
</div>

<template id="cp">
<div>
这段文本将会显示,但不会显示按钮
<button v-show="isShow">这个按钮将不会出现,isShow 从组件内中获取,值为 false</button>
</div>
</template>

<script>
const vm = new Vue({
el: '#app',
data: {
isShow: true // 父组件的 isShow
},
components: {
cp: {
template: '#cp',
data() {
return {
isShow: false
}
}
}
}
})
</script>
作用域插槽

这是比较难以理解的一个点,而且官方文档说的不够直白。在这里用一句话对其进行总结:父组件替换插槽展示的格式,但是内容由子组件来提供,即从子组件把数据传给父元素,父元素重新组合 DOM 结构后传回子组件。

例如,子组件中包含一组数据: fLanguages: ['English', 'Chinese', 'Japanese', 'French', 'Spanish'],插槽默认按 li 标签展示,但此时如果希望它用 - 分隔后做横排展示呢? 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!-- HTML 部分 -->
<!-- 新版写法 -->

<div id="app">
<cp> <!-- v-slot="cpSlot" 也可以写成 #default="cpSlot",不能写成 #="cpSlot",必须带插槽名 -->
<template v-slot="cpSlot"> <!-- 注意,这里特地起个驼峰名是为了说明可以使用驼峰命名 -->
{{ cpSlot.lang.join('-') }}
</template>
</cp>
</div>

<!-- 旧版写法 -->

<div id="app">
<cp>
<template slot-scope="cpSlot"> <!-- 注意,这里特地起个驼峰名是为了说明可以使用驼峰命名 -->
{{ cpSlot.lang.join('-') }}
</template>
</cp>
</div>

<!-- 模板部分 -->

<template id="cp"> <!-- 记住,自定义的要传到父组件的属性名是写在 slot 中,而不是最外层的 template 中 -->
<div>
<slot :laNG='fLanguages'> <!-- 注意,这里特地把 lang 写成 laNG 是为了再次说明 v-bind 默认把绑定的属性名转为小写,所以大写是没用的。上面仍然使用 .lang 来获取这个属性 -->
<ul>
<li v-for="lang in fLanguages" ></li>
</ul>
</slot>
</div>
</template>

<script>
const vm = new Vue({
el: '#app',
components: {
cp: {
template: '#cp',
data() {
return {
fLanguages: ['English', 'Chinese', 'Japanese', 'French', 'Spanish']
}
}
}
}
})
</script>
插槽注意点

如果仅仅是文本变化,就不要使用插槽了,直接使用 props 组件传值即可。比较复杂的变化,如结构改变,元素改变之类的再使用插槽。

生命周期

Vue 源码中总共有 12 个生命周期钩子函数,官网提到了常用的 8 个钩子。另外四个分别是 errorCaptured(2.5新增,处理异常的钩子)serverPrefetch(2.6新增 处理 ssr 的钩子)activateddeactivated拿到的 this.$route 是跳转后的 $route 和组件内守卫 beforeRouteLeave 拿到的 this.$route 是不一样的,守卫拿到的是跳转前的 $route)专用于 keep-alive,当组件在<keep-alive></keep-alive> 这个抽象组件内被切换,才会触发这两个钩子,并且 不会 触发 createddestroyed 钩子,因为切换时组件只是缓存而不是被销毁。根据业务选择合适的钩子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
new Vue({
data: {
msg: 'Vue 生命周期'
},
// 第一个参数 '%c%s' 表示后面的两个参数一个表示打印内容的css样式,一个表示正常打印的字符串内容,我们正常写的 console.log('msg') 其实就是 console.log('%s', 'msg') 的简写。第一个参数表示后面的参数是干嘛的。
beforeCreate() {
console.group('创建前的状态') // 可以把该函数内 console.log 弄成一组。
console.log('%c%s', 'color: red', 'el: ' + this.$el) // undefined
console.log('%c%s', 'color: red', 'data: ' + this.$data) // undefined
},
/* 常用钩子 */
created() {
console.gruop('创建后的状态')
console.log('%c%s', 'color: red', 'el: ' + this.$el) // undefined
console.log('%c%s', 'color: red', 'data: ' + this.$data) // data: [Object Object]
},
beforeMount() {
console.group('挂载前的状态')
console.log('%c%s', 'color: red', 'el: ' + this.$el) // undefined 虚拟 DOM 阶段,由于 Chrome 的 console.log 的折叠项是懒加载的,可能在此处就会打印出本应在 mounted 里面的真实 DOM
console.log('%c%s', 'color: red', 'data: ' + this.$data) //data: [Object Object]
},
/* 常用钩子 */
mounted() {
console.group('挂载后的状态')
console.log('%c%s', 'color: red', 'el: ' + this.$el) // el: [Object HTMLDivElement] <div id="app"></div> 真实 DOM 已被挂载
console.log('%c%s', 'color: red', 'data: ' + this.$data) //data: [Object Object]
},
beforeUpdate() {
// mounted 完成后,才会开始响应式的检测
console.group('更新前的状态')
console.log(this.msg) // 假设修改了 msg 的值,就会触发,并且这里拿到的是修改后的 msg 值,与 update 钩子的区别在于页面是否重新渲染,即 view 层此时没有变化,这是修改 data 的最后时机
// 任意 data 中的数据发生改变,都会触发该钩子
},
/* 常用钩子 */
updated() {
console.group('更新后的状态')
// 任意 data 中的数据发生改变,都会触发该钩子
// 注意,不要在这里进行数据修改
},
beforeDestroy() {
console.group('beforeDestroy 销毁前状态===============》')
// 一般在这里执行 清除计时器、清除非指令绑定的事件 等操作
},
destroyed() {
// 新组件的 created 一般在 旧组件的 destroyed 之前。因为销毁需要时间
console.group('beforeDestroy 销毁后状态===============》')
}
}).$mount('#app')

Vue 的 DOM 更新

Vue 在下一个事件循环开始执行更新时才会进行必要的 DOM 更新。

进入页面中,会先渲染根组件,再一步步渲染子组件,也因此,可能根组件已经到了 mounted 阶段,而子组件仍然处于 created 阶段。而此时,如果获取 DOM,就会出现一些问题。如果想要等到子组件也渲染完毕才获取 DOM,可以在 this.$nextTick() 中操作。

this.$nextTick()

tick: 事件循环

在下次 DOM 更新循环结束之后执行延迟回调。因此可以满足上面的全部组件渲染完毕之后回调的需求。

1
2
3
4
5
6
7
{
mounted() {
this.$nextTick(() => {
// ... 操作 DOM,此时子组件也已渲染完毕(图片等资源可能还未加载,但 img 标签已经在页面中)
})
}
}

另外的用法是,在修改数据之后立即使用这个方法,可以获取更新后的 DOM。因为只要监听到数据变化, Vue 将开启一个队列,并缓冲同一事件循环中发生的所有数据变更(在这个队列中还包括了去重功能,比如同一个 watcher 被多次触发,只会被推入到队列中一次,避免了不必要的计算和 DOM 操作),也就是说,在一定时间之后 Vue 根据该队列的内容更新 DOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('example', {
template: '<span>{{ msg }}</span>',
date() {
return {
msg: '未更新'
}
}
mounted() {
this,msg = '已更新'
console.log(this.$el.textContent) // 未更新
// 这种用法要跟在数据修改后面
this.$nextTick(() => {
console.log(this.$el.textContent) // 已更新
})
}
})
与 updated 的比较

上面我们说到,在发生数据修改的时候都会触发 beforeUpdateupdated 这两个钩子函数,如果要对特定的修改执行 updated 函数非常麻烦。官方为此提供了另外一种解决办法,那就是$nextTick,它只会触发一次, updated 将会一直监听。

this.$nextTick 执行的比 updated 更早,它是在某个或某几个数据(调用 nextTick 方法之前的数据修改)被修改之后,被影响的 DOM 渲染完成后调用, updated 是在受数据影响的 DOM 全部重新渲染完毕时,才触发。

在 created 中修改数据,不会触发 updated 钩子,但会触发 $nextTick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
data() {
return {
a: '',
b: '',
c: ''
}
},
mounted() {
this.a = 'nextTick'
this.b = 'nextTick'
this.$nextTick(() => {
console.log('nextTick done') // 先执行
})
this.c = 'updated'
},
updated() {
console.log('updated done') // 在 nextTick 之后执行
}
}

额外

Vue.use

Vue.use 实际是调用了传入对象的 install 方法,同时会传进来 Vue。

1
2
3
4
5
6
7
// 将会打印定义的内容
Vue.use({
install(Vue) {
console.log(Vue)
console.log('Vue.use call install')
}
})

Vue.$option.data()

可以通过 Vue.$option.data() 拿到最初传入组件的 data 对象,可以用于初始化。调用的时候如果用来访问 props 或 methods,注意 this 的指向,应该写成 Vue.$options.data.call(this)

1
2
3
4
5
6
7
8
9
10
11
12
{
data() {
return {
msg: 'before'
}
},
created() {
this.msg = 'after'
console.log(this.msg) // after
console.log(this.$options.data().msg) // before
}
}

vue-cli 做的事,一切皆组件。

Vue 实例上 template 和 el 的关系,编译时 template 中的内容将完全替换 el 挂载的元素,此时页面中不会有 id 为 app 的元素了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!--
在这个例子中,id 为 app 的 div 将被替换成不带 id 的 <div<div>,即
<div>
<div>hellow</div>
<div>world</div>
<button>Click here</button>
<div>
同时,如果在渲染完成之后修改 vm.msg 的值,比如改成 'hi',页面中的 hello 也会变成 hi,这是因为 vue 保存了一份 id 为 app 的 div 的模板,所以它仍然知道哪里需要修改,即使页面中现在已经找不到 id 为 app 的 div 了。
-->

<div id="app">

</div>

<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello',
name: 'world'
},
template: `
<div>
<div>{{msg}}</div>
<div>{{name}}</div>
<button @click="clickHandle">Click here</button>
<div>
`,
methods: {
clickHandle() {}
}
})
</script>

此时看上去,vm 的代码复杂又冗余,到时候修改起来非常不方便,这时候就需要抽离 template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<div id="app">

</div>

<script>
// 把 template 抽离出来之后,发现根组件的 data 和 methods 也就没用了,都需要放到 App 组件中,那就都放进去。
const App = {
template: `
<div>
<div>{{msg}}</div>
<div>{{name}}</div>
<button @click="clickHandle">Click here</button>
<div>
`,
data: {
msg: 'hello',
name: 'world'
},
methods: {
clickHandle() {}
}
}

// const vm = 也可以省了,因为我们其实不需要 vm 这个实例来获取里面的属性,以上只是为了 console 方便才获取一下这个对象
new Vue({
el: '#app',
template: '<App />', // 需要注册 App 组件之后才能使用! 当然,这里也可以写 <app /> 毕竟 vue 会帮我们做大小写转换。
components: {
App
}
})
</script>


之后可能会演变成多个 js 文件,而 main.js 文件将会变得非常简洁

<!-- main.js -->
import Vue from 'vue' // 当然这一行代码需要在 webpack.config,js 里配置一下别名 alias,选中带编译模块功能的 vue 版本 vue.esm.js
import App from 'App.js'
new Vue({
el: '#app',
template: '<App />', // 需要注册 App 组件之后才能使用! 当然,这里也可以写 <app /> 毕竟 vue 会帮我们做大小写转换。
components: {
App
}
})

<!-- App.js -->
export default {
template: `
<div>
<div>{{msg}}</div>
<div>{{name}}</div>
<button @click="clickHandle">Click here</button>
<div>
`,
data: {
msg: 'hello',
name: 'world'
},
methods: {
clickHandle() {}
}
}

这大概就是 vue-cli 最初的想法。

之后为了把 template 和 js 代码分离开来,出现了 .vue 文件,把 template 与 js 区分,顺便带上了 css, 它需要 webpack 安装 vue-loader(让 webpack 认识 vue 文件,只负责加载,无法解析) 和 vue-template-complier(用于编译模板,解析模板) 支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<div>{{msg}}</div>
<div>{{name}}</div>
<button @click="clickHandle">Click here</button>
<div>
</template>

<script>
export default {
name: 'App', // 定义了组件名,这样在 vue-devtools 里可以找到这个组件
data: {
msg: 'hello',
name: 'world'
},
methods: {
clickHandle() {}
}
}
</script>

<style scoped>
</style>
分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理