Vue3 解密 (持续更新中)

本文最后更新于:2022年1月17日 晚上

背景

用Vue也写过不少项目了,科技立项的个人网盘以及这个寒假写的短链站、禁书目录等等。

但是会写了,也只是依样画葫芦,更多的是在Element Plus里复制粘贴,再加点自己的东西,没有系统的学习和了解Vue。

懒惰的武丑兄便打算给自己开个新坑,以Vue3官方文档为基础,真正去理解Vue,形成更加长远的记忆。

本博客将持续更新,具体形式为提出某个问题,并对该问题进行解析。

app是什么

每当我们用脚手架新建一个vue项目,脚手架会给你一个模板项目,一些固定的代码总是那样,比如下面的main.js里的代码,你一定很熟悉。

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

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

这里面一共出现了8个app,让人眼花缭乱,让人看不懂它们到底是什么。

首先我们看第二行的App,它是从单文件组件 App.vue 里引入的,所以我们可以把App称为一个组件。而通常情况下,这个App总是最外层的,我们可以把它叫做rootComponent,即根组件,它将是渲染的起点。

再看第一行的createApp,它是从vue这个模块里通过ES6中的解构语法导出的一个函数。

vue module

而第三行的app变量就是这个函数的返回值。那它是什么呢?在之前,我一直把这个app看作一个组件,因为createApp的参数里是App,是根组件。

但是查阅文档 应用 API | Vue.js (vuejs.org) 之后,我们可以知道,该函数的返回值,即这里的app,我们应该叫它为 应用实例

这个实例可以干很多事情。

  1. 比如全局注册一个组件,让该应用实例挂在的组件树都能够共享这个组件。

    1
    2
    3
    import AnComponent from './components/AnComponent.vue'
    //...
    app.component('AnComponent', AnComponent)
  2. 使用一个插件

    1
    2
    3
    4
    5
    import router from './router'
    import ElementPlus from 'element-plus'
    //...
    app.use(ElementPlus)
    app.use(router)

    这里就是使用了我们常见的Element Plus UI库,以及Vue Router路由,它们都是以插件的形式引入到项目中的。

我们最后看第四行的'#app'。它实际上是项目 public/index.html里面的一个div。

div #app

它算什么呢?我们从它给我们的注释里也可以知道,项目的根组件渲染的结果实际上是会放到这个<div id="app">中的内部的。

所以它应该被叫为 根组件实例

我们再来重新看app.mount('#app')这个语句,它是把app这个应用实例绑定到了 根组件实例上了嘛?不,app这个应用实例只是一个工具人,它干的事情是,把应用实例对应的根组件绑定到了根组件实例里。

即把App绑定到了#app上。

神秘的ref

在写项目的时候,当我还在写类似下面的结果的时候

1
2
3
4
5
6
7
8
9
<script>
export default ({
data() {
return {

}
}
})
</script>

Vue3已经推出了一种setup标签的语法糖,它可以让我们少些很多繁琐的结构,就比如上面的export default等等。

我还不太会写这种高阶的语法,但是我在看许多案例的时候都发现它们都使用了 vue module里面的ref,而它们干的事情好像又很简单,我就无法理解这个ref的作用。比如下面的例子。

1
2
3
4
5
6
7
8
9
10
<script setup>
import { ref } from 'vue'

const msg = ref('Hello World!')
</script>

<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>

你可以把这段代码复制到 Vue SFC PlayGround 里进行尝试。该单文件组件的功能就是将input输入的内容和msg绑定起来,输入框的内容一变,msg变量就会变。

v-model msg

我们注意到,msg变量定义的时候,用的是const,按理说msg的值是不可以改变的,除非它是一个对象。所以ref的返回值是一个对象。

通过查阅 Refs | Vue.js (vuejs.org) 文档,我们印证了这个观点。

文档:ref接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value property,指向该内部值。

所以ref存在的目的就是为了实现Vue中响应式数据的特点。那为什么我们用普通的写法不需要用到ref呢?这里我将语法改写为以下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
export default({
data() {
return {
msg: '123'
}
}
})
</script>

<template>
<h1>{{ msg }}</h1>
<input v-model="msg">
</template>

我们发现,同样可以实现和之前一样的效果。

普通写法

我们观察这个普通写法中的data 实际上返回了一个对象,msg是这个对象里的一个键,由于对象的特性,msg的值可以被随意更改,实现响应式。

而高阶语法里没有这种data对象,我们便需要用ref来创造一个只有value值的对象。实现数据的响应。

以上观点可以在Vue SFC Playground里得到印证,它向我们展示出我们这段js代码最终被编译后的样子。

data() 与 $data

还是拿这个常见的代码举例子。

1
2
3
4
5
6
7
8
9
<script>
export default ({
data() {
return {

}
}
})
</script>

这里的data是什么呢?我们如果以一个没有学过js的同学的视角看,它就是一个函数,和我们在C、C++定义函数的结构一致。

它确实是个函数,但是有几点值得说明。

首先这个它在一个对象内部,因为export default 导出了一个对象 {}。按理说一个对象都是键值的形式,那它就放一个函数,它的键和值都是什么呢?

实际上这是ES6 对于对象内部方法名的一种简写,请参考 3.2.3 ES6 对象 | 菜鸟教程 (runoob.com)

它实际上的样子应该是这样的。

1
2
3
4
5
6
7
8
9
<script>
export default ({
data: function() {
return {

}
}
})
</script>

这样就符合对象内部的键值关系了,它的键是data,它是一个function,返回值是一个对象。

我们都知道,在这个return里面定义的属性,我们可以在别的地方使用。

  1. 比如在模板里使用插值的形式。

  2. 再比如在methods方法里使用属性

所以一直一来我就认为组件实例的数据就是来自data,但是它又只是个函数,不是个对象,让我觉得非常奇怪。

在阅读 实例 property | Vue.js (vuejs.org) 后我发现,原来还有一个$data的东西,它是一个实例Property,应该可以叫为实例属性。

而之前一直写的data它只是一个函数,用来返回组件实例的data对象,即$data。

所以data只是一个函数,而它的返回值,一般来说它的返回值必须是一个对象,这个对象就会成为组件实例的$data,作为一个实例属性供之后调用。

这里看了文档以后后还明白了一点,我们平常在methods里调用属性,都会写this.wuuconix来使用属性,那这里的this指什么呢?我以前认为应该是指向组件本身,而看了 Data | Vue.js (vuejs.org) 后我发现它指向的是组件实例。

那这样就引出了另一个问题,组件实例的属性都存在$data这个对象里,那为什么我们可以使用this.wuuconix的形式来调用组件实例的属性呢?

实际上这里vue大概是为了方便操作为我们做了一层代理。我们把vm(取义自viewModel)看作组件实例。访问vm.wuuconnix等价于访问vm.$data.wuuconix

为了印证它,我在SFC Playground里做了测试,发现使用 this.wuuconix和this.$data.wuuconix 效果一致。


Vue3 解密 (持续更新中)
https://wuuconix.link/2022/01/13/vue/
作者
wuuconix
发布于
2022年1月13日
许可协议