<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<!-- javascript -->
<script type="module" src="./index.js"></script>
<!-- 笔记中所有的代码都在 ./index.js 中, 然后用 vite 启动在浏览器中查看效果 -->
<script
src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.js"
type="application/javascript"
></script>
</body>
</html>
在线文档
Vue.js 2.x
Vue.js 3.x (推荐)
vue3 在线体验
模板
v-bind :
const app = new Vue({
el: '#app',
// v-bind 的缩写是 :
// 一旦绑定后, vue 就会将msg替换成 data 中 msg 变量的值
// :style 绑定样式 :calss 绑定css类名
template: `
<div>
<p v-bind:data-a="msg">绑定普通属性</p>
<p :data-b="msg">:缩写</p>
<p :[attrName]="msg">属性名是变量, 如果是 falsy 无法绑定成功</p>
<hr>
<p :style="styleStr">字符串绑定style</p>
<p :style="{color: '#f00'}">对象语法绑定style</p>
<hr>
<p :class="['text-red', 'bg-black']">数组绑定className</p>
<p :class="{'text-red': true, 'bg-black': false}">对象绑定className</p>
</div>
`,
data: () => ({
styleStr: 'border: 1px solid #f00',
attrName: 'data-c',
msg: 'hello vue.js',
}),
});
// 绑定 style
// 1. 可以拼接写行内样式字符串
// 2. 开始用对象的语法 {属性:"值"}
// 绑定 className
// 1. 使用数组语法, 将数组中所有的值都绑定到元素上
// 2. 使用对象语法, 只有属性值为 truely 才能绑定到元素上
// 绑定 className
插值指令: v-text/v-html
const app = new Vue({
el: '#app',
template: `
<div>
<p v-text="str">1234</p>
<p v-html="str">1234</p>
</div>
`,
data: () => ({
str: '<span style="color:#f00;">text text text text text text text</span>',
}),
});
// v-text 不会解析 HTML 字符串: 类似 el.textContent = str
// v-html 会解析 HTML 字符串: 类似 el.innerHTML = str
双向绑定: v-model
双向绑定就是当视图中的数据变化, data 中的数据也会发生变化, 如果 data 中的数据变化, 视图也会及时更新 官方文档
const app = new Vue({
el: '#app',
template: `
<div>
<input v-model="email" type="text" />
<textarea v-model="intro"></textarea>
</div>
`,
data: () => ({
email: '',
intro: '',
}),
});
// 不能表单控件 vue.js 会使用不同的事件
// 同时忽略 value 和 selected 和 checked 等属性
// 这些可以在文档中找到
条件渲染: v-if/v-show
'use strict';
const app = new Vue({
el: '#app',
template: `
<div>
<p v-if="show">渲染</p>
<p v-if="hide">不会渲染</p>
<p v-show="show">显示出来</p>
<p v-show="hide">不显示但是会渲染</p>
</div>
`,
data: () => ({
show: true, // 显示
hide: false, // 隐藏
}),
});
// v-if: 控制是否渲染(是否会创建dom元素)
// v-show: 控制是否显示(只是控制 display 属性的值,无论显示都会创建dom元素)
// 如果要频繁的控制是否需要显示, 建议使用 v-show, 因为他并不会销毁dom元素
// 如果只是控制一次, 不需要频繁的切换, 建议使用 v-if
列表渲染: v-for
v-for: 用于遍历数据, 然后渲染, 也就是所谓的: 列表渲染官方文档
const app = new Vue({
el: '#app',
template: `
<div>
<ul class="navbar-container">
<li v-for="item of navs" :key="item.id" class="navbar-item">
<a :href="item.href">{{ item.text }}</a>
</li>
</ul>
</div>
`,
data: () => ({
navs: [
{ id: 101, text: 'Vue.js', link: 'https://github.com/vuejs/vue' },
{
id: 102,
text: 'VueRouter',
link: 'https://github.com/vuejs/vue-router',
},
{ id: 103, text: 'Vuex', link: 'https://github.com/vuejs/vuex' },
],
}),
});
// 为什么要绑定 key 属性?
// 建议查看: https://vue3js.cn/interview/vue/key.html
组件化
组件注册
注册组件和组件名的一些注意点 官方文档
// 注册全局组件(所有组件都可以使用): Vue.component(组件名, 组件)
// 注册局部组件(在哪个组件中注册就可以在哪个组件中使用): 在组件内部使用 components 选项
const Navs = {
name: 'navbar',
template: `<ul>
<li>首页</li>
<li>发现</li>
<li>商城</li>
<li>我的</li>
</ul>`,
};
Vue.component('global-header', {
template: `<div>
<p>全局 header 组件</p>
<navs />
</div>`,
// 只有这个注册了 Navs, 只有这个组件可以使用,
// 在 global-footer 和 app 根组件中都无法使用
components: { Navs },
});
Vue.component('global-footer', {
template: `<div>全局 footer 组件</div>`,
});
const app = new Vue({
el: '#app',
template: `<div>
<global-header/>
<global-footer/>
</div>`,
});
porps
// 使用 props 的好处:
// 一个共用的模板/样式传入不同的数据,就能渲染出不同的东西, 高度复用
// 单向数据流:
// 子组件(navs), 无法改变父组件的数据, 利于调试
// 数据只能 父组件 -> 子组件(无法修改) 这样在开发是就知道数据在哪里修改的
const Navs = {
template: `<ul>
<li v-for="item of courseList" :key="item.id">
{{ item.text }}
</li>
</ul>`,
props: {
courseList: {
type: Array,
required: true,
},
},
};
Vue.component('navs', Navs);
Vue.component('global-header', {
template: `<div>
<p>全局 header 组件</p>
<navs :course-list="courseList" />
</div>`,
data: () => ({
courseList: [
{ id: 101, text: 'js' },
{ id: 102, text: 'vue.js' },
],
}),
});
const app = new Vue({
el: '#app',
template: `<div>
<global-header/>
<navs :course-list="courses" />
</div>`,
data: () => ({
courses: [
{ id: 201, text: 'css' },
{ id: 202, text: 'tialwind' },
],
}),
});
data
定义响应式数据, 必须是一个函数, 不能是一个对象? 否则就报错, 为什么这么设计 查看
简而言之: 因为函数有单独的作用域, 不会导致组件互相影响, 比如: 在 app 根组件中, 用了两个 navs
组件(例如:n1, n2), 现在点击 n1
组件会导致数据变化, 如果是对象, 那么两个 navs
组件(n1, n2)都会发生变化, 这就不对了, 我只想要被点击 navs
组件(n1) 发生变化, n2 没被点击就不发生变化
// ${msg} 是解析变量
// {{msg}} 是插值表达式
let msg = 'hello';
Vue.component('navs', {
template: `<div>
<p>非响应式数据: ${msg}</p>
<p>响应式数据: {{reactiveMsg}}</p>
<button @click="f1">修改 msg</button>
</div>`,
data: () => ({
reactiveMsg: 'world',
}),
// data: {
// reactiveMsg: "world",
// },
methods: {
f1() {
msg = msg.toUpperCase();
this.reactiveMsg = this.reactiveMsg.toUpperCase();
},
},
});
const app = new Vue({
el: '#app',
template: `<div>
<navs />
<hr/>
<navs />
<hr/>
<navs />
</div>`,
});
methods
事件监听函数
// @ 接收事件(包括原生的事件 & 自定义事件): 绑定处理函数
// this.$emit: 向父组件发送一个自定义事件
Vue.component('navs', {
template: `<div>
<button @click="handleClick">点击</button>
</div>`,
methods: {
handleClick() {
console.log('子组件被点击了, 向父组件发送一个自定义事件');
// $emit(事件名, 携带的数据)
// 携带的数据可以在父组件中接收到
this.$emit('nav-click', { a: 1 });
},
},
});
const app = new Vue({
el: '#app',
template: `<div>
<button @click="f1">App</button>
<navs @nav-click="f2" />
</div>`,
methods: {
f1() {
console.log('原生事件');
},
f2(e) {
console.log('子组件发送的自定义事件');
console.info(e); // 子组件传递的数据
},
},
});
watch
监听数据变化
const app = new Vue({
el: '#app',
template: `<div>
<p>{{msg}}</p>
<button @click="updateMsg">修改数据</button>
</div>`,
data: () => ({
msg: 'hello world',
info: {
email: '',
},
}),
methods: {
updateMsg() {
const randStr = Math.random().toString(16).substring(2);
this.msg = randStr;
this.info.email = randStr + '@foxmail.com';
},
},
watch: {
// 监听 msg 属性变化
msg(newval, oldVal) {
// newval: 修改后的值 oldval: 修改前的值
console.log('newval, oldVal: ', { newval, oldVal });
},
// 监听 info 对象的 email 属性变化, 'info.email'
'info.email'(newval, oldVal) {
// 数据改变就会执行, 不管是否渲染到 模板中
console.info('数据改变了');
},
},
});
computed
计算属性, 相对于 methods 来说具有缓存的作用
const app = new Vue({
el: '#app',
template: `<div>
<p>计算属性: {{ fullName }}</p>
<button @click="updateData">修改数据</button>
<button @click="setFullName">设置计算属性</button>
</div>`,
data: () => ({
firstName: 'hello',
lastName: 'world',
}),
methods: {
updateData() {
this.firstName = 'Hello';
},
setFullName() {
this.fullName += '( computed )';
},
},
computed: {
fullName: {
get() {
// 获取计算属性的时候调用: {{ fullName }}
// 或者 依赖的数据更新就调用(firstName/lastName)
console.info('getter');
return this.firstName + '-' + this.lastName;
},
set(v) {
// 设置 计算属性的时候会调用, this.fullName = xxx
console.info('setter:', v);
},
},
},
});
lifecycle 生命周期(2.x 版本)
- beforeCreate: 组件创建之前
- created: 组件创建之后
- beforeMount: 组件挂载之前(vnode 已经替换完成, 但是还没有渲染到页面上)
- mounted: 组件挂载之后, 可以获取到 dom
- beforeUpdate: 组件数据变化, 更新之前
- updated: 组件数据变化, 更新之后
- beforeDestroy: 组件销毁之前
- destroyed: 组件销毁之后
Vue.component('navs', {
props: ['msg'],
template: `<div> {{msg}} </div>`,
beforeCreate() {
console.info('beforeCreate');
},
created() {
console.info('created');
},
beforeMount() {
console.info('beforeMount');
},
mounted() {
console.info('mounted');
},
beforeUpdate() {
console.log('beforeUpdate');
},
updated() {
console.log('updated');
},
beforeDestroy() {
console.log('beforeDestroy');
},
destroyed() {
console.log('destroyed');
},
});
const app = new Vue({
el: '#app',
template: `<div>
<div v-if="show">
<navs :msg="msg" />
</div>
<button @click="toggle">切换状态</button>
<button @click="changeData">修改数据</button>
</div>`,
data: () => ({
msg: 'message',
show: true,
}),
methods: {
changeData() {
this.msg = Math.random().toString(16).substring(2);
},
toggle() {
this.show = !this.show;
},
},
});
lifecycle 生命周期(3.x 版本 hooks)
vue3.x 这些 hooks 只能在 setup 函数中调用
const { reactive, onBeforeMount, onMounted, ojnBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } = Vue;
const Home = {
template: `<div>
<p>{{state.msg}}</p>
<button @click="updateData">修改数据</button>
</div>`,
setup() {
const state = reactive({
msg: 'hello',
});
onBeforeMount(() => {
console.log('beforeMount');
});
onMounted(() => {
console.log('mounted');
});
onBeforeUpdate(() => {
console.log('beforeUpdate');
});
onUpdated(() => {
console.log('updated');
});
onBeforeUnmount(() => {
console.log('beforeUnmount');
});
onUnmounted(() => {
console.log('unmounted');
});
function updateData() {
state.msg = Math.random().toString(16).substring(2);
}
return {
state,
updateData,
};
},
};
const App = {
template: `<div>
<div v-if="state.show">
<home />
</div>
<button @click="toggle">切换组件是否渲染</button>
</div>`,
components: { Home },
setup() {
const state = reactive({
show: true,
});
function toggle() {
state.show = !state.show;
}
return {
state,
toggle,
};
},
};
Vue.createApp(App).mount('#app');
插槽
// 具名插槽: 有名字的插槽
// 默认插槽: 默认的插槽, 不需要指定插槽名称
Vue.component('base-layout', {
template: `<div>
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>`,
});
const app = new Vue({
el: '#app',
template: `<div>
<base-layout>
<template v-slot:header> 这是头部 </template>
<p>中间内容部分1</p>
<p>中间内容部分2</p>
<p>中间内容部分3</p>
<template v-slot:footer> footer 底部 </template>
</base-layout>
</div>`,
data: () => ({
msg1: '<div>hello</div>',
msg2: '<a href="https://github.com">github</a>',
}),
methods: {
toggle() {
this.show = !this.show;
},
},
});
组件通信
props + emit
主要用于父子组件传值, 非常方便, 但是同级组件传值就维护非常麻烦
父组件给子组件传递数据(size 和 color): props
<template>
<div>
<h2>Props + Emit</h2>
<div class="btns">
<x-button @xbtn-click="handleClick" size="small">小按钮</x-button>
<x-button @xbtn-click="handleClick" size="big">大按钮</x-button>
<x-button @xbtn-click="handleClick" color="danger">中按钮</x-button>
<x-button @xbtn-click="handleClick" color="info">中按钮</x-button>
<x-button @xbtn-click="handleClick" color="warning">中按钮</x-button>
<x-button @xbtn-click="handleClick" color="primary">中按钮</x-button>
<x-button @xbtn-click="handleClick" color="success">中按钮</x-button>
</div>
</div>
</template>
<script>
import XButton from './XButton.vue';
export default {
components: { XButton },
methods: {
handleClick(e) {
console.info(e.target);
},
},
};
</script>
<style lang="scss" scoped>
.btns button {
margin-right: 20px;
}
</style>
组建给父组件传递数据 $this.emit 发射自定义事件
在子组件中不要直接修改 props 数据, 这会破坏单向数据流的规范, 导致维护时, 不知道数据在哪个子组件中修改的, 而且, 直接修改 props vue 是会报错的
<template>
<button :class="classNames" @click="emitClick">
<slot>XButton</slot>
</button>
</template>
<script>
export default {
name: 'XButton',
props: [
'size', // 控制按钮大小
'color', // 控制按钮颜色
],
methods: {
emitClick(event) {
this.$emit('xbtn-click', event);
},
},
computed: {
classNames() {
const allowColors = ['info', 'danger', 'warning', 'primary', 'success'];
const allowSizes = ['big', 'middle', 'small'];
const color = allowColors.includes(this.color) ? this.color : 'info';
const size = allowSizes.includes(this.size) ? this.size : 'middle';
return [color, size];
},
},
};
</script>
<style lang="scss" scoped>
button {
margin: 0;
padding: 0;
border: none;
color: #fff;
border-radius: 2px;
cursor: pointer;
}
.big {
padding: 20px 45px;
}
.middle {
padding: 10px 25px;
}
.small {
padding: 5px 15px;
}
.primary {
background: #3f9eff;
}
.danger {
background: #f42c2e;
}
.info {
background: #909399;
}
.warning {
background: #feb03b;
}
.success {
background: #67c23a;
}
</style>
vuex / pinia
官方推荐使用
- vuex: https://vuex.vuejs.org/zh/
- pinia: https://pinia.vuejs.org/introduction.html
- pinia 中文: https://pinia.web3doc.top/introduction.html
provider + inject
弊端很大, 知道在哪注入的, 不知道数据在哪改的, 不推荐使用, 建议使用 Vuex
而且注入的数据默认不是响应式的
eventBus
主要依靠发布订阅模式来实现, 弊端也很大, 因为所有的事件都是在全局下的
递归组件
一般用于组件不确定数据到底有多少层级, 比如无限分类的树形结构数据
<template>
<div>
<h2>MenuTree</h2>
<menu-tree :data="menus" />
</div>
</template>
<script>
import MenuTree from './MenuTree.vue';
export default {
components: { MenuTree },
data: () => ({
menus: [
{
id: 1,
title: '菜单-1',
},
{
id: 2,
title: '菜单-2',
children: [
{
id: 21,
title: '菜单-2-1',
},
{
id: 22,
title: '菜单-2-2',
children: [
{
id: 221,
title: '菜单-2-2-1',
},
{
id: 222,
title: '菜单-2-2-2',
},
{
id: 223,
title: '菜单-2-2-3',
},
],
},
{
id: 23,
title: '菜单-2-3',
},
],
},
{
id: 3,
title: '菜单-3',
},
],
}),
};
</script>
<template>
<div class="menu-tree-container">
<div class="menu" v-for="menu of data" :key="menu.id">
<!-- 普通item -->
<div class="menu-item">
<span> {{ menu.title }} </span>
<span v-if="menu.children" class="icon">></span>
</div>
<!-- 有子选项的 item, 递归的渲染所有子选项 -->
<div v-if="menu.children" class="sub-menus">
<menu-tree :data="menu.children" />
</div>
</div>
</div>
</template>
<script>
export default {
name: 'MenuTree', // 必须指定name, 否则无法在本组件 template 中使用 <menu-tree> 组件
props: ['data'],
};
</script>
<style lang="scss" scoped>
.menu {
width: 200px;
background: #000;
text-align: left;
color: #fff;
position: relative;
padding: 0 10px;
box-sizing: border-box;
.menu-item {
margin: 0;
padding: 0;
line-height: 50px;
position: relative;
.icon {
position: absolute;
top: 0;
right: 0;
}
}
.sub-menus {
position: absolute;
top: 0;
left: 100%;
}
}
</style>
动态组件
动态组件: 就是由内置的 Component 组件来渲染, 而不是直接渲染, 在运行时可能改变的组件, 非常类似 v-if 的效果
组件缓存: 就是切换组件时, 不会直接销毁组件, 而是缓存组件的vnode, 类似 v-show 的效果, 但是实现的原理不一样
动态组件 Component
<template>
<div>
<h2>动态组件 & 异步组件</h2>
<div class="tab-container">
<ul class="tabs">
<li
class="tab-item"
v-for="tab of tabs"
:key="tab.componentName"
:class="{
'active-tab': currentTab.componentName === tab.componentName,
}"
@click="changeTab(tab)"
>
{{ tab.title }}
</li>
</ul>
<div class="tab-content">
<component :is="currentTab" />
<!-- component 和 v-if 的效果非常相似 -->
<!-- <account-login v-if="currentTab === 'account-login'" /> -->
<!-- <qrcode-login v-if="currentTab === 'qrcode-login'" /> -->
<!-- <mobile-login v-if="currentTab === 'mobile-login'" /> -->
</div>
</div>
</div>
</template>
<script>
const AccountLogin = {
name: 'account-login',
template: `<h2>账号密码登录内容部分</h2>`,
};
const QrcodeLogin = {
name: 'qrcode-login',
template: `<h2>扫码登录内容部分</h2>`,
};
const MobileLogin = {
name: 'mobile-login',
template: `<h2>手机验证码登录内容部分</h2>`,
};
export default {
components: {
AccountLogin,
QrcodeLogin,
MobileLogin,
},
data: () => ({
tabs: [
{ title: '密码登录', componentName: 'account-login' },
{ title: '扫码登录', componentName: 'qrcode-login' },
{ title: '验证码登录', componentName: 'mobile-login' },
],
currentTab: 'account-login',
}),
methods: {
changeTab(tab) {
this.currentTab = tab.componentName;
},
},
};
</script>
<style lang="scss" scoped>
.tabs {
display: flex;
&,
.tab-item {
list-style: none;
margin: 0;
padding: 0;
}
.tab-item {
margin-right: 20px;
cursor: pointer;
}
}
</style>
组件缓存 Component & keep-alive
<!-- ... -->
<div class="tab-content">
<keep-alive>
<component :is="currentTab" />
</keep-alive>
<!-- component 和 v-show 的效果非常相似 -->
<!-- <account-login v-show="currentTab === 'account-login'" /> -->
<!-- <qrcode-login v-show="currentTab === 'qrcode-login'" /> -->
<!-- <mobile-login v-show="currentTab === 'mobile-login'" /> -->
</div>
<!-- ... -->
异步组件
所谓异步组件就是利用 ESModule 可以动态加载的特性, 在代码运行选择是否要加载组件, 而不是在加载的时候, 直接加载
异步组件不会直接打包到 main.js 中, 而是会单独打包, 因为需要在运行时单独加载
被异步加载的组件 XButton.vue
<!-- XButton.vue -->
<template>
<button :class="classNames" @click="emitClick">
<slot>XButton</slot>
</button>
</template>
<script>
export default {
name: 'XButton',
props: [
'size', // 控制按钮大小
'color', // 控制按钮颜色
],
methods: {
emitClick(event) {
this.$emit('xbtn-click', event);
},
},
computed: {
classNames() {
const allowColors = ['info', 'danger', 'warning', 'primary', 'success'];
const allowSizes = ['big', 'middle', 'small'];
const color = allowColors.includes(this.color) ? this.color : 'info';
const size = allowSizes.includes(this.size) ? this.size : 'middle';
return [color, size];
},
},
};
</script>
<style lang="scss" scoped>
button {
margin: 0;
padding: 0;
border: none;
color: #fff;
border-radius: 2px;
cursor: pointer;
}
.big {
padding: 20px 45px;
}
.middle {
padding: 10px 25px;
}
.small {
padding: 5px 15px;
}
.primary {
background: #3f9eff;
}
.danger {
background: #f42c2e;
}
.info {
background: #909399;
}
.warning {
background: #feb03b;
}
.success {
background: #67c23a;
}
</style>
路由
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/async',
name: 'AsyncComponent',
component: () => import(/* webpackChunkName: "async-comp" */ '../views/async/index.vue'),
// 这个注释 /* webpackChunkName: "xbutton" */
// 是用来指定 webpack 打包的时候, 将这个.vue 文件打包后输出的名字 xbutton.js
// 异步加载: 只有 vue-router 导航到当前页面的时候才会加载 xbutton.js,
// 在其他页面的时候不会加载这个js文件
},
];
Vue2异步组件
一个也页面中, 某个组件异步加载
<template>
<div>
<h2>Vue 2 Async Component</h2>
<x-button></x-button>
</div>
</template>
<script>
export default {
components: {
// 这个注释 /* webpackChunkName: "xbutton" */
// 是用来指定 webpack 打包的时候, 将这个.vue 文件打包后输出的名字 xbutton.js
// 异步加载: 只有 vue-router 导航到当前页面的时候才会加载 xbutton.js,
// 在其他页面的时候不会加载这个js文件
'x-button': () => import(/* webpackChunkName: "xbutton" */ './XButton.vue'),
},
};
</script>
Vue3异步组件
<template>
<div>
<h2>Vue 3 Async Component</h2>
<x-button></x-button>
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue';
const XButton = defineAsyncComponent({
loader: () => import(/* webpackChunkName: "xbutton" */ './XButton.vue'),
// loadingComponent: loadingComponent,
// errorComponent: errorComponent,
delay: 100,
timeout: 3000,
suspensible: false,
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
retry();
} else {
fail();
}
},
});
</script>
静态加载和异步加载的实现原理
- 利用 link 标签异步加载(不会阻塞) js 文件(异步组件打包后的js文件)
- 当路由匹配到对应组件的时候, 如果这个组件是异步的, 会再次请求. 此时:因为(link prefetch)请求过一次了, 默认情况下会有缓存, 所以速度就会特别快, 而且还不影响正常使用
为什么要使用异步加载?
- 因为所有的文件都会打包成js, 打包后的文件会放到一个js文件中, 同步文件非常多, 就会导致这个文件特别大, 就导致加载速度慢
- 使用异步组件可以将一部分暂时用不到的一部加载, 等需要的时候在请求一次就行了
为什么要请求两次?
- 第一次加载是为了速度, link 标签异步请求不会阻塞, 第二次请求的时候就直接使用缓存了, 速度非常快
- 第二次加载是为了确保正确的加载出来, 如果禁用了浏览器缓存, 第二次请求就可以确保数据能够正常的加载出来