Vue - 基础 + 组件

缘由

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如:

1
2
3
<div id="example">
{{ message.split('').reverse().join('') }}
</div>

在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 message 的翻转字符串。当你想要在模板中多包含此处的翻转字符串时,就会更加难以处理。

所以,对于任何复杂逻辑,你都应当使用计算属性

基础示例

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>计算属性</title>
</head>
<body>
<div id="root">
{{ message }} --- {{ message.split('').reverse().join('') }}
<hr>
{{ message }} --- {{ newMessage }}
<hr>
{{ msg }} --- {{ newMsg }}
<button v-on:click="changeMsgHandler()">改变msg的值</button>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
msg: "bbb",
message: "hello computed"
},
methods: {
changeMsgHandler() {
this.msg = "ccc"
}
},
computed: {
newMsg: function () {
return "aaa"
},
newMessage() {
return this.message.split('').reverse().join('')
}
},
})
</script>
</html>

特点

  • 不在 data 中定义,以 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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>方法</title>
</head>
<body>
<div id="root">
<h2>Message:{{ message }}</h2>
<hr>
<h2>Reversed message:{{ reversedMessage() }}</h2>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
message: "hello computed"
},
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
},
})
</script>
</html>

计算属性 vs 方法

我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是 计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

1
2
3
4
5
computed: {
now: function () {
return Date.now()
}
}

相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。

示例

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>计算属性VS方法</title>
</head>
<body>
<div id="root">
<h1>方法</h1>
<p>message:{{ message }}</p>
<p>Reversed message:{{ reversedMessage() }}</p>
<p>Reversed message:{{ reversedMessage() }}</p>
<h1>计算属性</h1>
<p>msg:{{ msg }}</p>
<p>Reversed msg:{{ newMsg }}</p>
<p>Reversed msg:{{ newMsg }}</p>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
message: "hello methods",
msg: "hello computed"
},
methods: {
reversedMessage: function () {
console.log("methods")
return this.message.split("").reverse().join("")
}
},
computed: {
newMsg: function () {
console.log("computed")
return this.msg.split("").reverse().join("")
}
}
})
</script>
</html>

运行结果

运行结果

计算属性和方法的区别(面试题)

  • 计算属性具有依赖性,如果依赖的那个数值没有发生改变,那么就会执行一次,除非依赖发生改变,它才会重新开始计算
  • 方法调用一次,就执行一次
  • 计算属性优于方法

侦听属性

1
2
3
4
5
6
7
8
侦听属性
选项
watch:{
需要监听的属性: function (newVal, oldVal) {

}
}
监听的属性和data中的初始状态名称一样,就是监听的这些值

Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch —— 特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch 回调。细想一下这个例子:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>侦听属性</title>
</head>
<body>
<div id="root">
<label>姓氏:
<input type="text" v-model="firstName">
</label>
<label>名字:
<input type="text" v-model="lastName">
</label>
姓名:{{ fullName }}
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
firstName: '',
lastName: '',
fullName: ''
},
watch: {
firstName: function (newVal, oldVal) {
this.fullName = this.lastName + newVal
},
lastName: function (newVal, oldVal) {
this.fullName = this.firstName + newVal
}
}
})
</script>
</html>

计算属性 vs 侦听属性

上面代码是命令式且重复的。将它与计算属性的版本进行比较:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>计算属性vs侦听属性</title>
</head>
<body>
<div id="root">
<label>姓氏:
<input type="text" v-model="firstName">
</label>
<label>名字:
<input type="text" v-model="lastName">
</label>
姓名:{{ fullName }}
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
firstName: '',
lastName: '',
},
computed: {
fullName: function () {
return this.firstName + this.lastName
}
},
})
</script>
</html>
  • 代码量上计算属性更好
  • 能用计算属性做的就用计算属性,但是不是绝对

侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

例如:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>侦听器</title>
</head>
<body>
<div id="root">
<p>
<label>Ask a yes/no question:
<input v-model="question">
</label>
</p>
<p>{{ answer }}</p>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script>
new Vue({
el: "#root",
data: {
question: '',
answer: 'I cannot give you an answer until you ask a question!'
},
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
created: function () {
// `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
// 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
// AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
// `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
// 请参考:https://lodash.com/docs#debounce
this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
},
methods: {
getAnswer: function () {
if (this.question.indexOf('?') === -1) {
this.answer = 'Questions usually contain a question mark. ;-)'
return
}
this.answer = 'Thinking...'
var vm = this
axios.get('https://yesno.wtf/api')
.then(function (response) {
vm.answer = _.capitalize(response.data.answer)
})
.catch(function (error) {
vm.answer = 'Error! Could not reach the API. ' + error
})
}
}
})
</script>
</html>

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

除了 watch 选项之外,您还可以使用命令式的 vm.$watch API

Class 与 Style 绑定

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

绑定 HTML Class

  • 绑定属性

    语法

    1
    2
    3
    4
    绑定属性
    v-bind:class = "变量"
    简写
    :class = "变量"

    示例

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>绑定属性</title>
    <style>
    .active {
    color: #007bff;
    }
    </style>
    </head>
    <body>
    <div id="root">
    <span :class="currentIndex === 0 ? 'active': '' " v-on:click="changePageHandler(0)">首页</span>
    <span :class="currentIndex === 1 ? 'active': '' " v-on:click="changePageHandler(1)">分类</span>
    <span :class="currentIndex === 2 ? 'active': '' " v-on:click="changePageHandler(2)">我的</span>
    <div class="content">
    <div v-if="currentIndex === 0" class="homeContent">首页</div>
    <div v-else-if="currentIndex === 1" class="kindContent">分类</div>
    <div v-else class="homeContent">我的</div>
    </div>
    </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
    new Vue({
    el: "#root",
    data: {
    currentIndex: 0
    },
    methods: {
    changePageHandler(id) {
    this.currentIndex = id
    }
    },
    })
    </script>
    </html>
  • 对象语法

    我们可以传给 v-bind:class 一个对象,以动态地切换 class:

    1
    <div v-bind:class="{ active: isActive }"></div>

    上面的语法表示 active 这个 class 存在与否将取决于数据 property isActivetruthiness

    示例

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>绑定属性</title>
    <style>
    .active {
    color: #ffffff;
    }

    .bgColor {
    background-color: #007bff;
    }
    </style>
    </head>
    <body>
    <div id="root">
    <span :class="{ active: currentIndex === 0, bgColor: currentIndex === 0 }"
    v-on:click="changePageHandler(0)">首页</span>
    <span :class="{ active: currentIndex === 1, bgColor: currentIndex === 1 }"
    v-on:click="changePageHandler(1)">分类</span>
    <span :class="{ active: currentIndex === 2, bgColor: currentIndex === 2 }"
    v-on:click="changePageHandler(2)">我的</span>
    <div class="content">
    <div v-if="currentIndex === 0" class="homeContent">首页</div>
    <div v-else-if="currentIndex === 1" class="kindContent">分类</div>
    <div v-else class="homeContent">我的</div>
    </div>
    </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
    new Vue({
    el: "#root",
    data: {
    currentIndex: 0
    },
    methods: {
    changePageHandler(id) {
    this.currentIndex = id
    }
    },
    })
    </script>
    </html>

    你可以在对象中传入更多字段来动态切换多个 class。此外,v-bind:class 指令也可以与普通的 class attribute 共存。

  • 数组语法

    我们可以把一个数组传给 v-bind:class,以应用一个 class 列表:

    1
    <div v-bind:class="[activeClass, errorClass]"></div>
    1
    2
    3
    4
    data: {
    activeClass: 'active',
    errorClass: 'text-danger'
    }

    示例

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>绑定属性</title>
    <style>
    .active {
    color: #ffffff;
    }

    .bgColor {
    background-color: #007bff;
    }
    </style>
    </head>
    <body>
    <div id="root">
    <span :class="{ active: currentIndex === 0, bgColor: currentIndex === 0 }"
    v-on:click="changePageHandler(0)">首页</span>
    <span :class="{ active: currentIndex === 1, bgColor: currentIndex === 1 }"
    v-on:click="changePageHandler(1)">分类</span>
    <span :class="{ active: currentIndex === 2, bgColor: currentIndex === 2 }"
    v-on:click="changePageHandler(2)">我的</span>
    <span>|</span>
    <span :class="[fontActive, bgActive]">数组写法</span>

    <div class="content">
    <div v-if="currentIndex === 0" class="homeContent">首页</div>
    <div v-else-if="currentIndex === 1" class="kindContent">分类</div>
    <div v-else class="homeContent">我的</div>
    </div>
    </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script>
    new Vue({
    el: "#root",
    data: {
    currentIndex: 0,
    fontActive: "active",
    bgActive: "bgColor"
    },
    methods: {
    changePageHandler(id) {
    this.currentIndex = id
    }
    },
    computed: {},
    watch: {},
    })
    </script>
    </html>
  • 用在组件上

绑定内联样式

  • 对象语法

    v-bind:style 的对象语法十分直观 —— 看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

    1
    <div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    1
    2
    3
    4
    data: {
    activeColor: 'red',
    fontSize: 30
    }

    直接绑定到一个样式对象通常更好,这会让模板更清晰:

    1
    <div v-bind:style="styleObject"></div>
    1
    2
    3
    4
    5
    6
    data: {
    styleObject: {
    color: 'red',
    fontSize: '13px'
    }
    }

    同样的,对象语法常常结合返回对象的计算属性使用。

  • 数组语法

    v-bind:style 的数组语法可以将多个样式对象应用到同一个元素上:

    1
    <div v-bind:style="[baseStyles, overridingStyles]"></div>
  • 自动添加前缀

    v-bind:style 使用需要添加浏览器引擎前缀的 CSS property 时,如 transform,Vue.js 会自动侦测并添加相应的前缀。

  • 多重值

    2.3.0+

    从 2.3.0 起你可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

    1
    <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

    这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex

生命周期

实例生命周期钩子

每个 Vue 实例在被创建时都要经过一系列的初始化过程 —— 例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做 生命周期钩子 的函数,这给了用户在不同阶段添加自己的代码的机会。

将 Vue 中自己带有的那些函数称之为钩子函数,并且给他们赋予了一定的含义,让他们运行于整个 Vue 实例中各个执行阶段,给了用户在不同阶段添加自己的代码的机会。

1
2
3
4
5
6
7
8
9
10
11
# 从开始到结束(10个)
beforeCreate
created # (有人会在此处进行ajax请求)
beforeMount # (有人会在此处进行ajax请求)
mounted # 常用,ajax请求、实例化对象(如果使用第三方插件,初始化实例)
beforeUpdate
updated # 常用,实例化对象(如果使用第三方插件,请求数据之后更新了视图,实例化)
beforeDestroy
destroyed # 常用,销毁操作
activated # 被 keep-alive 缓存的组件激活时调用。
deactivated # 被 keep-alive 缓存的组件停用时调用。

beforeCreate

  • 类型Function
  • 详细
    在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
  • 参考生命周期图示

created

  • 类型Function
  • 详细
    在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
  • 参考生命周期图示

beforeMount

  • 类型Function
  • 详细
    • 在挂载开始之前被调用:相关的 render 函数首次被调用。
    • 该钩子在服务器端渲染期间不被调用。
  • 参考生命周期图示

mounted(常用)

  • 类型Function
  • 详细
    • 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。
    • 注意 mounted 不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted 内部使用 vm.$nextTick
      1
      2
      3
      4
      5
      6
      mounted: function () {
      this.$nextTick(function () {
      // Code that will run only after the
      // entire view has been rendered
      })
      }

      该钩子在服务器端渲染期间不被调用。

  • 参考生命周期图示

beforeUpdate

  • 类型Function
  • 详细
    • 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
    • 该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。
  • 参考生命周期图示

updated(常用)

  • 类型Function
  • 详细
    • 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
    • 当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性watcher 取而代之。
    • 注意 updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick
      1
      2
      3
      4
      5
      6
      updated: function () {
      this.$nextTick(function () {
      // Code that will run only after the
      // entire view has been re-rendered
      })
      }

      该钩子在服务器端渲染期间不被调用。

  • 参考生命周期图示

activated

deactivated

beforeDestroy

  • 类型Function
  • 详细
    • 实例销毁之前调用。在这一步,实例仍然完全可用。
    • 该钩子在服务器端渲染期间不被调用。
  • 参考生命周期图示

destroyed(常用)

  • 类型Function
  • 详细
    • 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
    • 该钩子在服务器端渲染期间不被调用。
  • 参考生命周期图示

生命周期图示

下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。

生命周期图示

示例

  1. mounted - 单个请求

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>生命周期</title>
    </head>
    <body>
    <div id="root">
    <ul>
    <li v-for="(item, index) in list">
    {{ item.title }}
    </li>
    </ul>
    <ul>
    <li v-for="(item, index) in nav">
    {{ item.navName }}
    <img :src="item.navSrc" alt="">
    </li>
    </ul>
    </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
    new Vue({
    el: "#root",
    data: {
    list: [],
    nav: []
    },
    methods: {
    getListDataSuccessHandler(data) {
    console.log("list", data)
    this.list = data;
    },
    getNavDataSuccessHandler(data) {
    console.log("nav", data)
    this.nav = data;
    },
    getDataFailHandler(err) {
    console.log(err)
    }
    },
    mounted: function () {
    // 进入页面主动触发该函数,可以进行ajax请求
    var listdata = {
    params: {}
    };
    var navdata = {
    params: {}
    };
    myajax.axiosGet({
    url: "http://localhost:3000/api/list",
    data: listdata,
    success: this.getListDataSuccessHandler,
    fail: this.getDataFailHandler
    })
    myajax.axiosGet({
    url: "http://localhost:3000/api/nav",
    data: navdata,
    success: this.getNavDataSuccessHandler,
    fail: this.getDataFailHandler
    })
    }
    })
    </script>
    </html>
  2. mounted - 多个请求

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>生命周期</title>
    </head>
    <body>
    <div id="root">
    <ul>
    <li v-for="(item, index) in list">
    {{ item.title }}
    </li>
    </ul>
    <ul>
    <li v-for="(item, index) in nav">
    {{ item.navName }}
    <img :src="item.navSrc" alt="">
    </li>
    </ul>
    </div>
    </body>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script>
    var myajax = {
    axiosGet(option) {
    axios.get(option.url, option.data)
    .then(function (result) {
    option.success(result.data)
    })
    .catch(function (err) {
    option.fail(err)
    })
    }
    }

    var baseUrl = "http://localhost:3000/api/";
    new Vue({
    el: "#root",
    data: {
    list: [],
    nav: []
    },
    methods: {
    getListDataHandler() {
    return axios.get(baseUrl + "list");
    },
    getNavDataHandler() {
    return axios.get(baseUrl + "nav");
    },
    getDataSuccessHandler(listData, navData) {
    console.log("listData", listData)
    console.log("navData", navData)
    this.list = listData.data;
    this.nav = navData.data;
    }
    },
    mounted() {
    axios.all([this.getListDataHandler(), this.getNavDataHandler()])
    .then(axios.spread(this.getDataSuccessHandler))
    }
    })
    </script>
    </html>

什么是组件?

  • 组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 is 特性进行了扩展的原生 HTML 元素。
  • 所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。

组件注册

  • 组件名

    在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:

    1
    Vue.component('my-component-name', { /* ... */ })

    该组件名就是 Vue.component 的第一个参数。

    你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。

    你可以在风格指南中查阅到关于组件名的其它建议。

    组件名大小写

    定义组件名的方式有两种:

    • 使用 kebab-case (短横线分隔命名)
      1
      Vue.component('my-component-name', { /* ... */ })

      当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>

    • 使用 PascalCase (首字母大写命名)
      1
      Vue.component('MyComponentName', { /* ... */ })

      当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name><MyComponentName> 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。

  • 全局注册

    • 给 Vue 对象扩展了一个指令,在 new Vue 之前
      1
      2
      3
      4
      5
      <div id="root">
      <component-a></component-a>
      <component-b></component-b>
      <component-c></component-c>
      </div>
      1
      2
      3
      4
      5
      Vue.component('component-a', { /* ... */ })
      Vue.component('component-b', { /* ... */ })
      Vue.component('component-c', { /* ... */ })

      new Vue({ el: '#root' })

      这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中。

    • 组件其实也是 Vue 的一个实例,具有除了部分只有 new Vue 实例选项之外的所有的选项
    • new Vue 中的 data: {};组件中的 data: function (){ return {} }
    • 组件中没有 el 选项,因为 template 已经知道了哪些节点使用 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
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>全局注册组件</title>
      </head>
      <body>
      <div id="root">
      <v-app></v-app>
      <v-banner></v-banner>
      <v-list></v-list>
      </div>
      </body>
      <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
      var myajax = {
      axiosGet(option) {
      axios.get(option.url, option.data)
      .then(function (result) {
      option.success(result.data)
      })
      .catch(function (err) {
      option.fail(err)
      })
      }
      }

      Vue.component("v-app", {
      template: "<div>全局注册组件</div>"
      })
      Vue.component("v-banner", {
      template: "<div>全局注册banner组件</div>",
      data() {
      return {}
      },
      })
      Vue.component("v-list", {
      // 代码难以维护,编写易出错---template单独抽离出去
      template: `
      <div>
      全局注册list组件
      <ul>
      <li v-for="(item, index) in list">
      {{ item.title }}
      </li>
      </ul>
      </div>`,
      data: function () {
      return {
      list: []
      }
      },
      methods: {
      getListDataSuccessHandler(data) {
      console.log("list", data)
      this.list = data;
      },
      getDataFailHandler(err) {
      console.log(err)
      }
      },
      mounted: function () {
      var listdata = {
      params: {}
      };
      myajax.axiosGet({
      url: "http://localhost:3000/api/list",
      data: listdata,
      success: this.getListDataSuccessHandler,
      fail: this.getDataFailHandler
      })
      }
      })
      new Vue({
      el: "#root",
      })
      </script>
      </html>

      代码难以维护,编写易出错 ---> 将 template 单独抽离出去

    • 抽离 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
      65
      66
      67
      68
      69
      70
      71
      72
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>抽离template</title>
      </head>
      <body>
      <div id="root">
      <v-list></v-list>
      </div>
      </body>
      <template id="list">
      <div>
      全局注册list组件
      <ul>
      <li v-for="(item, index) in list">
      {{ item.title }}
      </li>
      </ul>
      </div>
      </template>

      <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
      var myajax = {
      axiosGet(option) {
      axios.get(option.url, option.data)
      .then(function (result) {
      option.success(result.data)
      })
      .catch(function (err) {
      option.fail(err)
      })
      }
      }

      Vue.component("v-list", {
      // 单独写一个template标签,写一个id属性,将内容放进该标签内部,其余操作一样
      template: "#list",
      data() {
      return {
      list: []
      }
      },
      methods: {
      getListDataSuccessHandler(data) {
      console.log("list", data)
      this.list = data;
      },
      getDataFailHandler(err) {
      console.log(err)
      }
      },
      mounted() {
      var listdata = {
      params: {}
      };
      myajax.axiosGet({
      url: "http://localhost:3000/api/list",
      data: listdata,
      success: this.getListDataSuccessHandler,
      fail: this.getDataFailHandler
      })
      }

      })
      new Vue({
      el: "#root",
      })
      </script>
      </html>

      单独写一个 template 标签,写一个 id 属性,将内容放进该标签内部,其余操作一样

  • 局部注册

    • 全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。

    • 在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:

      1
      2
      3
      var ComponentA = { /* ... */ }
      var ComponentB = { /* ... */ }
      var ComponentC = { /* ... */ }
    • 然后在 components 选项中定义你想要使用的组件:

      1
      2
      3
      4
      5
      6
      7
      new Vue({
      el: '#app',
      components: {
      'component-a': ComponentA,
      'component-b': ComponentB
      }
      })
    • 对于 components 对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。

    • 注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentAComponentB 中可用,则你需要这样写:

      1
      2
      3
      4
      5
      6
      7
      8
      var ComponentA = { /* ... */ }

      var ComponentB = {
      components: {
      'component-a': ComponentA
      },
      // ...
      }
    • 或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:

      1
      2
      3
      4
      5
      6
      7
      8
      import ComponentA from './ComponentA.vue'

      export default {
      components: {
      ComponentA
      },
      // ...
      }
    • 注意在 ES2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写,即这个变量名同时是:

      • 用在模板中的自定义元素的名称
      • 包含了这个组件选项的变量名
    • 示例

      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
      <!DOCTYPE html>
      <html lang="en">
      <head>
      <meta charset="UTF-8">
      <title>局部注册组件</title>
      </head>
      <body>
      <div id="root">
      <v-list></v-list>
      </div>
      </body>
      <template id="list">
      <div>
      全局注册list组件
      <ul>
      <li v-for="(item, index) in list">
      {{ item.title }}
      </li>
      </ul>
      </div>
      </template>

      <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
      <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
      <script>
      var myajax = {
      axiosGet(option) {
      axios.get(option.url, option.data)
      .then(function (result) {
      option.success(result.data)
      })
      .catch(function (err) {
      option.fail(err)
      })
      }
      }
      var List = {
      template: "#list",
      data() {
      return {
      list: []
      }
      },
      components: {},
      methods: {
      getListDataSuccessHandler(data) {
      console.log("list", data)
      this.list = data;
      },
      getDataFailHandler(err) {
      console.log(err)
      }
      },
      mounted() {
      var listdata = {
      params: {}
      };
      myajax.axiosGet({
      url: "http://localhost:3000/api/list",
      data: listdata,
      success: this.getListDataSuccessHandler,
      fail: this.getDataFailHandler
      })
      }
      }

      new Vue({
      el: "#root",
      components: {
      "v-list": {
      "v-list": List,
      "v-banner": List,
      "v-nav": List
      }
      },
      })
      </script>
      </html>
  • 局部注册分解

    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
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>局部注册组件分解</title>
    </head>
    <body>
    <div id="root">
    <v-list></v-list>
    </div>
    </body>
    <template id="list">
    <div>
    全局注册list组件
    <ul>
    <li v-for="(item, index) in list">
    {{ item.title }}
    </li>
    </ul>
    </div>
    </template>

    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="list.js"></script>
    <script src="myajax.js"></script>
    <script>
    new Vue({
    el: "#root",
    components: {
    "v-list": List,
    "v-banner": List,
    "v-nav": List
    },
    })
    </script>
    </html>
    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
    var List = {
    template: "#list",
    data() {
    return {
    list: []
    }
    },
    components: {
    "v-item": Item
    },
    methods: {
    getListDataSuccessHandler(data) {
    console.log("list", data)
    this.list = data;
    },
    getDataFailHandler(err) {
    console.log(err)
    }
    },
    mounted() {
    var listdata = {
    params: {}
    };
    myajax.axiosGet({
    url: "http://localhost:3000/api/list",
    data: listdata,
    success: this.getListDataSuccessHandler,
    fail: this.getDataFailHandler
    })
    }
    }

    list.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var myajax = {
    axiosGet(option) {
    axios.get(option.url, option.data)
    .then(function (result) {
    option.success(result.data)
    })
    .catch(function (err) {
    option.fail(err)
    })
    }
    }

    myajax.js

父子组件

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>父子组件</title>
</head>
<body>
<div id="root">
<v-list></v-list>
</div>
</body>
<template id="list">
<div>
全局注册list组件
<ul>
<li v-for="(item, index) in list">
{{ item.title }}
<v-item></v-item>
</li>
</ul>
</div>
</template>

<template id="item">
<div>
---------------------list的选项item
</div>
</template>

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="list.js"></script>
<script src="myajax.js"></script>
<script>
new Vue({
el: "#root",
components: {
"v-list": List,
"v-banner": List,
"v-nav": List
},
})
</script>
</html>
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
var Item = {
template: "#item",
}
var List = {
template: "#list",
data() {
return {
list: []
}
},
components: {
"v-item": Item
},
methods: {
getListDataSuccessHandler(data) {
console.log("list", data)
this.list = data;
},
getDataFailHandler(err) {
console.log(err)
}
},
mounted() {
var listdata = {
params: {}
};
myajax.axiosGet({
url: "http://localhost:3000/api/list",
data: listdata,
success: this.getListDataSuccessHandler,
fail: this.getDataFailHandler
})
}
}

list.js

1
2
3
4
5
6
7
8
9
10
11
var myajax = {
axiosGet(option) {
axios.get(option.url, option.data)
.then(function (result) {
option.success(result.data)
})
.catch(function (err) {
option.fail(err)
})
}
}

myajax.js