一、vue之#app、$refs、key、实例和组件异同、set原理、滚动加载、指令v-简写
附、响应式与非响应式的区别
(1)数据更新
A、响应式:自动触发渲染,如vue.js
B、非响应式:需手动调用updateView(),如jQuery.js
(2)多视图同步
A、响应式:数据一变,所有依赖视图同步更新
B、非响应式:容易遗漏更新,导致UI不一致
(3)复杂状态管理
A、响应式:内置依赖追踪,无需额外代码
B、非响应式:需自行实现发布-订阅或事件总线
1、index.html的#app元素与App.vue组件的关系,
(1)Vue2和Vue3都会保留#app容器元素,
(2)#app元素将其子节点替换为App.vue的根元素
2、$refs,获取DOM元素或组件的引用
(1)$refs,加在组件上,用this.$refs.name 获取到的是组件实例,可以使用组件的所有方法
(2)$refs,加在普通的元素上,用this.$refs.name获取到的是dom元素
3、vue组件的key属性
(1)key属性的作用,
A、标识节点,
B、让Diff算法更高效地识别节点、更新虚拟DOM
(2)index不能做key,
A、用index做key时,新增或删除节点的操作,
B、会使一个节点使用另一节点的index,进而使用它的key,进而使用它的data,进而产生错误
4、实例和组件的异同
(1)相同点:两者接收相同的配置,例如data、computed、watch、methods以及生命周期钩子等
(2)不同点:
A、自定义组件没有el配置项
B、Vue2.x和Vue3.x版本要求自定义组件的
a、data,默认值必须是一个工厂函数,用来返回默认对象
b、props的某项,如果是对象,默认值必须是一个工厂函数,用来返回默认对象
C、原因是
a、如果数据是对象,那么组件实例在不同的地方调用,数据指向的是相同的地址,此处数据改变,它处数据也改变
b、如果数据是函数,那么组件实例在不同的地方调用,数据指向数据此次的执行结果,是不同的地址,此处数据改变,它处数据不改变
5、vue2为什么能通过Vue.set强制触发视图更新
(1)vue2通过Object.defineProperty,实现对象的响应式,但无法检测对象新增属性或删除属性
A、Object.defineProperty,被用来劫持(拦截)对象的属性访问和修改,允许在属性的get和set时自动执行额外逻辑
(2)vue2通过重写数组的7个方法,实现数组的响应式,但无法检测数组通过索引修改元素或修改长度
A、7个数组方法,push、pop、shift、unshift、splice、sort、reverse
(3)Vue.set(或this.$set)的作用及工作原理
A、给响应式对象添加属性
a、Vue.set会调用defineReactive方法,为新增属性手动添加getter/setter
b、立即调用dep.notify()通知所有依赖项
B、给响应式数组修改长度或通过索引修改元素
a、用arr.splice(2)替代arr.length=2;用Vue.set(arr,2,5)或arr.splice(2,1,5)替代arr[2]=5
b、调用splice或Vue.set后,Vue内部会通过dep.notify()通知依赖更新
(4)Vue.delete(或this.$delete)的作用及工作原理
A、给响应式对象删除属性,相当于delete obj[key]
B、调用dep.notify(),通知视图同步更新
(5)Vue3中,这些问题已通过Proxy解决,无需特殊处理
6、滚动加载(vue-infinite-scroll)
<div v-infinite-scroll="loadMore" //滚动到底部时触发loadMore,发出请求,追加响应数据this.data.push({name:response.data.list});
infinite-scroll-throttle-delay="500" //滚动事件节流延迟(ms)
infinite-scroll-disabled="isBusy" //禁用状态(为true时停止加载)
infinite-scroll-distance="10" //触发距离(距底部10px时加载)
>
<div v-for="item in data" :key="item.id">{{item.name}}</div>
</div>
7、指令v-
(1)v-if的优先级高于v-show
(2)v-text,标签绑定文字,可以用{{}}代替
(3)v-model,在input textarea select中使用
A、v-model.lazy,只有在input输入框发生blur时才触发change事件,如<input v-model.lazy="msg">
B、v-model.trim,将用户输入的前后的空格去掉,如<input v-model.trim="msg">
C、v-model.number,将用户输入的字符串转换成number,如<input v-model.number="msg">
(4)v-on,标签的事件绑定
A、全写,<div v-on:click="doSomething">...</div>;
B、简写,<div @click="doSomething">...</div>
(5)自定义指令v-title的定义及使用
A、定义指令(main.js)
Vue.directive('title', {
update: function (el) { //执行时机:1、当指令data-title的值变化时;2、指令所在的组件更新时
document.title = el.getAttribute("data-title")
}
})
B、使用指令(form.vue)
<template>
<div v-title :data-title="basicData.gameName"></div>
</template>
(6)v-slot用法示例(可演示)
//调用组件时,如插槽为空,则使用后备内容
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.js"></script>
</head>
<body>
<div id="app">
<this-div :list="list">
<div>默认插槽</div>
<template v-slot:head="scope"><!-- 具名插槽,v-slot:head="scope"与#head="scope"等效,v-slot:head与#head等效 -->
<div v-if="scope.innerItem.id==2">{{scope.innerItem.name}}</div>
</template>
</this-div>
</div>
</body>
</html>
<script type="text/javascript">
//模板字符串加注释:`<div>${/*注释内容*/''}</div>`
//模板字符串加变量:`字符串${变量}`
Vue.component('this-div', {
props: ['list'],
template: `<div>
<slot/><!-- 默认插槽 -->
<div :key='item.id' v-for='item in list'>
<slot name='head' :innerItem='item'>后备内容</slot><!-- 具名插槽 -->
</div>
</div>`,
});
var vm = new Vue({
el: '#app',
data: {
list: [{
id: 1,
name: 'apple'
}, {
id: 2,
name: 'banane'
}, {
id: 3,
name: 'orange'
}]
}
});
</script>
(7)v-bind用法示例(可演示,)
A、v-bind:简写为:,
B、:是Vue的语法糖,用于绑定动态的值
C、vue2.x中,v-bind会覆盖静态属性(无论顺序)
代码,<div id="red" v-bind="{ id: 'blue' }"></div>
效果,<div id="blue"></div>
D、vue3.x 中,后面属性覆盖前面属性
代码1,<div id="red" v-bind="{ id: 'blue' }"></div>
效果1,id="blue"
代码2,<div v-bind="{ id: 'blue' }" id="red"></div>
效果2,id="red"
E、示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue 样式绑定示例</title>
<style>
.title {
font-size: 30px;
font-weight: bolder;
margin: 20px 0 10px;
}
.red { color: red; }
.green { color: green; }
.fontSize { font-weight: bolder; }
.example-section {
margin-bottom: 30px;
padding-left: 20px;
}
.example-item {
margin: 10px 0;
padding: 5px;
border-left: 3px solid #eee;
}
.example-label {
display: inline-block;
min-width: 120px;
font-weight: bold;
}
</style>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
</head>
<body>
<div id="box">
<!-- 1. class 绑定示例 -->
<div class="title">1、Class 绑定</div>
<div class="example-section">
<div class="example-item">
<span class="example-label">(1)三元表达式:</span>
<span :class="isTrue ? 'red' : 'green'">根据条件显示红色或绿色</span>
</div>
<div class="example-item">
<span class="example-label">(2)数组选择:</span>
<span :class="['red', 'green'][num1]">A、从1组样式中选择1个;</span>
<span :class="['one'=='one'?'green':null, 'two'=='two'?'fontSize':null]">B、生成1组样式</span>
</div>
<div class="example-item">
<span class="example-label">(3)对象语法:</span>
<span :class="{'red': isTrue, 'green': !isTrue}">根据对象属性应用样式</span>
</div>
</div>
<!-- 2. style 绑定示例 -->
<div class="title">2、Style 绑定</div>
<div class="example-section">
<div class="example-item">
<span class="example-label">(1)三元表达式:</span>
<span :style="isTrue ? {'color':'green', 'width':'200px'} : {'color':'red', 'width':'300px'}">A、整体三元;</span>
<span :style="{'color': isTrue? 'green': 'red', 'width': isTrue? '200px': '300px'}">B、分别三元</span>
</div>
<div class="example-item">
<span class="example-label">(2)数组语法:</span>
<span :style="{'color': ['red','green'][num1]}">从数组中选择颜色</span>
</div>
<div class="example-item">
<span class="example-label">(3)对象语法:</span>
<span :style="{'color': isTrue ? 'red':'green', 'width': isTrue ? '200px':'300px'}">多属性条件样式</span>
</div>
</div>
</div>
</body>
</html>
<script>
var vm = new Vue({
el: '#box',
data: {
isTrue: true,
num1: 1,
width: '200px',
height: '20px'
}
});
</script>
二、内置组件
1、5个内置组件
(1)component
渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。
(2)transition
A、作为单个元素/组件的过渡效果
B、只会把过渡效果应用到其包裹的内容上,而不会额外渲染DOM元素,也不会出现在可被检查的组件层级中
C、name属性:用于自动生成CSS动画类名
如果transition标签元素没有设置name属性,则对应的动画类名为v-XXX;如果设置了name属性,则对应的动画类名为属性值-XXX
D、appear属性:一开始就生效显示动画
E、示例,来源,https://blog.csdn.net/Superman_H/article/details/122851610
<template>
<div>
<button @click="bol = !bol">隐藏/显示</button>
<!-- transition 标签元素设置了 name、appear 属性 -->
<transition name="moveCartoon" appear>
<!-- 动画会在一开始便生效 -->
<h1 v-show="bol">组件动画效果</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return { bol: true };
},
};
</script>
<style>
/* 类名要对应回 name 的属性值 */
.moveCartoon-enter-active {
animation: move 1s;
}
.moveCartoon-leave-active {
animation: move 1s reverse;
}
@keyframes move {
from {
transform: translateX(-100%);
}
to {
transform: translate(0);
}
}
</style>
(3)transition-group
A、作为多个元素/组件的过渡效果。
B、可以使被包含的组件保留状态,或避免重新渲染。
C、它渲染一个真实的DOM元素。默认渲染<span>,可以通过tag、attribute配置哪个元素应该被渲染。它每个子节点必须有独立的key,动画才能正常工作。
D、标签里面的元素需要设置 key 属性,作为当前元素的唯一标识,
E、其他用法都和 transition 标签一样
(4)keep-alive
A、概念,包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
B、它是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
C、原理,在created函数调用时将需要缓存的VNode节点保存在this.cache中,
(5)slot
作为组件模板之中的内容分发插槽。它自身将被替换。
2、动态组件效果的实现方案
(1)在内置组件<component>里面使用 v-bind: is。没有keep-alive的配合,只能实现切换,不能实现缓存
<div id="app">
<component v-bind:is="whichcomp"></component>
<button v-on:click="choosencomp('a')">a</button>
<button v-on:click="choosencomp('b')">b</button>
<button v-on:click="choosencomp('c')">c</button>
</div>
var app=new Vue({
el: '#app',
components:{
acomp:{
template:`<p>这里是组件A</p>`
},
bcomp:{
template:`<p>这里是组件B</p>`
},
ccomp:{
template:`<p>这里是组件C</p>`
}
},
data:{whichcomp:""},
methods:{
choosencomp:function(x){
this.whichcomp=x+"comp"}
}
})
(2)把组件作为子组件放在内置组件<keep-alive>里,后者包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
A、include:字符串或正则表达式。只有名称匹配的组件会被缓存。
B、在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。
<keep-alive include="a,b" include='a' :include="/a|b/" :include="['a', 'b']">
/* 字符串、正则、数组,也可以没有这些 */
<component :is="view"></component>
</keep-alive>
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<transition>
<keep-alive>
<component :is="view"></component>
</keep-alive>
</transition>
三、组件通信 //“通信传值传参传数据”
附、单向数据流,
A、父组件可以直接向子组件传值,子组件只能通过回调函数或事件机制向父组件传值,
B、错误用法,逆向数据流,子组件直接修改父组件的props
附、Vuex,在不同组件之间实现数据共享
1、在Vue2中,有以下几种组件通信方式
(1)Props/$emit,父子传参
A、父组件通过props向子组件传递数据
B、子组件通过$emit触发自定义事件并向父组件传递数据
(2)Vuex,在不同组件之间实现数据共享
A、Vuex是一个专为Vue.js应用程序开发的状态管理模式
B、通过mutations、actions和getters来修改和获取状态
(3)Provide/Inject,祖先和后代组件通信
A、在祖先组件中使用provide提供数据或方法
B、在后代组件中使用inject来注入这些数据或方法。这种方式在Vue2中需要借助插件来实现响应式
C、示例
a、祖先组件(Provider 组件)
<template>
<div>
<child-component></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent,
},
provide() {
return {
message: 'Hello from parent',
};
},
};
</script>
b、后代组件(Consumer 组件)
<template>
<div>{{ injectedMessage }}</div>
</template>
<script>
export default {
inject: ['message'],
data() {
return {
injectedMessage: '',
};
},
created() {
this.injectedMessage = this.message;
},
};
</script>
(4)Event Bus(任意通信,自定义事件总线)
A、创建一个新的Vue实例作为事件总线
B、在组件中通过$emit触发事件,在其他组件中通过$on监听事件来实现通信
C、示例
a、创建事件总线
//eventBus.js
import Vue from 'vue';
export const eventBus = new Vue();
b、发送事件
<template>
<div>
<button @click="sendEvent">Send Event</button>
</div>
</template>
<script>
import { eventBus } from './eventBus';
export default {
methods: {
sendEvent() {
eventBus.$emit('customEvent', { message: 'Hello from component A' });
},
},
};
</script>
c、接收事件
<template>
<div>{{ receivedMessage }}</div>
</template>
<script>
import { eventBus } from './eventBus';
export default {
data() {
return {
receivedMessage: '',
};
},
created() {
eventBus.$on('customEvent', (data) => {
this.receivedMessage = data.message;
});
},
};
</script>
(5)$parent/$children(父子组件直接访问)
A、在子组件中可以通过$parent访问父组件实例
B、在父组件中可以通过$children访问子组件实例,但这种方式不推荐,因为它使组件之间的关系变得不清晰且难以维护
C、示例
a、父组件
<template>
<div>
<h1>Parent Component</h1>
<child-one></child-one>
<child-two></child-two>
<button @click="parentMethod">Parent Method</button>
<button @click="callChildrenMethods">Call Children Methods</button>
</div>
</template>
<script>
import ChildOne from './ChildOne.vue';
import ChildTwo from './ChildTwo.vue';
export default {
components: {
ChildOne,
ChildTwo
},
methods: {
parentMethod() {
console.log('Parent method called.');
},
callChildrenMethods() {
//使用 $children 访问子组件实例并调用子组件方法
this.$children.forEach(child => {
if (child.name === 'ChildOne') {
child.childOneMethod();
} else if (child.name === 'ChildTwo') {
child.childTwoMethod();
}
});
}
}
};
</script>
b、子组件1
<template>
<div>
<h2>Child One</h2>
<button @click="childOneMethod">Son Method</button>
</div>
</template>
<script>
export default {
name: 'ChildOne',
methods: {
childOneMethod() {
this.$parent.parentMethod();
console.log('Child One method called.');
}
}
};
</script>
c、子组件2
<template>
<div>
<h2>Child Two</h2>
<button @click="childTwoMethod">Son Method</button>
</div>
</template>
<script>
export default {
name: 'ChildTwo',
methods: {
childTwoMethod() {
this.$parent.parentMethod();
console.log('Child Two method called.');
}
}
};
</script>
2、在Vue3中,有以下几种组件通信方式
(1)Vuex(全局状态管理)
A、Vuex仍然是一种用于集中管理应用状态的方式
B、通过mutations、actions和getters来修改和获取状态,实现不同组件之间共享数据
(2)Props/emits(父子组件通信)
A、父组件通过props向子组件传递数据
B、子组件通过defineEmits定义并使用emit触发自定义事件向父组件传递数据
(3)Provide/Inject(祖先和后代组件通信)
A、在祖先组件中使用provide提供数据或方法
B、在后代组件中使用inject注入这些数据或方法。在Vue3中可以直接提供响应式数据
C、示例一
a、祖先组件(Provider 组件)
<script setup>
import { provide } from 'vue';
const message = 'Hello from provider!';
provide('sharedMessage', message);
</script>
b、后代组件(Consumer 组件)
<script setup>
import { inject } from 'vue';
const sharedMessage = inject('sharedMessage');
console.log(sharedMessage);
</script>
D、示例二,提供的数据是一个响应式对象
a、祖先组件(Provider 组件)
<script setup>
import { reactive, provide } from 'vue';
const sharedData = reactive({
count: 0
});
provide('sharedData', sharedData);
</script>
b、后代组件(Consumer 组件)
<script setup>
import { inject } from 'vue';
const sharedData = inject('sharedData');
function incrementCount() {
sharedData.count++;
}
</script>
(4)Event Bus(任意通信,自定义事件总线)
A、创建一个新的Vue实例作为事件总线
B、通过$emit和$on进行非父子组件之间的通信
C、示例
a、创建事件总线
//eventBus.js
import { createApp } from 'vue';
const eventBus = createApp({});
export default eventBus;
b、发送事件
<script setup>
import eventBus from './eventBus';
function sendEvent() {
eventBus.$emit('customEvent', { message: 'Hello from component A' });
}
</script>
c、接收事件
<script setup>
import eventBus from './eventBus';
eventBus.$on('customEvent', (data) => {
console.log(data.message);
});
</script>
(5)组合式API(CompositionAPI)中的响应式变量共享
A、通过创建一个包含响应式变量的模块
B、在多个组件中引入并使用这些变量来实现通信
C、示例
a、创建共享模块(sharedData.js)
import { reactive } from 'vue';
const sharedData = reactive({
count: 0,
message: 'Initial message',
});
const incrementCount = () => {
sharedData.count++;
};
const updateMessage = (newMessage) => {
sharedData.message = newMessage;
};
export { sharedData, incrementCount, updateMessage };
b、组件A使用共享数据
<script setup>
import { sharedData, incrementCount, updateMessage } from './sharedData';
console.log('Component A - Initial count:', sharedData.count);
console.log('Component A - Initial message:', sharedData.message);
incrementCount();
updateMessage('Updated message from Component A');
</script>
c、组件B使用共享数据
<script setup>
import { sharedData } from './sharedData';
console.log('Component B - Updated count:', sharedData.count);
console.log('Component B - Updated message:', sharedData.message);
</script>
(6)Teleport(传送组件内容)
A、不是严格意义上的组件通信方式
B、但可以将一个组件的内容传送到指定的DOM节点,在某些场景下可以实现特定的布局和交互效果
2、以下vue2通过属性传参(函数)实现子向父传值(可演示)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue子组件给父组件传值</title>
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
<div id="el">
<my-parent></my-parent>
</div>
<script>
Vue.component('my-parent', {
template: "<div>" + "<div>父组件改变值为:{{parentData}}</div><my-child v-bind:son-method='parentAdd'></my-child></div>",
data: function() {
return {
parentData: 0
}
},
methods: {
parentAdd: function(key) {
this.parentData = key;
}
},
})
Vue.component('my-child', {
template: "<div><button @click='thisMethod(\"222\")'>点击子组件-就向父组件传值</button></div>",
props: {
sonMethod: {
type: Function
}
},
data: function() {
return {
counter: 0,
sonData: 0
}
},
methods: {
thisMethod : function (key) {
this.counter += 1;
this.sonData = key + this.counter;
this.sonMethod(this.sonData)
}
},
})
new Vue({
el: '#el',
data: {
total: 0
}
})
</script>
</body>
</html>
3、以下vue3通过监听发射(自定义事件)实现子向父传值(可演示,包含element-plus、element-plus-icons-vue)
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<link href="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.2"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.full.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/element-plus-icons-vue/2.3.1/global.iife.js"></script>
</head>
<body>
<div id="app">
<div>以下是父组件内容</div>
<div>案例来源:http://vue3_demo.sweetysoft.com/</div>
<div>
<el-input v-model="douBao" :readonly="true" style="width:400px;">
<template #prefix>
<el-icon><Calendar /></el-icon>
</template>
</el-input>
</div>
<el-button @click="changeSon">改变自身,进而改变子组件</el-button>
<el-divider content-position="left" style="margin: 40px auto;" >分割线</el-divider>
<div>以下是子组件内容</div>
<my-component :custom-prop="douBao" @custom-event="changeSon"></my-component>
</div>
</body>
</html>
<script>
//以下是子组件的定义(在.html文件中,只能用vue2语法定义子组件)
var MyComponent = {
props: ['customProp'],
template:
`<div>
<div>{{ message }}</div>
<div>这是来自父组件的数据:{{ customProp }}(通过属性传参)</div>
<el-button @click="emitEvent">改变父组件,进而改变自身</el-button>
</div>`,
data() {
return {
message: '子组件自身数据',
};
},
methods: {
emitEvent() {
this.$emit('custom-event');
}
}
};
//以下是父组件的js
var app = Vue.createApp({ //创建一个Vue应用实例(app)
setup() {
var msg1 = '在.html文件中,用vue3.2定义数据并使用';
var msg2 = '在.html文件中,用vue3.2定义组件的属性、数据、方法';
var isFlag = true;
var douBao = Vue.ref(msg1);
const changeSon = (son) => {
isFlag = !isFlag;
douBao.value = isFlag? msg1: msg2;
};
return {
douBao,
changeSon
};
}
})
//以下是全局js
app.component('my-component', MyComponent); //在这个应用实例上注册全局组件
app.use(ElementPlus);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.mount('#app') //效果同app.mount(document.getElementById('app'))
</script>
四、两版Vue组件写法不同
1、vue2组件的6种写法//两版Vue组件写法不同-组件定义
来源:https://www.cnblogs.com/hanguidong/p/9381830.html
(1)字面量模板
Vue.component('my-checkbox', {
template:
`<div class="checkbox-wrapper" @click="check()">
<div :class="{ checkbox: true, checked: checked }"></div>
<div class="title"></div>
</div>`,
data() {
return { checked: false, title: 'Check me' }
},
methods: {
check() { this.checked = !this.checked; }
}
});
(2)x-template
<!-- 以下放在父组件的html里 -->
<script type="text/x-template" id="checkbox-template">
<div class="checkbox-wrapper" @click="check()">
<div :class="{ checkbox: true, checked: checked }"></div>
<div class="title"></div>
</div>
</script>
<!-- 以下放在js文件里 -->
Vue.component('my-checkbox', {
template: '#checkbox-template',
data() {
return { checked: false, title: 'Check me' }
},
methods: {
check() { this.checked = !this.checked; }
}
});
(3)inline-template
<!-- 以下放在父组件的html里 -->
<my-checkbox inline-template>
<div class="checkbox-wrapper" @click="check()">
<div :class="{ checkbox: true, checked: checked }"></div>
<div class="title"></div>
</div>
</my-checkbox>
<!-- 以下放在js文件里 -->
Vue.component('my-checkbox', {
data() {
return { checked: false, title: 'Check me' }
},
methods: {
check() { this.checked = !this.checked; }
}
});
(4)JSX
Vue.component("my-checkbox", {
data() {
return { checked: false, title: "Check me" };
},
methods: {
check() {
this.checked = !this.checked;
},
},
render() {
return (
<div class="checkbox-wrapper" onClick={this.check}>
<div class={{ checkbox: true, checked: this.checked }}></div>
<div class="title">{this.title}</div>
</div>
);
},
});
(5)render函数
Vue.component("my-checkbox", {
data() {
return { checked: false, title: "Check me" };
},
methods: {
check() {
this.checked = !this.checked;
},
},
render(createElement) {
return createElement(
"div",
{
attrs: {
class: "checkbox-wrapper",
},
on: {
click: this.check,
},
},
[
createElement("div", {
class: {
checkbox: true,
checked: this.checked,
},
}),
createElement(
"div",
{
attrs: {
class: "title",
},
},
[this.title]
),
]
);
},
});
(6)单文件
<template>
<div class="checkbox-wrapper" @click="check()">
<div :class="{ checkbox: true, checked: checked }"></div>
<div class="title">标题</div>
</div>
</template>
<script>
import comTab from '@/components/ComTab/com-tab'//导入别处组件
export default {
name: 'ComTpl',//组件名
components: {
comTab,//此组件依赖的组件
},
props: {//用于接收父组件向子组件传递的数据
tester: {
type: Object
}
},
data() {//本组件的数据
return {
tests: [],
selectedTest: {}
};
},
methods: {
handleClick () {
this.$router.push('/about')
}
},
computed:{//计算属性,所有get,set的this上下文都被绑定到Vue实例
fullName(){
return this.firstName+"."+this.lastName;
}
},
created() {//生命周期之一。在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
//ajax请求放在created里,因为此时已经可以访问this和操作DOM了。
this.classMap = ['a', 'b', 'c', 'd', 'e'];
//如进行异步数据请求
this.$http.get('/api/tests').then((response) => {
response = response.body;
if (response.errno === 0) {
this.goods = response.data;
}
});
},
mounted() { //在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
this.$nextTick(() => {
this._initScroll();
this._initPics();
});
},
filters: { //过滤器,可用来写,比如格式化日期的代码
//例
formatDate(time) {
let date = new Date(time);
return formatDate(date, 'yyyy-MM-dd hh:mm');
}
},
watch: {
//第一种方式:监听整个对象,每个属性值的变化都会执行handler
//注意:属性值发生变化后,handler执行后获取的 newVal 值和 oldVal 值是一样的
food: {//每个属性值发生变化就会调用这个函数
handler(newVal, oldVal) {
console.log('oldVal:', oldVal)
console.log('newVal:', newVal)
},
immediate: true,//立即处理 进入页面就触发
deep: true//深度监听 属性的变化
},
//第二种方式:监听对象的某个属性,被监听的属性值发生变化就会执行函数
'food.name'(newVal, oldVal) {//函数执行后,获取的 newVal 值和 oldVal 值不一样
console.log('oldVal:', oldVal) //冰激凌
console.log('newVal:', newVal) //棒棒糖
}
//其它
demo(val,oldVal) {
this.value = this.demo;
}
}
},
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
</style>
2、vue3组件的3种写法//两版Vue组件写法不同-组件定义
附、响应式API及用法
A、ref,创建基本类型(或引用类型)响应式数据。访问/修改需通过.value(模板中自动解包)
B、reactive,创建引用类型(对象/数组)响应式数据。直接修改属性触发更新,不能整体重新赋值
C、toRef,为响应式对象的单个属性创建引用。保持与原对象的关联,适合提取单个属性
D、toRefs,为响应式对象的所有属性创建引用。用于解构响应式对象,避免失去响应性
E、storeToRefs,为Pinia仓库的状态创建引用。保持状态的响应性,适合解构仓库数据
F、defineComponent,传入的属性可以验证,帮助TypeScript识别组件选项中的类型
import { defineComponent } from 'vue'
export default defineComponent({
props: {
name: { type: String, required: true },
age: { type: Number, default: 18 }
},
setup(props) {
console.log(props.name.toUpperCase()) //正确提示字符串方法
console.log(props.age.toFixed(1)) //正确提示数字方法
}
})
(1)Setup函数
A、特点
a、写在export default的setup选项中
b、接收props和context参数,需显式返回数据供模板使用
c、可与Vue2选项(如data、methods)共存
B、适用场景,需要兼容Vue2语法、复杂逻辑拆分
C、示例
<template>
<div class="example">
<h2>{{ title }}</h2>
<p>姓名:{{ user.name }}</p>
<p>全名:{{ fullName }}</p>
<p>年龄:{{ age }}</p>
<p>对象属性:{{ obj.name }}</p>
<button @click="changeName">修改姓名</button>
<button @click="sendData">发送数据</button>
<button @click="fn">更新aaa值</button>
<input @change="handleChange" placeholder="输入内容触发事件" />
<slot name="footer"></slot>
</div>
</template>
<script>
import { toRefs, reactive, ref, computed, watch, watchEffect, onMounted } from 'vue'
export default {
props: { //声明接收的props
title: {
type: String,
default: '默认标题'
}
},
beforeCreate() { //Vue2生命周期钩子(与setup共存示例)
console.log('beforeCreate执行了')
},
//Setup函数:组合式API入口
setup(props, context) {
//1、处理props与上下文
const { title } = toRefs(props) //将props转为响应式引用
const titleRef = toRef(props, 'title') //单个props属性的响应式引用
const { attrs, slots, emit, expose } = context //透传属性、插槽内容、事件发射器、暴露属性/方法给父组件
//上面props与attrs的区别
//props:是组件显式声明接收的属性,需要在组件的props选项中定义
//attrs:包含了所有未在props中声明的属性(除了class和style)
//2、响应式数据定义
const user = reactive({ name: '张三' })
const obj = reactive({ name: 'zs' })
const age = ref(0)
const firstName = ref('hello')
const lastName = ref('world')
const aaa = ref(1)
//3、计算属性
const fullName = computed(() => {
return firstName.value + '-·-' + lastName.value
})
//4、方法定义
const changeName = () => {
user.name = '李四'
emit('name-changed', user.name) //触发姓名变更事件
}
const sendData = () => {
emit('my-event', 1000) //触发自定义事件
}
const fn = () => {
aaa.value = 100 //修改ref值
}
const handleChange = (event) => {
emit("customChange", event.target.value) //触发输入变更事件
}
//5、数据监听
watch(
age,
(newValue, oldValue) => {
console.log('年龄变化:', newValue, oldValue)
}
)
watchEffect(() => {
console.log('obj.name变化:', obj.name) //自动追踪obj.name
})
//6、生命周期钩子
onMounted(() => {
console.log('组件挂载完成(setup内)')
console.log('DOM元素:', document.querySelector('.example'))
})
//7、暴露属性和方法给父组件(通过ref访问)
expose({
aaa,
fn,
user
})
//8、返回需要在模板中使用的数据和方法
return {
title,
user,
obj,
age,
fullName,
changeName,
sendData,
handleChange,
fn
}
}
}
</script>
<style scoped>
</style>
(2)Setup属性
A、特点
a、语法糖形式,
b、无需export default,自动将顶层变量/函数暴露给模板,使用defineProps/defineEmits等API
c、不兼容Vue2选项式语法
B、适用场景,新组件开发,追求简洁性和开发效率
C、示例
<template>
<div class="container">
<!-- 响应式API演示区 -->
<div class="reactive-demo">
<h3>响应式API示例</h3>
<p>基本类型(ref):{{ countRef }}</p>
<p>对象属性(reactive):{{ user.name }}</p>
<p>单个属性引用(toRef):{{ ageRef }}</p>
<p>解构属性(toRefs):{{ name }}</p>
<p>计算属性:{{ fullInfo }}</p> <!-- 新增计算属性展示 -->
</div>
<!-- 功能演示区 -->
<div class="function-demo">
<h3>功能综合示例</h3>
<p>计数(watch监听):{{ countWatch }}</p>
<button @click="increment">+1(触发事件)</button>
<button @click="updateUser('王五', 25)">修改用户信息</button> <!-- 新增方法调用 -->
<p>Props接收值:{{ state }}</p>
</div>
</div>
</template>
<script setup>
import { useAttrs, ref, reactive, toRef, toRefs, watch, watchEffect, onMounted, onBeforeUnmount, computed } from 'vue';
//1、处理props与上下文
const props = defineProps({
state: {
type: String,
required: true,
validator: (val) => ['success', 'error', 'loading'].includes(val) //验证合法值
}
});
const attrs = useAttrs() //访问特定的未声明属性
console.log(attrs.width)
console.log(attrs.height)
//上面defineProps与useAttrs的区别
//defineProps:是组件显式声明接收的属性,需要在组件的props选项中定义。不需要手动导入
//useAttrs:包含了所有未在props中声明的属性(除了class和style)。需要手动导入
//2、响应式数据定义
//基本类型响应式
const countRef = ref(0);
countRef.value = 1;
//对象类型响应式
const user = reactive({
name: '张三',
age: 20
});
user.age = 21;
//响应式属性引用
const ageRef = toRef(user, 'age'); //提取单个属性的响应式引用
const { name } = toRefs(user); //解构响应式对象(保持响应性)
//用于监听演示的计数变量
const countWatch = ref(0);
//3、计算属性
const fullInfo = computed(() => {
return `${name.value}(${user.age}岁)`;
});
//4、方法定义
//递增方法:修改计数并触发事件
const emit = defineEmits(['count-changed']); //声明自定义事件
const increment = () => {
countWatch.value++;
emit('count-changed', countWatch.value); //向父组件传递当前值
};
//修改用户信息的方法
const updateUser = (newName, newAge) => {
user.name = newName;
user.age = newAge;
};
//5、数据监听
//watch:精确监听countWatch变化
watch(
() => countWatch.value,
(newVal, oldVal) => {
console.log(`watch监听:计数从${oldVal}变为${newVal}`);
}
);
//watchEffect:自动追踪依赖(此处追踪countWatch)
watchEffect(() => {
console.log(`watchEffect自动监听:当前计数为${countWatch.value}`);
});
//监听user对象的name属性
watch(
() => user.name,
(newVal, oldVal) => {
console.log(`用户名从${oldVal}变为${newVal}`);
}
);
//6、生命周期钩子
onMounted(() => {
console.log('组件挂载完成:可以执行DOM操作或数据初始化');
//模拟初始化逻辑
const timer = setTimeout(() => {
user.name = '李四'; //延迟修改数据,验证响应式
}, 1000);
//清理定时器(避免内存泄漏)
onBeforeUnmount(() => {
clearTimeout(timer);
});
});
//组件卸载前的生命周期
onBeforeUnmount(() => {
console.log('组件即将卸载:可以清理定时器等资源');
});
//7、暴露属性和方法给父组件(通过ref访问)
defineExpose({
countRef,
user,
increment,
updateUser,
fullInfo
});
</script>
<style scoped>
</style>
(3)不实用写法
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.39/vue.global.js"></script>
</head>
<body>
<div id="box">
<vue3-component :myname="nameMy">插槽</vue3-component>
</div>
</body>
</html>
<script>
var obj = {
data(){
return {
nameMy:"张三"
}
},
methods:{},
computed:{}
}
var app = Vue.createApp(obj) //创建一个Vue应用实例(app)
app.component("vue3-component",{ //在这个应用实例上注册全局组件
props:["myname"],
template:`
<div>
{{myname}}
<slot></slot>
</div>
`
})
app.mount("#box") //挂载应用, //效果同app.mount(document.getElementById('box'))
</script>
五、两版Vue源码不同
附、两版VUE说明!!!A
1、两版VUE之方法、计算属性、侦听器
(1)方法的独特之处
A、主动触发:需手动调用,不会自动响应数据变化
B、无缓存:每次调用都会完整执行函数逻辑,适合动态计算
C、支持参数:可接收外部参数,灵活适配不同场景
D、自由副作用:适合执行异步操作(如请求)、DOM操作或其他非纯逻辑
(2)计算属性的独特之处
A、自动依赖追踪:基于响应式数据自动计算,依赖变化时重新求值
B、结果缓存:依赖未变化时直接返回缓存值,提升性能
C、必须返回值:用于派生数据
D、声明式设计:模板中像普通属性一样使用
附、vue2和vue3的计算属性在作为依赖方和被依赖方时的区别
a、vue2中,用相同的逻辑处理。都用getter处理
b、vue3中,用不同的逻辑处理。作为依赖方,用ReactiveEffect处理;作为被依赖方,由响应式系统统一处理
(3)侦听器的独特之处
A、响应数据变化:监听特定数据源,变化时执行回调(如路由、表单字段)
B、支持深度监听:配置deep:true可监听对象/数组内部变化
C、副作用中心:适合数据变化后执行异步请求、复杂逻辑或全局状态更新
D、手动控制:需显式指定监听目标(相比computed的自动依赖)
(4)方法、计算属性、侦听器、onMounted、updated的区别
A、方法,主动调用的函数,用于执行特定逻辑(如事件处理、异步操作等)
B、计算属性,由其他数据推导而来的衍生数据,本质是依赖多个源数据,生成一个目标数据
C、侦听器,某个数据变化后,执行额外操作,本质是当源数据变化时,触发副作用
D、onMounted,组件挂载后执行操作,本质是组件DOM就绪后触发初始化逻辑
E、updated,组件DOM更新后执行操作,本质是DOM更新完成后触发后续处理
附、生命周期中,不涉及Dom的可以用before
2、两版VUE之依赖存储与nextTick执行流程
(1)依赖存储机制,两版对比
A、Vue2的分散式依赖存储,通过为每个响应式属性创建独立的Dep实例来关联Watcher,没有全局统一的依赖存储结构
B、Vue3的集中式依赖存储,使用全局的targetMap(WeakMap)统一管理所有响应式对象的依赖关系
a、它的键是响应式对象的内存地址
b、在不同页面定义同名不同实例的对象,并作为键名添加到实例的键值对,不会冲突
(2)nextTick的具体执行流程,两版是一致的
A、用户修改Vue组件中的响应式数据
B、Vue将组件更新函数加入内部的异步更新队列
C、若队列未被调度(pending为false),Vue
a、标记pending为true,
b、创建微任务并追加到浏览器的微任务队列
D、用户调用Vue.nextTick,Vue将用户的回调追加到同一队列
E、当前同步代码执行完毕,JS引擎执行浏览器的微任务队列时,执行到该微任务
a、先执行DOM更新函数,更新真实DOM
b、再执行nextTick回调,获取最新DOM状态
c、执行完成后标记pending为false
F、浏览器检测到DOM变化,重新渲染页面
G、示例
a、Vue2示例
methods: {
getText () {
this.showText = true;
this.$nextTick(() => {//或Vue.nextTick,加上这层壳,下面内容在DOM更新后执行
var innerHTML = document.getElementById('divBox').innerHTML;
console.log(innerHTML);
})
}
},
b、Vue3示例
nextTick(() => {//加上这层壳,下面内容在DOM更新后执行
var innerHTML = document.getElementById('divBox').innerHTML;
console.log(innerHTML);
})
(3)相关术语
A、模板编译,将<template>转化为render函数
B、组件挂载,将组件从“内存中的JS对象”渲染为“页面中真实DOM元素”
3、VUE2从数据变化到页面渲染的过程
(1)初始化阶段
A、Vue使用Object.defineProperty对data选项中的每个属性创建getter/setter
B、递归处理所有嵌套对象属性,为每个属性创建独立的getter/setter
C、computed属性初始化时创建惰性求值的计算Watcher,watch配置初始化时创建立即执行(除非配置immediate:false)的用户Watcher
D、每个响应式属性都有一个Dep实例,通过闭包保存所有依赖它的Watcher,同时Watcher通过deps数组记录自己依赖的所有Dep
E、计算Watcher、用户Watcher、渲染Watcher(后续创建)
a、都会在首次访问数据时,触发getter、调用dep.depend
b、通过Dep.target机制建立数据与Watcher的双向关联
(2)模板编译
A、模板编译分为三个阶段(解析→优化→生成),最终产出render函数
B、组件挂载时,创建渲染Watcher(传入updateComponent函数),执行render函数
(3)数据变化检测
A、数据修改触发setter→调用dep.notify
B、Dep遍历subs数组(存储的Watcher集合),依次调用watcher.update
(4)更新调度与批量处理
A、Watcher.update将自身推入队列(queueWatcher)
B、使用has对象实现Watcher.id去重
C、通过nextTick将flushSchedulerQueue放入异步队列,确保:
a、同一事件循环内的多次数据变更合并为一次渲染
b、父组件的更新优先于子组件
(5)重新渲染与虚拟DOM
A、执行watcher.run→调用get→执行updateComponent→生成新vnode
B、diff算法核心逻辑:
a、同层比较(深度优先)
b、双端交叉对比(头头、尾尾、头尾、尾头)
c、基于key的复用优化
C、产出补丁指令(如INSERT、REMOVE、PROPS等)
(6)DOM更新
A、patch方法根据diff结果操作真实DOM
B、执行插入后的生命周期(updated)
4、VUE3从数据变化到页面渲染的过程
附、流程:数据变化→Proxy拦截→通知依赖→调度更新→生成新VNode→Diff对比→更新DOM
(1)初始化阶段
A、响应式系统基于Proxy,通过reactive创建对象代理,ref处理原始值
B、Proxy自动处理嵌套对象(惰性代理),动态属性变更可直接触发响应
C、computed返回ComputedRefImpl实例,watch/watchEffect创建ReactiveEffect
D、依赖关系存储在全局targetMap
E、计算属性(computed)、用户侦听器(watch/watchEffect)、渲染副作用(后续创建)
a、都会在首次访问数据时触发Proxy的get拦截,并通过track建立依赖追踪
b、通过activeEffect和全局依赖图(targetMap)建立响应式数据与副作用(ReactiveEffect)的双向关联
(2)模板编译
A、编译过程新增优化阶段:
a、静态提升(将静态节点提取到渲染函数外部)
b、补丁标记(PatchFlags:1=TEXT,2=CLASS,4=STYLE...)
c、区块树(BlockTree)追踪动态节点
B、组件挂载时:
a、创建渲染effect(setupRenderEffect)
b、执行render触发track→将当前activeEffect存入targetMap
c、建立响应式数据与effect的关联
(3)数据变化检测
A、数据修改触发Proxyset→调用trigger函数
B、从targetMap中获取对应属性的effects集合
(4)更新调度与副作用执行
A、调度系统(scheduler)特性:
a、使用queueJob管理任务(基于Set自动去重)
b、优先级:父组件更新优先,用户自定义watch可指定pre/post队列
B、默认使用微任务队列(Promise.then)批量执行
(5)虚拟DOM与Diff算法
A、动态节点对比优化:
a、静态节点完全跳过diff
b、带key的列表使用最长递增子序列(LIS)算法
c、基于patchflag的靶向更新(如仅比对class或style)
B、BlockTree机制:
a、动态节点按区块(Block)组织
b、通过dynamicChildren数组快速定位变化节点
(6)DOM更新
A、渲染器(renderer)根据diff结果执行精细化DOM操作
B、支持选择性挂载Tree-shaking的渲染模块
5、VUE2的异步批处理机制的说明
(1)原理!!!
A、利用事件循环机制,通过基础的更新队列处理逻辑,合并同一个事件循环内的所有响应式修改
B、优先使用Promise.then(微任务)统一触发DOM更新,但有降级策略以兼容旧环境
(2)响应式数据变化时
A、触发dep.notify,通知Watcher调用update,Watcher入全局队列(queue),通过has对象(以id为键)去重
B、同步代码执行完,更新调度机制以nextTick包装队列为微任务,浏览器异步触发
C、nextTick优先使用微任务(Promise.resolve().then、MutationObserver),不支持则降级为宏任务(setTimeout(fn,0)),确保同步代码后执行
(3)多次修改响应式变量,Watcher只执行一次。不同类型Watcher的执行顺序及其作用如下
A、计算属性Watcher:维护计算属性的值,依赖变化时标记为脏值,被访问时重新计算
B、用户Watcher(如watch):通过watch选项或$watch创建,监测指定数据变化并执行回调
C、渲染Watcher:负责组件模板渲染,依赖数据变化时触发重新渲染,基于最新数据更新DOM
6、VUE3的异步批处理机制的说明
(1)原理!!!
A、利用事件循环机制,通过优化的更新队列处理逻辑,合并同一个事件循环内的所有响应式修改
B、默认使用Promise.then(微任务)统一触发DOM更新,但提供了更灵活的调度机制,如优先级控制等
(2)响应式数据变化时
A、触发Proxy的set拦截器,调用trigger,从依赖图(targetMap)中获取关联的ReactiveEffect,推入全局执行队列queue(基于Set自动去重)
B、调度器通过Promise.resolve().then(flushJobs)将队列封装为微任务,同步代码执行后由浏览器触发flushJobs按优先级执行副作用函数
C、默认优先使用微任务(Promise.then),不支持时降级为queueMicrotask或MutationObserver,最终回退到宏任务setTimeout(fn,0)
(3)多次修改响应式变量,副作用只执行一次。不同类型副作用的执行顺序及其作用如下
A、前置阶段
a、beforeUpdate钩子,组件渲染前触发,可访问更新前的DOM
b、计算属性,惰性计算,仅标记为脏值
B、执行阶段
a、侦听器(默认flush:'pre'),watch和watchEffect执行
b、组件渲染,执行render函数,触发计算属性求值,生成VNode,更新DOM,期间会重新计算被访问的计算属性
C、后置阶段
a、updated钩子,DOM更新后触发,可安全操作更新后的DOM
b、侦听器(设置flush:'post'),watch和watchEffect执行
7、VUE2响应式机制的实现:依赖收集与触发流程(以计算属性为例)
(1)说明
A、初始化配置
a、响应式数据处理:通过Observer类将数据(对象/数组)转为响应式,对每个属性用Object.defineProperty定义get(读取)和set(修改)拦截
b、依赖管理器:每个响应式属性对应一个Dep实例(依赖容器),用于存储依赖该属性的Watcher
c、副作用封装:computed、watch、组件渲染等副作用被封装为Watcher实例,包含更新函数(如update)
B、依赖收集(get拦截阶段),适用于computed、watch、组件渲染等所有副作用场景
a、设置全局标记:执行Watcher时,将当前Watcher赋值给Dep.target(全局唯一)!!!A
b、触发get拦截:副作用函数执行时读取响应式属性,触发其get拦截
c、建立关联:在get拦截中,
当前Dep实例会将Dep.target(当前Watcher)添加到自身依赖列表(dep.addSub(watcher))
同时Watcher也会记录该Dep(watcher.addDep(dep)),形成双向关联
d、清除全局标记:副作用执行完毕后,Dep.target被重置为上一个Watcher(支持嵌套副作用)
C、触发更新(set拦截阶段)
a、修改数据触发set拦截:当响应式属性被修改时,触发其set拦截
b、通知依赖:set拦截中调用Dep.notify(),遍历依赖列表中的所有Watcher,执行其update方法
c、批量更新:Watcher进入异步更新队列(nextTick),避免频繁更新,最终执行run方法触发副作用重新执行(如重新渲染组件)
(2)Vue实例初始化时的计算属性
A、Vue实例初始化时调用initComputed
function initState(vm) {
vm._watchers = [];
const opts = vm.$options;
if (opts.computed) {
initComputed(vm, opts.computed); //触发整个流程
}
//...其他初始化逻辑...
}
B、初始化计算属性时调用defineComputed
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null);
for (const key in computed) {
const userDef = computed[key];
let getter = userDef;
if (!watchers[key]) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{
lazy: true, //标记为懒执行
computed: true //标记为计算属性Watcher
}
);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
C、定义计算属性的核心函数
function defineComputed(target, key, userDef) {
const shouldCache = !isServerRendering();
//处理计算属性的getter和setter
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? (shouldCache && userDef.cache !== false)
? createComputedGetter(key)
: userDef.get
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
//将计算属性定义到目标对象上(通常是vm实例)
Object.defineProperty(target, key, sharedPropertyDefinition);
}
D、创建计算属性的getter函数
function createComputedGetter(key) {
return function computedGetter() {
//获取当前计算属性对应的Watcher实例
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
//当计算属性依赖的“响应式数据”发生变化后,页面渲染时,依次调用computedGetter、evaluate、getter。!!!B
if (watcher.dirty) {
watcher.evaluate();
}
//如果存在活跃的依赖收集目标(通常是渲染Watcher)
//建立当前计算属性Watcher与目标Watcher的依赖关系
if (Dep.target) {
watcher.depend();
}
//返回计算结果
return watcher.value;
}
};
}
(3)Watcher类
A、Watcher类定义
class Watcher { //new watcher
constructor(vm, expOrFn, cb, options) {
this.vm = vm;
this.getter = expOrFn;
//1、expOrFn在不同场景下的类型
//(1)watch配置:可以是字符串(数据路径,如'a.b',即this.value=this.a.b,后者触发getter)或函数(返回监测值)
//(2)computed配置:是计算属性的getter函数
//(3)组件渲染:是组件的render渲染函数,生成VNode
this.cb = cb;
//2、cb在不同场景下的作用
//(1)watch配置:是数据变化时的回调函数(接收新值、旧值)
//(2)computed配置:是noop(空函数,因计算属性无需额外回调,仅需更新自身值)
//(3)组件渲染:是noop(空函数,渲染的目的是更新DOM,无需额外回调)
this.options = options;
this.lazy = options.lazy;
this.dirty = this.lazy;
this.id = ++uid;
this.deps = [];
this.newDeps = new Set();
this.value = this.get(); //添加订阅者1/6。!!!C
}
evaluate() {
this.value = this.get();
this.dirty = false;
}
get() {
pushTarget(this); //getter在执行前,其容器被放在全局
let value;
try {
value = this.getter.call(this.vm, this.vm); //添加订阅者2/6。
} finally {
popTarget();
}
return value;
}
update() {
if (this.lazy) {
this.dirty = true
//当计算属性依赖的“响应式数据”发生变化时,通知计算属性的Watcher,将其dirty标志设为true,不立即计算。!!!B
} else if (this.sync) {
//同步更新,立即执行。具体场景,极少出现,用户需要数据变化后立即执行回调
// this.$watch('someData', (newVal, oldVal) => {
// console.log('同步执行回调')
// }, { sync: true })
this.run()
} else {
//异步更新,加入调度队列。具体场景,每个组件的渲染Watcher、用户组件的watch配置
queueWatcher(this) //通知订阅者3/7。
}
}
run() {
const value = this.get(); //重新计算值
if (value !== this.value) { //如果值发生变化,执行回调
const oldValue = this.value
this.value = value
//执行回调(渲染/用户watch等)
this.cb.call(this.vm, value, oldValue) //通知订阅者7/7。
}
}
depend() {
this.deps.forEach(dep => dep.depend());
}
addDep(dep) {
const id = dep.id;
if (!this.newDeps.has(id)) {
//1、当一个Watcher多次访问一个响应式数据时,上面判断避免出现Dep与Watcher互相重复存储
this.newDeps.add(id);
dep.addSub(this); //添加订阅者5/6。
this.deps.push(dep);
//2、Dep实例与Watcher实例互相存储,建立双向关联,是上面两行代码的作用
//(1)Dep存储Watcher,dep.addSub(this)
// A、主要目的,派发更新。响应式数据变化时,让Dep通知Watcher,执行回调
// B、典型场景,一个Watcher依赖多个响应式数据时,多个Dep实例存储同一个Watcher实例
//(2)Watcher存储Dep,this.deps.push(dep)
// A、主要目的,依赖清理。让Watcher知道自己依赖了哪些Dep,将自身移出Dep
// B、典型场景,多个Watcher依赖一个响应式数据时,多个Watcher实例存储同一个Dep实例
}
}
teardown() { //依赖清理1/2。!!!D
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
}
B、Watcher实例的管理与执行
//以下,定义队列相关变量
var waiting = false;
let has = new Set();
const queue = [];
const callbacks = [];
let pending = false;
//以下,将Watcher去重后加入队列,并在下一个微任务中触发批量更新
function queueWatcher(watcher) {
const id = watcher.id;
if (!has.has(id)) {
has.add(id);
queue.push(watcher);
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue) //通知订阅者4/7。
}
}
}
//以下,按ID排序后,依次执行队列中的Watcher更新,并重置调度状态
function flushSchedulerQueue() {
let watcher, id
//排序:确保父组件Watcher先于子组件执行,用户Watcher先于渲染Watcher
queue.sort((a, b) => a.id - b.id)
for (let i = 0; i < queue.length; i++) {
watcher = queue[i]
id = watcher.id
has[id] = null //重置标记
watcher.run() //通知订阅者5/7。
}
resetSchedulerState()
}
//以下,将回调函数延迟到下一个微任务队列中异步执行,确保在DOM更新后触发
function nextTick (cb, ctx) { //nextTick定义
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
}
})
if (!pending) {
pending = true
Promise.resolve().then(flushCallbacks) //通知订阅者6/7。
//将flushCallbacks推入微任务队列
//注意:此时callbacks只有一个回调(当前刚添加的)
//在微任务执行前,可能会被后续nextTick调用追加更多回调
//浏览器在执行完同步任务后,执行此微任务
}
}
//以下,同步执行当前所有回调函数的副本并清空回调队列
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
//以下,重置队列相关变量
function resetSchedulerState() {
queue.length = 0
has = {}
waiting = false
}
(4)响应式核心逻辑(依赖收集与触发的底层实现)
//以下,递归观测对象
function observe(obj) {
if (typeof obj !== 'object' || obj === null) {
return;
}
new Observer(obj);
}
//以下,定义Observer类
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
//处理数组(重写数组方法)
this.observeArray(obj);
} else {
//处理对象(遍历属性添加 getter/setter)
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
}
observeArray(arr) {
arr.forEach(item => observe(item));
}
}
//以下,响应式对象创建(基于Object.defineProperty的响应式封装)
function defineReactive(obj, key, val) {
observe(val); //递归处理嵌套对象
const dep = new Dep(); //每个属性对应一个Dep实例(依赖管理器)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
if (Dep.target) {
dep.depend(); //添加订阅者3/6。
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); //通知订阅者1/7。!!!E
}
});
}
(5)Dep类
A、定义Dep类
class Dep {
constructor() {
this.id = ++depId;
this.subs = [];
}
depend() {
if (Dep.target) { //依赖存储
Dep.target.addDep(this); //添加订阅者4/6。
}
}
addSub(sub) {
this.subs.push(sub); //添加订阅者6/6。
}
notify() {
const subs = this.subs.slice(); //(用浅克隆)固定订阅者列表,避免在更新中修改原数组
subs.forEach(sub => sub.update()); //通知订阅者2/7。
}
}
B、与Dep类相关
Dep.target = null;
const targetStack = [];
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
8、VUE3响应式机制的实现:依赖收集与触发流程(以计算属性为例)
(1)说明
A、初始化配置
a、响应式数据处理:通过reactive或ref创建响应式对象,底层用Proxy代理整个对象,拦截get(读取)、set(修改)等操作
b、创建副作用:通过effect函数创建副作用,接收一个函数(副作用主体)和配置项(如scheduler调度器、lazy懒执行)
c、依赖管理:使用targetMap(WeakMap)存储依赖关系,结构为:targetMap->target->depsMap->key->effects(Set)
B、依赖收集(get拦截阶段),适用于computed、watch、组件渲染等所有副作用场景
a、设置全局标记:执行副作用前,将当前effect实例赋值给activeEffect(全局变量)!!!A
b、触发get拦截:副作用函数执行时读取响应式数据,触发Proxy的get拦截器
c、建立关联:
从targetMap中获取当前对象(target)的depsMap,若不存在则创建
从depsMap中获取当前属性(key)的effects集合,若不存在则创建
将activeEffect添加到effects集合中,同时effect实例会记录自身依赖的deps(便于清理)
d、清除全局标记:副作用执行完毕后,activeEffect重置为上一个副作用(支持嵌套)
C、触发更新(set拦截阶段)
a、修改数据触发set拦截:当响应式数据被修改时,触发Proxy的set拦截器
b、收集关联副作用:从targetMap中找到当前target和key对应的所有effects
c、执行副作用:遍历effects集合,
若副作用配置了scheduler则执行调度器(如watch的回调)
否则将副作用的run方法加入异步更新队列(queueJob),最终批量执行(通过nextTick实现)
(2)computed
A、定义
class Computed {
constructor(getterOrOptions) { //构造函数接收getter或配置对象
if (typeof getterOrOptions === 'function') {//初始化getter和setter
this.getter = getterOrOptions;
this.setter = () => console.warn('Write operation failed: computed value is readonly');
} else {
this.getter = getterOrOptions.get;
this.setter = getterOrOptions.set;
}
//缓存相关状态
this.dirty = true; //标记是否需要重新计算
this.value; //缓存计算结果
this.obj; //最终返回的响应式对象
//创建调度器
const createComputedScheduler = () => { //定义计算属性的调度器创建函数
return () => { //当依赖变化时,调度器被调用,不立即计算,将其dirty标志设为true,避免重复安排一个微任务来触发更新
if (!this.dirty) {
this.dirty = true;
queueJob(() => { //在下一次微任务中触发value的更新通知
trigger(this.obj, 'value'); //作为被“依赖方”,通知所有依赖该计算属性的effect(如渲染effect)进行更新。依赖触发2/4
});
}
};
};
//创建响应式副作用实例
this.effect = new ReactiveEffect( //是Vue响应式系统的核心,用于跟踪依赖和调度更新
this.getter, //是计算属性的计算逻辑
createComputedScheduler() //返回的调度器会在依赖变化时被调用
);
//定义计算属性的响应式对象(模拟Ref接口)
this.obj = {
__v_isRef: true,
get value() {
track(this.obj, 'value'); //作为被“依赖方”,计算属性收集依赖于自己的effect,如渲染effect。依赖收集1/4。!!!F
//当dirty为true时,计算属性依赖的响应式数据发生变化且尚未重新计算
//当dirty为false时,计算属性已根据最新依赖数据计算并缓存结果
if (this.dirty) {
this.value = this.effect.run(); //执行getter。作为“依赖方”,内部响应式数据收集当前(计算属性)的effect,计算新值。依赖收集3/4
this.dirty = false; //将dirty设为false表示已更新,后续访问直接返回缓存值
}
return this.value; //返回计算结果
},
set value(newVal) {
this.setter(newVal);
}
};
}
//对外暴露计算属性对象
getInstance() {
return this.obj;
}
}
//使用方式:const computedObj = new Computed(getter).getInstance();
B、使用
<template>
<div>
<input v-model="text" placeholder="输入文本">
<p>反转: {{ reversedText }}</p>
<p>长度: {{ textLength }} 字符</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const text = ref('')
const reversedText = computed(() =>
text.value.split('').reverse().join('')
)
const textLength = computed(() => text.value.length)
</script>
(3)ReactiveEffect,响应式副作用类
A、依赖存储
const targetMap = new WeakMap() //全局依赖存储
let activeEffect = null //当前活跃的effect
let shouldTrack = true //是否应该收集依赖
B、targetMap定义
const targetMap = new WeakMap()
targetMap.set(响应式对象,
new Map([
[属性名, new Set([effect1, effect2, ...])] //注意这里是数组形式[键, 值]
])
)
C、定义
class ReactiveEffect {
constructor(fn, scheduler = null) {
this.fn = fn
this.scheduler = scheduler
this.deps = []
this.active = true
}
run() {
if (!this.active) return this.fn()
try {
activeEffect = this
shouldTrack = true
return this.fn() //执行副作用函数。作为“依赖方”,内部响应式数据收集当前的effect。依赖收集4/4,
} finally {
shouldTrack = false
activeEffect = null
}
}
stop() {
if (this.active) {
cleanupEffect(this)
this.active = false
}
}
}
D、调度器机制的构成
a、调度函数scheduler,ReactiveEffect构造函数的第2个参数
b、任务队列管理模块
const queue = []
let isFlushing = false
let isFlushPending = false
function queueJob(job) { //起到去重作用。这是dirty标志的防御,正常情况下,不会重复添加任务
if (!queue.includes(job)) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
Promise.resolve().then(flushJobs) //!!!G
}
}
function flushJobs() {
isFlushPending = false
isFlushing = true
//排序保证父子组件更新顺序
queue.sort((a, b) => a.id - b.id)
try {
for (let i = 0; i < queue.length; i++) {
queue[i]()
}
} finally {
queue.length = 0
isFlushing = false
}
}
(4)响应式核心逻辑(依赖收集与触发的底层实现)
A、响应式方法ref的定义
function ref(value) {
//如果已经是ref直接返回
if (isRef(value)) {
return value
}
//如果是对象类型,自动调用reactive转换为响应式
if (typeof value === 'object' && value !== null) {
value = reactive(value)
}
//创建ref对象(变量名从r改为temp)
const temp = {
__v_isRef: true,
get value() {
//依赖收集,track定义
track(temp, 'value')
return value
},
set value(newVal) {
//只有值变化时才触发更新
if (hasChanged(newVal, value)) {
value = newVal
trigger(temp, 'value')
}
}
}
return temp
}
B、响应式方法reactive定义
const reactiveMap = new WeakMap(); //缓存响应式对象
function reactive(target) {
//非对象直接返回
if (typeof target !== 'object' || target === null) return target;
//已经是响应式代理则直接返回
if (target.__v_isReactive) return target;
//检查缓存
const cached = reactiveMap.get(target);
if (cached) return cached;
//创建代理处理器
const handler = {
get(target, key, receiver) {
//标记响应式对象
if (key === '__v_isReactive') return true;
const res = Reflect.get(target, key, receiver);
//依赖收集(简化参数:target, key)
track(target, key);
//递归转换对象为响应式
if (typeof res === 'object' && res !== null) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
//只有值变化时才触发更新(简化参数:target, key)
if (oldValue !== value) {
trigger(target, key);
}
return result;
}
};
//创建代理
const proxy = new Proxy(target, handler);
reactiveMap.set(target, proxy);
return proxy;
}
C、依赖收集
function track(target, key) {
if (!shouldTrack || !activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
trackEffects(dep)
}
function trackEffects(dep) {
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
//2、dep与activeEffect互相存储,建立双向关联,是上面两行代码的作用
//(1)dep存储activeEffect,dep.add(activeEffect)
// A、主要目的,派发更新。响应式数据变化时,让dep通知activeEffect,执行回调
// B、典型场景,一个activeEffect依赖多个响应式数据时,多个dep存储同一个activeEffect
//(2)activeEffect存储dep,activeEffect.deps.push(dep)
// A、主要目的,依赖清理。让activeEffect知道自己依赖了哪些dep,将自身移出dep
// B、典型场景,多个activeEffect依赖一个响应式数据时,多个activeEffect实例存储同一个dep
}
}
function cleanupEffect(effect) { //依赖清理2/2。
for (const dep of effect.deps) {
dep.delete(effect)
}
effect.deps.length = 0
}
D、依赖触发
function trigger(target, key) { //触发依赖本对象的effect
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = new Set()
const addEffects = (dep) => { //收集依赖集合中的所有effect
dep && dep.forEach(effect => {
effects.add(effect)
})
}
addEffects(depsMap.get(key))
effects.forEach(effect => {
if (effect.scheduler) { //有调度器的effect(如computed、watch),执行其自定义调度器
effect.scheduler()
} else { //无调度器的effect(如组件渲染effect),将“副作用函数”加入异步更新队列。!!!G
queueJob(effect.run.bind(effect))
}
})
}
六、两版Vue用法不同(区别)
来源,https://blog.51cto.com/knifeedge/5616852
(1)Vue2.x
Vue-cli: 3.x、4.x.
Vue-router: 3.x //渲染位置<router-view>
Vuex: 3.x
(2)Vue3.x
Vue-cli: 4.x
Vue-router: 4.x
Vuex: 4.x
1、实例化不同
(1)vue2下,用new获取和挂载vue实例
import Vue from 'vue';
import router from './router';
import store from './store';
new Vue({
el: '#app',
router,
data,
store,
render: createElement => createElement(App)//render执行,即createElement执行
})
//附、全局方法添加与调用
//以下添加
Vue.prototype.$myGlobalMethod = function () {
console.log(11111);
};
new Vue({})
//以下调用
export default {
mounted() {
this.$myGlobalMethod();
}
}
(2)vue3下,用createApp、mount,获取、挂载vue实例
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
//附、全局方法添加与调用
//以下添加
const app = createApp(App)
app.config.globalProperties.aaa = function(){
console.log( 22222 );
}
app.mount('#app') //效果同app.mount(document.getElementById('app'))
//以下调用
<script setup>
const { proxy } = getCurrentInstance()
proxy.aaa()
2、双向绑定原理不同
(1)vue2用ES5的一个Object.defineProperty对数据进行劫持,结合发布订阅模式的方式来实现的
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
return value;
},
set: function reactiveSetter(newVal) {
dep.notify();
},
});
(2)vue3用ES6的Proxy对数据进行代理
Proxy 的优势:
A、可以直接监听对象、数组的变化
B、有多达13种拦截方法
C、返回的是一个新对象供操作
var obj = {};
var thisObj = new Proxy(obj, {
get: function (target, key, receiver) {
console.log(obj === target); //true
if (key === 4) {
return 5;
}
return 6;
},
});
thisObj.count = 1;
console.log(thisObj.count);
console.log("count" in thisObj);
3、生命周期
(1)版本对比(不涉及Dom的可以用before)
A、vue2.2.0之前:
beforeCreate--、created--、beforeMount--、mounted--、beforeUpdate--、updated--、
“beforeDestroy--、destroyed--”、activated--、deactivated--、errorCaptured
B、vue2.2.0之后:
beforeCreate--、created--、beforeMount--、mounted--、beforeUpdate--、updated--、
“beforeUnmount--、unmounted--”、activated--、deactivated--、errorCaptured
C、vue3.0.0-beta.20之前,setup(props, context)作为组件选项,替代beforeCreate和created,与vue2的组件写法并存
“beforeCreate(setup)created”、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、
onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured
D、vue3.0.0-beta.21之后,通过<script setup>简化了该组件选项的写法,与vue2的组件写法二选一
“onbeforeCreate、oncreated”、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、
onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured
(2)生命周期触发及利用,vue2/vue3
A、创建
a、beforeCreate,实例初始化后、watcher生成前调用,data和methods未初始化
b、created,实例创建后调用,data等配置完成
B、挂载
c、beforeMount/onBeforeMount,实例挂载前调用,编译模板,调用render,生成vDom即虚拟DOM
d、mounted/onMounted,实例挂载后调用,可以访问操作DOM
C、更新
a、beforeUpdate/onBeforeUpdate,数据改变后、虚拟DOM重新渲染前调用
b、updated/onUpdated,虚拟DOM-重新渲染-后调用,==修改data数据,会造成死循环==
D、卸载
a、(beforeDestroy/beforeUnmount)/onBeforeUnmount,实例销毁-前调用
b、(destroyed/unmounted)/onUnmounted,实例销毁-后调用
E、请求
created、beforeMount、mounted,可以在里调用异步请求,进入更新生命周期
F、getCurrentInstance获取实例原理
a、在<script setup>中,顶层代码会在组件初始化时同步执行,执行时机与setup()函数内部一致
b、Vue3的组合式API在
c、
G、
(3)父子,vue2
A、加载顺序
a、父beforeCreate -> 父created -> 父beforeMount ->
b、子beforeCreate -> 子created -> 子beforeMount -> 子mounted ->
c、父mounted
B、更新顺序:父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
C、卸载顺序:
a、父(beforeDestroy/beforeUnmount) ->
b、子(beforeDestroy/beforeUnmount) -> 子(destroyed/unmounted) ->
c、父(destroyed/unmounted)
(4)父组件监听子组件的生命周期的方案
A、方案1
//Parent.vue
<Child @mounted="doSomething"/>
//Child.vue
mounted() {
this.$emit("mounted");
}
B、方案2
//Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
//Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
(5)全局属性注册与获取
A、全局属性注册
app.config.globalProperties.$hasPermit = (permission) => { //这一步将$hasPermit添加到应用的全局配置中
return checkPermission(permission);
};
B、组件实例化时
a、在<script setup>中,代码同步执行,时机与setup函数一致
b、在setup执行期间,Vue会临时存储当前组件实例(通过内部变量),并通过getCurrentInstance()暴露给用户!!!
c、该实例的proxy属性是一个Proxy,代理了对本地状态(如data、props)和全局属性(如$hasPermit)的访问
d、执行完setup后,Vue会清除临时存储的实例(以避免内存泄漏和错误使用)
C、全局属性获取
a、在组合式API中,通过getCurrentInstance().proxy可访问全局属性,功能上类似Vue2的this,官方不推荐过度依赖它
b、当访问getCurrentInstance().proxy.$hasPermit时,代理逻辑会先检查“本地属性”,后检查“全局属性”并返回对应方法
c、在setup同步作用域外或异步回调中,getCurrentInstance()可能返回null或不可靠结果
4、有无瞬移组件teleport不同
(1)vue2没有teleport
(2)vue3有teleport,瞬移组件到指定的dom中,相当于react中的ReactDOM.createPortal
A、代码1
<body>
<div id="app"></div>
<div id="modal"></div>
</body>
B、代码2
<template>
<teleport to="#modal">
<div v-if="visible" class="v3-modal">
<h2 class="v3-modal-title">{{ title }}</h2>
<div class="v3-modal-content">
<slot>This is a modal</slot>
</div>
<button @click="handleClose">close</button>
</div>
</teleport>
</template>
5、是否支持多根节点不同
附、template标签的核心作用是包裹组件的HTML结构,本身“不会”被渲染到最终的DOM中,不存在传统DOM意义上的“父子关系”!!!
(1)vue2不支持多根节点,必须有且仅有一个根节点
<!-- Layout.vue -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
(2)vue3支持多根节点,Fragment 特性
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
七、vuex,不同组件共享数据(“通信传值传参传数据”)
附、redux与Vuex的相似之处
A、redux:处理同步用 dispatch action(对象类型);处理异步用 dispatch action(函数类型)
B、Vuex:处理同步用 commit(mutations) ;处理异步用 dispatch(action),在action里执行 commit(mutations)
1、vuex3下(对应vue2下),
(1)定义,
A、模块定义//store/app.js
import Cookies from 'js-cookie'
import { makeRequest } from '@/prototype/httpRequest.js'
const app = {
namespaced: true, //为了解决不同模块命名冲突的问题
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
setCount(state, payload) {
state.count = payload;
},
},
actions: {
actionOut: ({commit},payload) { //也可以在此处解构payload
const { num } = payload; //解构payload中的num
httpRequest().then(res=>{
commit('setCount', num);
})
}
},
getters: {
//getters,是提取出来的computed属性,无法向它传参,可以让getter返回一个函数以接收参数,示例
//store.getters.getTodoById(2) //-> { id: 2, text: '...', done: false }
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
},
}
export default app
B、模块合并//store/index.js
//允许我们将 store 分割成 module,每个模块拥有自己的 state、mutation、action、getter、module
import Vuex from 'vuex';
import app from 'store/app.js'
import getters from 'store/getters.js'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
user,
permission
},
getters
})
export default store
(2)注入,
import store from 'store/index.js'
new Vue({
el: '#root',
store,
})
(3)使用
A、在.vue组件中使用
methods: {
increment() {
this.$store.commit('increment');
},
incrementBy(amount) {
this.$store.commit('incrementBy', { amount });
},
actionIn() {
this.$store.dispatch('actionIn');
},
incrementByAsync(amount) {
this.$store.dispatch('actionOut', { amount });
},
},
B、在.js中使用
import store from '../store';
store.dispatch('actionOut', { amount });
(4)辅助函数
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount']),
},
methods: {
...mapMutations(['increment']),
...mapActions(['actionIn']),
}
}
(5)示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vuex的使用</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.0.0/vuex.js"></script>
<script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
<style>
span{
display:inline-block;
width:200px;
}
div{
margin: 20px;
}
</style>
</head>
<body>
<div id="root">
<my-child></my-child>
</div>
</body>
</html>
<script>
Vue.component('my-child', {
template:
`<div>
<div><span>dataA:{{dataA}}</span><button @click='clickButton()'>普通函数改变dataA</button></div>
<div><span>stateA:{{stateA}}</span><button @click='mutationsA(100)'>mutations改变stateA</button></div>
<div><span>stateB:{{stateB}}</span><button @click='actionsC(2)'>actions改变stateB</button></div>
</div>`,
props: {},
data: function () {
return {
dataA: 1,
}
},
computed: {
...Vuex.mapState(['stateA','stateB']),
//...Vuex.mapState({stateD:'stateC'}),
//...Vuex.mapGetters(['getter0']),
},
methods: {
...Vuex.mapMutations([
'mutationsA',
]),
...Vuex.mapActions([
'actionsC'
]),
clickButton() {
this.dataA += 1;
},
},
})
const store = new Vuex.Store({
state: {
stateA: 100,
stateB: 1,
},
getters: {
getter0: (state, getters) => state.stateC//返回值可以为函数
},
mutations: {
mutationsA: (state, num) => {
state.stateA += num
},
mutationsB: (state, num) => {
state.stateB = state.stateB*num
}
},
actions: {
actionsC: ({commit},num) => {
setTimeout(function () {
commit('mutationsB', num)
}, 1000)
}
},
modules: { }
})
new Vue({
el: '#root',
store,
})
</script>
2、vuex4(对应vue3),
(1)定义
import { createStore } from 'vuex'
import app from 'store/app.js'
import getters from 'store/getters.js'
export default createStore({
modules: {
app,
user,
permission
},
getters
})
(2)注入实例//main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
const app = createApp(App);
app.use(store);
app.mount('#app'); //效果同app.mount(document.getElementById('app'))
(3)获取和使用
<template>
<div>
<h2>{{ store.state.count }}</h2>
<button @click="addCount">点击</button>
</div>
</template>
import { computed, useStore } from "vuex";
setup() {
var store = useStore();
var count = computed(() => store.state.count)
var addCount = function() {
console.log(store.state.count);
store.commit("addCount");
store.dispatch("addCount");
store.commit('increment');
store.dispatch('actionIn')
};
return { addCount };
}
3、vuex的替代--pinia
来源,https://pinia.web3doc.top/introduction.html
附、简介
A、2019年11月,pinia/piːnjʌ/出现
B、同步和异步都用actions,没有mutations,没有modules嵌套结构,没有命名空间
C、优势,热模块替换、支持服务端渲染
(1)主要方法
A、createPinia,执行结果配置到vue实例里
B、defineStore,定义状态管理
a、参数
第1个参数,是一个唯一的字符串标识符,用于区分不同的store
第2个参数,是一个配置对象,用于定义store的状态、actions和getters等
b、返回值,可以接受store为参数
C、storeToRefs,为pinia实例的属性创建引用
(2)定义
A、总状态,store/index.js,以下是该文件里的全部代码
const store = createPinia()
export default store
B、分状态,store/storeA.js
import { defineStore } from 'pinia'
const useStoreA = defineStore('storeA', { //第一个参数用于唯一标识这个store
state: () => ({
count: 0,
}),
actions: {
increment() {//不接收store参数
this.count++;
},
someAction(store) {//接收store参数
if (store.count > 5) {
this.value++;
}
},
getInfo() {
return getUserInfo().then(
(res) => {
SET_USER_INFO(this,data.loginUser)
SET_MENU(this,data.menuTreeList)
return Promise.reject('验证失败,请重新登陆!')
return Promise.resolve(res)
return getAAAInfo(res)
},
(error) => {
return Promise.reject(error)
}
)
},
},
});
(3)注入
A、vue2,
import store from './store'
new Vue({
el: '#app',
store,
})
B、vue3,
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
(4)获取和使用
<template>
<div>
Count: {{ count }}
</div>
</template>
<script setup>
import store from './store'
import useUserStore from '@/store/modules/user'
let userStore = useUserStore(store);//如果store已经在main.js里注入到项目,那么此处可以不传参(传入参数)store
userStore.getInfo().then(res =>{ })//使用方法
const { count } = storeToRefs(userStore);//使用属性
console.log( userStore.count );//使用属性
</script>
八、vue-router(含权限,渲染位置<router-view>)
来源,https://ezdoc.cn/docs/vue-router/advanced/navigation-failures
附1、url传参对应router中的query配置
window.open('#/create?domain=government&id=' + row.id)
http://test.cctv.com/#/create?domain=government&id=666
1、从加载到登录
(1)vue单文件页面,从首次加载到跳往登录页
A、开发工具启动并打开浏览器
a、运行npm run dev,开发工具(如Vite)启动服务器,并打开浏览器访问http://localhost:5173
b、浏览器请求/,服务器返回index.html
B、加载入口文件(index.html)
a、浏览器解析index.html,加载其中的JS/CSS(如main.js)
C、执行入口文件(main.js)
a、初始化Vuex/Pinia(store)和Vue Router(router)
b、如果项目中配置了权限控制(如@/permission.js),会在此处注册全局路由守卫(如router.beforeEach)
c、创建Vue实例(new Vue()或createApp(App)),并挂载到<div id="app"></div>,注入状态管理和路由
D、守卫触发
a、Vue Router根据当前浏览器URL(如/)匹配对应的路由规则
b、如果注册了全局路由守卫(如router.beforeEach),会触发守卫逻辑
c、检查用户登录状态(如从Vuex/Pinia或localStorage读取token)
d、如果未登录,调用next('/login')强制跳转到登录页
E、跳转登录页
a、URL更新为/login,Vue Router根据路由配置匹配Login.vue组件
b、渲染Login.vue,用户填写表单并提交登录请求
c、登录成功后,通常会在路由守卫中再次跳转到原目标页面(如/dashboard)
(2)执行入口文件 (main.js)
A、示例一
import store from '@/store' //初始化 Vuex
import router from './router' //初始化路由配置(但尚未注册守卫)
import '@/permission' //★ 核心:执行权限文件,注册全局路由守卫
new Vue({
el: '#app',
data: {},
router, //注入路由实例
store, //注入状态管理
render: h => h(App) //渲染根组件 App.vue
})
B、示例二
import store from '@/store' //初始化 Vuex
import router from './router' //初始化路由配置(但尚未注册守卫)
import '@/permission' //核心:执行权限文件,注册全局路由守卫
new Vue({
router, //注入路由实例
store, //注入状态管理
render: h => h(App) //渲染根组件App.vue
}).$mount('#app') //挂载到 DOM。 //效果同app.mount(document.getElementById('app'))
2、VueRouter实例
(1)生成实例//router.js
//注意,在80端口打开页面,有时成功有时失败,成功是因为80端口在其它项目中,执行过localStorage.setItem('token', 'xxx'),
import VueRouter from 'vue-router';
Vue.use(VueRouter);
var constantRoutes = [
{
path: '/',
name: 'HomePage'
component: HomePage,
},
{
path: '/login',
name: 'LoginPage',
component: LoginPage,
},
{
name: "首页",
path: "/home",
component: "main",
url: null,
children: [
{
name: "总编室总览",
//路径匹配,
path: "annotation",//根路径匹配/home,子路径匹配/annotation,属于嵌套路由
path: "/home/annotation",//根路径匹配/home/annotation,不属于嵌套路由
//路由配置既有path又有redirect,当访问path指定的路径时,自动使用redirect,渲染祖级到redirect的各层级组件
component: "home/index",
url: null
}
]
}
];
const router = new VueRouter({
mode: 'abstract',
//A、hash模式,最常用
// a、依赖于浏览器提供的 hashchange 事件。当 URL 的 hash 值发生变化时,浏览器会触发这个事件
// b、Vue-router在初始化的时候会监听这个事件,然后根据当前的hash值去匹配对应的路由规则,并通过这种方式来改变视图
// B、history模式,刷新浏览器时会出错,解决这个问题需要后台配合
// a、通过操作HTML5 History API来实现的,允许我们去创建一个更像原生应用的体验,没有hash(#)在URL中
// b、Vue-Router通过history.pushState来改变浏览器的地址栏而不重新加载页面
// c、当用户点击浏览器的前进后退按钮时,或者使用浏览器的地址栏进行导航时,可以监听popstate事件来处理路由的变化
// window.onpopstate = function(event) { //这是异步函数
// console.log( event.state, history.state, event.state == history.state, event, ); // true
// };
// C、abstract模式,抽象。为了在服务端渲染(SSR)而设计的。
// a、返回一个用于服务端的虚拟的 router-view 组件
// b、服务端可以获取到对应路由的 html 内容,然后将其嵌入到最终的 HTML 中,从而实现服务端渲染
routes: constantRoutes,
scrollBehavior: function (to,from,savedPosition){//使用前端路由,切换到新页面时,滚到顶部还是保持上次的位置
// “to”“from”和都是路由地址
// “savedPosition”可以为空,如果没有的话
return {x: 0, y: 0};//期望滚动到的位置
}
});
export default router//导出实例
(2)扩充实例,添加权限控制 //permission.js
附、跳转到登录页的情形,1、未登录,2、登录过期(写在总请求里),3、权限不足(请用高权限账户登录)
附、请求权限的情形,1、登录后,2、刷新浏览器后
import router from './router'
import useUserStore from '@/store/modules/user'
router.beforeEach(async(to, from, next) => {
//to,即将要进入的目标 路由对象
//from,当前导航正要离开的路由
//next,一定要调用该方法来resolve这个钩子,执行效果依赖next方法的调用参数
NProgress.start();
var isLogin = getLoginState();//从localStorage获取。会出现80端口设置过为true、81端口没设置过为false的情况
var whiteList = ['/login'];
var useStore = useUserStore();
/* 路由发生变化修改页面 title */
if (to.meta.title) {
document.title = to.meta.title //给title赋值
}
if(isLogin){//已登录
if(useStore.permissionList.length === 0){
//路过这里的情形:刷新浏览器,权限列表为空
useStore.getInfo();//获取权限清单
}
if(useStore.permissionList.indexOf(to.path) > -1 || whiteList.indexOf(to.path) > -1){
//路过这里的情形:首次打开页面,还没有获取权限列表
next(from);
}else{
//路过这里的情形:在浏览器输入路由,不在权限列表,不在白名单(公共页面)
//提示:没有权限,返回from
next();
}
} else {//未登录
if (whiteList.indexOf(to.path) > -1) {
next();
} else {
next('/login');//next(`/login?redirect=${to.path}`)
}
}
NProgress.done();
});
(3)无后台开发方案
A、模拟数据层 //src/mock/index.js
export default [
{
url: '/sjpt-platform-server/api/user/doLoginByPwd',
method: 'post',
timeout: 800,
response: ({ body }) => {
if (!body.username || !body.password) {
return { code: 400, message: '用户名或密码不能为空' }
}
const isLoginSuccess = body.username === 'admin' && body.password === '123456';
return isLoginSuccess ? {
code: 200,
data: {
token: 'mock_token_123456',
menuList: [1, 2, 3],
producer: { brief: '人民日报' }
},
message: '登录成功'
} : {
code: 401,
message: '用户名或密码错误'
}
}
},
{
url: '/sjpt-platform-server/api/user/getSessionID',
method: 'get',
response: () => ({
code: 200,
data: '5f3b07ae779644bea97f594a4be37a53',
message: '成功'
})
},
//补充用户信息接口(供getInfo调用)
{
url: '/sjpt-platform-server/api/user/info',
method: 'get',
response: () => ({
code: 200,
data: {
loginUser: { name: '管理员', role: 'admin' },
menuTreeList: [/* 菜单路由结构 */]
},
message: '成功'
})
}
]
B、Vite 配置 //vite.config.js
export default defineConfig(({ command, mode }) => {
return {
plugins: [
createVitePlugins(),
viteMockServe({
mockPath: "./src/mock/",
localEnabled: true, //开发环境开启
prodEnabled: false, //生产环境关闭
}),
],
}
})
C、状态管理 //src/vuex/index.js
actions: {
login(data) {
console.log(data);
SET_TOKEN(this, { "tokenName": "user_token", "tokenValue": data.token });
setToken({ "tokenName": "user_token", "tokenValue": data.token });
return Promise.resolve();
},
getInfo() {
return getUserInfo()
.then(res => {
const { data } = res;
SET_USER_INFO(this, data.loginUser);
SET_MENU(this, data.menuTreeList);
return { status: 3 }; //等价于resolve({status:3})
});
},
},
function SET_MENU(state, menuList) {
state.allMenus = menuList;
menuList.forEach(route => router.addRoute(route));
}
D、触发方法优化
//src/login/login.vue
quickLogin() {
useUserStore().getInfo().then(() => {
router.push({ path: 'sys/role' });
}).catch(err => {
console.error('获取信息失败', err);
});
}
//src/permission/permission.js
router.beforeEach((to, from, next) => {
useUserStore().getInfo()
.then(() => {
next({ path: 'sys/role' });
})
.catch(() => {
next({ path: '/login' }); //失败时跳转登录页
});
});
3、vueRouter三大守卫
(1)全局守卫-3个
router.beforeEach(function(to, from, next) {
//在路由跳转发生前被调用
});
router.beforeResolve(function(to, from, next) {
//在导航被确认之前,且在所有组件内守卫和异步路由组件被解析之后被调用
});
router.afterEach(function(to, from) {
//在路由跳转完成后被调用
});
export default router
(2)路由守卫-1个
const routes = [
//vue2中的this.$route,就是下面这样的对象
{
path: '/users/:id',
component: UserDetails,
meta: { title: '系统首页'},
beforeEnter: (to, from) => {
//reject the navigation
return false
},
},
]
(3)组件守卫-3个
const UserDetails = {
template: `...`, //name: 'YourComponent',
beforeRouteEnter(to, from) {
//在渲染该组件的对应路由被验证前调用
//不能获取组件实例 `this` !
//因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
//在当前路由改变,但是该组件被复用时调用
//举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
//由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
//因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
//在导航离开渲染该组件的对应路由时调用
//与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
4、页面传参(vue-router3及以前,渲染位置<router-view>)//“通信传值传参传数据”
(1)隐式传参,name与params搭配,参数不会出现在url上,页面刷新、数据丢失
A、在html中实现
<router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
B、在js中实现
methods: {
clickHand() {
//以下是传参
this.$router.push({
name: 'user',
params: { //参数不会出现在url路径上,
thisId: 'id',
}
})
//以下是接参
this.$route.params.thisId
}
}
C、在js中实现,vue-router任何版本,router与vuex搭配,适用于子传父,页面刷新,数据丢失
//比如兄弟页面跳到这里,这里初始化数据后又修改数据,进入子页面后回退至这里,这里显示修改后的数据
router.beforeEach((to, from, next) => {
//to到这里,from兄弟页面时,初始化数据
})
(2)显式传参,path与query搭配,参数出现在url上(问号的后面),页面刷新、数据不丢失,
A、在html中实现
<router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link>
B、在js中实现
methods: {
clickHand() {
//以下是传参
this.$router.push({
path: '/user',
query: { //参数会出现在url路径上,
thisId: 'id',
},
})
//以下是接参
this.$route.query.thisId
}
}
(3)动态路由传参,参数出现在url上,页面刷新、数据不丢失,
const constantRoutes = [
{
path: '/profile/set/:id',
name: 'set',
component: import('@/view/profile/set.vue')
}
]
<script>
var clickImage = function(){
this.$router.push({
path: '/profile/set/2'
})
}
</script>
<script>
console.log( this.$route.params.id );
</script>
5、页面传参(vue-router4及以后,渲染位置<router-view>)//“通信传值传参传数据”
附、从2022-08-22发布的vue-router4.1.4开始,
A、弃用name和params组合传参,
B、启用path和state组合传参
附、页面刷新
A、数据丢失:name与params,vuex
B、数据不丢失:path与query,path与state,localStorage
(1)隐式传参,参数不会出现在url上
A、在html中实现
<router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
B、在js中实现,vue-router4.1.4以前,name与params搭配,适用于父传子,页面刷新,数据丢失
<script setup>
import { useRouter, useRoute } from 'vue-router'
clickHand() {
//以下是传参
const userRouter = useRouter()
userRouter.push({
name: 'Home',
params: {
name: 'dx',
age: 18
}
})
//以下是接参
const route = useRoute()
console.log(route.params)
}
</script>
C、在js中实现,vue-router任何版本,router与vuex搭配,适用于子传父,页面刷新,数据丢失
//比如兄弟页面跳到这里,这里初始化数据后又修改数据,进入子页面后回退至这里,这里显示修改后的数据
router.beforeEach((to, from, next) => {
//to到这里,from兄弟页面时,初始化数据
})
D、在js中实现,vue-router4.1.4及以后,path与state搭配,适用于父传子,页面刷新,数据“不会”丢失
router.push({
path: './program',
query: {
sid: row.sid,
name:row.topicName
},
state: {//获取数据,history.state
filterClient : {...filterClient.value},
filterServer : {...filterServer.value},
page : {...page.value},
}
});
(2)显式传参,path与query搭配,参数出现在url上(问号的后面),页面刷新、数据不丢失,
A、在html中实现
<router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link>
B、在js中实现
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
//以下是传参
const jumpDetail = (row) => {
router.push({
path: '/sys/column-rule/detail',
query: {
sid: row.sid,
name: 'dx',
age: 18,
}
})
}
//以下是接参
const route = useRoute()
console.log(route.query)
</script>
(3)动态路由传参,参数出现在url上,页面刷新、数据不丢失,
const constantRoutes = [
{
path: '/profile/set/:id',
name: 'set',
component: import('@/view/profile/set.vue')
}
]
<script setup>
import { useRouter } from "vue-router"
const router = useRouter()
var clickImage = function(){
router.push({
path: '/profile/set/2'
})
}
</script>
<script setup>
import { onMounted } from 'vue'
import { useRoute } from "vue-router"
const useRoute = useRoute()
const useRouter = useRouter()
onMounted(function() {
console.log(useRoute.params)
});
</script>
九、vue-cli
1、vue主要版本发布时间
来源,https://github.com/vuejs/core/releases
附、vue语法插件:Vetur
(1)vue,1.0.0版,2015年10月27日
(2)vue,2.0.0版,2016年10月01日
(3)vue,3.0.0版,2020年01月04日,预发布
(3)vue,3.0.0版,2020年09月18日,正式发布
2、vue3的7个优点/优势
(1)体积小,按需编译,Tree shaking
(2)性能快,diff算法
(3)复合API,如setup,将逻辑相关的代码放在一起,有利于代码维护,Compostion API
(4)渲染API,如weex、myvue,Custom Renderer API
(5)多根组件,vue创建一个虚拟的Fragment节点
(6)更好地支持TS
(7)实时请求、实时编译,用vite开发构建工具编辑
3、vue-cli主要版本发布时间
来源,https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md
(1)vue-cli,3.0.0版,2018年08月10日
(2)vue-cli,4.0.0版,2019年10月16日
(3)vue-cli,4.5.0版,2020年07月24日,开始默认使用vue3
(4)vue-cli,5.0.0版,2022年02月17日
(5)vue-cli,内部高度集成了webpack,
A、项目根目录没有webpack.config.js文件,
B、只有vue.config.js文件,@vue/cli-service会自动加载该文件,去修改默认的webpack配置
(6)cli,命令行接口(Command Line Interface)
4、@vue/cli 是一个npm包,全局安装,提供了终端里的vue命令
(1)vue create,快速搭建一个新项目
(2)vue serve,构建新想法的原型
(3)vue ui,通过一套图形化界面管理我的所有项目
(4)vue --version,查看版本
(5)vue add XXX,用于在已创建的Vue项目中安装和调用Vue CLI插件
(6)安装命令 npm install -g @vue/cli,
(7)升级命令 npm update -g @vue/cli
(8)安装位置 C:\Users\Haier\AppData\Roaming\npm\node_modules\@vue\cli
5、@vue/cli-service 是一个npm包,安装在每个@vue/cli创建的项目中
(1)构建于 webpack 和 webpack-dev-server 之上
(2)内部的 vue-cli-service 命令,提供 serve、build 和 inspect 命令
(3)vue-cli-service serve,开启本地服务器,加载.env.development文件,把里面的键值对添加到process.env中
(4)vue-cli-service build,打包压缩项目,加载.env.production文件,把里面的键值对添加到process.env中
(5)vue-cli-service build --mode staging,项目测试,加载.env.staging文件,把里面的键值对添加到process.env中
6、引入插件
(1)引入echarts:import * as echarts from 'echarts';
(2)引入axios:import axios from 'axios';在plugins文件夹下进行配置
7、修改插件
(1)用config.module.rule('svg').exclude.add(resolve('src/icons')).end()对vue-cli-5.0里内置的'svg'模块规则进行修改
(2)用config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end()定义并向vue-cli-5.0里注入名为'icons'的模块规则
8、问题与解决
(1)问题1
现象:<router-view/>有下划红线
问题:无法使用 JSX,除非提供了 "--jsx" 标志
解决:在jsconfig.json(与package.json同级)里加"jsx": "preserve",
9、项目结构分析
(1)node_modules:项目依赖文件夹
(2)public:存放静态资源;webpack打包时,会将该文件夹中的静态资源原封不动地打包到dist文件夹中
favicon.ion:图标
index.html:模板,可通过webpack配置修改
(3)src:源码目录
assets:存放静态资源,webpack打包时,会把该文件夹中的静态资源当作一个模块,编译、打包到JS文件里面
components:存放非页面组件。组件中的name属性应与文件名一样,方便后期维护;组件名使用大驼峰
router:存放路由文件,主文件名为index.js
store:存放Vuex文件,主文件名为index.js
views:存放页面组件
App.vue:根组件,是所有组件的父组件
main.js:入口文件,是整个程序中最先执行的文件
(4).gitignore:用于配置不需要被Git管理的文件(夹)
(5)babel.config.js:babel的配置文件,用于处理ES语法的兼容问题
(6)jsconfig.json:配置webpack的文件
(7)package-lock.json:包版本控制文件。指定项目依赖包的版本号,保证其他人在执行npm i时下载的依赖包与原版本一致
(8)package.json:应用包配置文件
(9)postcss.config.js:配置(vue移动端自适应)。加载时机,在执行vue-cli里的开发或生产命令执行时,该文件在适当时机会被加载进去并执行--自悟。
(10)README.md:项目注释
(11).editorconfig,编辑器配置
# 告诉EditorConfig插件,这是根文件,不用继续往上查找
root = true
# 匹配全部文件
[*]
# 设置字符集
charset = utf-8
# 缩进风格,可选space、tab
indent_style = space
# 缩进的空格数
indent_size = 2
# 结尾换行符,可选lf、cr、crlf
end_of_line = lf
# 在文件结尾插入新行
insert_final_newline = true
# 删除一行中的前后空格
trim_trailing_whitespace = true
# 匹配md结尾的文件
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
(12).env,环境配置,开发、生产环境均有效
(13).env.development,开发环境配置
(14).env.production,生产环境配置
(15).env.staging,测试环境配置
(16).eslintignore,忽略eslint检查的文件列表,
(17).eslintrc.js,eslint检查规则,
(18).gitignore,忽略git上传文件列表,
十、mock拦截请求|模拟数据
1、说明
(1)原理:通过覆盖和模拟原生XMLHttpRequest来拦截Ajax请求
(2)拦截:拦截成功或失败,都不发出请求,network里无记录
(3)参数:Mock.mock(rurl?,rtype?,template|function(options))
来源,https://www.jianshu.com/p/b5c58ae144d9
来源,https://blog.csdn.net/qq_51357960/article/details/127285388
A、Mock.mock(template),根据模板,生成数据
B、Mock.mock(rurl,template),监听路由,根据模板,生成数据
C、Mock.mock(rurl,function(options)),监听路由,根据函数返回值,生成数据
D、Mock.mock(rurl,rtype,template),监听路由,根据请求方式、模板,生成数据
E、Mock.mock(rurl,rtype,function(options)),监听路由,根据请求方式、函数返回值,生成数据
F、参数说明
a、rurl:表示要拦截的接口,可以是绝对地址或者正则表达式
b、rtype:表示请求类型 get/post 等等
c、template|function(options):生成数据的模板
2、Mock.mock只有一个对象参数时的用法
来源,http://mockjs.com/examples.html
说明,对象参数就是数据模板,包含数据占位符
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>数据展示</title>
<style>
span{
color:red;
font-size: 16px;
}
.fontSize16{
font-size: 16px;
}
.result{
margin-left: 40px;
padding: 40px;
font-size: 12px;
background: rgb(231, 228, 228);
max-width: 400px;
white-space: pre-wrap;
}
</style>
<script src='https://cdn.jsdelivr.net/npm/mockjs@1.1.0/dist/mock.min.js'></script>
</head>
<body>
<pre>
<div class="fontSize16">以下来源,http://mockjs.com/examples.html</div>
<div style="display: flex;">
<div class="fontSize16">
//以下<span>数据占位符</span>
//以下基本数据
'@boolean'
'@natural(min, max)'
'@integer(min, max)'
'@range(start, stop, step)'
//上面,返回由数字构成的数组,
'@float(min,max, dmin, dmax)'
//上面,dmin(小数最少位数),
//上面,dmax(小数最多位数)
'@string'
//以下日期时间
'@date'
'@time'
'@datetime'
'@now'
//以下网络
'@domain'
'@email'
'@guid'
'@id'
'@ip'
'@url'
</div>
<div class="fontSize16">
//以下<span>数据占位符</span>
//以下英文数据
'@paragraph'
'@sentence'
'@word'
'@title'
'@first'
'@last'
'@name'
//以下中文数据
'@cparagraph'
'@csentence'
'@cword'
'@ctitle'
'@cfirst'
'@clast'
'@cname'
'@region'
'@province'
'@city'
'@county'
</div>
<div class="fontSize16">
//以下<span>数据模板</span>
//以下是data的定义
var data= Mock.mock({
'aaa': 200,
'bbb': /\d{4,7}/,
'ccc': '@boolean',
'ddd|2': [{
'title': '@cword(2, 4)',
'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
}],
'eee|2': {
'省级1': '@province',
'省级2': '浙江省',
'省级3': {
'河北省': '@city',
'河南省': {
'信阳市': {
'固始县': ['A',[1,2],'B',['3','4']],
//把数组某项换成'省级3'对象,看效果
}
}
}
},
'fff|2-5': '★',
});
<span>'ddd|2': 对象、数组、字符串,由2项构成</span>
<span>数据模板-包含-数据占位符;刷新页面,可以更新右图数据</span>
<span>2种JS方案-展示data,1、直接展示,2、格式化展示</span>
<span>JSON格式在线验证,https://www.bejson.com/</span>
</div>
<div id='div' class="result"></div>
</div>
</pre>
</body>
</html>
<script>
var data= Mock.mock({
'aaa': 200,
'bbb': /\d{4,7}/,
'ccc': '@boolean',
'ddd|2': [{
'title': '@cword(2, 4)',
'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
}],
'eee|2': {
'省级1': '@province',
'省级2': '浙江省',
'省级3': {
'河北省': '@city',
'河南省': {
'信阳市': {
'固始县': ['A',[1,2],'B',['3','4']],
//把数组某项换成'省级3'对象,看效果
}
}
}
},
'fff|2-5': '★',
});
//以下2种展示方式
//1、以下直接展示
//document.getElementById('div').innerText = JSON.stringify(data);
//2、以下格式化展示
function isArray(value) { return {}.toString.call(value) === "[object Array]"; }
function isObject(value) { return {}.toString.call(value) === "[object Object]"; }
function addLine(num) {//添加换行
var str ='\n';
for(var i = 0; i < num; i++) str +='\n'
return str;
}
function addSpace(num) {//添加空格
var str ='\xa0';
for(var i = 0; i < num; i++) str +='\xa0'
return str;
}
function addStr(data, num, isAddDot) {//添加字符串
var str = '';
if (isObject(data)) {
str += '{' + addLine(1);
var i = 0;
var strIn = ',';
var length = Object.keys(data).length;
for(var attr in data){
var value = data[attr];
i++;
if(i == length) strIn = '';
str += addSpace(4*num) + JSON.stringify(attr) + ":" + addStr(value, num+1) + strIn + addLine(1);
}
str += addSpace(4*(num-1)) + '}';
if(isAddDot) str += ',';
} else if (isArray(data)) {
str += '[';
for(var i=0;i<data.length;i++){
var value = data[i];
var isHasDot = i<data.length-1;
str += addStr(value, num, isHasDot);
}
str += ']';
if(isAddDot) str += ',';
} else {
str += JSON.stringify(data);
if(isAddDot) str += ',';
}
return str;
}
document.getElementById('div').innerText = addStr(data, 1);
</script>
3、示例,案例来源,online-class-manage
(1)添加依赖,package.json
{
"name": "vue-admin-template",
"scripts": {
"dev": "vue-cli-service serve",
},
"dependencies": {
"axios": "0.18.1",
"vuex": "3.1.0"
},
"devDependencies": {
"mockjs": "1.0.1-beta3",
},
}
(2)使用依赖,配置模拟,require
A、定义分散的路由与返回值
./mock/table.js
const Mock = require('mockjs')
const data = Mock.mock({
'items|30': [{
id: '@id', //数据占位符
title: '@sentence(10, 20)',
'status|1': ['published', 'draft', 'deleted'],
author: 'name',
display_time: '@datetime',
pageviews: '@integer(300, 5000)'
}]
})
module.exports = [
{
url: '/vue-admin-template/table/list',
type: 'get',
response: config => {
const items = data.items
return {
code: 20000,
data: {
total: items.length,
items: items
}
}
}
}
]
B、合并分散的路由与返回值,在生产环境中使用模拟数据
./mock/index.js
const Mock = require('mockjs') //覆盖和模拟原生XMLHttpRequest
const { param2Obj } = require('./utils')
const user = require('./user.js')
const table = require('./table.js')
const mock_ = [...user,...table] //合并分散的路由与返回值
function mockXHR() { //在生产环境中使用模拟数据
for (const i of mock_) {
Mock.mock(new RegExp(i.url), i.type || 'get', i.response) //直接传入i.response,似乎也行
}
}
//以下,导出数据
module.exports = {
mock_,
mockXHR
}
C、把每个定义的路由与返回值绑定
./mock/mock-server.js,
const chokidar = require('chokidar')
const bodyParser = require('body-parser')
const chalk = require('chalk')
const path = require('path')
const Mock = require('mockjs')
const { mock_ } = require('./mock/index.js')
const mockDir = path.join(process.cwd(), 'mock') //process.cwd(),获取当前工作目录
function registerRoutes(app) {
let mockLastIndex
const mocksForServer = mock_.map(route => {
return {
url: new RegExp(`${process.env.VUE_APP_BASE_API}${route.url}`),
//console.log(new RegExp("ab+c", "i"));///ab+c/i
//console.log(new RegExp(/ab+c/, "i"));///ab+c/i
type: route.type || 'get',
response: function(req, res) {
res.json(Mock.mock(route.response instanceof Function ? route.respond(req, res) : route.response))
}
}
})
for (const mock of mocksForServer) {
//----在node里,拦截并重新定义ajax请求,url可以用正则
app[mock.type](mock.url, mock.response)
mockLastIndex = app._router.stack.length
}
const mockRoutesLength = Object.keys(mocksForServer).length
return {
mockRoutesLength: mockRoutesLength,
//自己开始的索引:最后索引 - 路由长度;避免后来多删
mockStartIndex: mockLastIndex - mockRoutesLength
}
}
function clearModuleCache() { //项目原名,unregisterRoutes
Object.keys(require.cache).forEach(i => {
if (i.includes(mockDir)) {
delete require.cache[require.resolve(i)]//require.cache:引入的模块将被缓存在这个对象中
}
})
}
module.exports = function(app){
app.use(bodyParser.json()) //解析请求体中json数据
app.use(bodyParser.urlencoded({ //解析请求体中form表单数据
extended: true
}))
const mockRoutes = registerRoutes(app)
var mockRoutesLength = mockRoutes.mockRoutesLength
var mockStartIndex = mockRoutes.mockStartIndex
chokidar.watch(mockDir, { //初始化时,监听mock目录的变化
ignored: /mock-server/,
ignoreInitial: true
}).on('all', (event, path) => { //更新时,执行下列代码
if (event === 'change' || event === 'add') {
try {
clearModuleCache(); //1/3--这里清除“模块缓存”,下面会再次执行require('./mock/index.js'),更新“模块缓存”
app._router.stack.splice(mockStartIndex, mockRoutesLength) //2/3--清除旧的“路由栈”,以免与新的重复
const mockRoutes = registerRoutes(app); //3/3--生成新的“路由栈”,存储匹配关系
mockRoutesLength = mockRoutes.mockRoutesLength;
mockStartIndex = mockRoutes.mockStartIndex;
console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`))
} catch (error) {
console.log(chalk.redBright(error))
}
}
})
}
(3)使用模拟,require
A、开发环境中注入//vue.config.js
module.exports = {
devServer: { //开发环境中
before: require('./mock/mock-server.js') //添加新模拟后,自动更新
before: function(app, server) { //添加新模拟后,手动更新
//生成新的“路由栈”,存储匹配关系
app.get('/some/path', function(req, res) {
res.json({ custom: 'response' });;
});
}
}
};
B、生产环境中注入模拟数据//main.js
if (process.env.NODE_ENV === 'production') { //在生产环境中使用模拟数据,上线之前,请移除
const { mockXHR } = require('./mock/index.js')
mockXHR()
}