VueJS简明教程(三)之基本使用方法

组件注册

组件名大小写

在字符串模板或单个文件组件中定义组件时,定义组件名的方式有两种:

#使用 kebab-case

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

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

#使用 PascalCase

app.component('MyComponentName', {
  /* ... */
})

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

单文件组件通常被认为是字符串模板。

字符串模板

Vue.component('my-component', {
  template: '<div>{{ message }}</div>',
  data() {
    return {
      message: 'Hello, world!'
    }
  }
})

DOM (即非字符串的模板)

Vue.component('my-component', {
  data() {
    return {
      message: 'Hello, world!'
    }
  },
  // 使用组件模板中的 <template> 元素
  template: '#my-component-template'
})

// 在 HTML 中定义组件模板
<script type="text/x-template" id="my-component-template">
  <div>{{ message }}</div>
</script>

全局注册

const app = Vue.createApp({})

app.component('component-a', {
  /* ... */
})
app.component('component-b', {
  /* ... */
})
app.component('component-c', {
  /* ... */
})

app.mount('#app')

局部注册

ES2015 和 ES6 实际上是同一个版本,只是ES6是ECMAScript的第6个版本,而ES2015是ECMAScript 2015标准的正式名称。

但是ES6一般只用来指代该版本中的新功能,而ES2015则更加官方和标准化。

Props

props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // 或任何其他构造函数
}

这是Vue.js组件中定义props的常规写法,它并不是针对TypeScript的专门写法。

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。          -->
<blog-post is-published></blog-post>

这个写法实际上是合法的,它的意思是将 is-published 属性赋值为 true。在 HTML 中,如果一个属性没有赋值,那么它的值默认为 true。

单向数据流

Vue中prop的单向绑定机制以及它的限制。在Vue中,父组件通过prop向子组件传递数据,子组件不能直接修改父组件的数据,而只能通过向父组件发送事件来告知父组件修改数据。这样的设计可以使数据流向更加清晰,方便应用的开发和维护。

另外,由于prop的单向绑定机制,每当父组件的数据发生变化时,子组件中所有绑定了该prop的数据都会被更新为最新值。如果子组件试图在自身修改prop的值,Vue会在控制台中发出警告,因为这样做破坏了prop的单向绑定机制,容易导致数据流向不可控,引起应用的不可预料的错误。

 this.size.trim().toLowerCase()

整个表达式的作用是将 this.size 转换为小写字母并去除两端空格。

Prop 验证

app.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function() {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function(value) {
        // 力扣 – 中文网 助你高效提升编程技能 https://www.javascriptc.com/special/leetcode/
// 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    },
    // 具有默认值的函数
    propG: {
      type: Function,
      // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数
      default: function() {
        return 'Default function'
      }
    }
  }
})

validator 选项用于验证传入该组件的 propF 属性的值是否合法。

具体来说,该选项中的验证函数检查 propF 的值是否匹配预定义的字符串数组 ['success', 'warning', 'danger'] 中的任意一个字符串。如果匹配成功,则该 propF 被认为是合法的,否则就会被认为是不合法的。

在组件实例创建之前,Vue 会先进行 prop 的验证,而此时实例的其他属性(如 data、computed 等)还没有被初始化,所以你不能在 prop 的默认值或验证函数中使用这些属性。

类型检查

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

  • String

  • Number

  • Boolean

  • Array

  • Object

  • Date

  • Function

  • Symbol

    此外,type 还可以是一个自定义的构造函数

instanceof 是 JavaScript 中的一个操作符,用于判断一个对象是否是某个构造函数的实例。

其中,object 是要判断的对象,constructor 是要判断的构造函数。

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

const person = new Person('Alice');

console.log(person instanceof Person); // true

非 Prop 的 Attribute

Attribute 继承

一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。

<date-picker @change="submitChange"></date-picker>
app.component('date-picker', {
  created() {
    console.log(this.$attrs) // { onChange: () => {}  }
  }
})

submitChange 到 <date-picker> 组件时,这个 @change 实际上是一个非 prop 的 attribute,它会自动传递到 <date-picker> 组件的根节点上。

当 <date-picker> 组件被创建时,你可以使用 $attrs 这个属性来访问这些非 prop 的 attribute。在这个例子中,$attrs 对象应该包含 { onChange: () => {} },因为 @change 属性被绑定到 <date-picker> 组件上,并且它有一个值为 submitChange 的事件处理函数。

比如

app.component('date-picker', {
  template: `
    <div class="date-picker">
      <input type="datetime" />
    </div>
  `
})
<!-- 具有非prop attribute的Date-picker组件-->
<date-picker data-status="activated"></date-picker>

实际上渲染出来是

<!-- 渲染 date-picker 组件 -->
<div class="date-picker" data-status="activated">
  <input type="datetime" />
</div>

禁用 Attribute 继承

如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false。

如果需要将所有非 prop attribute 应用于 input 元素而不是根 div 元素,则可以使用 v-bind 缩写来完成。

app.component('date-picker', {
  inheritAttrs: false,
  template: `
    <div class="date-picker">
      <input type="datetime" v-bind="$attrs" />
    </div>
  `
})
<!-- Date-picker 组件 使用非 prop attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染 date-picker 组件 -->
<div class="date-picker">
  <input type="datetime" data-status="activated" />
</div>

多个根节点上的 Attribute 继承

在 Vue 中,当你使用组件时,你可以向组件传递一些 prop 和 attribute。对于具有单个根节点的组件来说,如果你传递了一些非 prop 的 attribute,Vue 会自动将这些 attribute 添加到根节点上作为普通 attribute。但是,对于具有多个根节点的组件来说,Vue 并不能自动将这些 attribute 添加到哪个节点上,因此你需要在组件中手动处理这些 attribute。

当一个组件具有多个根节点时,如果你没有在组件中显式地使用 $attrs 来绑定这些 attribute,Vue 会发出一个运行时警告,提醒你需要手动处理这些 attribute。因此,在这种情况下,你需要手动绑定这些 attribute,或者通过设置 inheritAttrs: false 来禁用自动 attribute 回退行为,从而使 $attrs 包含所有未知 attribute。

<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 力扣 – 中文网 助你高效提升编程技能 https://www.javascriptc.com/special/leetcode/
// 这将发出警告
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  `
})

// 没有警告,$attrs被传递到<main>元素
app.component('custom-layout', {
  template: `
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  `
})

自定义事件

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

this.$emit('my-event')
<my-component @my-event="doSomething"></my-component>

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

验证抛出的事件

app.component('custom-form', {
  emits: {
    // 没有验证
    click: null,

    // 验证submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm() {
      this.$emit('submit', { email, password })
    }
  }
})

对于 click 事件,没有进行任何验证,因此在组件实例中调用 $emit('click') 将始终触发该事件。

对于 submit 事件,定义了一个函数来验证事件的有效性。该函数接收一个包含 email 和 password 属性的对象作为参数,并根据这些属性是否存在来验证事件的有效性

v-model 参数

默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。我们可以通过向 v-model 传递参数来修改这些名称:

<my-component v-model:title="bookTitle"></my-component>

在本例中,子组件将需要一个 title prop 并发出 update:title 要同步的事件:

app.component('my-component', {
  props: {
    foo: String
  },
  emits: ['update:title'],
  template: `
    <input 
      type="text"
      :value="title"
      @input="$emit('update:title', $event.target.value)">
  `
})

这种写法是在自定义组件上使用 v-model 指令实现双向数据绑定的方式。默认情况下,v-model 会使用 modelValue 作为 prop 名称和 update:modelValue 作为事件名称。

但是有些情况下,我们可能需要使用不同的名称来指定 prop 和事件的名称。

比如在 <my-component v-model:title="bookTitle"></my-component> 中,使用了 title 作为 prop 名称和 update:title 作为事件名称。

多个 v-model 绑定

<user-name
  v-model:first-name="firstName"
  v-model:last-name="lastName"
></user-name>

在JavaScript中,变量名和属性名不能包含横线。如果需要使用横线,可以将其转换为驼峰式命名法或者下划线命名法。

app.component('user-name', {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['update:firstName', 'update:lastName'],
  template: `
    <input 
      type="text"
      :value="firstName"
      @input="$emit('update:firstName', $event.target.value)">

    <input
      type="text"
      :value="lastName"
      @input="$emit('update:lastName', $event.target.value)">
  `
})

可以使用,让大家抓不到北,很棒的写法。

处理 v-model 修饰符

在 Vue 2 中,你可以在组件的 v-model 上使用修饰符(例如:.trim、.lazy)来实现一些特定的逻辑,例如去除输入框的首尾空格,或在失去焦点时更新数据等。

但在 Vue 3 中,由于 v-model 的内部实现已经改变,不再支持这些修饰符。因此,如果你需要在组件上使用这些修饰符,你需要手动实现这些逻辑。

所谓硬编码支持,就是在组件内部手动实现了这些修饰符的逻辑,使得在使用 v-model 绑定时,这些修饰符的效果可以直接生效,而不需要额外的处理。

<my-component v-model.capitalize="myText"></my-component>
app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  template: `
    <input type="text" 
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)">
  `,
  created() {
    console.log(this.modelModifiers) // { capitalize: true }
  }
})

组件在接收 modelModifiers 时,可以获取到传入的修饰符。

它会在控制台打印出 modelModifiers 对象,这个对象中的 capitalize 属性值为 true,表示该组件被修饰符 .capitalize 修饰过。

<div id="app">
  <my-component v-model.capitalize="myText"></my-component>
  {{ myText }}
</div>
const app = Vue.createApp({
  data() {
    return {
      myText: ''
    }
  }
})

app.component('my-component', {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  methods: {
    emits: ['update:modelValue'],
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$emit('update:modelValue', value)
    }
  },
  template: `<input
    type="text"
    :value="modelValue"
    @input="emitValue">`
})

app.mount('#app')

对于带参数的 v-model 绑定,生成的 prop 名称将为 arg + "Modifiers":

<my-component v-model:description.capitalize="myText"></my-component>
app.component('my-component', {
  props: ['description', 'descriptionModifiers'],
  emits: ['update:description'],
  template: `
    <input type="text" 
      :value="description"
      @input="$emit('update:description', $event.target.value)">
  `,
  created() {
    console.log(this.descriptionModifiers) // { capitalize: true }
  }
})

插槽

<button class="btn-primary">
  <slot></slot>
</button>

插槽还可以包含任何模板代码,包括 HTML,或其他组件

如果 <todo-button> 的 template 中没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃

<!-- todo-button 组件模板 -->

<button class="btn-primary">
  Create a new item
</button>
<todo-button>
  <!-- 以下文本不会渲染 -->
  Add todo
</todo-button>

子组件必须要包含 才能渲染出来。

因此父组件中的变量和方法可以在子组件中使用

因此父组件不能访问子组件的作用域中的变量和方法。

讲道理的写法是

<template>
  <div>
    <slot class="my-slot"></slot>
  </div>
</template>

<style>
.my-slot {
  color: red;
}
</style>

但是由于slot里面可以包含任何模板代码,包括 HTML,或其他组件

所以我们一般在父组件采取

<todo-button>
  <div class="my-slot">123</div>
</todo-button>

后备内容

我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot> 标签内:

<button type="submit">
  <slot>Submit</slot>
</button>

具名插槽

对于这样的情况,<slot> 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

一个不带 name 的 <slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

现在 <template> 元素中的所有内容都将会被传入相应的插槽。

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

<!-- 无效,会导致警告 -->
<todo-list v-slot="slotProps">
  <i class="fas fa-check"></i>
  <span class="green">{{ slotProps.item }}</span>

  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</todo-list>

解构插槽 Prop

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 (opens new window)解构来传入具体的插槽 prop,如下:

<todo-list v-slot="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

花括号 {} 在这里表示使用了 ES2015 解构来传入具体的插槽 prop。使用花括号包裹 prop 名称,可以将 prop 的值解构为单独的变量,从而在插槽内容中使用这些变量。

const slotProps = { item: 'example' };
const { item } = slotProps; // 等价于 const item = slotProps.item;
console.log(item); // 输出:'example'

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

<todo-list v-slot="{ item: todo }">
  <i class="fas fa-check"></i>
  <span class="green">{{ todo }}</span>
</todo-list>

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

<todo-list v-slot="{ item = 'Placeholder' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

动态插槽名

动态指令参数也可以用在 v-slot 上,来定义动态的插槽名:

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>

如果动态插槽的名字是一个变量或者表达式,那么需要用方括号 [] 将其包裹起来,以表示它是一个 JavaScript 表达式。如果不用方括号包裹,那么它就会被视为一个静态字符串插槽名,而不是动态插槽名。

具名插槽的缩写

跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:

<!-- This will trigger a warning -->

<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:

<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

"#"符号后面应该跟着一个参数名称,

ES2015 解构

const [a, b, c] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

// 对象解构
const { x, y, z } = { x: 1, y: 2, z: 3 };
console.log(x); // 1
console.log(y); // 2
console.log(z); // 3

ES6 ES5

ES6(ECMAScript 2015)是JavaScript的第六个版本,引入了很多新特性和语法,如箭头函数、模板字符串、解构赋值、let和const关键字、类、模块等等。相比之下,ES5是JavaScript的第五个版本,是目前被广泛支持的JavaScript版本,其特性比ES6较为简单,但足以应对许多实际应用场景。

提供 / 注入

对于这种情况,我们可以使用 provide 和 inject 对。父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 provide 选项来提供数据,子组件有一个 inject 选项来开始使用这个数据。

你有一些深嵌套的组件,而你只需要来自深嵌套子组件中父组件的某些内容。在这种情况下,你仍然需要将 prop 传递到整个组件链中,这可能会很烦人。

例如,如果我们有这样的层次结构:

Root
└─ TodoList
   ├─ TodoItem
   └─ TodoListFooter
      ├─ ClearTodosButton
      └─ TodoListStatistics

如果要将 todo-items 的长度直接传递给 TodoListStatistics,我们将把这个属性向下传递到层次结构:TodoList -> TodoListFooter -> TodoListStatistics。通过 provide/inject 方法,我们可以直接执行以下操作:

const app = Vue.createApp({})

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide: {
    user: 'John Doe'
  },
  template: `
    <div>
      {{ todos.length }}
      <!-- 模板的其余部分 -->
    </div>
  `
})

app.component('todo-list-statistics', {
  inject: ['user'],
  created() {
    console.log(`Injected property: ${this.user}`) // > 注入 property: John Doe
  }
})

要访问组件实例 property,我们需要将 provide 转换为返回对象的函数

app.component('todo-list', {
  data() {
    return {
      todos: ['Feed a cat', 'Buy tickets']
    }
  },
  provide() {
    return {
      todoLength: this.todos.length
    }
  },
  template: `
    ...
  `
})

处理响应性

谢谢你,看文档解决了我多年未解之谜。

在上面的例子中,如果我们更改了 todos 的列表,这个更改将不会反映在注入的 todoLength property 中。这是因为默认情况下,provide/inject 绑定不是被动绑定。我们可以通过将 ref property 或 reactive 对象传递给 provide 来更改此行为。在我们的例子中,如果我们想对祖先组件中的更改做出反应,我们需要为我们提供的 todoLength 分配一个组合式 API computed property:

app.component('todo-list', {
  // ...
  provide() {
    return {
      todoLength: Vue.computed(() => this.todos.length)
    }
  }
})

app.component('todo-list-statistics', {
  inject: ['todoLength'],
  created() {
    console.log(`Injected property: ${this.todoLength.value}`) // > Injected property: 5
  }
})

我们可以使用 Vue.js 的响应式系统中提供的 computed 函数来创建一个响应式的 todoLength 属性。

是的,computed属性也具有响应式。当计算属性依赖的数据发生变化时,computed属性会自动重新计算其值,并将结果缓存起来。而且,当依赖的数据发生变化时,所有使用该计算属性的组件都会自动更新视图。

computed属性的响应式实现是基于Vue.js的响应式系统实现的,它可以在内部监听其所依赖的数据,并在这些数据发生变化时触发重新计算。由于计算属性的值是基于其他数据计算得出的,因此它可以通过依赖其他响应式数据来实现自动更新。这也是Vue.js的一大特色,使得开发者可以轻松实现复杂的响应式交互。

动态组件 & 异步组件

在动态组件上使用 keep-alive

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。

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

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

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component :is="currentTabComponent"></component>
</keep-alive>

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 有一个 defineAsyncComponent 方法:

const app = Vue.createApp({})

const AsyncComp = Vue.defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      resolve({
        template: '<div>I am async!</div>'
      })
    })
)

app.component('async-example', AsyncComp)

如你所见,此方法接受返回 Promise 的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve 回调。你也可以调用 reject(reason),以指示加载失败。

你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以这样使用动态导入:

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/AsyncComponent.vue')
)

app.component('async-component', AsyncComp)

当在本地注册组件时,你也可以使用 defineAsyncComponent

import { createApp, defineAsyncComponent } from 'vue'

createApp({
  // ...
  components: {
    AsyncComponent: defineAsyncComponent(() =>
      import('./components/AsyncComponent.vue')
    )
  }
})
import { defineAsyncComponent } from 'vue';
import AsyncComponent from './components/AsyncComponent.vue';

export default {
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue'))
  },
  data() {
    return {
      showAsyncComponent: false,
    };
  },
  methods: {
    toggleAsyncComponent() {
      this.showAsyncComponent = !this.showAsyncComponent;
    }
  },
  template: `
    <div>
      <button @click="toggleAsyncComponent">Toggle Async Component</button>
      <div v-if="showAsyncComponent">
        <AsyncComponent />
      </div>
    </div>
  `
}

异步组件可以选择退出 Suspense 控制,并通过在其选项中指定 suspensible:false,让组件始终控制自己的加载状态。

import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent({
  loader: () => import('./AsyncComp.vue'),
  suspensible: false
})

export default {
  components: {
    AsyncComp
  }
}

在 Vue 中,可挂起(suspense)指的是一个组件可以被挂起并在加载时显示一些占位内容,直到异步加载完成并准备好渲染为止。异步组件的挂起行为可以让页面看起来更加流畅,因为在加载较慢的组件时,不会出现一片空白。相反,占位内容可以让用户知道页面正在加载内容。

当一个异步组件被挂起时,Vue 将自动渲染其祖先中最近的一个 <Suspense> 组件的占位内容,以便在异步组件加载完成前提供一个占位符。这个行为可以通过在异步组件的选项中设置 suspensible 为 false 来关闭,这样异步组件将始终控制自己的加载状态而不受父组件的影响。

模板引用

当 ref 与 v-for 一起使用时,你得到的 ref 将是一个数组,

<template>
  <div>
    <my-component v-for="item in items" :key="item.id" ref="myComponents"></my-component>
  </div>
</template>

是的,当 ref 与 v-for 一起使用时,你得到的 ref 将是一个数组,数组中的每个元素对应着每个生成的组件实例,因此在这个例子中,所有 my-component 的 ref 都是 myComponents。你可以通过在组件实例数组上使用索引来访问每个组件实例,例如: this.$refs.myComponents[0] 访问第一个组件实例。

在组件的生命周期中,mounted 钩子函数是在组件渲染完成并挂载到 DOM 后调用的,而模板和计算属性是在组件渲染时计算和解析的。因此,在模板或计算属性中访问 $refs 可能会在组件挂载之前发生,这可能导致 $refs 不可用。相比之下,在 mounted 钩子函数中访问 $refs 则是安全的,因为它只有在组件渲染完成并挂载到 DOM 后才会被调用。

处理边界情况

强制更新

在 Vue 中,当状态发生变化时,Vue 会自动检测到这些变化,并在必要的情况下重新渲染视图。这是由 Vue 的响应式系统来实现的。通常情况下,我们不需要手动干预这个过程,因为 Vue 会自动处理所有状态的变化并重新渲染视图。

但是,在极少数情况下,可能会出现某些状态的变化无法被 Vue 响应式系统检测到或处理,这时就需要手动强制更新。强制更新可以让 Vue 重新渲染组件,即使组件的状态没有发生任何变化。

需要注意的是,强制更新是一种非常不推荐的做法。因为它会导致不必要的重新渲染,从而影响性能。在绝大多数情况下,我们应该依靠 Vue 的响应式系统来处理状态的变化。只有在极少数情况下才需要使用强制更新。

export default {
  methods: {
    // 强制更新
    forceUpdate() {
      this.$forceUpdate()
    }
  }
}

低级静态组件与 v-once

在 Vue 中渲染纯 HTML 元素的速度非常快,但有时你可能有一个包含很多静态内容的组件。在这些情况下,可以通过向根元素添加 v-once 指令来确保只对其求值一次,然后进行缓存,如下所示:

app.component('terms-of-service', {
  template: `
    <div v-once>
      <h1>Terms of Service</h1>
      ... a lot of static content ...
    </div>
  `
})

再次提醒,不要过度使用这种模式。虽然在极少数情况下需要渲染大量静态内容时很方便,但除非你注意到渲染速度——慢,否则就没有必要这样做—另外,这可能会在以后引起很多混乱。

过渡 & 动画概述

基于 class 的动画和过渡

.shake {
  animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
  transform: translate3d(0, 0, 0);
  backface-visibility: hidden;
  perspective: 1000px;
}

@keyframes shake {
  10%,
  90% {
    transform: translate3d(-1px, 0, 0);
  }

  20%,
  80% {
    transform: translate3d(2px, 0, 0);
  }

  30%,
  50%,
  70% {
    transform: translate3d(-4px, 0, 0);
  }

  40%,
  60% {
    transform: translate3d(4px, 0, 0);
  }
}
 animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;

这行代码定义了一个动画,名称为“shake”,持续时间为0.82秒,采用了一个缓动函数cubic-bezier(0.36, 0.07, 0.19, 0.97),它指定了在动画开始和结束时,元素的状态变化速度如何改变。最后一个参数“both”指定了动画应该同时应用于元素的进入和离开状态。

cubic-bezier 是 CSS 动画中的一个函数,用于定义动画的过渡效果。它的参数是一个四元组,表示贝塞尔曲线的四个控制点。控制点的坐标值在 [0, 1] 之间,它们的位置和数量决定了曲线的形状,从而影响动画效果。

在这个例子中,cubic-bezier(0.36, 0.07, 0.19, 0.97) 表示使用了一条类似于弹性的曲线,其中第一个和最后一个控制点较靠近曲线的两端,使得动画开始和结束时的变化较为平缓,而中间的两个控制点较靠近曲线的中间,产生较为剧烈的变化,从而产生一种类似于颤抖的效果。

 transform: translate3d(0, 0, 0);

transform: translate3d(0, 0, 0) 意思是将元素沿着三个轴(x、y、z)移动,其中 x 轴和 y 轴的偏移量为 0,z 轴的偏移量也为 0。

 30%,
  50%,
  70% {
    transform: translate3d(-4px, 0, 0);
  }

这段 CSS 动画代码是用来实现“抖动”效果的。其中,30%、50%、70% 分别表示动画进行到了总时间的 30%、50%、70% 的时候,元素会沿着 x 轴向左偏移 4px,即从当前位置向左移动 4px,通过这个动作达到抖动的效果。

过渡与 Style 绑定

<div id="demo">
  <div
    @mousemove="xCoordinate"
    :style="{ backgroundColor: `hsl(${x}, 80%, 50%)` }"
    class="movearea"
  >
    <h3>Move your mouse across the screen...</h3>
    <p>x: {{x}}</p>
  </div>
</div>

当鼠标移动到这个子元素 div 区域内时,@mousemove 事件会触发 xCoordinate 方法。

其中x是在组件实例的data属性中定义的一个变量,用于计算背景颜色的HSL色值中的色相(hue)属性,其值随着鼠标的X坐标而变化。80%和50%分别是该HSL色值的饱和度(saturation)和亮度(lightness)属性的值,是固定的。

.movearea {
  transition: 0.2s background-color ease;
}
它指定了当该元素的背景色发生变化时,变化过程的时长为 0.2 秒,并且变化速度是缓慢的(使用 ease 缓动函数)。
const Demo = {
  data() {
    return {
      x: 0
    }
  },
  methods: {
    xCoordinate(e) {
      this.x = e.clientX
    }
  }
}

Vue.createApp(Demo).mount('#demo')

通过访问事件对象的 clientX 属性获取鼠标在当前窗口中的水平坐标,并将该值赋值给组件实例中的 x 属性,以便在模板中渲染出当前鼠标位置的颜色。

硬件加速

如 perspective、backface-visibility 和 transform:translateZ(x) 等 property 将让浏览器知道你需要硬件加速。

如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):

perspective: 1000px;
backface-visibility: hidden;
transform: translateZ(0);

许多像 GreenSock 这样的 JS 库都会默认你需要硬件加速,并在默认情况下应用,所以你不需要手动设置它们。

其中,translateZ(0) 是一种比较特殊的方法,它可以强制触发元素的 3D 渲染,即使元素没有进行任何的 3D 变换,这样也能够实现硬件加速的效果。

perspective属性只对使用3D变换的元素有效果。它定义了一个透视视图,当你在三维空间中旋转元素时,它可以产生远近的效果,从而使动画看起来更加逼真。如果没有3D动画,perspective属性将没有任何作用。

单元素/组件的过渡

<div id="demo">
  <button @click="show = !show">
    Toggle
  </button>

  <transition name="fade">
    <p v-if="show">hello</p>
  </transition>
</div>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

这是Vue.js框架中的过渡动画组件。过渡动画组件是Vue.js提供的一种方便的方式来在DOM元素插入、更新或移除时添加动画效果。

.fade-enter-active和.fade-leave-active是Vue过渡效果中的类名,它们用于定义过渡期间元素的动画效果。

.fade-enter-active定义了在插入元素时过渡期间的样式,在本例中,它定义了透明度的变化和动画时间。

.fade-leave-active定义了在移除元素时过渡期间的样式,与.fade-enter-active类似,它也定义了透明度的变化和动画时间。这样,在元素被删除时,将使用相同的过渡动画。

  • .fade-enter-from 表示元素进入过渡时的开始状态,即 opacity 为 0。
  • .fade-leave-to 表示元素离开过渡时的结束状态,即 opacity 为 0。

过渡class

在进入/离开的过渡中,会有 6 个 class 切换。

  1. v-enter-from:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
  2. v-enter-active:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
  3. v-enter-to:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除),在过渡/动画完成之后移除。
  4. v-leave-from:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。
  5. v-leave-active:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
  6. v-leave-to:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被删除),在过渡/动画完成之后移除。

对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>,则 v- 是这些class名的默认前缀。如果你使用了 <transition name="my-transition">,那么 v-enter-from会替换为 my-transition-enter-from。

使用方法:

<transition>
  <div class="my-element" v-if="show">
    <!-- ... -->
  </div>
</transition>
.my-enter-from {
  transform: translateX(-100%);
}
.my-enter-to {
  transform: translateX(0);
}

自定义过渡 class 类名

我们可以通过以下 attribute 来自定义过渡类名:

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class
<link
  href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
  rel="stylesheet"
  type="text/css"
/>

<div id="demo">
  <button @click="show = !show">
    Toggle render
  </button>

  <transition
    name="custom-classes-transition"
    enter-active-class="animate__animated animate__tada"
    leave-active-class="animate__animated animate__bounceOutRight"
  >
    <p v-if="show">hello</p>
  </transition>
</div>

同时使用过渡和动画

在这种情况下你可以用 <transition> 组件上的 duration prop 定制一个显性的过渡持续时间 (以毫秒计):

<transition :duration="1000">...</transition>

你也可以定制进入和移出的持续时间:

<transition :duration="{ enter: 500, leave: 800 }">...</transition>

JavaScript 钩子

这些 JavaScript 钩子函数是用来控制过渡效果的行为和生命周期的,具体作用如下:

  • before-enter:在元素进入过渡之前被调用。
  • enter:在元素进入过渡之后被调用。
  • after-enter:在元素进入过渡之后被调用。
  • enter-cancelled:在进入过渡被取消之后被调用。
  • before-leave:在元素离开过渡之前被调用。
  • leave:在元素离开过渡之后被调用。
  • after-leave:在元素离开过渡之后被调用。
  • leave-cancelled:在离开过渡被取消之后被调用。

通过这些钩子函数,你可以在过渡过程中动态地修改样式或者执行自定义逻辑,实现更加灵活和复杂的过渡效果。例如,你可以在 before-enter 钩子中设置初始状态,然后在 enter 钩子中设置最终状态,这样可以使得过渡效果更加自然和流畅。

<transition
  @before-enter="beforeEnter"
  @enter="enter"
  @after-enter="afterEnter"
  @enter-cancelled="enterCancelled"
  @before-leave="beforeLeave"
  @leave="leave"
  @after-leave="afterLeave"
  @leave-cancelled="leaveCancelled"
  :css="false"
>
  <!-- ... -->
</transition>

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>

<div id="demo">
  <button @click="show = !show">
    Toggle
  </button>

  <transition
    @before-enter="beforeEnter"
    @enter="enter"
    @leave="leave"
    :css="false"
  >
    <p v-if="show">
      Demo
    </p>
  </transition>
</div>

这是一个 Vue 过渡钩子函数,具体来说是 before-enter 钩子,该钩子会在元素进入过渡之前被调用。

在这个钩子函数中,使用了一个第三方动画库 GreenSock Animation Platform (GSAP),通过 gsap.set() 方法设置元素的初始状态,将元素水平方向缩小为原来的 0.8 倍,垂直方向放大为原来的 1.2 倍。这样,当元素进入过渡动画后,会从这个状态开始,逐渐过渡到默认状态,实现一种从中心向四周弹出的效果。

const Demo = {
  data() {
    return {
      show: false
    }
  },
  methods: {
    beforeEnter(el) {
      gsap.set(el, {
        scaleX: 0.8,
        scaleY: 1.2
      })
    },
    enter(el, done) {
      gsap.to(el, {
        duration: 1,
        scaleX: 1.5,
        scaleY: 0.7,
        opacity: 1,
        x: 150,
        ease: 'elastic.inOut(2.5, 1)',
        onComplete: done
      })
    },
    leave(el, done) {
      gsap.to(el, {
        duration: 0.7,
        scaleX: 1,
        scaleY: 1,
        x: 300,
        ease: 'elastic.inOut(2.5, 1)'
      })
      gsap.to(el, {
        duration: 0.2,
        delay: 0.5,
        opacity: 0,
        onComplete: done
      })
    }
  }
}

Vue.createApp(Demo).mount('#demo')

初始渲染的过渡

可以通过 appear attribute 设置节点在初始渲染的过渡

<transition appear>
  <!-- ... -->
</transition>

多个元素的过渡

v-if`/`v-else-if`/`v-else
<transition>
  <button v-if="docState === 'saved'" key="saved">
    Edit
  </button>
  <button v-else-if="docState === 'edited'" key="edited">
    Save
  </button>
  <button v-else-if="docState === 'editing'" key="editing">
    Cancel
  </button>
</transition>

或者

<transition>
  <button :key="docState">
    {{ buttonMessage }}
  </button>
</transition>
// ...
computed: {
  buttonMessage() {
    switch (this.docState) {
      case 'saved': return 'Edit'
      case 'edited': return 'Save'
      case 'editing': return 'Cancel'
    }
  }
}

:key 属性是用来帮助 Vue 去判断新旧节点的,当 key 值相同时,Vue 认为这是同一个节点,不会执行过渡效果。而当 key 值不同时,Vue 认为这是不同的节点,会执行过渡效果

<transition> 内的内容会在切换时应用过渡效果,而 v-if/v-else-if/v-else 是根据条件来渲染不同的内容。在这个例子中,<button> 标签本身不会被添加/删除,而只是其内容发生变化。当 docState 的值变化时,<button> 中的内容会根据 key 属性的变化,被识别为新的节点,从而触发过渡效果。

过渡模式

  • in-out: 新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in: 当前元素先进行过渡,完成之后新元素过渡进入。

很快就会发现 out-in 是你大多数时候想要的状态 😃

<transition name="fade" mode="out-in">
  <!-- ... the buttons ... -->
</transition>

多个组件之间过渡

组件之间的过渡更简单 —— 我们甚至不需要 key 属性。

<div id="demo">
  <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label>
  <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label>
  <transition name="component-fade" mode="out-in">
    <component :is="view"></component>
  </transition>
</div>
const Demo = {
  data() {
    return {
      view: 'v-a'
    }
  },
  components: {
    'v-a': {
      template: '<div>Component A</div>'
    },
    'v-b': {
      template: '<div>Component B</div>'
    }
  }
}

Vue.createApp(Demo).mount('#demo')
.component-fade-enter-active,
.component-fade-leave-active {
  transition: opacity 0.3s ease;
}

.component-fade-enter-from,
.component-fade-leave-to {
  opacity: 0;
}

列表过渡

列表的排序过渡

<transition-group> 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move class,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name attribute 来自定义前缀,也可以通过 move-class attribute 手动设置。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>

<div id="flip-list-demo">
  <button @click="shuffle">Shuffle</button>
  <transition-group name="flip-list" tag="ul">
    <li v-for="item in items" :key="item">
      {{ item }}
    </li>
  </transition-group>
</div>
const Demo = {
  data() {
    return {
      items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
    }
  },
  methods: {
    shuffle() {
      this.items = _.shuffle(this.items)
    }
  }
}

Vue.createApp(Demo).mount('#flip-list-demo')
.flip-list-move {
  transition: transform 0.8s ease;
}

这段代码定义了一个名为 shuffle 的方法,该方法使用 Lodash 库中的 _.shuffle() 函数来随机重排 this.items 数组中的元素。该方法的效果是将数组中的元素打乱顺序,以达到一种“洗牌”的效果。

可复用的过渡

Vue.component('my-special-transition', {
  template: '\
    <transition\
      name="very-special-transition"\
      mode="out-in"\
      @before-enter="beforeEnter"\
      @after-enter="afterEnter"\
    >\
      <slot></slot>\
    </transition>\
  ',
  methods: {
    beforeEnter(el) {
      // ...
    },
    afterEnter(el) {
      // ...
    }
  }
})
<template>标记不允许跨行,因此需要使用反斜杠()来跨行编写模板代码。在这里,使用了反斜杠来跨行编写组件模板代码。这样做是为了使模板更易于阅读和维护。

  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `

Vue.js 的模板语法中使用反引号可以实现跨行。

函数式组件

Vue.component('my-special-transition', {
  functional: true,
  render: function(createElement, context) {
    var data = {
      props: {
        name: 'very-special-transition',
        mode: 'out-in'
      },
      on: {
        beforeEnter(el) {
          // ...
        },
        afterEnter(el) {
          // ...
        }
      }
    }
    return createElement('transition', data, context.children)
  }
})

状态动画与侦听器

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>

<div id="animated-number-demo">
  <input v-model.number="number" type="number" step="20" />
  <p>{{ animatedNumber }}</p>
</div>

通过侦听器我们能监听到任何数值 property 的数值更新

const Demo = {
  data() {
    return {
      number: 0,
      tweenedNumber: 0
    }
  },
  computed: {
    animatedNumber() {
      return this.tweenedNumber.toFixed(0)
    }
  },
  watch: {
    number(newValue) {
      gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue })
    }
  }
}

Vue.createApp(Demo).mount('#animated-number-demo')

lodash.min.js

来引入 Lodash 库的 JavaScript 文件的。Lodash 是一个实用的 JavaScript 工具库,提供了很多常用的函数和方法,比如数组和对象操作、函数式编程、字符串处理、数学计算等等。通过引入 Lodash 库,开发者可以方便地使用这些常用的函数和方法,提高开发效率和代码质量。这行代码引用的是 Lodash 4.17.15 版本的压缩文件,它可以在项目中使用 Lodash 库的所有功能。

混动加载

滚动条滚动到页面底部时,页面会继续加载新的内容,这就是所谓的“无限滚动”或“混动加载”。通常用于需要大量数据的网页或应用程序,以提高用户体验和性能。相比于传统的翻页方式,无限滚动可以让用户更快地获取到想要的信息,同时减少了页面刷新的次数,提升了用户体验。

混入

在 Vue 中,混入(Mixin)是一种可重用的代码组织方式。它可以将一些常用的代码块封装起来,然后在多个组件之间共享这些代码。混入将一些可复用的逻辑注入到组件中,从而使组件具有相同的功能,这种方式可以提高代码的复用性,降低重复代码的量,减少了开发的工作量,提高了开发效率。

选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

const myMixin = {
  data() {
    return {
      message: 'hello',
      foo: 'abc'
    }
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  data() {
    return {
      message: 'goodbye',
      bar: 'def'
    }
  },
  created() {
    console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }
  }
})

同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

const myMixin = {
  created() {
    console.log('mixin hook called')
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  created() {
    console.log('component hook called')
  }
})

// => "混入对象的钩子被调用"
// => "组件钩子被调用"

值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

const myMixin = {
  methods: {
    foo() {
      console.log('foo')
    },
    conflicting() {
      console.log('from mixin')
    }
  }
}

const app = Vue.createApp({
  mixins: [myMixin],
  methods: {
    bar() {
      console.log('bar')
    },
    conflicting() {
      console.log('from self')
    }
  }
})

const vm = app.mount('#mixins-basic')

vm.foo() // => "foo"
vm.bar() // => "bar"
vm.conflicting() // => "from self"

全局混入

const app = Vue.createApp({
  myOption: 'hello!'
})

// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
  created() {
    const myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

app.mount('#mixins-global') // => "hello!"

自定义选项合并策略

const app = Vue.createApp({
  custom: 'hello!'
})

app.config.optionMergeStrategies.custom = (toVal, fromVal) => toVal || fromVal

app.mixin({
  custom: 'goodbye!',
  created() {
    console.log(this.$options.custom) // => "goodbye!"
  }
})

在这段代码中,选项合并策略函数的参数是 toVal 和 fromVal,这两个参数的意义是:

  • toVal 表示目标选项的值,即正在被合并的组件的选项的值。
  • fromVal 表示源选项的值,即父组件的选项的值。

在这个自定义选项合并策略函数中,使用了 JavaScript 中的逻辑或运算符 || 来判断目标选项和源选项的值。如果目标选项的值存在(即非 null 或 undefined),则返回目标选项的值 toVal;否则返回源选项的值 fromVal。

所以,对于这个自定义选项合并策略函数来说,toVal 和 fromVal 的具体含义取决于函数的调用上下文,即在合并组件选项时,toVal 表示正在被合并的组件的选项的值,fromVal 表示父组件的选项的值。

自定义指令

只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态

const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  mounted(el) {
    // Focus the element
    el.focus()
  }
})

如果想注册局部指令,组件中也接受一个 directives 的选项:

directives: {
  focus: {
    // 指令的定义
    mounted(el) {
      el.focus()
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

<input v-focus />

钩子函数

  • beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用。在这里你可以做一次性的初始化设置。

  • mounted:在挂载绑定元素的父组件时调用。

  • beforeUpdate:在更新包含组件的 VNode 之前调用。

  • updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用。

  • beforeUnmount:在卸载绑定元素的父组件之前调用

  • unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次

动态指令参数

<div id="dynamicexample">
  <h3>Scroll down inside this section ↓</h3>
  <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
const app = Vue.createApp({
  data() {
    return {
      direction: 'right'
    }
  }
})

app.directive('pin', {
  mounted(el, binding) {
    el.style.position = 'fixed'
    // binding.arg is an argument we pass to directive
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
  }
})

app.mount('#dynamic-arguments-example')

函数简写

在很多时候,你可能想在 mounted 和 updated 时触发相同行为,而不关心其它的钩子。比如这样写:

app.directive('pin', (el, binding) => {
  el.style.position = 'fixed'
  const s = binding.arg || 'top'
  el.style[s] = binding.value + 'px'
})

对象字面量

如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。

<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
  console.log(binding.value.color) // => "white"
  console.log(binding.value.text) // => "hello!"
})

VNode

VNode(Virtual Node)是Vue中的一个概念,它是一个虚拟的DOM节点,用来描述一个视图在某个时刻的状态。在Vue中,组件的数据发生变化时,Vue会通过重新渲染VNode来更新视图,而不是直接操作DOM。这样可以提高性能,因为直接操作DOM通常比生成VNode的开销要大。

VNode是一个JavaScript对象,它包含了组件的标签名、属性、子节点等信息,可以通过它来构建DOM树。在更新包含组件的VNode之前,Vue会调用组件实例的beforeUpdate生命周期函数。这个生命周期函数可以用来执行一些在更新之前需要处理的逻辑,比如修改组件的数据或者触发一些异步操作。

在beforeUpdate生命周期函数中,组件实例可以通过this来访问组件的数据和方法。如果需要更新组件的数据,可以通过调用this.\(set或者this.\)forceUpdate来触发组件的重新渲染。当组件的VNode被重新渲染时,Vue会再次调用beforeUpdate生命周期函数,从而完成更新过程。

Teleport

Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。

让我们修改 modal-button 以使用 <teleport>,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。

app.component('modal-button', {
  template: `
    <button @click="modalOpen = true">
        Open full screen modal! (With teleport!)
    </button>

    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          I'm a teleported modal! 
          (My parent is "body")
          <button @click="modalOpen = false">
            Close
          </button>
        </div>
      </div>
    </teleport>
  `,
  data() {
    return { 
      modalOpen: false
    }
  }
})

一个常见的用例场景是一个可重用的 <Modal> 组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <teleport> 组件可以将其内容挂载到同一个目标元素。顺序将是一个简单的追加——稍后挂载将位于目标元素中较早的挂载之后。

<teleport to="#modals">
  <div>A</div>
</teleport>
<teleport to="#modals">
  <div>B</div>
</teleport>

<!-- result-->
<div id="modals">
  <div>A</div>
  <div>B</div>
</div>

渲染函数

我们来尝试使用 render 函数重写上面的例子:

const app = Vue.createApp({})

app.component('anchored-heading', {
  render() {
    const { h } = Vue

    return h(
      'h' + this.level, // tag name
      {}, // props/attributes
      this.$slots.default() // array of children
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})
  • {}:这是一个对象,包含元素的属性和其他特性。在这种情况下,我们没有传递任何属性或特性,所以这是一个空对象。
  • this.$slots.default():这是一个函数调用,返回一个包含虚拟节点数组的函数。这些节点将成为组件的子元素。 $slots是 Vue.js 的一个特殊属性,表示插槽内容的对象。在这种情况下,default是默认插槽的名称,因为该组件没有具名插槽。

DOM 树

,这可以是在一个模板里:

<h1>{{ blogTitle }}</h1>

或者一个渲染函数里:

render() {
  return Vue.h('h1', {}, this.blogTitle)
}

虚拟 DOM 树

h() 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

h() 参数

h() 函数是一个用于创建 vnode 的实用程序。也许可以更准确地将其命名为 createVNode(),但由于频繁使用和简洁,它被称为 h() 。它接受三个参数:

// @returns {VNode}
h(
  // {String | Object | Function | null} tag
  // 一个 HTML 标签名、一个组件、一个异步组件,或者 null。
  // 使用 null 将会渲染一个注释。
  //
  // 必需的。
  'div',

  // {Object} props
  // 与 attribute、prop 和事件相对应的对象。
  // 我们会在模板中使用。
  //
  // 可选的。
  {},

  // {String | Array | Object} children
  // 子 VNodes, 使用 `h()` 构建,
  // 或使用字符串获取 "文本 Vnode" 或者
  // 有 slot 的对象。
  //
  // 可选的。
  [
    'Some text comes first.',
    h('h1', 'A headline'),
    h(MyComponent, {
      someProp: 'foobar'
    })
  ]
)
  1. 第一个参数是要创建的元素的标签名、组件名、异步组件名或者 null(创建注释节点)。
  2. 第二个参数是一个对象,包含了要设置的元素属性和事件监听器等。
  3. 第三个参数是一个包含了子节点的数组,每个子节点可以是字符串、VNode 或者组件等。

举个例子,h('div', { class: 'example' }, [ 'Hello, world!', h('span', 'This is a span.') ]) 会创建一个包含了一个带有 class 为 example 的 div 元素和一个带有文本内容 This is a span. 的 span 元素的 VNode 对象。

const app = Vue.createApp({})

/** Recursively get text from children nodes */
function getChildrenTextContent(children) {
  return children
    .map(node => {
      return typeof node.children === 'string'
        ? node.children
        : Array.isArray(node.children)
        ? getChildrenTextContent(node.children)
        : ''
    })
    .join('')
}

children 是一个 VNode 数组,它表示一个组件的所有子节点。map() 方法是 JavaScript 中的一个数组方法,用于遍历数组中的每一个元素并执行指定的操作。在这个代码中,children.map(node => {...}) 表示遍历 children 数组中的每个 VNode 节点,对每个节点执行一些操作。
如果子节点是一个文本节点,则返回该节点的文本内容。
如果子节点是一个包含其他子节点的元素节点,则递归调用 getChildrenTextContent() 函数,以获取其所有子节点的文本内容,并将其作为一个字符串返回。
如果子节点不是一个文本节点或元素节点,则返回空字符串。

app.component('anchored-heading', {
  render() {
    // create kebab-case id from the text contents of the children
    const headingId = getChildrenTextContent(this.$slots.default())
      .toLowerCase()
      .replace(/\W+/g, '-') // /\W+/g 是一个正则表达式,用来匹配所有的非单词字符。其中 \W 表示匹配所有非字母、非数字、非下划线的字符,加上 + 表示匹配多个连续的这样的字符。/g 表示全局匹配,不止匹配第一个符合条件的字符。
      .replace(/(^-|-$)/g, '') //(^-|-$)/g 匹配字符串开头和结尾的 - 字符,^ 表示字符串开头,$ 表示字符串结尾,| 表示或者的意思。g 表示全局匹配。使用空字符串 '' 替换匹配到的 - 字符,这样就可以去除字符串开头和结尾的 -。

    return Vue.h('h' + this.level, [
      Vue.h(
        'a',
        {
          name: headingId,
          href: '#' + headingId
        },
        this.$slots.default()
      )
    ])
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

约束

#VNodes 必须唯一

组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:

render() {
  const myParagraphVNode = Vue.h('p', 'hi')
  return Vue.h('div', [
    // 错误 - 重复的Vnode!
    myParagraphVNode, myParagraphVNode
  ])
}

返回的是一个 div 元素的 VNode 对象,它有两个子元素,它们都是 myParagraphVNode,也就是同一个 p 元素的 VNode 对象。这是错误的

如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:

render() {
  return Vue.h('div',
    Array.from({ length: 20 }).map(() => {
      return Vue.h('p', 'hi')
    })
  )
}

使用 JavaScript 代替模板功能

v-if 和 v-for

只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if 和 v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

这些都可以在渲染函数中用 JavaScript 的 if/else 和 map() 来重写:

props: ['items'],
render() {
  if (this.items.length) {
    return Vue.h('ul', this.items.map((item) => {
      return Vue.h('li', item.name)
    }))
  } else {
    return Vue.h('p', 'No items found.')
  }
}

v-model

v-model 指令扩展为 modelValue 和 onUpdate:modelValue 在模板编译过程中,我们必须自己提供这些prop:

props: ['modelValue'],
emits: ['update:modelValue'],
render() {
  return Vue.h(SomeComponent, {
    modelValue: this.modelValue,
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
  })
}

v-on

我们必须为事件处理程序提供一个正确的prop名称,例如,要处理 click 事件,prop名称应该是 onClick。

render() {
  return Vue.h('div', {
    onClick: $event => console.log('clicked', $event.target)
  })
}

事件修饰符

对于 .passive 、 .capture和 .once 事件修饰符,Vue提供了处理程序的对象语法:

实例:

render() {
  return Vue.h('input', {
    onClick: {
      handler: this.doThisInCapturingMode,
      capture: true
    },
    onKeyUp: {
      handler: this.doThisOnce,
      once: true
    },
    onMouseOver: {
      handler: this.doThisOnceInCapturingMode,
      once: true,
      capture: true
    },
  })
}

对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:

修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键: .enter, .13 if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码(opens new window)
修饰键: .ctrl, .alt, .shift, .meta if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey, shiftKey, 或 metaKey)

这里是一个使用所有修饰符的例子:

render() {
  return Vue.h('input', {
    onKeyUp: event => {
      // 如果触发事件的元素不是事件绑定的元素
      // 则返回
      if (event.target !== event.currentTarget) return
      // 如果向上键不是回车键,则中止
      // 没有同时按下按键 (13) 和 shift 键
      if (!event.shiftKey || event.keyCode !== 13) return
      // 停止事件传播
      event.stopPropagation()
      // 阻止该元素默认的 keyup 事件
      event.preventDefault()
      // ...
    }
  })
}

这里的 text 是插槽的 prop 名称,它被定义在父组件中,用于将数据传递到子组件中的插槽中。当子组件接收到这个 prop 值时,它可以在插槽的模板中使用这个 prop,这样就可以将传递的数据渲染到页面中。

这不是JSX,这是使用render函数手动创建VNode的方式。Vue3的render函数可以使用JSX语法,但是这里的代码使用的是普通的JavaScript。

JSX

特别是对应的模板如此简单的情况下:

<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>

这就是为什么会有一个 Babel 插件 (opens new window),用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。

import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
  el: '#demo',
  render() {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

使用 JSX 需要使用 Babel 进行编译,因为 JSX 是一种语法扩展,不是原生 JavaScript,需要经过编译才能在浏览器中运行。Babel 可以将 JSX 转换为普通的 JavaScript 代码,使得浏览器可以识别并执行它们。

插件

插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element(opens new window)
  2. 添加全局资源:指令/过滤器/过渡等。如:vue-touch (opens new window))
  3. 通过全局混入来添加一些组件选项。(如vue-router (opens new window))
  4. 添加全局实例方法,通过把它们添加到 config.globalProperties 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。
// plugins/i18n.js
export default {
  install: (app, options) => {
    app.config.globalProperties.$translate = key => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }

    app.provide('i18n', options)
  }
}

深入响应性原理

我们把对象包装在 Proxy 里的同时可以对其进行拦截。这种拦截被称为陷阱。

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop) {
    console.log('intercepted!')
    return target[prop]
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos

在上面的代码中,handler 对象包含一个 get 方法,当通过 proxy 访问 meal 属性时,该方法将被调用。在这个 get 方法中,我们使用 console.log 输出了一条消息 "intercepted!",然后返回了 target[prop],也就是 dinner 对象的 meal 属性的值。

此外,Proxy 还提供了另一个特性。我们不必像这样返回值:target[prop],而是可以进一步使用一个名为 Reflect 的方法,它允许我们正确地执行 this 绑定,就像这样:

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop, receiver) {
    return Reflect.get(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

// tacos

...arguments 是 JavaScript 中的一种特殊语法,也称为 "剩余参数"(rest parameters)。它可以将一个函数中的多余参数封装成一个数组。

const dinner = {
  meal: 'tacos'
}

const handler = {
  get(target, prop, receiver) {
    track(target, prop)
    return Reflect.get(...arguments)
  },
  set(target, key, value, receiver) {
    trigger(target, key)
    return Reflect.set(...arguments)
  }
}

const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)

reactive 相当于 Vue 2.x 中的 Vue.observable() API ,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响嵌套对象传递的所有 property。

使用 readonly 防止更改响应式对象

有时我们想跟踪响应式对象 (ref 或 reactive) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被 provide 的响应式对象时,我们不想让它在注入的时候被改变。为此,我们可以基于原始对象创建一个只读的 proxy 对象:

import { reactive, readonly } from 'vue'

const original = reactive({ count: 0 })

const copy = readonly(original)

// 在copy上转换original 会触发侦听器依赖

original.count++

// 转换copy 将导失败并导致警告
copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."
posted @ 2023-05-10 14:28  yjxQWQ  阅读(73)  评论(0)    收藏  举报