用Vue也写过不少项目了,科技立项的个人网盘以及这个寒假写的短链站、禁书目录等等。
但是会写了,也只是依样画葫芦,更多的是在Element Plus里复制粘贴,再加点自己的东西,没有系统的学习和了解Vue。
懒惰的武丑兄便打算给自己开个新坑,以Vue3官方文档为基础,真正去理解Vue,形成更加长远的记忆。
本博客将持续更新,具体形式为提出某个问题,并对该问题进行解析。
每当我们用脚手架新建一个vue项目,脚手架会给你一个模板项目,一些固定的代码总是那样,比如下面的main.js里的代码,你一定很熟悉。
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中的解构语法导出的一个函数。
而第三行的app变量就是这个函数的返回值。那它是什么呢?在之前,我一直把这个app看作一个组件,因为createApp的参数里是App,是根组件。
但是查阅文档 应用 API | Vue.js (vuejs.org) 之后,我们可以知道,该函数的返回值,即这里的app,我们应该叫它为 应用实例。
这个实例可以干很多事情。
比如全局注册一个组件,让该应用实例挂在的组件树都能够共享这个组件。
import AnComponent from './components/AnComponent.vue'
//...
app.component('AnComponent', AnComponent)
使用一个插件
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 id="app">
中的内部的。
所以它应该被叫为 根组件实例。
我们再来重新看app.mount('#app')
这个语句,它是把app这个应用实例绑定到了 根组件实例上了嘛?不,app这个应用实例只是一个工具人,它干的事情是,把应用实例对应的根组件绑定到了根组件实例里。
即把App绑定到了#app上。
在写项目的时候,当我还在写类似下面的结果的时候
<script>
export default ({
data() {
return {
}
}
})
</script>
Vue3已经推出了一种setup标签的语法糖,它可以让我们少些很多繁琐的结构,就比如上面的export default等等。
我还不太会写这种高阶的语法,但是我在看许多案例的时候都发现它们都使用了 vue module里面的ref,而它们干的事情好像又很简单,我就无法理解这个ref的作用。比如下面的例子。
<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变量就会变。
我们注意到,msg变量定义的时候,用的是const,按理说msg的值是不可以改变的,除非它是一个对象。所以ref的返回值是一个对象。
通过查阅 Refs | Vue.js (vuejs.org) 文档,我们印证了这个观点。
文档:ref接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个
.value
property,指向该内部值。
所以ref存在的目的就是为了实现Vue中响应式数据的特点。那为什么我们用普通的写法不需要用到ref呢?这里我将语法改写为以下。
<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代码最终被编译后的样子。
SFC After Complier
还是拿这个常见的代码举例子。
<script>
export default ({
data() {
return {
}
}
})
</script>
这里的data是什么呢?我们如果以一个没有学过js的同学的视角看,它就是一个函数,和我们在C、C++定义函数的结构一致。
它确实是个函数,但是有几点值得说明。
首先这个它在一个对象内部,因为export default 导出了一个对象 {}
。按理说一个对象都是键值的形式,那它就放一个函数,它的键和值都是什么呢?
实际上这是ES6 对于对象内部方法名的一种简写,请参考 3.2.3 ES6 对象 | 菜鸟教程 (runoob.com)。
它实际上的样子应该是这样的。
<script>
export default ({
data: function() {
return {
}
}
})
</script>
这样就符合对象内部的键值关系了,它的键是data,它是一个function,返回值是一个对象。
我们都知道,在这个return里面定义的属性,我们可以在别的地方使用。
比如在模板里使用插值的形式。
插值
<template>
<h1>{{ wuuconix }}</h1>
</template>
<script>
export default ({
data() {
return {
wuuconix: 'yyds'
}
}
})
</script>
再比如在methods方法里使用属性
methods
<template>
<h1 @click="test">{{ wuuconix }}</h1>
</template>
<script>
export default ({
data() {
return {
wuuconix: 'yyds'
}
},
methods: {
test() {
console.log(this.wuuconix)
}
}
})
</script>
所以一直一来我就认为组件实例的数据就是来自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 效果一致。
对$data的测试
<template>
<h1 @click="test">{{ wuuconix }}</h1>
</template>
<script>
export default ({
data() {
return {
wuuconix: 'yyds'
}
},
methods: {
test() {
console.log(this.$data.wuuconix)
}
}
})
</script>