Vue笔记

背景

看了Angular之后,准备也看看其他前端框架,
由于vue在github上目前星星最多,
因此决定先看看vue.

感觉angular更符合个人开发习惯(概念和哲学),
而且angular的cli提供了其他两个框架不具备的稳定性和功能.
因此计划3-4天写完这个笔记,并不太多着眼vue的使用.

介绍

vue也是一个前端的框架.一个发端于2013年的个人项目.
作者是尤雨溪(Evan You),在谷歌工作时吸收angularjs的一些想法写了vue.
2014年正式发布,2015年发布1.0版本,还有其他许多工具vue-router,vuex,vue-cli.
2016年的2.0版本时吸收了Virtual Dom的方案,支持了服务端渲染.
2020年发布的3.0版本,源码终于使用了TypeScript.

其特色可能是

  • 概念简单(一开始只管DOM)
  • 性能不错.

和Angular的初步对比

功能的实现上

angular更接近python哲学,做一件事有且最好只有一种办法,
其工程特征明显,至少在文件的分离上更彻底.

vue虽然简化了一些概念(生命周期,变更检测).
但达成一个目的却有太多种写法,这不得不让人考虑多种方法到底是谁覆盖谁的关系.
如果同样作用的代码散落在一个不为人知的部分,眼前的代码就是被看穿也难以发现问题.

在组件配置中使用 template 关键字定义组件的外观时,
在vue官网给出的练习网站上,可以看到 template 关键字定义的内容覆盖了HTML框框里写的内容.
而在vue的cli生成的项目中,即使在main.js中写同样的js代码,直接挂载在index.html中的app元素上,
却看不到写的效果.

教程印象上

angular强调面,因本身就无所不包,更适合后端人员,不用做太多选型,就像一个模糊的教程视频.
vue强调点,就像一系列高清幻灯片,有可能看了后还是不会开始做项目.
另外vue的教程还时常提及其他工具和概念(状态管理,vuex,webpack).
的确能够让人大开眼界.读来犹如 <<江湖奇侠传>>
不过这种直接从细致角度切入的习惯很容易让人迷失在众多的工作量中而忘记大局是什么样子.

安装

Arch安装的vue会各种报错,还是需要使用yarn本地安装.

1
yarn add @vue/cli

然后就能以 ./node_modules/@vue/cli/bin/vue.js 作为vue脚手架命令使用了.

hello-world

1
2
3
4
vue create hello-vue
vue serve --open
# 或者更简单的
vue ui

即可初始化一个默认的项目.

最简单的一个应用

基础的vue应用可以使用普通的文件格式完成.
除了css以外,剩下的文件如下.

html

1
2
3
<div id="hello-vue" class="demo"> <!-- 挂载应用的目标 -->
{{ message }} <!-- 变量的绑定 -->
</div>

js

1
2
3
4
5
6
7
8
9
10
11
const HelloVueApp = {
data() {
return {
message: 'Hello Vue!!' // 组件中的数据
}
}
}

// 以HelloVueApp对象为默认组件配置创建一个应用
// 然后挂载到之前的DOM结构中去
Vue.createApp(HelloVueApp).mount('#hello-vue')

看起来直到vue3,语法也依然像是angularjs一样,在js里声明各种东西,而不是使用元数据.

另外也能看出vue使用了Virtual DOM的痕迹,
在内存里虚拟出一个DOM结构,加快节点的查找和操作,
然后和前一个DOM对比后,将变化的部分替换到页面上.
大多数情况下是比直接操作DOM快一些.

上面的内容可能有些分散,如果写在一个html文件里可能会方便看一些.
(我想vue是这么想的)

1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<style>
/* css内容 */
</style>

<script>
// 组件内容
</script>

<body>
<!-- 模板内容 -->
</body>
</html>

但距离愉快的开发还是有点距离

  1. 不想写head,meta等
  2. 想要IDE提供更好的注释语法,语法高亮,缩进规则等等

于是vue提供了 .vue 为后缀的文件来实现,
具体参考cli的默认案例.

cli默认案例

默认案例比较寒酸,项目里仅仅有两个vue组件.其他基本都没有.
或许要等到一些主题的默认包才会有一些复杂的内容.

目录结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── babel.config.js
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── README.md
├── src
│ ├── App.vue(app组件)
│ ├── assets
│ │ └── logo.png
│ ├── components(其他组件)
│ │ └── HelloWorld.vue
│ └── main.js(启动脚本)
└── yarn.lock

其中的app组件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 模板部分 -->
<template>
<img alt="Vue logo" src="./assets/logo.png">
<!-- 使用selector引用组件 -->
<!-- 并在组件中以属性的方式定义一些要传给该组件的内容 -->
<HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

<!-- 组件逻辑部分 -->
<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'App',// 声明该部分的selector
components: {
HelloWorld // 声明要使用一个外部的组件
}
}
</script>

<!-- css部分 -->
<style>
/* 随便写点什么 */
</style>

被引用的HelloWorld组件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div class="hello">
<h1>{{ msg }}</h1><!-- 绑定变量的一种方式 -->
</div>
</template>

<script>
export default {
name: 'HelloWorld',// 声明selector
props: {
msg: String // 相当于angular的@Input用来声明一个外部传入的变量
}
}
</script>

<!-- scoped 表示css仅作用于该组件 -->
<style scoped>
/* 不重要 */
</style>

最终由入口文件加载整个应用

1
2
3
4
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

其中在 public/index.html 中有一句

1
<div id="app"></div>

用于将app挂载在该元素上.

组件基础

组件也是vue的基本单位.
在组件里也依然可以定义组件的变量,方法等.
vue的组件也拥有生命周期,可以在写代码时考虑.
命名方式上,虽然vue没有限制,但还是希望能人为限制成kebab-case.

创建方式

在整个vue中,许多特性的定义方法都被分成了 声明式函数式 的两种,
可能是声明式的信息完整性,和函数式的灵活性,两个都无法割舍.

  1. 全局注册,函数式: 在app实例上使用compnent函数引入,
    在app对象上使用函数注册的组件是全局可见的,哪里都能通过selector使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 一口气定义完
    Vue.createApp({...}).component('my-component-name', {
    // ... 选项 ...
    })

    // 或者在app对象上一顿操作
    const app = Vue.createApp({}) // 得到app实例

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

    app.mount('#app') // 得到根组件实例
  2. 局部注册,声明式: 在createApp内部,以配置components字段的方式引入
    局部注册的方式导致该组件在其他地方就不能用.但其实不能用也不见得是坏事.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const ComponentA = {            // 这时候component又是普通的对象,几乎不属于哪个类
    /* ... */
    }
    const ComponentB = {
    /* ... */
    }

    const app = Vue.createApp({
    components: { // 这里的声明应该能算是app.component()逐个添加的批量版本
    'component-a': ComponentA,
    'component-b': ComponentB
    }
    })

单文件组件的引入

通常vue的代码不是写在html里也不是写在js里,
而是将这三部分整合到一个文件中,用于表示一个组件.
本质上讲,引入的方式也是局部注册.

1
2
3
4
5
6
7
8
// App.vue
export default { // 处处透着ES6的气息
name: 'App',// 声明该部分的selector
}

// main.js
import App from './App.vue' // default中的配置放入了变量App当中
createApp(App).mount('#app')

如果要让组件B能够使用一个被局部注册的组件A,
则要求在组件B中使用 components 选项来配置可用的外部组件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const app = Vue.createApp({
components: {
'component-a': ComponentA, // 组件A,B都是局部注册的
'component-b': ComponentB // 也都仅仅能在根组件中使用
}
})

const ComponentA = {
/* ... */
}

const ComponentB = {
components: {
'component-a': ComponentA // 如果组件B要使用组件A,则需要专门声明要用组件A
}
// ...
}

组件的可配置选项

组件有许多可配置的字段

  • template 用于配置组件的外观
  • data 用于配置属性
  • methods 用于配置组件的一些方法
  • computed 计算属性,将一些对属性的计算从组件外部移动到内部
  • watch computed的同义词,专门用于处理一些需要大量计算的过程
  • props 从外部指定的一些属性
  • 生命周期钩子 beforeCreate,created,beforeMount,mounted等等
  • setup 为了让功能更加模块化,从空间上统一管理同一个业务的方法

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.createApp({
data() {
return { count: 1} // 组件的属性
},
created() { // 生命周期函数
// `this` 指向组件本身
console.log('count is: ' + this.count) // => "count is: 1"
},
methods: { // 其他自定义方法
click() {
// ... 响应点击 ...
// data()部分定义好的东西可以使用$data来访问
// 为了图片$data这个自带变量的特殊性,用上了$符号
console.log(this.$data.count);
}
}
})

template用于定义模板

用来定义该组件的外观部分,不过书写麻烦,常常只用来写一些示例内容.

通常被以下两种方法排挤.

  • 直接将一个html中的某元素作为模板(预先绑定一些变量),然后利用元素上的锚点,将写好的组件挂载上去
  • 单文件组件中直接就有用于写template的部分
1
2
3
4
5
6
7
8
9
10
const HelloVueApp = {
template: `ls`,
data() {
return {
message: 'Hello Vue!!'
}
}
}

Vue.createApp(HelloVueApp).mount('#app')

data函数

用于开辟一个区域用来保存组件的数据.
返回多个可以用逗号分隔,看起来就像返回一个大对象.

访问时可以

  1. 一般可以使用$data来访问
  2. 顶层的数据可以直接用vm访问
1
2
3
4
5
6
7
8
9
10
11
12
13
const app = Vue.createApp({     // 得到app对象
data() {
return {
count: 4,
msg: "hello world"
}
}
})

const vm = app.mount('#app') // 得到组件对象

console.log(vm.$data.count) // 可以用$data访问
console.log(vm.count) // 也可以直接访问(不过这个要求是顶级property)

需要注意:

  1. 如果不在data()内部定义属性,后续使用vm来添加,不会被响应系统跟踪
  2. 不要用名称为data的属性,造成混乱

methods

不像data一样是一个函数,是一个配置选项,定义了组件的方法.

1
2
3
4
5
6
7
8
9
const app = Vue.createApp({
// ...
methods: {
increment() {
// `this` 指向该组件实例
this.count++
}
}
})

一般不让在methods里面写异步进程,可以写在updated生命周期钩子里.
vue没有防抖和节流功能,不知道为什么推荐了Lodash等方法来实现,
个人感觉相比rxjs,代码要难看一些.

computed选项

有些数据太过复杂,以至于直接在模板里写一个长长的表达式不美观,
或者有些过于 动态 的内容,使用平常的拼凑变量名的方法根本就写不出来.
于是可以先在computed选项里计算好,然后放到页面上.这些属性称为 计算属性
特点在于,computed选项后的数据会在数据发生改变时,也一起改变.

其实如果写在methods选项里也一样可以在数据变化时重新计算结果.
不过真实的情况是,methods里面的数据可能计算得过于勤快(即使相关数据没有变化也会重新计算).
如果使用computed,则会缓存结果,利好性能表现.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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` points to the vm instance
return this.author.books.length > 0 ? 'Yes' : 'No' // 表达式过长
}
}
}).mount('#computed-basics')

尽管通常使用getter的功能,
但偶尔也可以自定义setter功能.
比如让计算属性的setter作为一个批量更改data的工具函数.

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) { // 比如传入'Jane Doe'
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1] // 达到了批量更改[firstName, lastName]两个数据的效果
}
}
}

watch选项

该选项是computed选项的同义词.
通常用来完成一些计算属性不方便完成或不能完成的功能.

  1. 发送网络请求前,需要先使用一个临时的结果,计算属性无法完成.
  2. 计算开销很大,希望加入一些防抖的效果.计算属性似乎也无法完成.

另外作为watch的特性,可以同时处理新旧值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const watchExampleVM = Vue.createApp({
watch: {
// question属性变化时就会触发
question(newQuestion, oldQuestion) {
if (newQuestion.indexOf('?') > -1) {
this.getAnswer()
}
}
},
methods: {
getAnswer() {
this.answer = 'Thinking...' // 先使用一个临时的答案
axios // 然后去请求真的答案
.get('https://yesno.wtf/api')
.then(response => {
this.answer = response.data.answer
})
.catch(error => {
this.answer = 'Error! Could not reach the API. ' + error
})
}
}
}).mount('#watch-example')

设置好的结果可以使用 vm.$watch 来获取.

组件的生命周期

1
2
3
4
5
6
7
8
mount()
-> Init -> (beforeCreate) -> Init -> (created) -> 渲染模板
-> (beforeMount) -> 渲染完成并挂载 -> (mounted) // 此时才将mount()的任务完成

-> 数据改变 -> (beforeUpdate) -> (updated)
-> 数据改变 -> (beforeUpdate) -> (updated) // 如此往复

-> (beforeUnmount) -> unmount() -> (unmounted)

其中不带括号的是行为或状态,带括号的是可用的生命周期钩子.
可以看出来,vue对生命周期的实现比较简单明了.尤其是 (beforeUpdate) -> (updated) 的部分.

prop声明外部传入的数据

子组件可以使用props来开放一些从外部向内部传递数据的通道

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

传递数据的不一定需要是一个组件,只要是用到这个组件的地方都看成这个组件的父级

1
2
3
4
5
<div id="blog-post-demo" class="demo">
<blog-post title="My journey with Vue"></blog-post>
<blog-post title="Blogging with Vue"></blog-post>
<blog-post title="Why Vue is so fun"></blog-post>
</div>

注意vue的规定很奇怪,
通常传入动态数据,需要使用 v-bind 包裹以示这是动态的,
传入静态值不用.比如上面使用 title="xxx".
但传入静态值时,用上 v-bind 也不会报错.比如 :title="12",
再比如下面的 :author="{ name: 'Veronica' }.
vue从来就没有想过去找这样的变量吗?

可以传的数据有很多

  • 静态的字符串,数字,布尔值

  • 普通变量

  • 数组

  • 对象字面量

    1
    <blog-post :author="{ name: 'Veronica', company: 'Veridian Dynamics' }"></blog-post>
  • 对象变量

    1
    2
    3
    4
    5
    6
    7
    8
    <script>
    post: {
    id: 1,
    title: 'My Journey with Vue'
    }
    </script>

    <blog-post v-bind="post"></blog-post>
  1. 命名规则

    不知道为什么vue允许props里使用两种方式

    • kebab-case
    • camelCase

    但DOM模板中大小写不敏感,只能使用

    • kebab-case

    还煞费苦心地做了一些转换,让定义成camelCase的内容可以以kebab-case的方式使用

  2. props还可以拥有一些验证规则

    • 数据类型
    • 是否必要
    • 默认值
    • 自定义规则
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    app.component('my-component', {
    props: {
    propZ: null, // null或undefined会通过所有类型,相当于angular的any
    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) { // validator来自定义验证函数
    // 这个值必须匹配下列字符串中的一个
    return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
    },
    }
    })
  3. 数据流是单向的

    只有通过父组件才能更改数据,子组件对数据的更改会被忽略.这样便于对数据的跟踪和理解.
    vue能够将数据的操作简化至此,为什么不把其他地方的语法也简化成只有一种方式呢?!
    如果非要在子组件里更改这些数据,可以让子组件抄一份数据,仅仅让传入的值作为初始值.
    但归根结底,子组件还是给能改变该数据在父组件那里的值,不过看起来是新的值就好.

    1
    2
    3
    4
    5
    6
    props: ['initialCounter'],      // 传入数据
    data() { // 可以是data,也可以是computed等
    return {
    counter: this.initialCounter // 数据作为初始值
    }
    }

emits子组件向父组件发送事件

子组件可以发出事件,然后父组件可以针对这个特定的自定义事件名称定义具体的处理方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 父组件中 -->
<div :style="{ fontSize: postFontSize + 'em' }">
<blog-post
<!-- 都是可以忽略的细节
v-for="post in posts"
:key="post.id"
:title="post.title" -->
@enlarge-text="postFontSize += 0.1" <!-- 该事件的处理由父组件控制 -->
></blog-post>
</div>

<!-- 子组件中 -->
<button @click="$emit('enlarge-text')"> <!-- 发出自定义事件 -->
Enlarge text
</button>

另外发出信息时,还可以带上数值,而接收信息时可以使用 $event 参数来代表接到的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 子组件中 -->
<button @click="$emit('enlarge-text', 0.1)">
Enlarge text
</button>

<!-- 父组件中 -->
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
<!-- 或者将事件的处理移动到methods中 -->
<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
<script>
methods: {
onEnlargeText(enlargeAmount) {
this.postFontSize += enlargeAmount
}
}
</script>

不知道为什么即使不用 emits 规定能发射哪些事件,父组件却依然知道有哪些的.
但如果用了emits的高级形式,可以像对props做验证一样,对发射的事件做一些验证.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 数组语法
app.component('todo-item', {
emits: ['check'],
created() {
this.$emit('check')
}
})

// 对象语法
app.component('reply-form', {
emits: {
// 没有验证函数
click: null,

// 带有验证函数
submit: payload => {
if (payload.email && payload.password) {
return true
} else {
console.warn(`Invalid submit event payload!`)
return false
}
}
}
})

使用方法成谜, $emit('submit',form) 吗,
使用意义成谜, 如果有form相关组件的话不是功能重复吗?
就算没有form相关组件,验证函数全写在函数里,模板里不写的话可读性也太差了.

模板知识

变量的绑定

vue对于模板的显示逻辑上,
除了有一些angular也可以实现的,看起来就像自定义的指令(v-once 这种)以外,
最有代表性的就是指令的扩展,可以用来实现许多功能,比如once等限制.

  1. 插值绑定 {{}}

    1
    2
    3
    4
    5
    6
    <span>Message: {{ msg }}</span> <!-- 会随着msg数据的改变而重新渲染 -->
    <span v-once>这个将不会改变: {{ msg }}</span> <!-- msg数据变化也不会改变外观了 -->

    {{ number + 1 }} <!-- 可以在括号内做些简单的运算 -->
    {{ ok ? 'YES' : 'NO' }}
    {{ message.split('').reverse().join('')}}
  2. 从组件到模板的绑定 v-bind

    1
    2
    3
    4
    5
    6
    7
    8
    <div v-bind:id="dynamicId"></div>

    <button v-bind:disabled="isButtonDisabled">按钮</button>
    <button disabled>按钮</button> <!-- 如果isButtonDisabled的值是true的话 -->
    <!-- 看起来是 [disabled]="isButtonDisabled" 的倒车版 -->

    <!-- 不过好在由于使用频率高,可以简写 -->
    <div :id="dynamicId"></div>

    另外还提供了许多花里胡哨的功能,当然也不是没有用.

    1
    2
    3
    4
    5
    <!-- 拼接取值用的变量名 -->
    <div v-bind:id="'list-' + id"></div>

    <!-- 拼接绑定到的变量名 -->
    <a v-bind:[attributeName]="url"> ... </a>

    另外官方专门为绑定class和style写了一部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <!-- 对象方式: 让组件决定预置的class是否显示 -->
    <div
    class="static"
    :class="{ active: isActive, 'text-danger': hasError }"
    ></div>

    <!-- 数组方式: 让组件决定显示什么class名 -->
    <div :class="[activeClass, errorClass]"></div>

    <!-- 组件上使用会出现merge现象 -->
    <!-- 可以使用 $attrs.class来获取组件外部的设置 -->

    <!-- 绑定style也同样 -->
    <!-- 对象方式1: 让组件决定预置的style数值应该是多少 -->
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

    <!-- 对象方式2: 让组件决定整个健值对 -->
    <!-- 比如组件提供一个对象
    styleObject: {
    color: 'red',
    fontSize: '13px'
    } -->
    <div :style="styleObject"></div>

    <!-- 数组方式: 组件可以传递出多个对象,只不过在模板里写成数组 -->
    <div :style="[baseStyles, overridingStyles]"></div>
  3. 事件绑定 v-on

    1
    2
    3
    4
    5
    <!-- 感觉还是(click)="doSomething"的倒车版 -->
    <a v-on:click="doSomething"> 组件中需要定义doSomething函数 </a>

    <!-- 好在还是能缩写 -->
    <a @click="doSomething"> ... </a>
  4. 双向绑定 v-model

    一般不用双向绑定,这会让数据的流向变得不明确.
    但涉及到form时通常会用双向绑定

    1
    2
    <input v-model="message" placeholder="edit me" /> <!-- input输入的内容会传递给组件中的表单实例 -->
    <p>Message is: {{ message }}</p> <!-- 表单实例的内容又显示到页面上 -->

指令

  1. v-if

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <div v-if="type === 'A'"> <!-- 跟变量或简单的表达式 -->
    A
    </div>
    <div v-else-if="type === 'B'"> <!-- 提供的功能太多,给人一种写jsp的感觉 -->
    B
    </div>
    <div v-else-if="type === 'C'">
    C
    </div>
    <div v-else>
    Not A/B/C
    </div>

    也能一个变量控制多个元素

    1
    2
    3
    4
    5
    6
    7
    <!-- 用div也行,不过这样会多引入一层缩进 -->
    <!-- template标签更好一些,不会真的渲染出一个标签 -->
    <template v-if="ok">
    <h1>Title</h1>
    <p>Paragraph 1</p>
    <p>Paragraph 2</p>
    </template>
  2. v-show

    同样控制元素显示与否,
    但原理是简单地切换 display 属性.

  3. v-for

    通常用在对列表的遍历显示中,
    比如当有一个 items: [{ message: 'Foo' }, { message: 'Bar' }] 变量时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <ul id="array-rendering">
    <li v-for="item in items"> <!-- 不带索引 -->
    {{ item.message }}
    </li>
    </ul>

    <ul id="array-with-index">
    <li v-for="(item, index) in items"> <!-- 带索引 -->
    {{ index }} - {{ item.message }}
    </li>
    </ul>

    看起来还用的是ES5的 for ... in 语法,有点不够时髦.

    或许angular中也可以,不过的确是在vue教程中第一次知道for还能对对象使用
    比如一个数据

    1
    2
    3
    4
    5
    myObject: {
    title: 'How to do lists in Vue',
    author: 'Jane Doe',
    publishedAt: '2020-03-22'
    }

    搭配for使用时可以是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <!-- 简单的取value而不取key名 -->
    <!-- 效果类似 -->
    <!--
    - How to do lists in Vue
    - Jane Doe
    - 2020-03-22
    -->
    <ul id="v-for-object" class="demo">
    <li v-for="value in myObject">
    {{ value }}
    </li>
    </ul>

    <!-- 还可以带上key名 -->
    <!--
    title: How to ...
    author: Jane Doe
    publishedAt: xxxx-xx-xx
    -->
    <ul id="v-for-object" class="demo">
    <li v-for="(value, name) in myObject">
    {{ name }}: {{ value }}
    </li>
    </ul>

    <!-- 不能理解的是一个对象里面的每个key还能拍个序 -->
    <!--
    0. title: How to ...
    1. author: Jane Doe
    2. publishedAt: xxxx-xx-xx
    -->
    <ul id="v-for-object" class="demo">
    <li v-for="(value, name, index) in myObject">
    {{ index }}. {{ name }}: {{ value }}
    </li>
    </ul>

    列表中数据改变时,vue默认不更改数据的排序,仅更改数据本身.称为 就地更新
    如果想同时更新排序,则需要使用 key 指定一个排序的依据.

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

    由于vue检测数据变化的方式特殊,只能用vue规定的push,pop等方式更新数据
    (吸收了react不可变数据的思想)

    • push
    • pop
    • shift
    • unshift
    • slice
    • sort
    • reverse

    v-for还能用 n in 10 这种写法,真是贪多,就不怕嚼不烂吗

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

    同样的也能借助 template 来操作多个元素

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

    不过真的看不出来太多的必要性,包一层div还能强调这是一个基本的循环单元.

  4. v-once

    用于声明该部分的模板只渲染一次

    1
    <span v-once>这个将不会改变: {{ msg }}</span> <!-- msg数据变化也不会改变外观了 -->
  5. v-html

    想让纯文本以html方式渲染时使用
    比如变量 rawHtml 的值是 <span style="color: red">This should be red.</span>
    使用 v-html="rawHtml" 可以渲染出红色文本

    1
    2
    3
    4
    5
    6
    7
    <!-- 得到字符串本身 -->
    <p>{{ rawHtml }}</p>

    <!-- 得到红色的This should be red. -->
    <p>
    <span v-html="rawHtml"></span>
    </p>

TODO 特色的修饰符

在事件绑定中,会有一些常用的需求

  1. 希望只执行一次
  2. 检测到具体的按键再执行
  3. 表单数据传递到组件中时要去掉多余空格

vue提供了修饰符来完成这些功能

  • 事件修饰符
    • stop
    • prevent 搞不清楚
    • capture 该事件要自身先处理(原则上由子节点先处理)
    • self 要求事件只能由自己触发,而不能由子节点发出
    • once 只触发一次
    • passive
  • 按键修饰符
    • enter
    • tab
    • delete
    • esc
    • space
    • up
    • down
    • left
    • right
    • page-down
    • 系统修饰符
      • ctrl
      • alt
      • shift
      • meta
    • exact 确切修饰,要求有且只有规定的按键被按下,如果多按了按键则不能触发事件
  • 鼠标修饰符
    • left
    • right
    • middle
  • 表单相关修饰符
    • lazy 等到数据改变再改变组件中的值,而不是一检测到输入就行动
    • number 在传之前转换为数字
    • trim 过滤首尾空白字符

而且修饰符还能连用,不过顺序很重要

1
2
3
4
5
<!-- 点击事件将只会触发一次 -->
<a @click.once="doThis"></a>

<!-- alt+enter清理内容 -->
<input @keyup.alt.enter.exact="clear" />

而且还能自定义修饰符 TODO 例子

v-is突破结构限制

通常html中标签有结构限制,比如 <li> 只能出现在 <table> 中.
通常 <table> 中也只能写点 <li> 等,出现其他标签会报错

1
2
3
<table>
<blog-post-row></blog-post-row> <!-- 会报错 -->
</table>

因此可以使用正常的标签来伪装,然后使用 v-is 来寄生一下
但有个奇怪的要求是,其值必须是带引号的字符串.因此例子中才双引号套但引号.

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

组件深入

动态组件和组件切换

有时想让组件变得 动态 一些,比如以一个变量的值作为组件名,随时根据变量来渲染对应的组件.
此时可以:

  1. 可以使用 <component> 标签作为占位符,
  2. 使用 :is 绑定判定要展示的组件之后,替换为真正要显示的组件.

类似一个小范围的路由实现.

比如已经有三个组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase() // 动态计算组件名
}
}
})

app.component('tab-home', {
template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
template: `<div class="demo-tab">Posts component</div>`
})
app.component('tab-archive', {
template: `<div class="demo-tab">Archive component</div>`
})

app.mount('#dynamic-component-demo')

然后在页面上使用 :is 加一个代表组件名的变量名即可

1
2
3
4
5
6
7
8
9
10
11
12
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs" <!-- 显示选项 -->
:key="tab" <!-- 随便一个排序 -->
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab" <!-- 控制点击后选取的逻辑 -->
>
{{ tab }}
</button>

<component :is="currentTabComponent" class="tab"></component>
</div>

但是有个问题是每次切换,这几个组件都会重新初始化一个实例出来,
浪费性能不要紧,丢掉数据就不好了.
解决方法倒也简单,使用 <keep-alive></keep-alive> 包裹就好.

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

我还以为vue的补救方法是

  • 使用一个全新的指令,就像 v-is
  • 在定义组件时使用新的配置关键字,就像 inheritAttrs: false

结果竟然都不是,vue还真是能整活儿

slot用于放置调用组件时书写的内容

使用组件时,会觉得使用attribute的方式有点不符合习惯
(当然个人认为框架应该限制为功能创造其他实现方式以保持唯一性,即便是不符合习惯)

1
<my-component msg="some message"></my-component>

希望使用另外一种方式

1
2
<a>some message</a>
<my-compnent>some message</my-component>

但这种方式会使输入的信息显示在组件外的某个地方,而不是真正想的组件内部.
vue提供一种slot的方式,让这些信息可以同时显示在组件内部,并称之 分发
假如组件如下

1
2
3
4
5
6
7
8
app.component('alert-box', {
template: `
<div class="demo-alert-box"> // 该class会渲染一个红色的方框,文字出现在方框内部
<strong>Error!</strong>
<slot></slot> // 此处用于显示,调用组件时组件获得的值
</div>
`
})

使用时正常使用即可,会发现 Something bad happend. 出现在红色的方框内部

1
<alert-box>Something bad happened.</alert-box>
  1. 插槽可以显示字符串

    1
    <alert-box>Something bad happened.</alert-box>
  2. 插槽也可以显示html代码

    1
    2
    3
    4
    5
    <todo-button>
    <i class="fas fa-plus"></i> <!-- 这里是一个图标 -->
    <font-awesome-icon name="plus"></font-awesome-icon> <!-- 这里是其他组件 -->
    Add todo <!-- 这里是文字 -->
    </todo-button>
  3. slot可以有默认值

    1
    2
    3
    <button type="submit">
    <slot>Submit</slot> <!-- 当不传入内容时可以显示默认值 -->
    </button>
  4. slot可以拥有名字

    可以使用 name 为slot起名,然后使用 v-slot 指定将内容放在哪个slot

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <!-- 定义的组件 -->
    <div class="container">
    <header>
    <slot name="header"></slot>
    </header>
    <main>
    <slot></slot>
    </main>
    <footer>
    <slot name="footer"></slot>
    </footer>
    </div>

    <!-- 使用时,可以借助template来圈定范围 -->
    <base-layout>
    <template v-slot:header> <!-- 指定要放在名为header的slot中 -->
    <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>

    另外slot名也可以是动态的

    1
    2
    3
    4
    5
    <base-layout>
    <template v-slot:[dynamicSlotName]>
    ...
    </template>
    </base-layout>

    但动态了有什么应用场景呢?

  5. 不能提前使用组件的内容

    1
    2
    3
    <todo-button>
    Delete a {{ item.name }} <!-- 此时的内容还不知道todo-button组件有item这个属性 -->
    </todo-button>

    如果特别希望提前访问组件内部的值,可以对slot本身使用属性绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <ul>
    <li v-for="( item, index ) in items">
    <slot :item="item"></slot> <!-- 将"item"通过:item暴露出去 -->
    </li>
    </ul>

    <!-- 在使用时,就能在外部访问内部的变量 -->
    <todo-list>
    <template v-slot:default="slotProps"> <!-- 指代组件的slot标签里的所有属性 -->
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span> <!-- 这里就访问所有属性中名为item的那个 -->
    </template>
    </todo-list>

    当只有一个默认的slot时,上面的v-slot后的default可以省略,即变成 v-slot="slotProps"
    但不知道为什么vue会说,框架会分不清 v-slot:"header"v-slot="header"

  6. v-slot可以使用解构语法

    上面的例子中,使用组件时声明的是 slotProps 变量,然后使用了 slotProps.item.
    那为什么不能直接将 slotProps 看作一个对象,即看成 {item: todo, 其他属性:其他值},
    然后访问 todo 就相当于 slotProps.item 呢?能

    1
    2
    3
    4
    <todo-list v-slot="{ item: todo }">
    <i class="fas fa-check"></i>
    <span class="green">{{ todo }}</span>
    </todo-list>
  7. v-slot可以缩写成 #

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <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>

    <!-- 由于太奇怪以至于规定不让使用 -->
    <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>

attribute的继承

有些attribute,即使props里不定义,也会继承给到子组件内部
这些属性的特征是,它们从不用 v-bind 来预先告知这需要一个prop来接收.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
app.component('date-picker', { // 组件里没有定义prop等
template: `
<div class="date-picker">
<input type="datetime" />
</div>
`
})
</script>

<!-- 使用时非要给data-picker组件一个data-status的属性 -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染的结果 -->
<div class="date-picker" data-status="activated"> <!-- 组件也不敢乱丢,直接拿过来了 -->
<input type="datetime" />
</div>

事件监听器也是一种属性,
比如这里传给组件一个事件监听器

1
<date-picker @change="submitChange"></date-picker>

在组件里使用 $attrs 是能读取到这个属性的

1
2
3
4
5
app.component('date-picker', {
created() {
console.log(this.$attrs) // { onChange: () => {} }
}
})
  1. 多个根节点

    上面的例子中之所以能顺利继承,是因为组件只有一个根节点,
    如果组件有多个根节点,则需要专门使用 v-bind="$attrs" 指定一个继承的节点.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <!-- 使用组件时传入许多属性 -->
    <custom-layout id="custom-layout" @click="changeValue"></custom-layout>

    <!-- 如果组件长这样会报错 -->
    <header>...</header>
    <main>...</main>
    <footer>...</footer>

    <!-- 需要指定具体谁继承 -->
    <header>...</header>
    <main v-bind="$attrs">...</main>
    <footer>...</footer>
  2. 禁用继承

    在组件配置阶段定义

    1
    2
    3
    4
    app.component('date-picker', {
    inheritAttrs: false,
    // 其他东西
    })

    但这仅仅是禁用了从组件的selector到组件根元素的自动传递,
    数据本身依然保存在 $attrs 当中,可以人为将该属性用在别的地方

    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>
    `
    })

提供/注入

如果父组件想给孙组件传递数据,又不想链式使用prop.
有个解决方案就是使用 provideinject 来配置.
当然这种方法也不限制只能父传给孙而不能传给子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const app = Vue.createApp({})

app.component('todo-list', { // 父组件
// ... 其他部分
provide: {
user: 'John Doe'
},
})

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

但这种方法不能提供复杂的数据(需要运算的都算复杂),需要把 provide 改成 provide()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
todoLength: this.todos.length // 将会导致错误 'Cannot read property 'length' of undefined`
},
provide() { // 这样才可以
return {
todoLength: this.todos.length
}
},
})

但这种方法不能随着数据的变化而输出变化的值,
需要用 Vue.computed() 包装一下

1
2
3
4
5
6
7
8
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})

异步组件

在最初初始化的时候,出于一些考虑(性能,空间)没有加载更多组件.
app.createApp() 中没有配置.
在程序运行中,从别的地方加载出来(比如从服务器请求出组件),就是异步组件的任务.
主要靠 defineAsyncComponent 来实现

1
2
3
4
5
6
7
8
9
10
11
12
const app = Vue.createApp({})

const AsyncComp = Vue.defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
// 这里返回一些正常在 app.compnent('name',{})这个大括号里的内容
})
})
)

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

也可以从文件中用import加载出来

1
2
3
4
5
6
7
import { defineAsyncComponent } from 'vue'

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

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

引用模板中的元素

属性绑定让程序员脱离了 query(".input").onEvent('changed',function())
事件绑定让程序员脱离了 query(".button").onEvent('click',function())
但程序员有时还是需要在组件的js代码里访问模板中的某个元素.
比如有时要操作的这个元素根本不在自己的组件内.
又或者想在一个极其特殊的情况下想让页面上的一个input被聚焦

此时可以使用 ref 来提前标记模板上要被引用的元素
在组件中使用 this.$refs.标记名 来引用

1
2
3
4
5
6
7
8
9
10
11
12
13
app.component('base-input', {
template: `
<input ref="input" /> // 先用ref标记,名字为input
`,
methods: {
focusInput() {
this.$refs.input.focus() // 然后对input名字代表的元素使用focus方法
}
},
mounted() { // 不一定是mounted,也可以是其他苛刻的条件
this.focusInput()
}
})

如果ref使用在组件上,那么ref到的内容将有更大的作用域,
比如

1
<base-input ref="usernameInput"></base-input>

当使用ref来执行函数时,组件会被认为该函数是由父组件在子组件上触发的.

1
this.$refs.usernameInput.focusInput()

mixins用来重用功能

组件的一些设置,data,methods等,可以放在一个对象里,
混入到其他组件中,让其他的组件也可以使用.
这个对象可以使用 mixins 来引入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// define a mixin object
const myMixin = {
created() {
this.hello()
},
methods: {
hello() {
console.log('hello from mixin!')
}
}
}

// define an app that uses this mixin
const app = Vue.createApp({
mixins: [myMixin]
})

app.mount('#mixins-basic') // => "hello from mixin!"

除了组件可以使用混入,app也可以使用 app.mixin() 方法来进行全局混入

1
2
3
4
5
6
7
8
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})

有混入就会有重复,通常的原则是

  1. 配置级别不覆盖只合并
  2. 具体的配置内容,如果不被覆盖,混入的数据就放在前面
  3. 如果有覆盖,是组件覆盖mixin

特殊情况下(比如组件中定义为空),需要优先mixin的内容以使之作为默认值.
此时可以自定义混入策略.

1
2
3
4
app.config.optionMergeStrategies.custom = (toVal, fromVal) => {
console.log(fromVal, toVal)
return fromVal || toVal
}

其中 toVal 代表mixin中的值?TODO

自定义指令

除了 v-if, v-for 等指令,vue还能自定义自己的指令.
比如一个自动聚焦的指令,使用时如同 v-focus.

可以使用 app.directive('focus' {}) 来全局定义.

1
2
3
4
5
6
7
8
9
10
const app = Vue.createApp({})

// 一个自动聚焦的指令
app.directive('focus', {
// 使用了该指令的元素,加载到页面上时
mounted(el) {
// 调用focus方法
el.focus()
}
})

当然也可以在组件的配置中,使用 directives 关键字来配置一个局部的指令.

1
2
3
4
5
6
7
8
directives: {
focus: {
// 指令的定义
mounted(el) {
el.focus()
}
}
}
  1. 可以使用的函数

    如上所属,指令可以使用一些类似 mounted 等的生命周期钩子函数.具体有

    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • beforeUnmount
    • unmounted

    虽然不是全部,但也足够使用了.

  2. 不指定钩子

    如果不指定生命周期,则默认是mounted和updated时对应的内容,
    定义时如下

    1
    2
    3
    4
    5
    app.directive('pin', (el, binding) => {
    el.style.position = 'fixed'
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
    })
  3. 函数可以使用的参数

    另外钩子函数中可以使用的参数也不仅仅 el,还有

    • el
    • binding
    • vnode
    • prevNnode

    其中binding用来存放使用指令时自带的参数

    1
    <p v-pin="200"></p>

    binding.value为"200"

    1
    <p v-pin:[direction]="pinPadding"></p>

    若组件中的变量direction值为"right",pinPadding值为200,
    则binding.arg为"right",binding.value为200.

    1
    2
    3
    4
    updated(el, binding) {
    const s = binding.arg || 'top'
    el.style[s] = binding.value + 'px'
    }

    再比如,从传值的地方传入一个对象,则binding.value可以继续向下寻址

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

teleport

为了模块化,一系列代码(html的,js的)通常隶属与一个组件.
如果代码希望控制组件html代码以外的部分,比如做一个模态框遮住整个页面.
则需要使用 teleport 将html页面内容发送到整个窗口的其他位置.

使用时也很简单,使用 to 指令发送到哪里即可,
teleport 标签后的内容将作为 to 到的元素的子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
}
}
})

比较欣喜的是,子组件依然能从父组件获得想获得的数据.
另外如果一个目标被多次传送,
结果是简单的叠加

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

基于proxy的变更检测思路

vue不是在变量发生变化时进行检测,而是在变量即将变化前拦截.
vue3利用ES6中新增的Proxy方式,拦截原有请求,并出发相关数据的重新计算.

基本原理

使用方式基本上是

1
const 包装后 = new Proxy(包装前, 如何包装)

简单举例如下

1
2
3
4
5
6
7
8
9
10
11
12
const dinner = {
meal: 'tacos'
}

const handler = {
get(target, prop) {
return target[prop]
}
}

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

不过正常使用时由于考虑this的指向问题,会使用Reflect(几乎相当于其他语言中的super)

1
2
3
4
5
6
7
8
9
10
11
12
const dinner = {
meal: 'tacos'
}

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

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

只要分别在getter和setter中拦截操作就可以实现对变化的监视

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const dinner = {
meal: 'tacos'
}

const handler = {
get(target, prop, receiver) {
track(target, prop) // 加入getter拦截器,通常称为陷阱
return Reflect.get(...arguments) // 正式的使用Reflect来执行原本的操作
},
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(...arguments)
}
}

const proxy = new Proxy(dinner, handler)

具体表现

vue在data()中

1
2
3
4
5
data() {
return {
modalOpen: false
}
}

总是返回一个变量的Proxy版本,以保证对该变量的改动能被监听到.
完整这个监听工作的对象称为 侦听器, 每个组件都会对应一个.

不过如果底层使用原生的 new Proxy 方式,
那么用户就一定会有一个被包装前的变量.
据说这些变量如果使用 .filter.map 时会出现一些错误.

于是干脆决定,后台使用其他API来创建响应式的对象.
将data中数据处理后返回.

  • reactive
  • ref
1
2
3
4
5
6
7
8
9
10
11
12
13
import { reactive } from 'vue'
import { ref } from 'vue'

// 响应式状态
const state = reactive({
count: 0
})

// 单纯的值
const count = ref(0)

// 这两个状态似乎加起来才能等于rxjs的of()
// ref专门用于处理只有一个值的情况

rxjs是比较厌恶响应式对象的嵌套的,
然而vue却热衷于将ref嵌套进reactive中,不知道是怎么想的.

响应式对象的访问

使用 ref 创建好的对象,只有一个成员 value,可以用来得到内部的值.

1
2
const count = ref(0)
console.log(count.value) // 0

不过如果是data()里声明的内容,因为太过常用,可以省略掉 .value 的部分
但这种脑残设定实在给人添乱.
因为要分太多情况来判断,到底什么时候能省什么时候不能省.
能省的情况

  • 该ref数据被定义在data()的返回值中
  • 该ref数据被reactive()包裹
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const count = ref(0)            // 好好的一个ref
const state = reactive({ // 被state包裹在里面
count
})

console.log(state.count) // 访问时全称state.count.value,可省value

state.count = 1 // 修改(也是访问)时也可以省value
console.log(count.value) // 1

// 官方教程真是想到什么说什么,完全没有逻辑和章法
const otherCount = ref(2)
state.count = otherCount // state内部的count被使用其他ref赋值了,
console.log(state.count) // 2,因此值也就替换了
console.log(count.value) // 1,不过原先的count还在,暂时还能访问到值

不能省的情况:
即使ref被reactive包裹,但也需要直接包裹,
如果ref被原生的 [](或者称为Array), Map 包裹,
后面的 .value 还是不能省.

1
2
3
4
5
6
7
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
  1. 解构前嵌套一层

    解构会使内部的数据失去响应性,因此在解构时要加上 toRefs 进行特殊的处理.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { reactive } from 'vue'

    const book = reactive({
    author: 'Vue Team',
    title: 'Vue 3 Guide',
    })

    // 不做处理
    let { author, title } = book
    title = 'test'
    console.log(book.title) // 还是原来的

    // 做处理
    let { author, title } = toRefs(book)
    title.value = 'Vue 3 Detailed Guide'
    console.log(book.title) // 'Vue 3 Detailed Guide'
  2. 使用readonly防止更改

    ref外面可以包reactive,reactive外面也可以包其他以达到各种目的,
    比如包 readonly 就得到一个不能改变的对象,不过被包的变量还是能改变.

    1
    2
    3
    4
    5
    6
    7
    8
    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."

模块化的另一种思路 setup()

使用setup的方式,又称为组合式API,
目的是让同一逻辑的代码写在一起,比如放在一个函数中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default {
components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
props: {
user: { type: String }
},
data () {
return {
repositories: [], // 业务1data
filters: { ... }, // 业务2data
searchQuery: '' // 业务3data
}
},
computed: {
filteredRepositories () { ... }, // 业务3computed
repositoriesMatchingSearchQuery () { ... }, // 业务2computed
},
watch: {
user: 'getUserRepositories' // 业务1watch
},
methods: {
getUserRepositories () {}, // 使用 `this.user` 获取用户仓库,业务1method
updateFilters () { ... }, // 业务3method
},
mounted () {
this.getUserRepositories() // 业务1生命周期
}
}

业务1,2,3的各种逻辑分散的不同地方,
vue管得很宽,非要提供一种方式让这些东西写在一起,
代价就是需要在函数里将 data(), computed, watch, methods, mounted 等的功能再次实现一遍.
希望的效果是

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
someWhere () {
// 等效业务1data
// 等效业务1watch
// 等效业务1method

// 等效业务2data
// 等效业务2computed
// 等效业务2method

return {} // 这里返回的任何内容都可以用于组件的其余部分
}
}

这样的一个地方就是setup.并且vue对setup的使用做了一些规定.

  • setup在创建组件之前执行
    • 没有组件实例,不能使用 this
  • setup可以使用的参数有
    • props(响应式的,props更新,setup内部会得到新值)
    • context(包含attrs,slots,emit等内容,并非响应式)
  • setup返回的内容将暴露给其余部分(computed,method,生命周期等)

使用起来效果如下

1
2
3
4
5
6
7
8
9
10
11
export default {
props: {
user: { type: String }
},
setup(props) {
// 业务1
// 业务2
// 业务3
return {} // 这里返回的任何内容都可以用于组件的其余部分
}
}

具体例子比如

1
2
3
4
5
6
7
8
9
10
11
setup (props) {
const repositories = ref([]) // data内容,一个数据
const getUserRepositories = async () => { // method内容,如何更新这个数据
repositories.value = await fetchUserRepositories(props.user)
}

return { // 向外部暴露
repositories,
getUserRepositories
}
}

用渲染函数等效模板

提供一种基于函数的方法,方便在函数中表达模板的构建.
就像以anko layout来写安卓应用的视图.
这也使得模板的定义能够变得比v-for,v-if等更加动态,
比如直接使用变量表达h1到h6的标签.

可以直接使用render配置字的方式定义模板

1
2
3
4
5
6
7
app.component('anchored-heading', {
render() {
return Vue.h('h1', // tag名
{}, // 属性
this.blogTitle) // 内容,或者子节点列表
}
})

能够以函数的方式去书写DOM结构,
其实与vue采用虚拟DOM是分不开的.
等效出的效果是

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

也能在setup中使用渲染函数

1
2
3
4
5
6
7
8
export default {
setup() {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })
// 这里的ref又不能自动展开了
return () => h('div', [readersNumber.value, book.title])
}
}
  1. 使用约束

    自带列表中的VNode必须唯一,不能用同一个内存地址

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

    正确使用方法

    1
    2
    3
    4
    5
    6
    7
    render() {
    return Vue.h('div',
    Array.apply(null, { length: 20 }).map(() => {
    return Vue.h('p', 'hi') // 生成了20个不同的实例
    })
    )
    }
  2. 等效指令

    尽管稍微麻烦,上面的代码也几乎实现了v-for应该完成的功能.
    同理,v-if也能被简单实现.

    而其他的 v-model, v-on 等更多情况下作为属性型指令存在的指令,
    在渲染函数中则需要使用特别的关键字来等效.
    甚至有些修饰符,还可以像yaml一样,通过在配置字的下级使用配置字来继续配置.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    props: ['modelValue'],
    render() {
    return Vue.h(SomeComponent, {
    modelValue: this.modelValue, // 等效v-model
    'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
    })
    }

    render() {
    return Vue.h('div', {
    onClick: $event => console.log('clicked', $event.target) // 等效v-on
    })
    }

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

    而vue混乱就混乱在不是所有的修饰符都有配置关键字
    有些事件相关的关键字就需要自己手动实现

    修饰符 等价操作
    .stop event.stopPropagation()
    .prevent event.preventDefault()
    .self if(event.target !== event.currentTarget) return;
    .enter if(event.keyCode !== 13) return;
    .ctrl if(!event.ctrlKey) return;
  3. 等效slot

    1
    2
    3
    4
    5
    6
    7
    props: ['message'],
    render() {
    // `<div><slot :text="message"></slot></div>`
    return Vue.h('div', {}, this.$slots.default({
    text: this.message // 这里不仅等效了slot,还为slot设置了默认值
    }))
    }
  4. JSX

    一种js代码写模板的风格,用于简化js代码.
    使用时需要配置Babel

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    new Vue({
    el: '#demo',
    render() {
    return (
    <AnchoredHeading level={1}>
    <span>Hello</span> world!
    </AnchoredHeading>
    )
    }
    })

在setup里写响应式data

使用reactive或ref

1
2
3
4
5
6
setup() {
const count = ref(0)
return {
count
}
}

等效computed

之前能用 readonly 包,现在能用 computed 包,包完还是响应式对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const count = ref(1)
// 简单版包装setter
const plusOne = computed(() => count.value++)

// 复杂版getter,setter一起包装
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})

plusOne.value = 1
console.log(count.value) // 0,虽然被包了,但是ref对象还是可以单独访问

等效watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 侦听reactive内部的ref
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)

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

效果上约等于

1
2
3
4
watch: {
count(newValue, oldValue) {
/* ... */
}

当然也可以侦听多个数据源

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

等效生命周期

相比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setup (props) {
const repositories = ref([]) // data内容,一个数据
const getUserRepositories = async () => { // method内容,如何更新这个数据
repositories.value = await fetchUserRepositories(props.user)
}

return { // 向外部暴露
repositories,
getUserRepositories
}
}

mounted () {
this.getUserRepositories() // 1
}

或许将mounted部分的逻辑也拿到setup里来,做事更加彻底.
于是vue计划在setup中做一些等效生命周期的工作

1
2
3
4
5
6
7
8
9
10
11
12
13
setup (props) {
const repositories = ref([])
const getUserRepositories = async () => {
repositories.value = await fetchUserRepositories(props.user)
}

onMounted(getUserRepositories) // 等效mounted配置

return {
repositories,
getUserRepositories // 依然会向外提供
}
}

函数名通常是加一个前缀 on 即可.

配置字 setup内部API
beforeCreate
created
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTriggered onRenderTriggered

setup可以说运行时期与 beforeCreate, created 相同,
因此如果希望把 created 等的内容写在setup里,
直接写就好,不需要使用等效的API.

等效provide,inject

一个provide的配置

1
2
3
4
5
6
7
provide: {
location: 'North Pole',
geolocation: {
longitude: 90,
latitude: 135
}
}

可以等效为

1
2
3
4
5
6
7
setup() {
provide('location', 'North Pole') // key名变成了字符串
provide('geolocation', {
longitude: 90,
latitude: 135
})
}

取出内容时,由于key名消失了,依然需要使用变量存放一下

1
2
3
4
5
6
7
8
9
setup() {
const userLocation = inject('location', 'The Universe') // 用变量存放一下
const userGeolocation = inject('geolocation')

return {
userLocation,
userGeolocation
}
}

也可以提供响应式的值,这个要看传出来的是什么

1
2
3
4
5
6
7
8
9
10
setup() {
const location = ref('North Pole')
const geolocation = reactive({
longitude: 90,
latitude: 135
})

provide('location', location)
provide('geolocation', geolocation)
}

但问题是作者都认为尽量提供响应式的值,那为什么不把传递定值的方法禁用呢?

ref还能引用模板

通常在js代码中引用DOM结构中的某个节点,需要一些特别的手段.
然而不知道为什么vue将模板的引用也交给了 ref.比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<!-- 使用了组件中的root变量 -->
<!-- 将该DOM结构与root变量连接在了一起 -->
<div ref="root">This is a root element</div>
</template>

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

export default {
setup() {
const root = ref(null) // 组件中的root变量是一个空的ref

onMounted(() => { // 挂载后root变量即可获得模板的引用,并且打印其值
console.log(root.value) // <div>This is a root element </div>
})

return {
root // 需要向外暴露root变量
}
}
}
</script>

路由

简单的路由
定义路由表和组件的对应关系
计算的出需要使用的组件后
使用渲染函数在指定的地方(Angular中的路由出口)渲染之.

可以有vue-router使用
普通的路由定义方法
普通的路由访问方法
并不分离的路由出口
不方便重用的路由守卫(或者作者赞成使用函数的方式来重用?)
动态加载的模块可能以其他方式实现?

测试

单元测试用Jest
组件测试可以用官方的 vue-test?
e2e可以用 Cypress

混合应用开发

Capacitor或NativeScript

不常见问题

强制更新

vue的响应系统通常很可靠,但如果排除了其他使用姿势不对,就是系统bug的情况下,
可以使用传说中的 $forceUpdate 来强制更新页面上的数据.
官方没有给出例子.

非必要不用使用v-once

有一些大段静态文字的组件,可能会用到 v-once 来强制不要渲染以心理安慰减少负载.

1
2
3
4
5
6
7
8
app.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})

但官方说这是不必要的,vue很快.
甚至如果有人没有注意到角落里写的 v-once, 他会非常困惑为什么vue不正常了.

使用插件

官方给的例子很简单

1
2
3
4
5
6
7
8
const app = createApp(Root)
const i18nStrings = { // 提前声明使用插件时的一些配置
greetings: {
hi: 'Hallo!'
}
}

app.use(i18nPlugin, i18nStrings) // 直接用use来使用插件

用户直接访问深链接的问题

说用户直接访问深链接会报404

体会

好的东西不是做小就行,
如果连初始化代码启动都报错,还有什么方便可言.
先自行提供保证运行的所有代码,减少碎片化现象.
然后再开放安装和替换的自由不就好了.

当你认为做前端应该用A+B+C+D+…等等强约束来限制程序员不要跑偏的时候,
事实上你已经花费了许多时间和精力去挑选和实践.
angular给出了他们的选择和实践.那你信也不信?
而且angular cli在三个框架里又是强大无比,不会初始化项目都报个错.

作为一个标榜自己是后端程序员的人,
个人认为angular无论在代码组织还是概念划分上,
(service,DI)
都很符合开发习惯

参考

  1. 官网
  2. 三大框架的评价
  3. vue简史