Vue3简明教程(一)之基本使用方法

flex布局水平垂直居中对其

.center-img {
        display: flex;
        justify-content: center;
        align-items: center;
        text-align: center;
      }

具体来说,这个样式代码使用了 CSS 的 Flex 布局,通过 display: flex; 属性将元素转换为 Flex 容器。接着,justify-content: center; 属性将 Flex 容器内的子元素水平居中对齐,align-items: center; 属性将 Flex 容器内的子元素垂直居中对齐。最后,text-align: center; 属性将子元素内的文本内容居中对齐。

因此,如果给一个元素添加了 .center-img 类,那么该元素就会在水平和垂直方向上居中显示,并且其中的文本内容也会水平居中对齐。

Vue3全局组件

Vue中的全局组件可以在应用的任何地方被使用,而不必在每个组件中都引入。

const app = Vue.createApp({})
app.component('SearchInput', SearchInputComponent)

链式

Vue.createApp({})
  .component('SearchInput', SearchInputComponent)
  .directive('focus', FocusDirective)
  .use(LocalePlugin)

根组件

createApp 的选项用于配置根组件

const RootComponent = { /* 选项 */ }
const app = Vue.createApp(RootComponent)
const vm = app.mount('#app')

在Vue中,每个组件都可以有选项(options)来配置组件的行为和渲染方式,例如:data、methods、computed、watch等等。而根组件也是一个组件,同样也有这些选项来定义其行为和渲染方式。在上述代码中,RootComponent即为根组件的选项对象,通过Vue.createApp方法创建Vue应用,并将其挂载到id为app的DOM节点上。因此,RootComponent是应用的入口组件,用来定义应用的整体行为和渲染方式。

const app = Vue.createApp({
  data() {
    return { count: 4 }
  }
})

原始 HTML

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用v-html 指令

<div v-html="html"></div>

Web 应用程序

Web 应用程序是指通过 Web 浏览器运行的软件应用程序

每一个动态网站都是web应用程序

Attribute

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

v-bind 是 Vue.js 的一个指令,它的作用是动态地绑定 HTML 属性,如 idclasshref 等等。

动态Attribute

<a v-bind:[attributeName]="url"> ... </a>

v-bind 指令后的方括号表示这是一个动态属性名,attributeName 是一个 JavaScript 表达式,它的值可以是一个计算属性或者组件实例中定义的数据属性,决定了绑定的属性名是什么。

修饰符

<form v-on:submit.prevent="onSubmit">...</form>

event.preventDefault()

event.preventDefault() 是一个 JavaScript 方法,它用于阻止事件的默认行为。在 Web 开发中,事件的默认行为通常包括表单提交、超链接跳转和鼠标右键菜单等操作。

在一个表单中,如果你不希望页面刷新,而只是需要提交表单数据到服务器,你可以使用 event.preventDefault() 方法阻止表单的默认提交行为。

缩写

<!-- 完整语法 -->
<a v-bind:href="url"> ... </a>

<!-- 缩写 -->
<a :href="url"> ... </a>
<!-- 完整语法 -->
<a v-on:click="doSomething"> ... </a>

<!-- 缩写 -->
<a @click="doSomething"> ... </a>

动态参数表达式

动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:

<!-- 这会触发一个编译警告 -->
<a v-bind:['foo' + bar]="value"> ... </a>

顶级 data property 名称

Vue 使用 $ 前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _ 前缀。你应该避免使用这两个字符开头的的顶级 data property 名称。

const app = Vue.createApp({
  data() {
    return { count: 4 }
  },
     methods: {
    increment() {
      // `this` 指向该组件实例
      this.count++
    }
  }
})

const vm = app.mount('#app')
vm.increment()
console.log(vm.$data.count) // => 4
console.log(vm.count)       // => 4

防抖

Vue 没有内置支持防抖和节流,但可以使用 Lodash (opens new window)等库来实现。

如果某个组件仅使用一次,可以在 methods 中直接应用防抖:

<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js"></script>
<script>
  Vue.createApp({
    methods: {
      // 用 Lodash 的防抖函数
      click: _.debounce(function() {
        // ... 响应点击 ...
      }, 500)
    }
  }).mount('#app')
</script>

但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created 里添加该防抖函数:

app.component('save-button', {
  created() {
    // 用 Lodash 的防抖函数
    this.debouncedClick = _.debounce(this.click, 500)
  },
  unmounted() {
    // 移除组件时,取消定时器
    this.debouncedClick.cancel()
  },
  methods: {
    click() {
      // ... 响应点击 ...
    }
  },
  template: `
    <button @click="debouncedClick">
      Save
    </button>
  `
})

计算属性

<div >
  <span>{{ publishedBooksMessage }}</span>
</div>

Vue.createApp({
  data() {
    return {
      author: {
        name: 'John Doe',
        books: [
          'Vue 2 - Advanced Guide',
          'Vue 3 - Basic Guide',
          'Vue 4 - The Mystery'
        ]
      }
    }
  },
  computed: {
    // 计算属性的 getter
    publishedBooksMessage() {
      // `this` 指向 vm 实例
      return this.author.books.length > 0 ? 'Yes' : 'No'
    }
  }
}).mount('#computed-basics')

计算属性是一个基于响应式依赖进行缓存的函数,当它所依赖的响应式数据发生变化时,它才会重新求值。这意味着多次访问计算属性在其所依赖的数据没有发生变化时会立即返回之前的计算结果,而不必再次执行函数。

方法则没有缓存的概念,每次访问都会重新执行函数。因此,如果一个值不依赖响应式数据,或者需要在每次访问时动态计算,那么最好使用方法而不是计算属性。

需要注意的是,如果在计算属性中访问的值不是响应式数据,那么计算属性也不会缓存。相比之下,如果在方法中访问的值不是响应式数据,每次访问时都会重新计算。

总之,计算属性适用于基于响应式依赖进行缓存的场景,而方法适用于动态计算或不依赖响应式数据的场景。

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

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

// ...
computed: {
  fullName: {
    // getter
    get() {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set(newValue) {
      const names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

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

watch和计算属性有什么区别

watch 用于监听特定的数据变化,它并不像计算属性一样返回一个值。

计算属性必须要有返回值,在计算属性中不应该执行有副作用的方法。

data() {
  return {
    firstName: 'John',
    lastName: 'Doe'
  }
},
watch: {
  firstName(newValue, oldValue) {
    console.log('firstName changed from', oldValue, 'to', newValue)
  }
}

Class 与 Style 绑定

你可以在对象中传入更多字段来动态切换多个 class。此外,:class 指令也可以与普通的 class attribute 共存。当有如下模板:

<div
  class="static"
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

数组语法

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

<div :class="[activeClass, errorClass]"></div>
data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger'
  }
}

如果你想根据条件切换列表中的 class,可以使用三元表达式:

<div :class="[isActive ? activeClass : '', errorClass]"></div>

这样写将始终添加 errorClass,但是只有在 isActive 为 truthy[1] 时才添加 activeClass。

不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:

<div :class="[{ active: isActive }, errorClass]"></div>

对象语法

<div :style="styleObject"></div>
data() {
  return {
    styleObject: {
      color: 'red',
      fontSize: '13px'
    }
  }
}

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

<div :style="[baseStyles, overridingStyles]"></div>

自动添加前缀

假设你要在Vue组件的style标签中使用CSS transform属性来旋转元素,你可能会这样写:

<style>
    .my-element {
        transform: rotate(45deg);
    }
</style>

然而,不同的浏览器可能对于transform属性有不同的实现方式,因此需要添加浏览器引擎前缀以确保在各种浏览器中都能正常工作。比如,Webkit引擎的浏览器需要添加-webkit-前缀,而Firefox浏览器需要添加-moz-前缀。

因此,为了确保你的CSS样式在不同的浏览器中都能正常工作,你需要这样写:

<style>
    .my-element {
        -webkit-transform: rotate(45deg);
        -moz-transform: rotate(45deg);
        transform: rotate(45deg);
    }
</style>

好的,让我举个例子来说明。

假设你要在Vue组件的style标签中使用CSS transform属性来旋转元素,你可能会这样写:

phpCopy code<style>
    .my-element {
        transform: rotate(45deg);
    }
</style>

然而,不同的浏览器可能对于transform属性有不同的实现方式,因此需要添加浏览器引擎前缀以确保在各种浏览器中都能正常工作。比如,Webkit引擎的浏览器需要添加-webkit-前缀,而Firefox浏览器需要添加-moz-前缀。

因此,为了确保你的CSS样式在不同的浏览器中都能正常工作,你需要这样写:

phpCopy code<style>
    .my-element {
        -webkit-transform: rotate(45deg);
        -moz-transform: rotate(45deg);
        transform: rotate(45deg);
    }
</style>

但是,Vue会自动检测浏览器引擎前缀,并自动将它们添加到你的CSS样式中,因此你只需要这样写就可以了:

<style>
    .my-element {
        transform: rotate(45deg);
    }
</style>

v-if vs v-show

v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。

v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

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

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

v-ifv-for 一起使用时,v-if 具有比 v-for 更高的优先级。(不推荐)

列表渲染(加上key!)

v-for 把一个数组对应为一组元素

我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名

<ul id="array-rendering">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>
Vue.createApp({
  data() {
    return {
      items: [{ message: 'Foo' }, { message: 'Bar' }]
    }
  }
}).mount('#array-rendering')

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

<ul id="array-with-index">
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>
Vue.createApp({
  data() {
    return {
      parentMessage: 'Parent',
      items: [{ message: 'Foo' }, { message: 'Bar' }]
    }
  }
}).mount('#array-with-index')

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

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

v-for 里使用对象

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

<ul id="v-for-object" class="demo">
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>
Vue.createApp({
  data() {
    return {
      myObject: {
        title: 'How to do lists in Vue',
        author: 'Jane Doe',
        publishedAt: '2016-04-10'
      }
    }
  }
}).mount('#v-for-object')

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

<li v-for="(value, name) in myObject">
  {{ name }}: {{ value }}
</li>
<li v-for="(value, name, index) in myObject">
  {{ index }}. {{ name }}: {{ value }}
</li>

维护状态

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

这个问题涉及到 Vue.js 的虚拟 DOM(Virtual DOM)和响应式系统,下面我会尝试简单地解释一下。

当你使用 v-for 指令渲染一个元素列表时,Vue 会根据你提供的数据源生成一组虚拟 DOM 节点。这些节点与实际的 DOM 元素相对应,但是它们只是内存中的对象,而不是真实的浏览器节点。当数据源发生变化时,Vue 会重新生成这些虚拟节点,并通过比较前后两个虚拟 DOM 树的差异,来决定需要更新哪些实际的 DOM 元素以反映最新的数据变化。

当 Vue 使用“就地更新”策略时,它会尝试就地修改现有的实际 DOM 元素,而不是删除旧元素并插入新元素。这意味着,如果数据源中的元素顺序发生变化,Vue 仍然会在相同的位置更新每个元素,而不是移动元素到新的位置。这样做可以减少 DOM 操作的次数,提高性能。

如果 Vue 使用就地更新策略,它将不会移动第二个和第三个 li 元素到新的位置,而是会直接在每个位置更新元素。

总之,“就地更新”策略会在相同的位置更新元素,而不是移动元素到新的位置,这可以提高性能并减少不必要的 DOM 操作。

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

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

<div v-for="item in items" :key="item.id">
  <!-- content -->
</div>

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

数组更新检测

变更方法

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

example1.items = example1.items.filter(item => item.message.match(/Foo/))

item.message 表示当前元素中的 message 属性,而 match(/Foo/) 表示对这个属性值进行正则表达式匹配,判断这个属性值是否包含字符串 "Foo"。如果包含,那么 match 方法将返回一个非空的数组,表示匹配成功;如果不包含,那么 match 方法将返回 null,表示匹配失败。

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

在这个例子中,使用非变更方法 filter() 过滤数组,生成一个新数组后,用新数组替换旧数组,并不是要让旧的数组消失或失效,而是为了让 Vue 能够检测到数组的变化,从而更新 DOM。

Vue 在处理数据变化时,会使用一些启发式算法来最大程度地重用已经存在的 DOM 元素,避免不必要的销毁和创建操作,从而提高性能。如果使用变更方法来修改数组,Vue 可以很容易地检测到数组的变化,并只更新必要的 DOM 元素。但如果使用非变更方法生成了一个新数组,Vue 无法直接知道新数组和旧数组的关系,因此需要通过替换整个数组来触发重新渲染,但这并不意味着旧的数组消失或失效,而是为了让 Vue 可以重新渲染整个列表,以保证数据和 DOM 的同步。

需要注意的是,在实际使用中,应该避免频繁地替换整个数组,因为这可能会影响性能。如果需要对数组进行频繁的变更操作,应该使用变更方法来修改数组,以便 Vue 可以更高效地更新 DOM。

v-for 里使用值的范围

v-for 也可以接受整数。在这种情况下,它会把模板重复对应次数。

<div id="range" class="demo">
  <span v-for="n in 10">{{ n }} </span>
</div>

在 template 中使用v-for

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

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

注意我们推荐在同一元素上使用 v-ifv-for

当它们处于同一节点,v-if 的优先级比 v-for 更高,这意味着 v-if 将没有权限访问 v-for 里的变量:

<!-- This will throw an error because property "todo" is not defined on instance. -->

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>

可以把 v-for 移动到 <template> 标签中来修正:

<template v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo }}
  </li>
</template>

内联处理器中的方法

<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>
// ...
methods: {
  warn(message, event) {
    // now we have access to the native event
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}

多事件处理器

事件处理程序中可以有多个方法,这些方法由逗号运算符分隔:

<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)">
  Submit
</button>
// ...
methods: {
  one(event) {
    // first handler logic...
  },
  two(event) {
    // second handler logic...
  }
}

事件修饰符

<!-- 阻止单击事件继续传播 -->
<a @click.stop="doThis"></a>

<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰符可以串联 -->
<a @click.stop.prevent="doThat"></a>

<!-- 只有修饰符 -->
<form @submit.prevent></form>

<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div @click.capture="doThis">...</div>

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

.stop` 修饰符可以阻止事件继续传播,即防止父级元素或其他元素接收到该事件。

.prevent 修饰符可以阻止事件的默认行为,比如防止表单提交页面重载。

在 Vue.js 中,事件冒泡和事件捕获是 DOM 事件机制中的两个概念。事件冒泡指的是事件在子元素上触发后,会向父级元素逐级传递,直到传递到顶级元素。事件捕获与事件冒泡相反,指的是事件从父级元素开始捕获,逐级向下,直到传递到子元素。

Vue.js 的事件修饰符 .capture.self 都是用于事件的处理阶段,可以用来控制事件的冒泡或捕获行为。

.capture 修饰符可以让事件在父元素上先进行捕获,再由子元素进行冒泡,即先触发父元素上的事件处理函数,再触发子元素上的事件处理函数。

.self 修饰符可以限制事件只在元素自身触发,而不会在内部子元素上触发事件。例如,一个包含文本和按钮的元素,当点击文本时不触发事件处理函数,而点击按钮时才触发事件处理函数,就可以在按钮上添加 .self 修饰符。

v-on:click.prevent.self 是不是先阻止一个元素里面所有元素的冒泡默认行为,并且执行父元素绑定的方法 v-on:click.self.prevent 是只能点父元素并且阻止父元素冒泡行为

事件冒泡的主要用途是方便对于嵌套元素的事件处理。假设你有一个嵌套结构的 HTML 元素,内部嵌套了许多子元素,并且你想要处理子元素上的事件。在没有事件冒泡的情况下,你需要手动为每个子元素添加事件处理函数,这样非常麻烦。而有了事件冒泡,你只需要在父元素上添加一个事件处理函数,该事件处理函数会自动处理所有子元素上发生的事件,这样就大大简化了代码。

<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发   -->
<!-- 而不会等待 `onScroll` 完成                   -->
<!-- 这其中包含 `event.preventDefault()` 的情况   -->
<div @scroll.passive="onScroll">...</div>

.passive 修饰符用于优化性能,表示该事件处理函数不会调用 event.preventDefault(),也就是不会阻止默认的滚动行为。这样一来,在滚动过程中就不需要等待 onScroll 方法执行完毕,从而提高了滚动的性能。需要注意的是,只有当你确定你的滚动事件处理函数不会调用 event.preventDefault() 时,才可以使用 .passive 修饰符。

默认滚动行为是指当滚动内容超出元素的可见区域时,浏览器会自动添加滚动条,允许用户通过滚动条来滚动内容。在滚动过程中,浏览器会自动更新滚动条的位置,并同时更新被滚动内容的显示位置。

阻止默认的滚动行为通常用于实现自定义的滚动效果,例如滚动到某个位置时触发动画效果等。在这种情况下,我们需要阻止浏览器默认的滚动行为,以便我们可以使用 JavaScript 控制滚动行为。另外,阻止默认的滚动行为也可以用于避免一些意外的行为,例如在拖拽元素时防止页面滚动等。

不要把 .passive.prevent 一起使用,因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive 会告诉浏览器你不想阻止事件的默认行为。

passive 修饰符告诉浏览器,事件处理函数不会调用 event.preventDefault() 方法,也就是不会阻止事件的默认行为。浏览器可以利用这个信息来对滚动事件进行优化,例如在滚动时不需要等待事件处理函数完成,直接执行滚动操作,提高滚动的流畅度和响应速度。

但如果同时使用了 .prevent 修饰符,就会调用 event.preventDefault() 方法,阻止事件的默认行为,这将与 .passive 修饰符的意图相悖,可能会对浏览器的优化产生影响。因此,不建议在同时使用 .passive 和 .prevent 修饰符。如果需要阻止事件的默认行为,可以在事件处理函数中手动调用 event.preventDefault() 方法。

按键修饰符

<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
<input @keyup.enter="submit" />

这个代码使用了 @keyup.page-down 事件监听器来捕获键盘上的 PageDown 按键事件,并在触发时调用 onPageDown 方法进行处理。具体来说,当用户在 <input> 元素中按下 PageDown 键时,事件将被捕获并传递给 onPageDown 方法进行处理。

在上述示例中,处理函数只会在 $event.key 等于 'PageDown' 时被调用。

PageDown是电脑键盘上的一个按键,通常位于键盘的右上角。它的作用是向下翻页,可以在浏览网页、文档等长页面时使用,也可以在一些软件中使用。

按键别名

Vue 为最常用的键提供了别名:

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

系统修饰键

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

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

meta 对应的按键是操作系统或者设备相关的,通常在 Mac 上是 Command 键,在 Windows 上是 Windows 键。

例如:

<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

.exact 修饰符

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

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button>

<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>

鼠标按钮修饰符

  • .left
  • .right
  • .middle

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

<template>
  <div @mousedown.left="onLeftMouseDown" @mousedown.right="onRightMouseDown"></div>
</template>

表单输入绑定

多个复选框,绑定到同一个数组:

<div id="v-model-multiple-checkboxes">
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames" />
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
  <label for="mike">Mike</label>
  <br />
  <span>Checked names: {{ checkedNames }}</span>
</div>
Vue.createApp({
  data() {
    return {
      checkedNames: []
    }
  }
}).mount('#v-model-multiple-checkboxes')
<div id="v-model-radiobutton">
  <input type="radio" id="one" value="One" v-model="picked" />
  <label for="one">One</label>
  <br />
  <input type="radio" id="two" value="Two" v-model="picked" />
  <label for="two">Two</label>
  <br />
  <span>Picked: {{ picked }}</span>
</div>
Vue.createApp({
  data() {
    return {
      picked: ''
    }
  }
}).mount('#v-model-radiobutton')

选择框 (Select)

单选时:

<div id="v-model-select" class="demo">
  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
  <span>Selected: {{ selected }}</span>
</div>
Vue.createApp({
  data() {
    return {
      selected: ''
    }
  }
}).mount('#v-model-select')

如果 v-model 表达式的初始值未能匹配任何选项,<select> 元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change 事件。因此,更推荐像上面这样提供一个值为空的禁用选项。

<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />

true-valuefalse-value 属性是 v-model 在处理复选框的时候使用的。默认情况下,如果复选框被选中,v-model 绑定的值为 true,如果未选中,则绑定的值为 false。但是,在某些情况下,你可能想绑定其他值,而不仅仅是 true 或 false。比如,在某些情况下,你想绑定一个字符串值,而不是布尔值。这时候,你可以使用 true-valuefalse-value 属性来指定绑定的值。如果复选框被选中,v-model 绑定的值将会是 true-value 属性指定的值;如果复选框未选中,则绑定的值将会是 false-value 属性指定的值。如果你不使用这两个属性,v-model 仍然能够正常工作,但是绑定的值将只能是 true 或 false。

<input type="radio" v-model="pick" v-bind:value="a" />

v-bind:value="a" 是为了将变量 a 的值作为 input 元素的值,即当这个 radio 被选中时,v-model 绑定的变量 pick 的值将变为 a。如果没有设置 v-bind:value,则 input 元素的值默认为 on

修饰符

.lazy

这意味着用户输入时数据不会实时同步到 Vue 实例的数据中,而是等到输入完成并且用户离开输入框后才同步。

input 元素本身就有 change 事件,它会在 input 元素的 value 值改变并失去焦点时触发。

.number

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

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

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

trim

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

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

组件基础

通过 Prop 向子组件传递数据

在 Vue.js 组件中,我们可以使用 props 选项定义一组可以从父组件传递到子组件的自定义属性。当这些属性在父组件中更新时,会自动传递到子组件中并更新对应的属性值。

const App = {
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}

const app = Vue.createApp(App)

app.component('blog-post', {
  props: ['title'],
  template: `<h4>{{ title }}</h4>`
})

app.mount('#blog-posts-demo')
<div id="blog-posts-demo">
  <blog-post
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
  ></blog-post>
</div>

监听子组件事件

父级组件可以像处理 native DOM 事件一样通过 v-on@ 监听子组件实例的任意事件:

<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>

同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:

<button @click="$emit('enlarge-text')">
  Enlarge text
</button>

多亏了 @enlarge-text="postFontSize += 0.1" 监听器,父级将接收事件并更新 postFontSize 值。

我们可以在组件的 emits 选项中列出已抛出的事件。

app.component('blog-post', {
  props: ['title'],
  emits: ['enlarge-text']
})

使用事件抛出一个值

有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 <blog-post> 组件决定它的文本要放大多少。这时可以使用 $emit 的第二个参数来提供这个值:

<button @click="$emit('enlarge-text', 0.1)">
  Enlarge text
</button>

然后当在父级组件监听这个事件的时候,我们可以通过 $event 访问到被抛出的这个值:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

或者,如果这个事件处理函数是一个方法:

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>

那么这个值将会作为第一个参数传入这个方法:

methods: {
  onEnlargeText(enlargeAmount) {
    this.postFontSize += enlargeAmount
  }
}

在组件上使用 v-model

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

<input v-model="searchText" />

等价于:

<input :value="searchText" @input="searchText = $event.target.value" />

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

<custom-input
  :model-value="searchText"
  @update:model-value="searchText = $event"
></custom-input>

这里的 model-value 是一个自定义属性,用来接收父组件传递过来的值。在组件内部,我们监听了一个 update:model-value 事件,并将事件的参数赋给了组件的值。这样,当父组件中的 searchText 发生变化时,子组件的值也会自动更新。同时,当子组件的值发生变化时,也会通过 update:model-value 事件通知父组件,从而完成双向绑定。

使用了 DOM 模板中的 kebab-case,即短横线连接的命名方式,比如 model-value。而在 JavaScript 中常用的是 camelCase 命名方式,即小驼峰式命名方式,比如 modelValue

在 Vue 中,当你在组件中使用 kebab-case 的 prop 名称时,Vue 会自动将其转换为 camelCase,因此你可以在组件中使用 camelCase 的名称来定义 props 和事件。

在HTML中,属性名不区分大小写,因此无论你使用v-bind还是直接在标签上使用属性绑定,大小写都会被自动转换为小写。这意味着当你在Vue组件中使用props或事件处理函数时,你需要注意属性名的命名方式。

Vue组件的props和事件处理函数通常使用驼峰式命名,例如myPropmyEventHandler。然而,在DOM模板中,你需要使用它们的等效的kebab-case(横线字符分隔)格式,例如my-propmy-event-handler

这是因为当你在模板中使用驼峰式属性名时,Vue将尝试将其转换为kebab-case格式,并在组件的根元素上绑定对应的DOM属性。这样做的目的是为了确保组件的属性名和标准HTML属性名之间的一致性。

因此,如果你想在Vue组件中定义一个props或事件处理函数,并在模板中使用它,你需要使用kebab-case格式的属性名。同样,如果你想在组件的根元素上绑定一个普通的HTML属性,你也需要使用kebab-case格式。例如,如果你在组件中定义了一个名为myProp的prop,并想在模板中使用它,你需要这样写:<my-component my-prop="someValue"></my-component>

总之,由于HTML属性名不区分大小写,因此在Vue组件中使用props和事件处理函数时需要使用kebab-case格式的属性名,以确保组件能够正确地绑定和处理这些属性。

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

  • 字符串模板 (例如:template: '...')
  • 单文件组件
  • <script type="text/x-template">
app.component('custom-input', {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  template: `
    <input
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    >
  `
})

但是谁这么写啊,可重用性太低了。

<template>
  <custom-input v-model="message"></custom-input>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  }
}
</script>

<template>
  <custom-input v-model="message"></custom-input>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  }
}
</script>

动态组件

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>

后端管理系统比较喜欢用。

组件注册

组件名大小写

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

#使用 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 是指传向一个组件,但是该组件并没有相应 propsemits 定义的 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 事件,定义了一个函数来验证事件的有效性。该函数接收一个包含 emailpassword 属性的对象作为参数,并根据这些属性是否存在来验证事件的有效性

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-onv-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较为简单,但足以应对许多实际应用场景。

提供 / 注入

对于这种情况,我们可以使用 provideinject 对。父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 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> 组件的占位内容,以便在异步组件加载完成前提供一个占位符。这个行为可以通过在异步组件的选项中设置 suspensiblefalse 来关闭,这样异步组件将始终控制自己的加载状态而不受父组件的影响。

模板引用

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

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

是的,当 refv-for 一起使用时,你得到的 ref 将是一个数组,数组中的每个元素对应着每个生成的组件实例,因此在这个例子中,所有 my-componentref 都是 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 属性,以便在模板中渲染出当前鼠标位置的颜色。

硬件加速

perspectivebackface-visibilitytransform: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')
  }
})

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

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

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!"
  }
})

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

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

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

所以,对于这个自定义选项合并策略函数来说,toValfromVal 的具体含义取决于函数的调用上下文,即在合并组件选项时,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')

函数简写

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

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-ifv-for

只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-ifv-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/elsemap() 来重写:

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 指令扩展为 modelValueonUpdate: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 防止更改响应式对象

有时我们想跟踪响应式对象 (refreactive) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被 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."

响应式计算和侦听

watchEffect和watch的区别

watchEffect适用于简单的数据变化处理,而watch适用于复杂的数据变化处理和异步操作

watchEffect是一个自动追踪依赖的响应式函数,它会在其内部使用到的任何响应式数据发生变化时自动执行。

watch是一个手动追踪依赖的函数,它需要显式地指定要监视的响应式数据,并在回调函数中处理数据变化。因此,watch适用于处理需要复杂逻辑或异步操作的数据变化。

停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

但是,在某些情况下,可能需要手动停止侦听器,例如:

  1. 当组件被卸载之前,需要手动停止 watchEffect 创建的侦听器。
  2. 当需要根据某些条件动态地停止侦听器时,可以将 watchEffect 的返回值赋值给一个变量,并在需要停止侦听器时调用该变量。

清除副作用

watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

id 的值改变或者 watchEffect 停止侦听时,会调用 onInvalidate 函数注册的回调函数,用于清除之前的异步操作。

代码中的 onInvalidate 回调函数用于注册一个取消异步操作的逻辑,即调用 token.cancel()

为了避免异步操作未完成就被取消,我们需要在执行异步操作时记录一个标识符 token,用于在需要时取消异步操作。

onInvalidate 回调函数是用于注册清除失效时的回调,用于取消之前未完成的异步操作。在上述代码中,当 watchEffect 侦听到 id 的值发生变化时,会调用注册的 onInvalidate 回调函数,用于取消之前未完成的异步操作。

但是,在异步函数完成之前,即异步函数的 Promise 对象没有被 resolve 或 reject 时,watchEffect 不会认为副作用已经失效,因此不会触发 onInvalidate 回调函数。只有当异步函数完成后,Promise 对象被 resolve 或 reject 时,watchEffect 才会认为副作用已经完成,并且会重新触发副作用函数,此时才会调用 onInvalidate 回调函数。

侦听器调试

onTrackonTrigger 选项可用于调试侦听器的行为。

  • onTrack 将在响应式 property 或 ref 作为依赖项被追踪时被调用。
  • onTrigger 将在依赖项变更导致副作用被触发时被调用。

这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:

watchEffect(
  () => {
    /* 副作用 */
  },
  {
    onTrigger(e) {
      debugger
    }
  }
)

onTrackonTrigger 只能在开发模式下工作

watch

侦听单个数据源

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

侦听多个数据源

侦听器还可以使用数组同时侦听多个源:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

watchEffect 共享的行为

watchwatchEffect共享停止侦听清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入)、副作用刷新时机侦听器调试行为。

组合式 API

生命周期钩子注册内部 setup

组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on:即 mounted 看起来像 onMounted

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'

// in our component
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  onMounted(getUserRepositories) // on `mounted` call `getUserRepositories`

  return {
    repositories,
    getUserRepositories
  }
}

watch

export default {
  data() {
    return {
      counter: 0
    }
  },
  watch: {
    counter(newValue, oldValue) {
      console.log('The new counter value is: ' + this.counter)
    }
  }
}

const { user } = toRefs(props)

这是一种 JavaScript 中的对象解构写法,它可以将 props 对象中的 user 属性解构出来,并将其存储在一个名为 user 的变量中。

它将一个响应式对象转换为普通对象,其中每个属性都是一个 ref 对象。

独立的 computed 属性

refwatch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。让我们回到我们的 counter 例子:

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

Setup

参数

使用 setup 函数时,它将接受两个参数:

  1. props
  2. context

props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

不能使用 ES6 解构,因为它会消除 prop 的响应性。

果需要解构 prop,可以通过使用 setup 函数中的 toRefs 来完成此操作:

// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
	const { title } = toRefs(props)

	console.log(title.value)
}

为了使 props 具有响应性,我们可以使用 Vue.js 3 中提供的 toRefs 函数,将 props 对象中的每个属性转换为响应式引用。通过这种方式,我们可以在子组件中创建一个对 props 的响应式引用,从而使其能够在子组件中进行监听和更改。

然而,如果我们使用 ES6 解构来从 props 中提取属性并创建变量,这些变量就不再是响应式的了。这是因为 ES6 解构会将属性从它们原始的响应式引用中解开,并将它们作为普通的变量赋值给新的变量。这样,我们失去了对原始响应式引用的引用,因此无法对其进行监听和更改。

context

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

// MyBook.vue
export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}

执行 setup 时,组件实例尚未被创建。因此,你只能访问以下 property:

  • props
  • attrs
  • slots
  • emit

换句话说,你将无法访问以下组件选项:

  • data
  • computed
  • methods

结合模板使用

如果 setup 返回一个对象,则可以在组件的模板中像传递给 setupprops property 一样访问该对象的 property:

<!-- MyBook.vue -->
<template>
  <div>{{ readersNumber }} {{ book.title }}</div>
</template>

<script>
  import { ref, reactive } from 'vue'

  export default {
    setup() {
      const readersNumber = ref(0)
      const book = reactive({ title: 'Vue 3 Guide' })

      // expose to template
      return {
        readersNumber,
        book
      }
    }
  }
</script>

注意,从 setup 返回的 refs 在模板中访问时是被自动解开的,因此不应在模板中使用 .value

选项式 API Hook inside setup
beforeCreate Not needed*
created Not needed*
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

在这些钩子中编写的任何代码都应该直接在 setup 函数中编写。

提供/注入

在 Vue.js 中,provideinject 是用于在组件之间共享数据和方法的高级技术。

Pinia 是 Vue 3 中的一种新的状态管理库,它提供了一个非常简单的 API,使您能够使用类似 Vuex 的方式管理您的应用程序状态。与传统的 Vue 组件中的 provide 和 inject 类似,Pinia 也可以在应用程序中传递状态,但是它是基于类的 API,而不是基于函数的 API。

然而,有时候您可能希望使用更原始的 provide 和 inject API。这可能是因为您不希望使用 Pinia,或者您认为使用 provide 和 inject 更符合您的设计需求。另外,有些开发者可能更熟悉 provide 和 inject,因此他们更愿意使用这种方式来传递状态。

模板引用

JSX 中的用法

export default {
  setup() {
    const root = ref(null)

    return () =>
      h('div', {
        ref: root
      })

    // with JSX
    return () => <div ref={root} />
  }
}

v-for 中的用法

组合式 API 模板引用在 v-for 内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:

<template>
  <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
    {{ item }}
  </div>
</template>

<script>
  import { ref, reactive, onBeforeUpdate } from 'vue'

  export default {
    setup() {
      const list = reactive([1, 2, 3])
      const divs = ref([])

      // 确保在每次更新之前重置ref
      onBeforeUpdate(() => {
        divs.value = []
      })

      return {
        list,
        divs
      }
    }
  }
</script>

Vue.set 方法用于在 Vue 实例的响应式对象上添加响应式属性,以确保这些新添加的属性也能触发视图的重新渲染。

在 JavaScript 中,添加新属性时,如果该对象是响应式的,则新添加的属性将不会触发视图的更新。这是因为 Vue 在实例化时会将所有响应式属性转换为 getter/setter,但是它不能检测到属性的添加或删除。

举个例子,假设你有一个对象 obj

const obj = {
  a: 1
}


如果你在模板中使用 {{ obj.a }},那么这个值会响应式地被渲染到视图上。但是,如果你使用 Vue.set 添加一个新属性 b

Vue.set(obj, 'b', 2)

那么这个新属性就会被添加到对象中,并且可以在模板中使用 {{ obj.b }} 渲染出来。更重要的是,因为使用了 Vue.set,这个新属性也会触发视图的更新,而不是仅仅被添加到对象中但没有反应到视图上。

Object.assign()_.extend() 都是用来将对象合并成一个新的对象的方法。合并的对象可以是一个或多个对象。它们的主要用途是将一个或多个对象的属性合并到目标对象中。

import Vue from 'vue'
import _ from 'lodash'

const vm = new Vue({
  data: {
    obj: {
      a: 1
    }
  }
})

// 使用Object.assign()将新属性合并到响应式对象中
Object.assign(vm.obj, { b: 2 })

// 或使用Lodash的_.extend()方法
_.extend(vm.obj, { c: 3 })

对于数组

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

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

例如:

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

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

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

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

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

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

vm.items.splice(newLength)

单文件组件

单元测试

Jest 是一个专注于简易性的 JavaScript 测试框架。一个其独特的功能是可以为测试生成快照 (snapshot),以提供另一种验证应用单元的方法。

Mocha 是一个专注于灵活性的 JavaScript 测试框架。因为其灵活性,它允许你选择不同的库来满足诸如侦听 (如 Sinon) 和断言 (如 Chai) 等其它常见的功能。另一个 Mocha 独特的功能是它不止可以在 Node.js 里运行测试,还可以在浏览器里运行测试。

组件测试

Vue Testing Library (@testing-library/vue)

Vue Testing Library 是一组专注于测试组件而不依赖实现细节的工具。由于在设计时就充分考虑了可访问性,它采用的方案也使重构变得轻而易举。

它的指导原则是,与软件使用方式相似的测试越多,它们提供的可信度就越高。

Vue Test Utils

Vue Test Utils 是官方的偏底层的组件测试库,它是为用户提供对 Vue 特定 API 的访问而编写的。如果你对测试 Vue 应用不熟悉,我们建议你使用 Vue Testing Library,它是 Vue Test Utils 的抽象。

端到端 (E2E) 测试

Cypress.io

Cypress.io 是一个测试框架,旨在通过使开发者能够可靠地测试他们的应用,同时提供一流的开发者体验,来提高开发者的生产率。

#Nightwatch.js

Nightwatch.js 是一个端到端测试框架,可用于测试 web 应用和网站,以及 Node.js 单元测试和集成测试

#Puppeteer

Puppeteer 是一个 Node.js 库,它提供高阶 API 来控制浏览器,并可以与其他测试运行程序 (例如 Jest) 配对来测试应用。

TestCafe

TestCafe 是一个基于端到端的 Node.js 框架,旨在提供简单的设置,以便开发者能够专注于创建易于编写和可靠的测试。

TypeScript 支持

推荐配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    // 力扣 – 中文网 助你高效提升编程技能 https://www.javascriptc.com/special/leetcode/
// 这样就可以对 `this` 上的数据属性进行更严格的推断`
    "strict": true,
    "jsx": "preserve",
    "moduleResolution": "node"
  }
}

Webpack 配置

如果你使用自定义 Webpack 配置,需要配置 ' ts-loader ' 来解析 vue 文件里的 <script lang="ts"> 代码块:

// webpack.config.js
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        loader: 'ts-loader',
        options: {
          appendTsSuffixTo: [/\.vue$/],
        },
        exclude: /node_modules/,
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      }
      ...

开发工具

#项目创建

# 1. Install Vue CLI, 如果尚未安装
npm install --global @vue/cli@next

# 2. 创建一个新项目, 选择 "Manually select features" 选项
vue create my-project-name

# 3. 如果已经有一个不存在TypeScript的 Vue CLI项目,请添加适当的 Vue CLI插件:
vue add typescript

与 Options API 一起使用

interface Book {
  title: string
  author: string
  year: number
}

const Component = defineComponent({
  data() {
    return {
      book: {
        title: 'Vue 3 Guide',
        author: 'Vue Team',
        year: 2020
      } as Book
    }
  }
})

注释返回类型

由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注释返回类型的计算属性。

import { defineComponent } from 'vue'

const Component = defineComponent({
  data() {
    return {
      message: 'Hello!'
    }
  },
  computed: {
    // 需要注释
    greeting(): string {
      return this.message + '!'
    }

    // 在使用setter进行计算时,需要对getter进行注释
    greetingUppercased: {
      get(): string {
        return this.greeting.toUpperCase();
      },
      set(newValue: string) {
        this.message = newValue.toUpperCase();
      },
    },
  }
})

有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数:

const year = ref<string | number>('2020') // year's type: Ref<string | number>

year.value = 2020 // ok!

移动端

混合应用开发

#Capacitor

Capacitor (opens new window)是一个来自 Ionic Team (opens new window)的项目,通过提供跨多个平台运行的 API,开发者可以使用单个代码库构建原生 iOS、Android 和 PWA 应用。

#NativeScript

NativeScript (opens new window)使用已熟悉的 Web 技能为跨平台(真正的原生)移动应用提供支持。两者结合在一起是开发沉浸式移动体验的绝佳选择。

服务端渲染

是指将应用程序的页面结构在服务器端生成并发送到浏览器端展示,而不是像传统的前端渲染(Client Side Rendering,CSR)那样,先将页面结构以空壳的形式发送到浏览器端,再通过浏览器端的 JavaScript 进行数据填充和渲染。服务端渲染能够提高应用程序的首屏加载速度和搜索引擎优化,使得应用程序能够更快速地被用户访问和被搜索引擎收录。Vue 官方提供了 Vue SSR 的实现方式和详细的指南,同时也有社区项目 Nuxt.js 和 Quasar Framework 等提供了更高层次的 SSR 开发框架和工具。

无障碍

使用 aria-labelledby 属性可以将元素与其他元素配对,从而提供更好的可访问性,让屏幕阅读器能够更好地理解标签的含义。在这个例子中,使用了 aria-labelledby 来指定一个与输入框关联的标题,这个标题是通过 id="billing" 属性定义在 h1 元素上的,这样屏幕阅读器就可以将输入框的标签与标题相关联,提高了表单的可访问性

屏幕阅读器是一种可以帮助视障人士获取电脑屏幕信息的辅助技术。它通过将文本和其他屏幕内容转换为音频、Braille或者语音,使得用户能够听到或者触摸到屏幕上显示的信息。屏幕阅读器可以帮助视障人士在电脑上阅读文本、浏览网页、发送电子邮件等。常见的屏幕阅读器包括JAWS、NVDA、VoiceOver等。

aria-label

你也可以给输入一个带有aria-label (opens new window)的可访问名称。

<label for="name">Name</label>
<input
  type="text"
  name="name"
  id="name"
  v-model="name"
  :aria-label="nameLabel"
/>

aria-labelledby

使用 aria-labelledby (opens new window)类似于 aria-label,除非标签文本在屏幕上可见。它通过 id 与其他元素配对,你可以链接多个 id

<form
  class="demo"
  action="/dataCollectionLocation"
  method="post"
  autocomplete="on"
>
  <h1 id="billing">Billing</h1>
  <div class="form-item">
    <label for="name">Name:</label>
    <input
      type="text"
      name="name"
      id="name"
      v-model="name"
      aria-labelledby="billing name"
    />
  </div>
  <button type="submit">Submit</button>
</form>

aria-describedby

aria-describedby (opens new window)的用法与 aria-labelledby 相同,预期提供了用户可能需要的附加信息的描述。这可用于描述任何输入的标准:

<form
  class="demo"
  action="/dataCollectionLocation"
  method="post"
  autocomplete="on"
>
  <h1 id="billing">Billing</h1>
  <div class="form-item">
    <label for="name">Full Name:</label>
    <input
      type="text"
      name="name"
      id="name"
      v-model="name"
      aria-labelledby="billing name"
      aria-describedby="nameDescription"
    />
    <p id="nameDescription">Please provide first and last name.</p>
  </div>
  <button type="submit">Submit</button>
</form>
posted @ 2023-05-10 14:10  yjxQWQ  阅读(233)  评论(0)    收藏  举报