如何使用Vue框架的记录
徐徐 抱歉选手

创建Vue应用

使用const app = Vue.createApp({})创建Vue应用,在创建完Vue应用之后,还要利用app.mount("#vue-app")把当前Vue应用mount/挂载到对应html节点中去,而html节点用id唯一标识<div id="vue-app"></div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div class="vue-app"></div>

<script>
const app = Vue.createApp({
template: "<div><h1>HI</h1></div>",
});
app.mount(".vue-app");
</script>
</body>
</html>

其实只要是一个选择器都能实现挂载,利用class对应点选择器也能实现挂载,问题在于一个class可以对应到多个html标签,而使用id是因为id的唯一性。因此约定都适用id选择器和标签的id属性。

template syntax

在创建template的时候,使用引号将html语言包裹起来,能正常加载,但是问题在于必须在一行内全部写完所有html内容,因此使用模版字符串中来实现分栏。

image-20201225142100746image-20201225142124760

Vue.js使用了基于HTML的模版语法,利用模版字符串可以在Vue实例的template中直接写入合法的HTML。

在HTML文件中,将想要传递简单值值的属性用双括号(“Mustache” Syntax)包裹(如果是复杂值参考computed attribute field)。但是只能在HTML元素两个尖括号之间使用,作为HTML元素内容。

1
<tag>{{ Mustache-Syntax }}</tag>

双括号既可以用在我们想要mount id特定的原生html中,也可以用在我们创建的Vue实例的template中。

template syntax和v-directive中的v-text很相似,但是v-text的问题在于不管template中父亲元素内部有什么内容,都会被v-text的内容替换/覆盖。

生命周期钩子

每个Vue实例被创建时都要经过一系列初始化过程, 而实例生命周期钩子就是在初始化过程中执行的函数,可以在特定的初始化阶段插入我们想要的代码。

beforeCreate(页面载入之前)/created(Vue实例被创建之后),例子就比如在刚进入页面,或者刷新页面的时候。

breforeMount(挂载之前)/mounted(挂载成功):如果在html元素内部有内容却没有在vue实例的template定义内容,那么将html元素的outerHTML编译成template;如果有template,将template编译成render function。

mounted和beforeMount的例子就比如在页面间路由,如果从当前页面跑到另外一个页面去,那么当前页面的vue实例就会经历beforeUnmount以及unmounted,而去往的目的页面的vue实例就会经历beforeMount以及mounted。

beforeUpdate(数据改变,更新之前)/updated(更新完毕),例如用户在文本框输入了数据,传递到vue,那么就会重新渲染显示到页面。

注意不能使用箭头函数来定义一个生命周期方法,因为箭头函数没有指向实例的this。

生命周期函数定义在methods field中,以函数的形式定义。

lifecycle

v- directives指定约定

A directive’s job is to reactively apply side effects to the DOM when the value of its expression changes.

指令是带有v-前缀的特殊的由vue来处理的attribute。指令接受单个JavaScript表达式(v-for和v-on除外)。attribute的值都需要从我们定义的vue实例中去寻找,而不是默认的DOM对象。

Directives are prefixed with v- to indicate that they are special attributes provided by Vue, and they apply special reactive behavior to the rendered DOM.

Vue为最常用的v-bindv-on提供了缩写指令。v-bind语法中可以直接省去v-bind,直接在html标签中用冒号传参数:url/:[computed];v-on可以用@符号替代v-on:,直接在html中用at符号和事件名@click/@[event]

修饰符 (modifier) 是以半角句号.指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。每个v-directive可用的修饰符见具体的说明模块。

v-bind:

实现值的绑定

在v-指令后面添加一个冒号,让指令接受一个html attribute参数,v-指令会让该html attribute和等号后传入的单个JavaScript表达式的值绑定。(主要任务是绑定当前vue实例中的值)

v-bind让我们把html field中任何一个attribute的值和定义在vuejs中的data联系起来,因为我们把value放在引号中,如果没有v-bind,html中的attribute会直接被理解为字符串值传递过去。比如href=”url”的意思就是href的值是url,而不是url这个变量所指向的具体的url地址。

例如:

1
<a v-bind:href="url"> ... </a>

在这里 href 是参数,告知 v-bind 指令将该元素的 href attribute 与表达式 url 的值绑定。

又如:

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

在这里click监听事件名称是参数,告知v-on指令将该html元素的click事件与表达式doSomething回调函数绑定。

实现属性的绑定

既然属性值可以从vuejs的data中传递过来,那么属性是否也可以从vuejs的data中传递过来呢?通过方括号将变量括起来放到v-directive的冒号后面,vue首先会将方括号里的变量当作一个JavaScript表达式在vuejs的代码部分查找到匹配名字的变量后求值后再作为静态的html attribute传递给html(在vue的data field中定义这个属性一定要是字符串)与等号后JavaScript表达式值的绑定。

例如:

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

如果[attributename]被求值出来的结果是一个null字符串,表示移除该绑定

在DOM中使用模版是(也就是直接在html文件中撰写模版)避免在方括号中使用大写字符,因为DOM会把代码转换为小写。如<a v-bind:[someAttr]="value"> ... </a>就会被自动转换为v-bind:[someattr],除非js文件的vue实例中确实有return一个someattr,否则代码不会工作。

不要在方括号内书写空格和引号,因为空格和引号在html的attribute名里都是无效的。

绑定html class

通过对象true/false切换class

通过给:class一个对象,实现动态切换class

html class在该对象的object field中定义,而该class是否存在取决于该对象的obejct field value的truthiness

:class指令可以与html的普通的class共存。


  1. 内联定义方式

当然该对象可以直接内联定义在html template中,如:

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

  1. 解耦定义方式

也可以在vue实例的data field中return,如html中定义:

1
<div :class="classObject"></div>

并在JavaScript中定义:

1
2
3
4
5
6
7
8
data() {
return {
classObject: {
active: true,
'text-danger': false
}
}
}

通过数组值应用多个class

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

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

这两个部分组合起来渲染结果就是

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

绑定html style

和绑定html class遵循相似的方式,对象语法和数组语法都可以。

v-model表单输入双向绑定

v-bindv-model的区别

v-bind虽然能够实现js中的数据传递向html(也就是视图),但是用开发工具查看当用户在视图上的input框里输入的内容是不会自动同步到js中去的,也就是说这是一个单向绑定。

如果实现双向绑定,直接使用v-model,v-model隐含的语义就是在v-bind实现的单项数据基础上,再把反向增加一个数据流,作用的属性是value。

v-model的具体使用

v-model指令用于表单input textarea 以及 select元素上创建和JavaScript的data对象的双向数据绑定

v-model会忽略HTML文件中表单元素的各种属性的初始值,只会依据JavaScript文件中的实例数据作为初始数据来源

Tag Property Event
text/textarea Value Input
checkbox/radio Checked Change
select Value Change

选择框单选时需要注意提供一个值为空的初始禁用选项,选择框多选时绑定到一个数组。

使用v-model修饰符

v-model的绑定是默认用户在focus时每次输入都要vue去响应去同步,这是很不高效的。因此就有了v-model.lazy修饰符,这告诉vue,等用户的focus从当前表单输入离开了之后,你再去更新vue实例反向的数据流。

有时候用户会在输入文本头尾添加无意义的空格,我们不希望接受这些无意义的输入,因此就有了v-model.trim修饰符,这告诉vue,你把用户输入的头尾空格都修剪掉在作为同步到vue实例对应的地方去储存值。

默认接受用户输入是字符串格式,有时候希望把字符串作为number形式储存在vue 实例中,因此就有了v-model.number修饰符。

v-once一次性渲染DOM

v-once 代表只在DOM中渲染一次该组件,但是该组件中涉及的数据仍然由vue组件管控,会在vue devtools的data field中(如果有的话)被更新。但是被v-once标记的元素不会再初始渲染后再次重新渲染。

v-html在template中插入html代码

在template中的元素上使用v-html会告诉vue,父亲标签里面的字标签应当被当作原生html渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h1 v-html="title"></h1>
</template>

<script>
export default {
data(){
return {
title: "<h1>Hey Vue3</h1>",
}
}
}
</script>

v-if v-else-if v-else条件渲染

v-if option的使用决定了这个标签是否会被销毁或者再造,即是否存在于DOM树中。如有必要还可以v-else-if v-else紧邻配对使用

v-if option只能加载一个单独的元素标签上,如果要对好多个元素标签使用条件渲染,应该把需要条件渲染的所有元素标签都放在<template v-if="??"> <\template>标签中。

不推荐一起使用v-ifv-for因为当二者存在于同一个元素节点上时,v-if有更高的优先级,总是先测试它。因此v-if没有权限访问v-for作用域内的变量。

v-showCSS切换

使用了v-show option的元素总是会被渲染并且留在DOM中,它的作用仅仅是切换CSS property是否display。给插入的父亲标签添加一个<h1 style="display: none;">这里是script中的template的内容</h1>

v-for多选素渲染

使用v-for来循环渲染包含多个元素的内容,可以是遍历数组元素,也可以是遍历对象属性。

Vue使用v-for进行渲染的时候,默认使用就地更新(in-place patch strategy)。如果数据项的顺序改变,Vue不会移动DOM元素来匹配数据项的顺序,而是就地更新,并确保它们在每个索引位置正确渲染(因此会造成不必要的渲染)。因此,为了确保Vue正确识别节点,在使用v-for的时候带上v-bind:key=""

修饰符modifiers

.lazy在Change事件之后进行同步,(比如,不是在用户选中对话框且输入时就同步,而是用户光标离开对话框后再同步)

.number自动将用户的输入值转为数值类型

.trim自动过滤用户输入的收尾空白字符

v-on事件监听

利用v-on: event-listener=""(short for @event-listener="" )。

赋值语句后跟一个javascript简单语法(这个语句和在vue实例中执行的语句有相同的效果比如@submit.prevent="newHero = 'Wonder Woman'"就是可以的),或者就是在js文件的methods field中预先定义好的(带参数的)回调函数。

当在HTML的内联语句中需要访问原始的DOM事件时,可以将原始DOM事件利用$event特殊变量传入回调函数中。

对于一个事件的多个事件处理函数用逗号分开回调函数。

v-on修饰符

在事件处理函数中经常需要调用 event.preventDefault() 或者 event.stopPropagation()。这属于DOM细节的考虑范畴,因此通过事件修饰符将这个操作框定在html中。虽然可以在Vue的methods中去实现,但methods更多需要处理的事数据逻辑而非DOM细节。由此引入事件修饰符,directive.postfix-event-modifiers

  • .stop
  • .prevent在DOM中如果发生了表单提交行为,是默认会重新刷新当前网页,并且路径会变成http://localhost:8080/?,这意味着表单提交了,我们不希望这样的默认行为发生,因此引入了v-on:submit.prevent这样的修饰符,vue会阻止DOM在发生了submit事件是自动提交表单刷新页面的行为。
  • .capture
  • .self
  • .once 点击事件只触发一次,也可以被用到component events中
  • .passive 相当于addEventListenerpassive选项,执行事件的默认行为

监听键盘事件时需要检查详细按键,Vue允许为v-on在监听键盘事件时添加按键修饰符。KeyBoardEvent.key中规定的任意有效按键名转换为kebab-case(短横线形式)作为按键修饰符即可。

1
2
<input v-on:keyup.page-down="onPageDown"/>
//处理函数只会在$event.key等于PageDown时才会被调用。

vue组件实例全体field约定

创建组件实例通过调用Vue.createApp({})方法来实现,通过给它添加data/computed/watch/methods/lifecycle-hooks这些field来描述他的数据和数据操作。

1
2
const app = Vue.createApp({})
const vm = app.mount('#app')

在JavaScript文件中定义的组件通过const vm = app.mount('#app')的mount方法挂载到对应的html中去。

Vue 使用 $ 前缀通过组件实例暴露自己的内置field。它还为内部 property 保留 _ 前缀。

field中的所有return{}的内容都可以在挂载后的html中直接访问到,无需经过该field。

Vue中所有field内,this都指向当前组件实例

data field约定

1
2
3
4
5
6
7
const app = Vue.createApp({
data() {
return {
property1: value1
}
}
})

实例中的data()是一个function,每当新建一个实例,Vue就会调用这个函数。

我们需要保证所有需要用到的数据都在data()函数的return{}中,因为这些实例属性(instance properties)只会在instance第一次被创建的时候被Vue的响应式系统记录在$data对象中。如果存在一些属性你不知道他一开始要取何值,null undefined占位

访问/修改Vue实例中的data部分,可以通过两种形式的点访问法:vm.$data.property1或着vm.property1

通过以上两个方式修改的vue实例的data部分都会同步到实例中去。而实例属性的变化会对应的渲染到视图UI中去,这属于响应式更新。

但是,只有Vue实例被创建时已经存在于数据对象中的属性才是响应式的。通过使用Object.freeze(data)会阻止修改data中已有的property,意味着响应系统无法再追踪变化。

methods field约定

在Vue实例的methods field内部可以有多个方法,一般结合@[event]/@click之类的v-指令当作事件监听的回调函数使用。

如果method方法内部有用到任何data field中的响应式数据,那么method会自动把响应式数据作为渲染依赖项进行跟踪

this指向当前实例,因为Vue自动将this和当前实例绑定在了一起。注意不要使用箭头函数,默认情况下Vue的自动绑定this执行环境,但在箭头函数中没有指向该vue实例的this,但我们在methods中需要用到vue实例中的data function里面的数据,并且每一次使用vue实例中的数据都要在this.variablename

method中的方法不应该包含异步操作?或者说不应该调用它?,异步操作应该出现在生命周期钩子中。

computed attribute field约定

我们使用Mustache Syntax的时候建议在花括号内仅存放简单表达式,如果花括号内的template syntax不再简明易懂,即复杂到包含了reactive data的情况下,需要使用计算属性

计算属性是Data和Methods这两个field之间的一个中间量。

复杂表达式抽象成一个函数,在in-template expression中只使用这个函数名{{complex-expression}},在JS中添加如下

1
2
3
4
5
6
7
8
9
10
Vue.createApp({ 
computed: {
// a computed getter
// no side effects
complex-expression(){
// `this` points to vm instance
return this.????
}
}
})

computed与method比较

为什么这个函数放在computed option中而不是methods option中?

  • 计算属性的值依赖于表达式中的reactive data。

    reactive data的变化会让该计算属性的值自动更新。

    一方面,如果reactive data没有变化,那么计算属性会一直使用上一次计算过的缓存的值。

    另一方面,如果依赖的值不是reactive data,那么计算属性的值无论如何都不会改变。

    Computed properties are cached based on their reactive dependencies.

    计算属性的值是通过基本的、简单的属性值计算而来的,这些用于计算的属性值就是这里所说的reactive dependency。

  • 如果将属性计算通过方法定义,就意味着每一次Vue实例的重新渲染都会导致函数(可能是不必要的)调用。

getter & setter

在上面的例子中通过简单的计算return一个项目的时候,我们其实是在定义一个getter。在有必要时也可以提供一个setter,修改属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]
}
}
}

在html中用mustache syntax访问fullName会调用get(),而在javascript中调用vm.fullname=’John Doe’时就会调用set()函数。

watch field

适用于执行数据变化的异步或开销较大的操作,需要谨慎使用。

对于数据改变所引起的其他数据的改变,选择使用计算属性而非侦听器。

在watch内部定义一个接受依赖数据项作为参数的函数,当这些依赖数据项发生改变的时候,该定义在watch内的函数就会运行。这个函数可以调用methods中定义的异步函数。

emits field自定义事件

详情见组件-emit实现自底向上数据流-利用emits field自定义事件。

refs field获得元素引用

相当于DOM中的getElementByXX中获取html元素的方法。只需要在vue实例的template中为组件添加< ref="RefName"/>属性,双引号中即为ref名。当我们在当前实例中需要获得这个元素的时候,只需要使用this.$refs.RefName就可以获得元素的引用并在该元素上添加方法。

例如,我们为表单的input添加一个名为InputRef,并且希望在当前页面挂载的时候自动聚焦到当前页面,那么在script的mounted field中需要使用mounted() {this.$refs.InputRef.focus();}让输入文本框自动聚焦。

组件

Since components are reusable instances, they accept the same options as a root instance, such as data, computed, watch, methods, and lifecycle hooks.

在HTML文件中被当做可以被重复利用的用户自定义元素(custom element)。

创建自定义组件的两种方式

全局注册

1
2
3
4
5
const app = Vue.createApp({}) //根组件 root instance

app.component('component-name', {})//全局自定义组件,即root instance的custom element

app.mount('#components-demo') // 挂载root instance

这样子的调用默认是全局注册的组件(根实例的子实例),可以被根实例中的template和根实例中的所有其他子组件使用。

挂载后的root instance通过id绑定到html中去,在root instance的内部,可以像一个自定义组件custom element一样去使用用.component('',{})定义的全局注册的组件,自定义的html标签就是传入component的第一个字符串。

1
2
3
<div id="components-demo">
<button-counter></button-counter>
</div>

局部注册

全局注册往往是不够有效率的,因为全局注册的组件意味着就算它不再被使用,它仍然会被包含在最终的构建结果中,这会造成用户下载多余的JavaScript代码。

可以通过一个JavaScript对象来定义组件,并在父组件的components field中用绑定使用。任何一个组件想要使用另外一个局部注册的、通过对象定义的组件,都需要把它绑定到components field中后才可以访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
const ComponentA = {}
// 在根组件中绑定局部注册的组件
const app = Vue.createApp({
components: {
'component-a': ComponentA
}
})
// 在其他局部注册的子组件中绑定另一个局部注册的子组件
const ComponentB = {
components: {
'component-a': ComponentA
}
}

使用了ES6/Babel/webpack等模块系统中的局部注册

建议把所有局部注册的组件都放置在components目录中,如果组件A的定义需要使用到同层级的组件B和组件C,需要在局部注册组件A之前导入同级别文件夹下的组件B和组件C,过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
// current file is ComponentA.vue or ComponentA.js
import ComponentA from './ComponentB'
import ComponentC from './ComponentC'

export default {
components: {
ComponentB,
ComponentC
// 注意以上写法其实是ComponentB: ComponentB的缩写
}
// ...
}

自定义组件名的两种方式

kebab-case

第一种是kebab-case,遵循W3C规范,即字母全部小写且必须包含连字符,如<my-component-name>

PascalCase

第二种是PascalCase,即首字母大写命名组件,如<MyComponentName>

直接在dom中使用自定义组件时(即非字符串的模版),只有kebab-case有效。

非prop的attribute

这里所说的非prop的attribute,就是那些在定义vue组件时,没有在props field和emits field中定义的attribute;也可以说是html标签自带的诸如class/style/id之类的属性。

如何访问

如何访问这些非prop的attribute?在vue组件内部调用this.$attr就可以访问。

root element的attribute inheritance

在js文件中定义组件时返回的是单个根节点时,在html文件中调用vue组件时定义的非prop的attribute将在vue的渲染结果中自动添加到根节点去

以下是在js文件中定义组件:

1
2
3
4
5
6
7
app.component('date-picker', {
template: `
<div class="date-picker">
<input type="datetime" />
</div>
`
})

以下是在html文件中调用组件:

1
<date-picker data-status="activated"></date-picker>

以下是vue经过一系列操作的结果:

1
2
3
<div class="date-picker" data-status="activated">
<input type="datetime" />
</div>

如果有单个根节点,绑定的非prop的attribute自动添加到渲染后的html的根节点。

如果有vue组件返回多个根元素,必须在定义vue组件时显式地在某个根元素上v-bind: "$attrs"。否则会有runtime warning。

禁用attribute inheritance

如果不希望根元素自动属性继承,需要在定义vue组件时inheritAttrs:false

在禁用根元素的元素继承的同时,我们又希望根元素内的其他元素去继承这个非prop的attribute,如何控制?需要使用v-bind方法和当前组件的$attr属性,这个属性会自动包括除了vue组件内部定义的prop field和emits field内的所有属性。

以上两个操作都是在vue组件所在js文件中完成的。

1
2
3
4
5
6
7
8
app.component('date-picker', {
inheritAttrs: false,
template: `
<div class="date-picker">
<input type="datetime" v-bind="$attrs" />
</div>
`
})

props实现自顶向下的数据流

Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance.

怎么解释props的出现?就比如html的a标签有src属性,这里自定义的组件使用方式也就是html的某种标签,既然是用户自定义元素,那么应当也有用户自定义元素属性。props定义了这个自定义元素有什么属性,通过给这个属性传值,将数据从View/UI/html传递到Model/data/JavaScript。

用字符串数组定义prop

在定义组件的时候直接引入一个props: []选项即可。如:

1
2
3
4
5
6
const app = Vue.createApp({})
app.component('blog-post', {
props: ['title'],
template: `<h4>{{ title }}</h4>`
})
app.mount('#blog-post-demo')

在html中的使用遵循如下形式:

1
2
3
4
5
6
7
<div id="blog-post-demo" class="demo">
<!--首先要把自定义组件包裹在mount id下的对应根组件中-->
<blog-post title="My journey with Vue"></blog-post>
<!--自定义组件的props可以当作元素标签的attribute使用-->
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
</div>

当然,这些数据最好和View/UI/html分开存放比较好,根组件的data field可以用来存放这些数据。

因此,整个过程就变成了:首先,在JavaScript中定义根组件和子组件的各个field;其次,在HTML中调用JS文件中定义组件,通过已有html tag的id/class属性或者自定义的custom element attribute绑定数据,数据来自于JavaScript文件的根实例。


用对象列出prop

通常会为每个prop都指定取值类型,对象中的每一个名称和值分别时prop各自的名称和类型,如

1
2
3
4
5
6
7
8
9
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise // 或任何其他构造函数
}

如果想把一个对象的所有property都当作prop传入,使用不带参数的v-bind取代v-bind:prop-name即可,如

1
2
3
4
post: { //这个对象的所有property都要传入
id: 1,
title: 'My Journey with Vue'
}

在html中调用如下:

1
<blog-post v-bind="post"></blog-post>

这个表达式就等价于:

1
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

prop验证

为组件的prop指定验证的要求,比如说基础的类型检查(包括String/Number/Boolean/Array/Object/Date/Function/Symbol基本类型,以及自定义的构造函数),是否有必填的字符串,是否带默认值,自定义验证函数等功能。验证内容都是对当前prop的描述,也就是一种元数据的概念(描述数据的数据)。

前面提到的用对象列出prop,提供了最简单的prop类型检查,如果在props field中定义的prop名称后面跟一个花括号包裹的对象,就能实现prop验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.component('my-component', {
props: {
// 基础的类型检查
// (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
propF: {
validator: function(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
},
}
}

注意prop的验证会在组件实例创建之前进行,因此实例的data/computed等field中涉及的变量在prop验证中不能使用。

prop书写约定

因为在HTML中attribute名是大小写不敏感的,因此浏览器会把所有大写字符全部解释为小写字符,因此在JavaScript中使用的camelCase应用到HTML中就变成对应的kabab-case。

父子组件prop的单向下行绑定

这里的单项数据流是从父组件的prop流动到子组件的prop,也就是说:父级prop的更新会向下流动到子组件中,每一次父组件发生变更时,子组件的所有prop都会刷新为最新的值;子组件不应该变更父组件的状态

虽然子组件不应该变更父组件的状态,但也是可以实现改变的,因为JavaScript的对象和数组是通过引用传入的。

如果在子组件上变更对象或数组定义的prop本身,父组件的状态就会被改变,Vue也会在控制台发出警告。

当尝试在一个子组件内部改变prop时,不要直接对props field中的原始prop进行变更,因为这个prop时从父组件中传递过来的,或者是从View/UI/html中传递过来的。

当需要直接利用prop中的数据时,正确的做法是在当前组件的data field中定义一个新的data property(用react中的说法也就是当前state),并用this.prop作为它的初始值。

1
2
3
4
5
6
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}

当需要经过某些计算再利用prop中的数据时,正确的做法是在当前组件的computed field中定义一个新的computed property,并用this.prop经过一系列的运算后返回想要的数据。

1
2
3
4
5
6
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}

emit实现自底向上的数据流

事件名称约定

不像components和props会自动在各种书写方式之间自动转换,事件名称在js文件中被emit的时候的名称必须和在html文件中被监听的名称一致。

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

如果定义的时候使用camelCase/PascalCase,在监听的时候却又使用kebab-case,是不会有效果的。

所以总是建议使用kebab-case来定义事件名称

emit作用

emit的作用是什么?

在子组件上通过emit定义一个事件/注册一个事件,如果父组件/父亲页面的.vue文件中的该子组件被点击了,就可以监听该事件,然后调用event handler,并且事件处理函数可以使用父亲页面/父亲组件中的数据去处理子组件上的事件。

比如,html中button这个元素本身就有click事件,我们直接使用了button这个元素和click这个事件。按照vue的思考方式,在button这个组件内部定义好了button本身以及click事件。因此我们才可以在html中使用这个元素并且监听这个事件。

总结:emit能够在组件上自定义事件供父组件监听发生在该组件上的行为并添加回调函数,虽然组件上的自定义事件很有可能也是通过默认事件如click触发,但是emit可以放在method中中供其他method调用,比如用户点击了某个组件就出发一个窗口消失的emit事件,或用户完成登陆之后在method中自动emit一个窗口消失的事件。

实现emit

某个子组件与用户发生了某种交互,或者说用户给子组件传递了某些值,子组件此时需要emit an event on itself by calling the built-in $emit method, passing the name of the event,如:

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

当然也可以把$emit(‘enlarge-text’)抽象到methods中的某个函数去,当调用这个函数的时候就有这个事件,而不必须发生click。

1
2
3
4
5
methods: {
close() {
this.$emit("enlarge-text");
},
}

而父组件可以通过v-on或者@标志来监听子组件上的任何事件,如:

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

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

事件发生在子组件上,但是这个事件通过v-on/@标志被传递到了父组件上,且回调函数却是由父组件定义的,或者可以直接用methods field中定义的函数。


在定义子组件的时候提供emits选项,方法如下:

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

emit an event’s value

有时候需要在子组件中emit一个定义在父组件上的事件,并向父组件传递一个参数,例如:

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

在父组件中,可以通过$event访问到这个event value。

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

利用emits field自定义事件

在定义组件的时候在emits field中用数组或者对象的形式定义custom events,只要出现在emits field中,就是自定义事件。custom event会覆盖native event。

建议所有的emitted event都定义在emits field中。

1
2
3
app.component('custom-form', {
emits: ['in-focus', 'submit']
})

obejct syntax to validate

通过数组array syntax定义的事件不能被validate,因此建议使用对象object syntax。

event name在object中就变成了key,对应value是一个验证函数,该函数接受传递给$emit call的payload作为参数,对参数进行操作后返回boolean值去验证这个事件是有效还是无效的。如果不希望event有验证,直接讲key对应value取null。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
app.component('custom-form', {
emits: {
// No validation
click: null,

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

插槽slots

插槽就是在js文件中定义vue组件的template field中插入的一个标志元素,形如<slot></slot>。当在html文件中调用该vue组件时,直接在vue组件开闭合标签内添加想要插入的内容,该内容就会替代原js template中的插槽。

注意如果希望不是用插槽,那么调用vue组件的开闭合标签内不能有任何文字,甚至是comment也不行。

插槽也能够在指定渲染域中访问数据,该渲染域是和它在同一DOM树层级的兄弟节点,也就是说插槽不能访问父亲节点中的数据。

1
2
3
4
5
6
7
8
<todo-button action="delete">
Clicking here will {{ action }} an item
<!--
The `action` will be undefined, because this content is passed
_to_ <todo-button>, rather than defined _inside_ the
<todo-button> component.
-->
</todo-button>

之前讨论的插槽标签之间都是空内容的,有时候为一个插槽提供后备内容fallback content也很有用,该内容会在html文件调用该组件没有传入内容时被渲染。<slot>Submit</slot>这个时候Submit就是后备内容。

当需要多个插槽的时候,需要在js文件中定义组件时给每一个插槽一个name attribute,形如<slot name="header"></slot>,一个不带有name的插槽隐含的名字就是default

为了在html文件中调用该组件并为特定的插槽提供特定的内容,将想要提供的内容包裹在<template>元素中。给该<template>元素一个v-slot:header指令,冒号后面就是name attribute的取值;或者直接在#hash符号后面跟上插槽的名字。

1
2
3
<template v-slot:header></template>
<template v-slot:default></template>
<template #header></template>

teleport

teleport用于解决逻辑与视图不一致的情况。比如在当前组件中存在一个子组件,虽然该子组件的数据逻辑归属于当前组件,但是UI视图并不从属于当前视图。那么就需要在当前组件的template中将该子组件用<teleport> 标签包裹,并在<teleport to="">中选择html元素,指定该子组件在DOM树中的位置。

比如网站首页点击登陆之后会弹出登陆信息框,虽然登陆的信息由首页的导航栏维护,但是在视图层面并不属于导航栏这个vue实例,而是属于body元素的子元素。

1
2
3
4
5
6
7
8
9
<template>
<AppHeader :isLoggedIn="isLoggedIn" @open-login-model="isLoginOpen = true" />
<div class="w-full flex">
<router-view></router-view>
</div>
<teleport to="body">
<LoginModel v-if="isLoginOpen" @close-login="isLoginOpen = false" />
</teleport>
</template>

Composition API

  • 本文标题:如何使用Vue框架的记录
  • 本文作者:徐徐
  • 创建时间:2020-10-17 20:55:16
  • 本文链接:https://machacroissant.github.io/2020/10/17/learn-vue/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论