基本使用

安装

:::warning 注意:Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有兼容 ECMAScript 5 的浏览器。 :::

创建一个 Vue 实例

每个 Vue 应用都是通过用 Vue 函数创建一个新的 Vue 实例开始的:

  1. var vm = new Vue({
  2. // 选项
  3. })

数据与方法

当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。

  1. // 我们的数据对象
  2. var data = { a: 1 }
  3. // 该对象被加入到一个 Vue 实例中
  4. var vm = new Vue({
  5. data: data
  6. })
  7. // 获得这个实例上的 property
  8. // 返回源数据中对应的字段
  9. vm.a == data.a // => true
  10. // 设置 property 也会影响到原始数据
  11. vm.a = 2
  12. data.a // => 2
  13. // ……反之亦然
  14. data.a = 3
  15. vm.a // => 3

声明式编程

命令式编程、声明式编程、函数式编程:TODO

Vue 采用声明式编程。

Vue 的 MVVM

TODO

一个完整样例

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta >
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  8. <title>HelloVuejs</title>
  9. </head>
  10. <body>
  11. <div id="app">
  12. <h2>{{ message }}</h2>
  13. <p>{{ name }}</p>
  14. </div>
  15. <script>
  16. const app = new Vue({
  17. el: "#app", // 用于挂载要管理的元素
  18. data: { // 定义数据
  19. message: "HelloVuejs",
  20. name: "vue"
  21. }
  22. })
  23. </script>
  24. </body>
  25. </html>

在浏览器控制台中,改变 vue 对象的 message 值,页面显示也随之改变。

{{ message }} 表示将变量 message 输出到标签 h2 中。

所有的 vue 语法都必须在 vue 对象挂载的 div 元素中,如果在 div 元素外使用是不生效的。
el: "#app" 表示将 id 为 app 的 div 挂载在 vue 对象上,data 表示变量对象。

插值

文本插值

数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:

  1. <span>Message: {{ msg }}</span>

Mustache 标签将会被替代为对应数据对象上 msg 的值。无论何时,绑定的数据对象上 msg 发生了改变,插值处的内容都会更新。

Mustache 是胡须的意思,因为 {{}} 像胡须。
Mustache 语法又叫大括号语法。

修改插值符号

  1. let vm = new Vue({
  2. el: '#app',
  3. delimiters: ['{[', ']}'] // 修改插值符号
  4. })

v-once 指令

通过使用 v-once 指令,能执行一次性地插值,当数据改变时,插值处的内容不会更新。

  1. <div id="app">
  2. <h2>{{ message }}</h2>
  3. <h2 v-once>{{ message }}</h2>
  4. </div>

上面代码中 {{ message }} 的 message 修改后,第一个 h2 标签数据会自动改变,第二个 h2 不会。

原始 HTML(v-html 指令)

双大括号会将数据解释为普通文本,而非 HTML 代码。

如果要输出真正的 HTML,可以使用 v-html 指令:

  1. <p>Using mustaches: {{ rawHtml }}</p>
  2. <p>Using v-html directive: <span v-html="rawHtml"></span></p>

image.png

这个 span 的内容将会被替换成为 property 值 rawHtml,直接作为 HTML,即会忽略解析 property 值中的数据绑定。

:::warning 注意:动态渲染的任意 HTML 都可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。 :::

v-text 指令

v-text 会覆盖 Dom 元素中的数据,相当于 js 的 innerHTML 方法。

  1. <div id="app">
  2. <h2>不使用v-text</h2>
  3. <h2>{{ message }},啧啧啧</h2>
  4. <h2>使用v-text,以文本形式显示,会覆盖</h2>
  5. <h2 v-text="message">,啧啧啧</h2>
  6. </div>
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  8. <script>
  9. const app = new Vue({
  10. el: "#app",
  11. data: {
  12. message: "你好啊"
  13. }
  14. })
  15. </script>

如图所示,使用 {{ message }} 是拼接变量和字符串,而是用 v-text 是直接覆盖字符串内容:
image.png

v-pre 指令

有时候我们期望直接输出 {{ message }} 这样的字符串,而不是被 {{}} 语法转化的 message 的变量值,此时我们可以使用v-pre标签。

  1. <div id="app">
  2. <h2>不使用v-pre</h2>
  3. <h2>{{ message }}</h2>
  4. <h2>使用v-pre,不会解析</h2>
  5. <h2 v-pre>{{message}}</h2>
  6. </div>
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  8. <script>
  9. const app = new Vue({
  10. el: "#app",
  11. data: {
  12. message: "你好啊"
  13. }
  14. })
  15. </script>

结果:
image.png

使用 JavaScript 表达式

对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。

  1. {{ number + 1 }}
  2. {{ ok ? 'YES' : 'NO' }}
  3. {{ message.split('').reverse().join('') }}

注意:每个绑定都只能包含单个表达式,所以下面的例子都不会生效:

  1. <!-- 这是语句,不是表达式 -->
  2. {{ var a = 1 }}
  3. <!-- 流控制也不会生效,请使用三元表达式 -->
  4. {{ if (ok) { return message } }}

:::warning 模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不应该在模板表达式中试图访问用户定义的全局变量。 :::

解决闪烁问题(v-cloak 指令)

这个指令保持在元素上直到关联实例结束编译。
和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。

示例:

  1. <div id="app">
  2. {{context}}
  3. </div>
  1. <script>
  2. let app = new Vue({
  3. el: '#app',
  4. data: {
  5. context:'互联网头部玩家钟爱的健身项目'
  6. }
  7. });
  8. </script>

执行结果:
3386108-cc0cda1d980b9586.webp

下面在 div 标签上加入 v-cloak 指令,并设置 CSS 样式:

  1. <div id="app" v-cloak>
  2. {{context}}
  3. </div>
  1. [v-cloak] {
  2. display: none;
  3. }

执行结果:
a7yhy-ksgis.gif

动态绑定属性(v-bind)

v-bind 的基本使用

Mustache 语法不能作用在 HTML attribute 上,遇到这种情况应该使用 v-bind 指令:

  1. <div v-bind:id="dynamicId"></div>

对于布尔 attribute (它们只要存在就意味着值为 true),v-bind 工作起来略有不同,在这个例子中:

  1. <button v-bind:disabled="isButtonDisabled">Button</button>

如果 isButtonDisabled 的值是 null、undefined 或 false,则 disabled attribute 甚至不会被包含在渲染出来的 <button> 元素中。

HTML attribute 中也可以使用 JavaScript 表达式:

  1. <div v-bind:id="'list-' + id"></div>

可以缩写为:

  1. <div :id="'list-' + id"></div>

绑定 class

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

对象语法

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

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

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

下面是一个样例:
image.png
a02bs-fwxpi.gif

可以在对象中传入更多字段来动态切换多个 class。

此外,v-bind:class 指令也可以与普通的 class 共存。当有如下模板:

  1. <div
  2. class="static"
  3. v-bind:class="{ active: isActive, 'text-danger': hasError }"
  4. ></div>

和如下 data:

  1. data: {
  2. isActive: true,
  3. hasError: false
  4. }

结果渲染为:

  1. <div class="static active"></div>

当 isActive 或者 hasError 变化时,class 列表将相应地更新。例如,如果 hasError 的值为 true,class 列表将变为 "static active text-danger"。

绑定的数据对象不必内联定义在模板里,而是可以抽出来:

  1. <div v-bind:class="classObject"></div>
  1. data: {
  2. classObject: {
  3. active: true,
  4. 'text-danger': false
  5. }
  6. }

渲染的结果和上面一样。

我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:

  1. <div v-bind:class="classObject"></div>
  1. data: {
  2. isActive: true,
  3. error: null
  4. },
  5. computed: {
  6. classObject: function () {
  7. return {
  8. active: this.isActive && !this.error,
  9. 'text-danger': this.error && this.error.type === 'fatal'
  10. }
  11. }
  12. }

数组语法

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

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

渲染为:

  1. <div class="active text-danger"></div>

在组件上使用 class 属性

当在一个自定义组件上使用 class property 时,这些 class 将被添加到该组件的根元素上面。这个元素上已经存在的 class 不会被覆盖。

一个 v-bind:class 和 v-for 结合的小样例

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge">
  7. <title>v-for 和 v-bind 结合的小样例</title>
  8. <style>
  9. .active{
  10. color:red;
  11. }
  12. </style>
  13. </head>
  14. <body>
  15. <div id="app">
  16. <ul>
  17. <li v-for="(item, index) in movies" :key="index" :class="{active:index===currentIndex}" @click="changeColor(index)" >{{index+"---"+item}}</li>
  18. </ul>
  19. </div>
  20. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  21. <script>
  22. const app = new Vue({
  23. el: "#app",
  24. data: {
  25. currentIndex: 0,
  26. movies: ["海王", "海贼王", "火影忍者", "复仇者联盟"]
  27. },
  28. methods: {
  29. changeColor(index){
  30. this.currentIndex = index
  31. }
  32. },
  33. })
  34. </script>
  35. </body>
  36. </html>

4.4-1.gif

绑定内联 style

对象语法

v-bind:style 的对象语法十分直观,看着非常像 CSS,但其实是一个 JavaScript 对象。

CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:

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

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

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

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

数组语法

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

  1. <div v-bind:style="[baseStyles, overridingStyles]"></div>

自动添加前缀

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

计算属性和监听器

计算属性的基本使用

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。
对于任何复杂逻辑,都应当使用计算属性。

现在有变量姓氏和名字,要得到完整的名字:

  1. <div id="app">
  2. <h2>{{ firstName+ " " + lastName }}</h2> <!-- 直接写表达式 -->
  3. <h2>{{ getFullName() }}</h2> <!-- 方法 -->
  4. <h2>{{ fullName }}</h2> <!-- 计算属性 -->
  5. </div>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  7. <script>
  8. const app = new Vue({
  9. el: "#app",
  10. data: {
  11. firstName: "skt t1",
  12. lastName: "faker"
  13. },
  14. computed: {
  15. fullName: function() {
  16. return this.firstName + " " + this.lastName
  17. }
  18. },
  19. methods: {
  20. getFullName() {
  21. return this.firstName + " " + this.lastName
  22. }
  23. }
  24. })
  25. </script>

在模板中可以像使用 data 一样使用计算属性。

计算属性 vs 方法

直接看代码,分别使用计算属性和方法获得 fullName 的值:

  1. <div id="app">
  2. <!-- methods,即使firstName和lastName没有改变,也需要再次执行 -->
  3. <h2>{{ getFullName() }}</h2>
  4. <h2>{{ getFullName() }}</h2>
  5. <h2>{{ getFullName() }}</h2>
  6. <h2>{{ getFullName() }}</h2>
  7. <!-- 计算属性有缓存,只有关联属性改变才会再次计算 -->
  8. <h2>{{ fullName }}</h2>
  9. <h2>{{ fullName }}</h2>
  10. <h2>{{ fullName }}</h2>
  11. <h2>{{ fullName }}</h2>
  12. </div>
  13. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  14. <script>
  15. const app = new Vue({
  16. el: "#app",
  17. data: {
  18. firstName: "skt t1",
  19. lastName: "faker"
  20. },
  21. computed: {
  22. fullName(){
  23. console.log("调用了计算属性 fullName");
  24. return this.firstName + " " + this.lastName
  25. }
  26. },
  27. methods: {
  28. getFullName(){
  29. console.log("调用了 getFullName");
  30. return this.firstName + " " + this.lastName
  31. }
  32. }
  33. })
  34. </script>

本例中分别使用方法和计算属性获取了四次 fullName,结果如图:
image.png
计算属性只调用了一次,而方法被调用了四次。

由此可见计算属性有缓存,在 this.firstName + " " + this.lastName 的属性不变的情况下,methods 调用了 4 次,而计算属性才调用了 1 次,性能上计算属性明显比 methods 好。

而且在改动 firstName 的情况下,计算属性只调用 1 次,methods 依然要调用 4 次:
image.png

计算属性的 getter、setter

计算属性默认只有 getter,不过在需要时也可以提供一个 setter:

  1. // ...
  2. computed: {
  3. fullName: {
  4. // getter
  5. get: function () {
  6. return this.firstName + ' ' + this.lastName
  7. },
  8. // setter
  9. set: function (newValue) {
  10. var names = newValue.split(' ')
  11. this.firstName = names[0]
  12. this.lastName = names[names.length - 1]
  13. }
  14. }
  15. }
  16. // ...

现在再运行 vm.fullName = 'John Doe' 时,setter 会被调用,vm.firstName 和 vm.lastName 也会相应地被更新。

计算属性 vs 监听器

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

细想一下这个例子:

  1. <div id="demo">{{ fullName }}</div>
  1. var vm = new Vue({
  2. el: '#demo',
  3. data: {
  4. firstName: 'Foo',
  5. lastName: 'Bar',
  6. fullName: 'Foo Bar'
  7. },
  8. watch: {
  9. firstName: function (val) {
  10. this.fullName = val + ' ' + this.lastName
  11. },
  12. lastName: function (val) {
  13. this.fullName = this.firstName + ' ' + val
  14. }
  15. }
  16. })

上面代码是命令式且重复的。

将它与计算属性的版本进行比较:

  1. var vm = new Vue({
  2. el: '#demo',
  3. data: {
  4. firstName: 'Foo',
  5. lastName: 'Bar'
  6. },
  7. computed: {
  8. fullName: function () {
  9. return this.firstName + ' ' + this.lastName
  10. }
  11. }
  12. })

好得多了,不是吗?

监听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。
当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

例如:

  1. <div id="watch-example">
  2. <p>
  3. Ask a yes/no question:
  4. <input v-model="question">
  5. </p>
  6. <p>{{ answer }}</p>
  7. </div>
  1. <!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 -->
  2. <!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 -->
  3. <script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
  4. <script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
  5. <script>
  6. var watchExampleVM = new Vue({
  7. el: '#watch-example',
  8. data: {
  9. question: '',
  10. answer: 'I cannot give you an answer until you ask a question!'
  11. },
  12. watch: {
  13. // 如果 `question` 发生改变,这个函数就会运行
  14. question: function (newQuestion, oldQuestion) {
  15. this.answer = 'Waiting for you to stop typing...'
  16. this.debouncedGetAnswer()
  17. }
  18. },
  19. created: function () {
  20. // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
  21. // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
  22. // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
  23. // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
  24. // 请参考:https://lodash.com/docs#debounce
  25. this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  26. },
  27. methods: {
  28. getAnswer: function () {
  29. if (this.question.indexOf('?') === -1) {
  30. this.answer = 'Questions usually contain a question mark. ;-)'
  31. return
  32. }
  33. this.answer = 'Thinking...'
  34. var vm = this
  35. axios.get('https://yesno.wtf/api')
  36. .then(function (response) {
  37. vm.answer = _.capitalize(response.data.answer)
  38. })
  39. .catch(function (error) {
  40. vm.answer = 'Error! Could not reach the API. ' + error
  41. })
  42. }
  43. }
  44. })
  45. </script>

结果:
动画.gif

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

条件判断与循环

条件判断(v-if、v-else、v-else-if)

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染:

  1. <h1 v-if="awesome">Vue is awesome!</h1>

也可以用 v-else 添加一个 “else 块”:

  1. <h1 v-if="awesome">Vue is awesome!</h1>
  2. <h1 v-else>Oh no 😢</h1>

v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。

v-else-if,顾名思义,充当 v-if 的 “else-if 块”,可以连续使用:

  1. <div v-if="type === 'A'">
  2. A
  3. </div>
  4. <div v-else-if="type === 'B'">
  5. B
  6. </div>
  7. <div v-else-if="type === 'C'">
  8. C
  9. </div>
  10. <div v-else>
  11. Not A/B/C
  12. </div>

类似于 v-else,v-else-if 也必须紧跟在带 v-if 或者 v-else-if 的元素之后。

在 <template> 元素上使用 v-if 条件渲染分组

因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。

  1. <template v-if="ok">
  2. <h1>Title</h1>
  3. <p>Paragraph 1</p>
  4. <p>Paragraph 2</p>
  5. </template>

用 key 管理可复用的元素

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
这么做除了使 Vue 变得非常快之外,还有其它一些好处。
例如,如果你允许用户在不同的登录方式之间切换:

  1. <template v-if="loginType === 'username'">
  2. <label>Username</label>
  3. <input placeholder="Enter your username">
  4. </template>
  5. <template v-else>
  6. <label>Email</label>
  7. <input placeholder="Enter your email address">
  8. </template>

那么在上面的代码中切换 loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input> 不会被替换掉,仅仅是替换了它的 placeholder。

动画.gif

这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达 “这两个元素是完全独立的,不要复用它们”。
只需添加一个具有唯一值的 key attribute 即可:

  1. <template v-if="loginType === 'username'">
  2. <label>Username</label>
  3. <input placeholder="Enter your username" key="username-input">
  4. </template>
  5. <template v-else>
  6. <label>Email</label>
  7. <input placeholder="Enter your email address" key="email-input">
  8. </template>

现在,每次切换时,输入框都将被重新渲染:
动画.gif

注意,<label> 元素仍然会被高效地复用,因为它们没有添加 key attribute。

v-show 指令

另一个用于根据条件展示元素的选项是 v-show 指令。用法大致一样:

  1. <h1 v-show="ok">Hello!</h1>

不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS property display。

:::warning 注意,v-show 不支持 <template> 元素,也不支持 v-else。 :::

v-if vs v-show

v-if 是 “真正” 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。

相比之下,v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。

一般来说,**v-if** 有更高的切换开销,而 **v-show** 有更高的初始渲染开销。
因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。

循环(v-for)

我们可以用 v-for 指令基于一个数组来渲染一个列表。

v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。

  1. <ul id="example-1">
  2. <li v-for="item in items" :key="item.message">
  3. {{ item.message }}
  4. </li>
  5. </ul>
  1. var example1 = new Vue({
  2. el: '#example-1',
  3. data: {
  4. items: [
  5. { message: 'Foo' },
  6. { message: 'Bar' }
  7. ]
  8. }
  9. })

结果:
image.png

在 v-for 块中,我们可以访问所有父作用域的 property。v-for 还支持一个可选的第二个参数,即当前项的索引。

  1. <ul id="example-2">
  2. <li v-for="(item, index) in items">
  3. {{ parentMessage }} - {{ index }} - {{ item.message }}
  4. </li>
  5. </ul>
  1. var example2 = new Vue({
  2. el: '#example-2',
  3. data: {
  4. parentMessage: 'Parent',
  5. items: [
  6. { message: 'Foo' },
  7. { message: 'Bar' }
  8. ]
  9. }
  10. })

结果:
image.png

也可以用 of 替代 in 作为分隔符,因为它更接近 JavaScript 迭代器的语法:

  1. <div v-for="item of items"></div>

在 v-for 里使用对象

也可以用 v-for 来遍历一个对象的 property:

  1. <ul id="v-for-object" class="demo">
  2. <li v-for="value in object">
  3. {{ value }}
  4. </li>
  5. </ul>
  1. new Vue({
  2. el: '#v-for-object',
  3. data: {
  4. object: {
  5. title: 'How to do lists in Vue',
  6. author: 'Jane Doe',
  7. publishedAt: '2016-04-10'
  8. }
  9. }
  10. })

结果:
image.png

也可以提供第二个的参数为 property 名称(也就是键名):

  1. <div v-for="(value, name) in object">
  2. {{ name }}: {{ value }}
  3. </div>

image.png

还可以用第三个参数作为索引:

  1. <div v-for="(value, name, index) in object">
  2. {{ index }}. {{ name }}: {{ value }}
  3. </div>

image.png

在遍历对象时,会按 Object.keys() 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。

避免 v-if 和 v-for 一起使用

:::warning 永远不要把 v-if 和 v-for 同时用在同一个元素上。 :::

详解

当 Vue 处理指令时,v-for 比 v-if 具有更高的优先级,所以这个模板:

  1. <ul>
  2. <li
  3. v-for="user in users"
  4. v-if="user.isActive"
  5. :key="user.id"
  6. >
  7. {{ user.name }}
  8. </li>
  9. </ul>

将会经过如下运算:

  1. this.users.map(function (user) {
  2. if (user.isActive) {
  3. return user.name
  4. }
  5. })

因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。

通过将其更换为在如下的一个计算属性上遍历:

  1. computed: {
  2. activeUsers: function () {
  3. return this.users.filter(function (user) {
  4. return user.isActive
  5. })
  6. }
  7. }
  1. <ul>
  2. <li
  3. v-for="user in activeUsers"
  4. :key="user.id"
  5. >
  6. {{ user.name }}
  7. </li>
  8. </ul>

我们将会获得如下好处:

  • 过滤后的列表只会在 users 数组发生相关变化时才被重新运算,过滤更高效。
  • 使用 v-for="user in activeUsers" 之后,我们在渲染的时候只遍历活跃用户,渲染更高效。
  • 解耦渲染层的逻辑,可维护性 (对逻辑的更改和扩展) 更强。

为了获得同样的好处,我们也可以把:

  1. <ul>
  2. <li
  3. v-for="user in users"
  4. v-if="shouldShowUsers"
  5. :key="user.id"
  6. >
  7. {{ user.name }}
  8. </li>
  9. </ul>

更新为:

  1. <ul v-if="shouldShowUsers">
  2. <li
  3. v-for="user in users"
  4. :key="user.id"
  5. >
  6. {{ user.name }}
  7. </li>
  8. </ul>

通过将 v-if 移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers 为否的时候运算 v-for。

反例

  1. <ul>
  2. <li
  3. v-for="user in users"
  4. v-if="user.isActive"
  5. :key="user.id"
  6. >
  7. {{ user.name }}
  8. </li>
  9. </ul>
  1. <ul>
  2. <li
  3. v-for="user in users"
  4. v-if="shouldShowUsers"
  5. :key="user.id"
  6. >
  7. {{ user.name }}
  8. </li>
  9. </ul>

好例子

  1. <ul>
  2. <li
  3. v-for="user in activeUsers"
  4. :key="user.id"
  5. >
  6. {{ user.name }}
  7. </li>
  8. </ul>
  1. <ul v-if="shouldShowUsers">
  2. <li
  3. v-for="user in users"
  4. :key="user.id"
  5. >
  6. {{ user.name }}
  7. </li>
  8. </ul>

维护状态

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用 “就地更新” 的策略。
如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。

这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key attribute:

  1. <div v-for="item in items" v-bind:key="item.id">
  2. <!-- 内容 -->
  3. </div>

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

不要使用对象或数组之类的非基本类型值作为 v-for 的 key。请用字符串或数值类型的值。

在 <template> 上使用 v-for

类似于 v-if,也可以利用带有 v-for 的 <template> 来循环渲染一段包含多个元素的内容。比如:

  1. <ul>
  2. <template v-for="item in items">
  3. <li>{{ item.msg }}</li>
  4. <li class="divider" role="presentation"></li>
  5. </template>
  6. </ul>

数组更新检测

变更方法

Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

替换数组

变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()、concat() 和 slice()。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:

  1. example1.items = example1.items.filter(function (item) {
  2. return item.message.match(/Foo/)
  3. })

你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。

检测变化的注意事项

由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。
尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。

对于数组

Vue 不能检测以下数组的变动:

  1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength

举个例子:

  1. var vm = new Vue({
  2. data: {
  3. items: ['a', 'b', 'c']
  4. }
  5. })
  6. vm.items[1] = 'x' // 不是响应性的
  7. vm.items.length = 2 // 不是响应性的

为了解决第一类问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:

  1. // Vue.set
  2. Vue.set(vm.items, indexOfItem, newValue)
  1. // Array.prototype.splice
  2. vm.items.splice(indexOfItem, 1, newValue)

你也可以使用 vm.$set 实例方法,该方法是全局方法 Vue.set 的一个别名:

  1. vm.$set(vm.items, indexOfItem, newValue)

为了解决第二类问题,你可以使用 splice:

  1. vm.items.splice(newLength)

对于对象

Vue 无法检测 property 的添加或移除。
由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
例如:

  1. var vm = new Vue({
  2. data:{
  3. a:1
  4. }
  5. })
  6. // `vm.a` 是响应式的
  7. vm.b = 2
  8. // `vm.b` 是非响应式的

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:

  1. Vue.set(vm.someObject, 'b', 2)

您还可以使用 vm.$set 实例方法,这也是全局 Vue.set 方法的别名:

  1. this.$set(this.someObject,'b',2)

有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。

  1. // 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
  2. this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

事件处理(v-on)

可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。

v-on 的基本使用

使用 v-on:click 给 button 绑定监听事件以及回调函数,@ 是 v-on: 的语法糖,也就是简写也可以使用@click。

  1. <div id="example-1">
  2. <button v-on:click="counter += 1">Add 1</button>
  3. <p>The button above has been clicked {{ counter }} times.</p>
  4. </div>
  1. var example1 = new Vue({
  2. el: '#example-1',
  3. data: {
  4. counter: 0
  5. }
  6. })

结果:
动画.gif

v-on 的参数传递

  1. <div id="app">
  2. <button @click="btnClick">按钮1</button>
  3. <button @click="btnClick()">按钮2</button>
  4. <button @click="btnClick2(123)">按钮3</button>
  5. <button @click="btnClick3($event, 123)">按钮6</button>
  6. </div>
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  8. <script>
  9. const app = new Vue({
  10. el: "#app",
  11. methods: {
  12. btnClick(){
  13. console.log("点击XXX");
  14. },
  15. btnClick2(value) {
  16. console.log(value+"----------");
  17. },
  18. btnClick3(event,value) {
  19. console.log(event+"----------"+value);
  20. }
  21. }
  22. })
  23. </script>

总结:

  1. 如果没加括号,形参是原始 DOM 事件对象。
  2. 如果加了括号,但里面没写参数,则没有形参。
  3. 如果加了括号,并且有参数,则形参就是这些参数。
  4. 如果函数中既要用到自己传的参数,还要用到原始 DOM 事件对象,可以将 $event 传入。

事件修饰符

在事件处理程序中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。

为了解决这个问题,Vue.js 为 v-on 提供了事件修饰符。

.stop 修饰符

事件冒泡:如果有两个嵌套的元素,它们都有点击事件,此时点击内层元素时,先会触发内层元素的事件处理程序,再触发外层元素的事件处理程序。(因为内层元素被点击,外层元素肯定也被点击了)。

而很多时候我们只需要让内层元素触发事件,而禁用外层元素的事件,对于这个需求,vue 提供了简单的方式:在内层元素的 @click 后面加上 .stop,即 @click.stop="function_name()"。

  1. <!-- 阻止单击事件继续传播 -->
  2. <a v-on:click.stop="doThis"></a>

.prevent 修饰符

.prevent 修饰符会阻止默认的事件,相当于调用 event.preventDefault()。

关于 event.preventDefault() 的介绍:

比如在一个 form 表单中,点击 submit 按钮时,表单数据会被自动提交。
此时可以为表单加上 @onclick.prevent="function_name()" 禁用默认的事件,同时指定一个新事件。

  1. <!-- 提交事件不再重载页面 -->
  2. <form v-on:submit.prevent="onSubmit"></form>

.self 修饰符

  1. <!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
  2. <!-- 即事件不是从内部元素触发的 -->
  3. <div v-on:click.self="doThat">...</div>

.once 修饰符

使事件只能触发一次。

  1. <a v-on:click.once="doThis"></a>

:::warning 注:不像其它只能对原生的 DOM 事件起作用的修饰符,.once 修饰符还能被用到自定义的组件事件上。 :::

只用修饰符

可以只用修饰符而不必指定新事件,比如:

  1. <!-- 只有修饰符 -->
  2. <form v-on:submit.prevent></form>

此时只会禁用表单的默认提交事件,而不会绑定一个新事件。

修饰符串联

  1. <!-- 修饰符可以串联 -->
  2. <a v-on:click.stop.prevent="doThat"></a>

:::warning 使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。 :::

其他修饰符

除了上述修饰符,还有:

  • .capture
  • .passive

具体内容可以查看文档:

按键修饰符

对于这样一个输入框:

  1. <input type="text" @keyup="function_name()"

每次输入一个字符时,都会触发 keyup 事件,而很多时候我们只需要在用户按回车时让它触发事件,这时就可以使用 .enter 修饰符:

  1. <input type="text" @keyup.enter="function_name()"

:::info 按键事件有 keyup,keydown 等,开发时常用 keyup。 :::

除了 .enter 外,还有很多其他此类修饰符,见名知义:

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

除了以上这些常用的按键修饰符,实际上只要是 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 都可以使用:

  1. <input v-on:keyup.page-down="onPageDown">

另外,按键码也是支持的:

  1. <input v-on:keyup.13="submit">

:::warning 但需要注意的是,按键码的事件用法已经被废弃了并可能不会被最新的浏览器支持。
所以最好不要使用按键码,而是用按键名。 :::

系统修饰键

基本用法

可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器:

  • .ctrl
  • .alt
  • .shift
  • .meta

:::info 在 Mac 系统键盘上,meta 对应 command 键 (⌘);
在 Windows 系统键盘上, meta 对应 Windows 徽标键 (⊞);
在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆);
在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”;
在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。 :::

例如:

  1. <!-- Alt + C -->
  2. <input v-on:keyup.alt.67="clear">
  3. <!-- Ctrl + Click -->
  4. <div v-on:click.ctrl="doSomething">Do something</div>

:::warning 注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。
换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。
如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17。 :::

.exact 修饰符

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

  1. <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
  2. <button v-on:click.ctrl="onClick">A</button>
  3. <!-- 有且只有 Ctrl 被按下的时候才触发 -->
  4. <button v-on:click.ctrl.exact="onCtrlClick">A</button>
  5. <!-- 没有任何系统修饰符被按下的时候才触发 -->
  6. <button v-on:click.exact="onClick">A</button>

鼠标按钮修饰符

  • .left
  • .right
  • .middle

这些修饰符会限制处理函数仅响应特定的鼠标按钮。

表单输入绑定(v-model)

v-model 的基本使用

v-model:双向绑定。

  1. <div id="app">
  2. <input type="text" v-model="message">
  3. <p>{{ message }}</p>
  4. </div>
  5. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  6. <script>
  7. const app = new Vue({
  8. el: "#app",
  9. data: {
  10. message: "content"
  11. }
  12. })
  13. </script>

当修改输入框中的内容时,Vue 对象中的 message 也会同步修改:
动画.gif

当修改 Vue 对象中的 message 时,输入框中的内容也会同步修改:
动画.gif

v-model 的原理

下面不用 v-model 实现双向绑定,而是用 v-bind + v-on 实现,就能明白 v-model 的原理了:

  1. <div id="app">
  2. <!-- $event.target.value 获取输入框的值 -->
  3. <input type="text" :value="message" @input="valueChange($event.target.value)">
  4. <p>{{ message }}</p>
  5. </div>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  7. <script>
  8. const app = new Vue({
  9. el: "#app",
  10. data: {
  11. message: "content"
  12. },
  13. methods: {
  14. valueChange(value) {
  15. this.message = value
  16. }
  17. }
  18. })
  19. </script>

使用 v-bind 给输入框的 value 绑定 message,此时 message 改变,输入框的值也会改变。
但是改变输入框的值并不会改变 message 的值,此时需要一个 v-on 绑定一个方法,监听事件,当输入框的值改变的时候,将最新的值赋值给 message 即可。

v-model 结合 radio 类型

  1. <div id="app">
  2. <label for="male">
  3. <input type="radio" id="male" name="sex" value="男" v-model="sex">男
  4. </label>
  5. <label for="female">
  6. <input type="radio" id="female" name="sex" value="女" v-model="sex">女
  7. </label>
  8. <div>你选择的性别是:{{ sex }}</div>
  9. </div>
  10. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  11. <script>
  12. const app = new Vue({
  13. el: "#app",
  14. data: {
  15. sex: "男"
  16. },
  17. })
  18. </script>

结果:
动画.gif

:::info radio 按钮的 name 属性是互斥的,如果使用 v-model,可以不使用 name 属性就实现互斥。 :::

v-model 结合 checkbox 类型

checkbox 可以结合 v-model 做单选框,也可以多选框。

  1. <div id="app">
  2. <!-- checkbox 单选框 -->
  3. <h2>单选框</h2>
  4. <label for="agree">
  5. <input type="checkbox" id="agree" v-model="isAgree">同意协议
  6. </label>
  7. <div>你选择的结果是:{{ isAgree }}</div>
  8. <button :disabled="!isAgree">下一步</button>
  9. <!-- checkbox多选框 -->
  10. <h2>多选框</h2>
  11. <label v-for="(item, index) in oriHobbies" :key="index" :for="item">
  12. <input type="checkbox" name="hobby" :value="item" :id="item" v-model="hobbies">{{ item }}
  13. </label>
  14. <div>你的爱好是:{{ hobbies }}</div>
  15. </div>
  16. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  17. <script>
  18. const app = new Vue({
  19. el: "#app",
  20. data: {
  21. isAgree: false,
  22. hobbies: [],
  23. oriHobbies: ["篮球", "足球", "羽毛球", "乒乓球"]
  24. }
  25. })
  26. </script>

结果:
动画.gif

v-model 结合 select 标签

  1. <div id="app">
  2. <!-- select 单选 -->
  3. <h2>select 单选</h2>
  4. <select name="fruit" v-model="fruit">
  5. <option value="苹果">苹果</option>
  6. <option value="香蕉">香蕉</option>
  7. <option value="西瓜">西瓜</option>
  8. </select>
  9. <p>你选择的水果是:{{ fruit }}</p>
  10. <!-- select 多选 -->
  11. <h2>select 多选</h2>
  12. <select name="fruits" v-model="fruits" multiple>
  13. <option value="苹果">苹果</option>
  14. <option value="香蕉">香蕉</option>
  15. <option value="西瓜">西瓜</option>
  16. </select>
  17. <p>你选择的水果是:{{ fruits }}</p>
  18. </div>
  19. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  20. <script>
  21. const app = new Vue({
  22. el: "#app",
  23. data: {
  24. fruit: "苹果",
  25. fruits: []
  26. }
  27. })
  28. </script>

结果:
动画.gif

修饰符

.lazy 修饰符

在默认情况下,v-model 在每次 input 事件触发后将输入框的值与数据进行同步。
你可以添加 .lazy 修饰符,从而转为在 change 事件之后进行同步:

  1. <!-- 在“change”时而非“input”时更新 -->
  2. <input v-model.lazy="msg">

.number 修饰符

如果想自动将用户的输入值转为数值类型,可以给 v-model 添加 .number 修饰符:

  1. <input v-model.number="age" type="number">

这通常很有用,因为即使在 type="number" 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat() 解析,则会返回原始的值。

.trim 修饰符

如果要自动过滤用户输入的首尾空白字符,可以给 v-model 添加 trim 修饰符:

  1. <input v-model.trim="msg">

Vue 实例的生命周期

基本介绍

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

比如 created 钩子可以用来在一个实例被创建之后执行代码:

  1. new Vue({
  2. data: {
  3. a: 1
  4. },
  5. created: function () {
  6. console.log('a is: ' + this.a) // `this` 指向 vm 实例
  7. }
  8. })
  9. // => "a is: 1"

也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 destroyed 等。

:::warning 注意:生命周期钩子的 this 上下文指向调用它的 Vue 实例。不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch('a', newValue => this.myMethod())。因为箭头函数并没有 this,this 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefined 或 Uncaught TypeError: this.myMethod is not a function 之类的错误。 ::: :::info 但是 ES6 中函数定义(function() {})的简化写法是可以的,比如 mounted() {}。 :::

生命周期图示

image.png

  1. <div id="app">
  2. <h1>测试生命周期</h1>
  3. <div>{{ msg }}</div>
  4. <h3>测试beforeUpdate和update两个钩子函数</h3>
  5. <button @click="handlerUpdate">更新数据</button>
  6. </div>
  7. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  8. <script>
  9. var app = new Vue({
  10. el: "#app",
  11. data: {
  12. msg: "12345"
  13. },
  14. methods: {
  15. handlerUpdate: function () {
  16. this.msg = this.msg.split("").reverse().join("");
  17. },
  18. },
  19. beforeCreate: function () {
  20. console.log("调用了beforeCreate钩子函数");
  21. },
  22. created: function () {
  23. console.log("调用了created钩子函数");
  24. },
  25. beforeMount: function () {
  26. console.log('调用了beforeMount钩子函数')
  27. },
  28. mounted: function () {
  29. console.log('调用了mounted钩子函数')
  30. },
  31. beforeUpdate: function () {
  32. console.log("调用了beforeUpdate钩子函数")
  33. },
  34. updated: function () {
  35. console.log("调用了updated钩子函数");
  36. },
  37. beforeDestroy: function () {
  38. console.log("调用了beforeDestroy钩子函数")
  39. },
  40. destroyed: function () {
  41. console.log("调用了destroyed钩子函数");
  42. },
  43. });
  44. </script>

运行结果:
image.png

初始化页面时依次调用:

  1. beforeCreate 钩子函数
  2. created 钩子函数
  3. beforeMount 钩子函数
  4. mounted 钩子函数

点击更新数据后,12345变成54321,此时调用:

  1. beforeUpdate 钩子函数
  2. updated 钩子函数

打开控制台,输入app.$destroy()主动销毁 Vue 实例,此时调用:

  1. beforeDestroy 钩子函数
  2. destroyed 钩子函数

beforeCreate 之前

初始化钩子函数和生命周期。

beforeCreate 和 created 之间

进行数据观测 (data observer) ,也就是在这个时候开始监控 data 中的数据变化了,同时初始化事件。

created 和 beforeMount 之间

image.png

el

有 el 时:

  1. new Vue({
  2. el: '#app',
  3. beforeCreate: function () {
  4. console.log('调用了beforeCreat钩子函数')
  5. },
  6. created: function () {
  7. console.log('调用了created钩子函数')
  8. },
  9. beforeMount: function () {
  10. console.log('调用了beforeMount钩子函数')
  11. },
  12. mounted: function () {
  13. console.log('调用了mounted钩子函数')
  14. }
  15. })

结果:
image.png

无 el 时:

  1. new Vue({
  2. beforeCreate: function () {
  3. console.log('调用了beforeCreat钩子函数')
  4. },
  5. created: function () {
  6. console.log('调用了created钩子函数')
  7. },
  8. beforeMount: function () {
  9. console.log('调用了beforeMount钩子函数')
  10. },
  11. mounted: function () {
  12. console.log('调用了mounted钩子函数')
  13. }
  14. })

结果:
image.png

证明没有 el,则停止编译,也意味着暂时停止了生命周期,生命周期到 created 钩子函数就结束了。

而不加 el,但是手动执行 vm.$mount(el) 方法的话,也能够使暂停的生命周期进行下去,例如:

  1. let app = new Vue({
  2. beforeCreate: function () {
  3. console.log('调用了beforeCreat钩子函数')
  4. },
  5. created: function () {
  6. console.log('调用了created钩子函数')
  7. },
  8. beforeMount: function () {
  9. console.log('调用了beforeMount钩子函数')
  10. },
  11. mounted: function () {
  12. console.log('调用了mounted钩子函数')
  13. }
  14. })
  15. app.$mount('#app')

结果:
image.png

template

image.png

同时使用 template 和 HTML,查看优先级:

  1. <h1>测试 template 和 HTML 的优先级</h1>
  2. <div id="app">
  3. <p>HTML 优先</p>
  4. </div>
  5. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  6. <script>
  7. let app = new Vue({
  8. el: "#app",
  9. data: {
  10. msg: "template 优先"
  11. },
  12. template: "<p>{{ msg }}</p>"
  13. });
  14. </script>

结果:
image.png

结论:

  1. 如果 Vue 实例对象中有 template 参数选项,则将其作为模板编译。
  2. 如果没有 template 参数选项,则将外部的 HTML 作为模板编译,也就是说,template 参数选项的优先级要比外部的 HTML 高。
  3. 如果 template 参数选项和外部 HTML 都没有,则报错。

beforeMount 和 mounted 之间

image.png

beforeMount 之前,已经完成了 data 的初始化和模板的编译,但 data 中的数据还没有填入模板中。
这里可以在渲染前最后一次更改数据,不会触发其他的钩子函数。一般可以在这里做初始数据的获取。

beforeMount 和 mounted 之间,数据会被填入模板中。

beforeUpdate 和 updated

image.png

在 Vue 中,修改数据会导致重新渲染,依次调用 beforeUpdate 和 updated 钩子函数。

:::warning 注意:只有写入模板的数据才会被追踪,如果待修改的数据没有载入模板中,不会调用这两个钩子函数。 :::

beforeDestroy 和 destroyed

image.png

Vue 实例被销毁前会调用 beforeDestroy。
销毁(解除绑定,销毁子组件、事件监听器)后,会调用 destroyed。

:::info 销毁后,DOM 元素依旧存在,只是不再受 Vue 控制。 :::

每个钩子函数内适合干什么

  • beforecreate:可以在这加 loading 事件。
  • created:在这结束 loading,还做一些初始数据的获取。
  • mounted:在这发起 Ajax 请求,拿回数据,配合路由钩子做一些事情。
  • beforeDestroy:一般在这里善后 —— 清除计时器、清除非指令绑定的事件等等。
  • destroyed:当前组件已被删除,清空相关内容。

组件化开发

基本使用

分为三步:

  1. 创建组件
  2. 注册组件
  3. 使用组件
  1. <div id="app">
  2. <!-- 3. 使用组件 -->
  3. <my-cpn></my-cpn> <!-- 使用全局组件 -->
  4. <cpnc></cpnc> <!-- 使用局部组件 -->
  5. </div>
  6. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  7. <script>
  8. // 1. 创建组件
  9. const cpnc = Vue.extend({
  10. template: `
  11. <div>
  12. <h2>标题</h2>
  13. <p>内容1...</p>
  14. <p>内容2...</p>
  15. </div>`
  16. })
  17. // 2. 注册组件
  18. Vue.component('my-cpn', cpnc) // 注册全局组件
  19. const app = new Vue({
  20. el: "#app",
  21. data: {},
  22. components: { // 注册局部组件
  23. cpnc: cpnc
  24. }
  25. })
  26. </script>

结果:
image.png

组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是 my-cpn。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用: <my-cpn></my-cpn>。

:::warning 注意:每个组件必须只有一个根元素。 :::

组件名

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

  1. kebab-case
  2. PascalCase

使用 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> 都是可接受的。

:::warning 注意:尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。 :::

全局组件和局部组件

全局组件可以在多个 Vue 实例中使用,使用 Vue.component('my-cpn', cpnc) 方式注册。

局部组件只能在当前 Vue 实例挂载的元素中使用,类似于局部变量,有块级作用域。
注册方式:

  1. const app = new Vue({
  2. el: "#app",
  3. components: { // 注册局部组件
  4. cpnc: cpnc
  5. }
  6. })

注册组件的语法糖

  1. <div id="app">
  2. <cpn1></cpn1>
  3. <cpn2></cpn2>
  4. </div>
  5. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  6. <script>
  7. // 注册全局组件
  8. Vue.component('cpn1', {
  9. template: `
  10. <div>
  11. <h2>全局组件</h2>
  12. <p>全局组件</p>
  13. </div>`
  14. })
  15. const app = new Vue({
  16. el: "#app",
  17. components: { // 注册局部组件
  18. cpn2: {
  19. template: `
  20. <div>
  21. <h2>局部组件</h2>
  22. <p>局部组件</p>
  23. </div>`
  24. }
  25. }
  26. })
  27. </script>

组件模板的分离写法

在 script 标签中写模板

  1. <script type="text/x-template" id="cpn1">
  2. <div>
  3. <h2>组件模板的分离写法</h2>
  4. <p>script标签注意类型是text/x-template</p>
  5. </div>
  6. </script>

:::warning 注意:

  1. script 标签的类型是 text/x-template。
  2. 模板不能写在 Vue 实例或其他组件绑定的元素内。 :::

在 template 标签中写模板

  1. <template id="cpn2">
  2. <div>
  3. <h2>组件模板的分离写法</h2>
  4. <p>template标签</p>
  5. </div>
  6. </template>

:::warning 注意:模板不能写在 Vue 实例或其他组件绑定的元素内。 :::

如何使用上述两种模板

  1. const app = new Vue({
  2. el: "#app",
  3. components: {
  4. cpn1: {
  5. template: '#cpn1' // 通过 id 使用
  6. },
  7. cpn2: {
  8. template: '#cpn2'
  9. }
  10. }
  11. })

解析 DOM 模板时的注意事项

有些 HTML 元素,诸如 <ul>、<ol>、<table> 和 <select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>、<tr> 和 <option>,只能出现在其它某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

  1. <table>
  2. <blog-post-row></blog-post-row>
  3. </table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 is attribute 给了我们一个变通的办法:

  1. <table>
  2. <tr is="blog-post-row"></tr>
  3. </table>

需要注意的是如果我们从以下来源使用模板的话,这条限制是不存在的:

  • 字符串 (例如:template: '...')
  • 单文件组件 (.vue)
  • <script type="text/x-template">

组件的属性

组件就是一个 vue 实例,vue 实例的属性,组件也可以有,例如 data、methods、computed 等。

但需要注意的是,组件中 data 不能再像这样直接提供一个对象:

  1. data: {
  2. count: 0
  3. }

而是要写一个函数,函数返回一个对象:

  1. data: function () {
  2. return {
  3. count: 0
  4. }
  5. }

原因是,一个组件可能会在多个地方复用,它们共用 data。而如果将 data 写成函数的形式,则每个复用的地方都有自己的 data,它们互不干扰。

组件嵌套

组件的组织

通常一个应用会以一棵嵌套的组件树的形式来组织:
image.png
例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

父子组件嵌套示例

  1. <div id="app">
  2. <cpn-parent></cpn-parent>
  3. </div>
  4. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  5. <script>
  6. // 创建子组件
  7. const cpnChild = Vue.extend({
  8. template: `
  9. <div>
  10. <h2>子组件标题</h2>
  11. <p>子组件段落</p>
  12. </div>`
  13. })
  14. // 创建父组件,父组件使用了子组件
  15. const cpnParent = Vue.extend({
  16. template: `
  17. <div>
  18. <h2>父组件标题</h2>
  19. <p>父组件段落</p>
  20. <cpn-child></cpn-child>
  21. </div>`,
  22. components: {
  23. cpnChild
  24. }
  25. })
  26. const app = new Vue({
  27. el: "#app",
  28. components: {
  29. cpnParent
  30. }
  31. })
  32. </script>

父组件直接访问子组件($refs)

尽管存在 prop 和事件,但有时你可能仍然需要直接访问子组件。为此,可以使用 ref 属性为子组件或 HTML 元素指定引用 ID。

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <cpn ref="childCpn"></cpn> <!-- 子组件上写 ref 属性 -->
  4. <button @click="btnClick">按钮</button>
  5. </div>
  6. <!-- 子组件 -->
  7. <template id="cpn">
  8. <div>
  9. <p>我是子组件</p>
  10. </div>
  11. </template>
  12. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  13. <script>
  14. const cpn = {
  15. template: "#cpn",
  16. data() {
  17. return {
  18. message: "我是子组件的 message"
  19. }
  20. },
  21. };
  22. const app = new Vue({
  23. el: "#app",
  24. components: {
  25. cpn
  26. },
  27. methods: {
  28. btnClick() {
  29. alert(this.$refs.childCpn.message); // 通过 $refs 获取子组件的数据
  30. }
  31. }
  32. })
  33. </script>

:::info Vue 2.x 中还有 $children 可以用来访问子组件,但 Vue 3.x 中已将其移除,不再支持。 :::

子组件直接访问父组件($parent)

子组件中使用 $parent 可以获取到其父组件对象。

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <child-cpn></child-cpn>
  4. </div>
  5. <!-- 子组件 -->
  6. <template id="child-cpn">
  7. <button @click="btnClick">子组件中的按钮</button>
  8. </template>
  9. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  10. <script>
  11. const childCpn = {
  12. template: "#child-cpn",
  13. methods: {
  14. btnClick() {
  15. alert(this.$parent.message); // 用 $parent 获取父组件的数据
  16. }
  17. }
  18. };
  19. const app = new Vue({
  20. el: "#app",
  21. components: {
  22. childCpn
  23. },
  24. data: {
  25. message: "我是父组件的 message"
  26. }
  27. })
  28. </script>

props 属性

使用 props 可以实现父组件到子组件的数据传递。

基本示例

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <cpn :c-user="user"></cpn> <!-- 将父组件的数据传递给子组件 -->
  4. </div>
  5. <!-- 子组件 -->
  6. <template id="cpn">
  7. <h2>{{ cUser }}</h2>
  8. </template>
  9. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  10. <script>
  11. const cpn = {
  12. template: "#cpn",
  13. props: {
  14. cUser: Object,
  15. }
  16. };
  17. const app = new Vue({
  18. el: "#app",
  19. components: {
  20. cpn
  21. },
  22. data: {
  23. user: 'MT'
  24. }
  25. })
  26. </script>

单向数据流

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

prop 的大小写

HTML 中的属性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。

这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

  1. Vue.component('blog-post', {
  2. // 在 JavaScript 中是 camelCase 的
  3. props: ['postTitle'],
  4. template: '<h3>{{ postTitle }}</h3>'
  5. })
  1. <!-- 在 HTML 中是 kebab-case 的 -->
  2. <blog-post post-title="hello!"></blog-post>

:::info 如果使用字符串模板,那么这个限制就不存在了。 :::

props 的数组写法

  1. props: ['cMovies', 'cMessage'] // 多个 prop 可以写在数组中

props 的对象写法及验证机制

我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。

为了定制 prop 的验证方式,你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:

  1. Vue.component('my-component', {
  2. props: {
  3. propA: Number, // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  4. propB: [String, Number], // 多个可能的类型
  5. propC: { // 必填的字符串
  6. type: String,
  7. required: true
  8. },
  9. propD: { // 带有默认值的数字
  10. type: Number,
  11. default: 100
  12. },
  13. propE: { // 带有默认值的对象
  14. type: Object,
  15. default: function () { // 对象或数组默认值必须从一个工厂函数获取
  16. return { message: 'hello' }
  17. }
  18. },
  19. propF: { // 自定义验证函数
  20. validator: function (value) {
  21. // 这个值必须匹配下列字符串中的一个
  22. return ['success', 'warning', 'danger'].indexOf(value) !== -1
  23. }
  24. }
  25. }
  26. })

当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。

:::warning 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data、computed 等) 在 default 或 validator 函数中是不可用的。 :::

type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

额外的,type 还可以是一个自定义的构造函数,并且通过 instanceof 来进行检查确认。例如,给定下列现成的构造函数:

  1. function Person (firstName, lastName) {
  2. this.firstName = firstName
  3. this.lastName = lastName
  4. }

你可以使用:

  1. Vue.component('blog-post', {
  2. props: {
  3. author: Person
  4. }
  5. })

来验证 author prop 的值是否是通过 new Person 创建的。

自定义事件($emit)

使用自定义事件可以实现子组件到父组件的消息传递。

基本示例

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <cpn @itemclick="cpnClick"></cpn> <!-- 监听自定义的事件 -->
  4. <!-- cpnClick 不写参数默认传递 btnClick 的 item -->
  5. </div>
  6. <!-- 子组件 -->
  7. <template id="cpn">
  8. <div>
  9. <button v-for="(item, index) in categoties" :key="index" @click="btnClick(item)">{{ item.name }}</button>
  10. </div>
  11. </template>
  12. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  13. <script>
  14. const cpn = {
  15. template: "#cpn",
  16. data() {
  17. return {
  18. categoties: [
  19. {id: 'aaa', name: '热门推荐'},
  20. {id: 'bbb', name: '手机数码'},
  21. {id: 'ccc', name: '家用家电'},
  22. {id: 'ddd', name: '电脑办公'},
  23. ]
  24. }
  25. },
  26. methods: {
  27. btnClick(item) {
  28. this.$emit('itemclick', item); // 触发一个事件。第一个参数是事件名,其他参数是事件的参数值。
  29. }
  30. },
  31. };
  32. const app = new Vue({
  33. el: "#app",
  34. components: {
  35. cpn
  36. },
  37. methods: {
  38. cpnClick(item) {
  39. console.log('cpnClick: ' + item.name);
  40. }
  41. }
  42. })
  43. </script>

动画.gif

事件名

不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:

  1. this.$emit('myEvent')

则监听这个名字的 kebab-case 版本是不会有任何效果的:

  1. <!-- 没有效果 -->
  2. <my-component v-on:my-event="doSomething"></my-component>

不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent —— 导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名。

在组件上使用 v-model

自定义事件也可以用于创建支持 v-model 的自定义输入组件。记住:

  1. <input v-model="searchText">

等价于:

  1. <input
  2. v-bind:value="searchText"
  3. v-on:input="searchText = $event.target.value"
  4. >

当用在组件上时,v-model 则会这样:

  1. <custom-input
  2. v-bind:value="searchText"
  3. v-on:input="searchText = $event"
  4. ></custom-input>

为了让它正常工作,这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 value 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 input 事件抛出

写成代码之后是这样的:

  1. Vue.component('custom-input', {
  2. props: ['value'],
  3. template: `
  4. <input
  5. v-bind:value="value"
  6. v-on:input="$emit('input', $event.target.value)"
  7. >
  8. `
  9. })

现在 v-model 就应该可以在这个组件上完美地工作起来了:

  1. <custom-input v-model="searchText"></custom-input>

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的。model 选项可以用来避免这样的冲突:

  1. Vue.component('base-checkbox', {
  2. model: {
  3. prop: 'checked',
  4. event: 'change'
  5. },
  6. props: {
  7. checked: Boolean
  8. },
  9. template: `
  10. <input
  11. type="checkbox"
  12. v-bind:checked="checked"
  13. v-on:change="$emit('change', $event.target.checked)"
  14. >
  15. `
  16. })

现在在这个组件上使用 v-model 的时候:

  1. <base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。

注意你仍然需要在组件的 props 选项里声明 checked 这个 prop。

将原生事件绑定到组件(.native 修饰符)

你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 v-on 的 .native 修饰符:

  1. <base-input v-on:focus.native="onFocus"></base-input>

在有的时候这是很有用的,不过在你尝试监听一个类似 <input> 的非常特定的元素时,这并不是个好主意。比如上述 <base-input> 组件可能做了如下重构,所以根元素实际上是一个 <label> 元素:

  1. <label>
  2. {{ label }}
  3. <input
  4. v-bind="$attrs"
  5. v-bind:value="value"
  6. v-on:input="$emit('input', $event.target.value)"
  7. >
  8. </label>

这时,父级的 .native 监听器将静默失败。它不会产生任何报错,但是 onFocus 处理函数不会如你预期地被调用。

为了解决这个问题,Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如:

  1. {
  2. focus: function (event) { /* ... */ }
  3. input: function (value) { /* ... */ },
  4. }

有了这个 $listeners property,你就可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

  1. Vue.component('base-input', {
  2. inheritAttrs: false,
  3. props: ['label', 'value'],
  4. computed: {
  5. inputListeners: function () {
  6. var vm = this
  7. // `Object.assign` 将所有的对象合并为一个新对象
  8. return Object.assign({},
  9. // 我们从父级添加所有的监听器
  10. this.$listeners,
  11. // 然后我们添加自定义监听器,
  12. // 或覆写一些监听器的行为
  13. {
  14. // 这里确保组件配合 `v-model` 的工作
  15. input: function (event) {
  16. vm.$emit('input', event.target.value)
  17. }
  18. }
  19. )
  20. }
  21. },
  22. template: `
  23. <label>
  24. {{ label }}
  25. <input
  26. v-bind="$attrs"
  27. v-bind:value="value"
  28. v-on="inputListeners"
  29. >
  30. </label>
  31. `
  32. })

现在 <base-input> 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 <input> 元素一样使用了:所有跟它相同的 attribute 和监听器都可以工作,不必再使用 .native 监听器。

.sync 修饰符

在有些情况下,我们可能需要对一个 prop 进行 “双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。

这也是为什么我们推荐以 update:myPropName 的模式触发事件取而代之。举个例子,在一个包含 title prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图:

  1. this.$emit('update:title', newTitle)

然后父组件可以监听那个事件并根据需要更新一个本地的数据 property。例如:

  1. <text-document
  2. v-bind:title="doc.title"
  3. v-on:update:title="doc.title = $event"
  4. ></text-document>

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

  1. <text-document v-bind:title.sync="doc.title"></text-document>

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model。

当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind 配合使用:

  1. <text-document v-bind.sync="doc"></text-document>

这样会把 doc 对象中的每一个 property (如 title) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 v-on 监听器。

将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

非父子关系组件通信

组件通信时,如果两个组件不是上下层的关系,就无法使用父子组件通信的那套方式。

有两种方式可以使用:vuex 和 EventBus

先说结论:不提倡使用 EventBus 的方式,而是用 vuex,因为当通信比较复杂时,EventBus 会导致代码难以维护。

事件总线(EventBus)

Vue 对象有分发事件和监听事件的能力,分发事件时可以携带数据,监听到事件后可以拿到这些数据。

因此,可以创建一个 Vue 对象专门用于分发事件和监听事件,这就是 EventBus。

  1. import Vue from 'vue';
  2. const EventBus = new Vue();

由于我们只需要用到它分发事件和监听事件的功能,所以不需要挂载 DOM、router 等,因此这个 Vue 对象很轻量级。

那么如何使用它呢?

在 A 组件中:

  1. EventBus.$emit(EVENT_NAME, DATA)

在 B 组件中:

  1. $on(EVENT_NAME, CALLBACK)

这样就完成了 A —> B 的通信。

可以看到,这样的通信方式很直接,相当于直接在两个组件之间建立一个数据通路,也因此导致这种方式难以维护,因为很难监听到数据的变化。

vuex

TODO

插槽

基本使用

子组件内可以用 <slot></slot> 标签创建一个插槽,标签内部的内容是其默认值。
父组件内使用子组件时,标签内部的内容会替换子组件的 <slot></slot。

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <cpn></cpn>
  4. <cpn>
  5. <span style="color: red;">这是插槽内容222</span> <!-- 这些内容会替换子组件的 <slot></slot -->
  6. </cpn>
  7. <cpn>
  8. <i style="color: red;">这是插槽内容333</i>
  9. </cpn>
  10. <cpn></cpn>
  11. </div>
  12. <!-- 子组件 -->
  13. <template id="cpn">
  14. <div>
  15. <div>
  16. {{ message }}
  17. </div>
  18. <slot> <!-- 定义插槽 -->
  19. <button>button</button> <!-- 插槽默认值 -->
  20. </slot>
  21. </div>
  22. </template>
  23. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  24. <script>
  25. const cpn = {
  26. template: "#cpn",
  27. data() {
  28. return {
  29. message: "我是子组件"
  30. }
  31. },
  32. }
  33. const app = new Vue({
  34. el: "#app",
  35. components: {
  36. cpn
  37. },
  38. data() {
  39. return {
  40. message: "我是父组件消息"
  41. }
  42. },
  43. })
  44. </script>

image.png

具名插槽

普通的插槽填充时是按顺序填充的,就像 Python 中的位置参数。而具名插槽则类似于关键字参数,根据名字来确定填充的位置。

  1. <!-- 父组件 -->
  2. <div id="app">
  3. <cpn>
  4. <span>没具名</span>
  5. <!-- 这种写法已废弃 -->
  6. <span slot="left">这是左边具名插槽的内容</span>
  7. <!-- 新语法 -->
  8. <template v-slot:center>这是中间具名插槽的内容</template>
  9. <!-- 新语法缩写 -->
  10. <template #right>这是右边具名插槽的内容</template>
  11. </cpn>
  12. </div>
  13. <!-- 子组件 -->
  14. <template id="cpn">
  15. <div>
  16. <slot name="left">左边</slot>
  17. <slot name="center">中间</slot>
  18. <slot name="right">右边</slot>
  19. <slot>没有具名的插槽</slot>
  20. </div>
  21. </template>
  22. <script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
  23. <script>
  24. const cpn = {
  25. template: "#cpn"
  26. }
  27. const app = new Vue({
  28. el: "#app",
  29. components: {
  30. cpn
  31. },
  32. })
  33. </script>

image.png

没有具名的插槽排在最后,因为在定义组件的时候,排在了最后,如果有多个则按顺序排列。

:::info 对于没有具名的插槽可以认为其名字为 default。 :::

作用域插槽

由于作用域的原因,插槽内容是无法直接访问子组件中的数据的。

但有时让插槽内容能够访问子组件中才有的数据是很有用的。

例如,设想一个带有如下模板的 <current-user> 组件:

  1. <span>
  2. <slot>{{ user.lastName }}</slot>
  3. </span>

我们可能想换掉默认内容,用名而非姓来显示。如下:

  1. <current-user>
  2. {{ user.firstName }}
  3. </current-user>

然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user,而我们提供的内容是在父级渲染的。

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:

  1. <span>
  2. <slot v-bind:user="user">
  3. {{ user.lastName }}
  4. </slot>
  5. </span>

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

  1. <current-user>
  2. <template v-slot:default="slotProps">
  3. {{ slotProps.user.firstName }}
  4. </template>
  5. </current-user>

另外,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

  1. <current-user v-slot:default="slotProps">
  2. {{ slotProps.user.firstName }}
  3. </current-user>

这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:

  1. <current-user v-slot="slotProps">
  2. {{ slotProps.user.firstName }}
  3. </current-user>

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

  1. <!-- 无效,会导致警告 -->
  2. <current-user v-slot="slotProps">
  3. {{ slotProps.user.firstName }}
  4. <template v-slot:other="otherSlotProps">
  5. slotProps is NOT available here
  6. </template>
  7. </current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

  1. <current-user>
  2. <template v-slot:default="slotProps">
  3. {{ slotProps.user.firstName }}
  4. </template>
  5. <template v-slot:other="otherSlotProps">
  6. ...
  7. </template>
  8. </current-user>

作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:

  1. function (slotProps) {
  2. // 插槽内容
  3. }

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件或现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:

  1. <current-user v-slot="{ user }">
  2. {{ user.firstName }}
  3. </current-user>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 user 重命名为 person:

  1. <current-user v-slot="{ user: person }">
  2. {{ person.firstName }}
  3. </current-user>

你甚至可以定义默认内容,用于插槽 prop 是 undefined 的情形:

  1. <current-user v-slot="{ user = { firstName: 'Guest' } }">
  2. {{ user.firstName }}
  3. </current-user>

动态组件

基本使用

有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:
动画.gif

上述内容可以通过 Vue 的 <component> 元素加一个特殊的 is attribute 来实现:

  1. <!-- 组件会在 `currentTabComponent` 改变时改变 -->
  2. <component v-bind:is="currentTabComponent"></component>

在上述示例中,currentTabComponent 可以包括

  • 已注册组件的名字,或
  • 一个组件的选项对象

:::warning 注意:这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value 这样的 property,若想让其如预期般工作,你需要使用 [.prop](https://cn.vuejs.org/v2/api/#v-bind) 修饰器。 :::

在动态组件上使用 keep-alive

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。也就是所谓的组件缓存。

被 keep-alive 包含的组件不会被再次初始化,也就意味着不会重走生命周期函数。

对于这个例子:

  1. <component v-bind:is="currentTabComponent"></component>

动画.gif
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例。

重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。

  1. <!-- 失活的组件将会被缓存!-->
  2. <keep-alive>
  3. <component v-bind:is="currentTabComponent"></component>
  4. </keep-alive>

修改后的结果:
动画.gif

:::warning 注意:这个 <keep-alive> 要求被切换到的组件都有自己的名字,不论是通过组件的 name 选项还是局部/全局注册。 :::

不使用 keep-alive:当页面组件从 A 变成 B 时,A 组件会被销毁(A 的生命周期函数 destroyed 被调用),如果又切换到 A,A 会被重新渲染,所以,A 原先的状态丢失了。

使用 keep-alive:页面组件从 A 变成 B 时,A 组件不会被销毁,而是缓存在内存中(A 的生命周期函数 destroyed 不会被调用),如果又切换到 A,直接从缓存中调出 A,而非重新渲染,所以,A 原先的状态被保留下来了。

被 keep-alive 包含的组件的 destroyed 不会被调用,而有时我们希望在组件切换时进行一些操作,所以,被包含在 keep-alive 中的组件会多出两个生命周期的钩子: activated 与 deactivated。

:::info keep-alive 是 vue 内置的组件,但经常与 vue-router 内置的组件 router-view 一起使用。 :::