有关Vue相关知识的汇总,从基础到原理
兄弟组件之间相互传递数据
首先创建一个vue的空白实例(兄弟间的桥梁)
两个兄弟之间建立一个js文件
import Vue from 'vue'
export default new Vue()
子组件 childa
发送方使用 $emit 自定义事件把数据带过去
<template>
<div>
<span>A组件->{{msg}}</span>
<input type="button" value="把a组件数据传给b" @click ="send">
</div>
</template>
<script>
import vmson from "../../../util/emptyVue"
export default {
data(){
return {
msg:{
a:'111',
b:'222'
}
}
},
methods:{
send:function(){
vmson.$emit("aevent",this.msg)
}
}
}
</script>
自组件childb
<template>
<div>
<span>b组件,a传的的数据为->{{msg}}</span>
</div>
</template>
<script>
import vmson from "../../../util/emptyVue"
export default {
data(){
return {
msg:""
}
},
mounted(){
//绑定自定义事件
vmson.$on("aevent",this.getEvnt)
},
methods:{
getEvnt(val){
console.log(val);
}
},
beforeDestory(){
//及时销毁,否则可能导致内存泄漏
event.$off('aevent',this.getEvnt)
}
}
</script>
父组件:
<template>
<div>
<childa></childa>
<br />
<childb></childb>
</div>
</template>
<script>
import childa from './childa.vue';
import childb from './childb.vue';
export default {
components:{
childa,
childb
},
data(){
return {
msg:""
}
},
methods:{
}
}
</script>
父组件与自组件之间的生命周期
父组件--created
子组件--created
子组件--mounted
父组件--mounted
父组件--before update
子组件--before update
子组件--updated
父组件--updated
vue自定义组件使用 v-model 进行双向数据绑定
<input v-model="something">
是我们常用的双向绑定方法,如果在自定义组件中如何使用v-model进行双向绑定呢?
首先我们必须要清除v-model绑定的原理如下:
其实v-model的语法糖是这样包装而成的:
<input
:value="something"
@:input="something = $event.target.value">
而一个组件上使用时则会简化成这样子:
<custom-input
:value="something"
@input="value => { something = value }">
</custom-input>
因此,对于一个带有 v-model 的组件(核心用法),它应该如下:
带有v-model的父组件通过绑定的value值(即v-model的绑定值)传给子组件,子组件通过 prop接收一个 value;
子组件利用 $emit 触发 input 事件,并传入新值value给父组件;
this.$emit('input', value);
废话不多说了,直接上栗子;
<div id="app">
<my-component v-model="msg"></my-component>
msg: {{msg}}
<my-counter v-model="num"></my-counter>
num: {{num}}
</div>
对应的JS
Vue.component('my-component', {
template: `<div>
<input type="text" :value="currentValue" @input="handleInput"/>
</div>`,
data: function () {
return {
currentValue: this.value //将prop属性绑定到data属性上,以便修改prop属性(Vue不允许直接修改prop属性的值)
}
},
props: ['value'], //接收一个 value prop
methods: {
handleInput(event) {
var value = event.target.value;
this.$emit('input', value); //触发 input 事件,并传入新值
}
}
});
Vue.component("my-counter", {
template: `<div>
<h1>{{value}}</h1>
<button @click="plus">+</button>
<button @click="minu">-</button>
</div>`,
props: {
value: Number //接收一个 value prop
},
data: function() {
return {
val: this.value
}
},
methods: {
plus() {
this.val = this.val + 1
this.$emit('input', this.val) //触发 input 事件,并传入新值
},
minu() {
if(this.val>0){
this.val = this.val-1
this.$emit('input', this.val) //触发 input 事件,并传入新值
}
}
}
});
new Vue({
el: '#app',
data: {
msg: 'hello world',
num: 0
}
})
slot 作用域插槽
也就是父组件中用子组件中slot的参数值
1.父组件:
<template>
<div class="wrapper">
<Box>
<template v-slot="slotProps">{{slotProps.slotData.name}}</template>
</Box>
</div>
</template>
<script>
import Box from './box.vue'
export default {
data(){
return {
name:'xiao'
}
},
components:{
Box
},
}
</script>
2.子组件
<template>
<div>
<slot :slotData="info">{{info.age}}</slot>
</div>
</template>
<script>
export default {
data() {
return {
info:{
name:'xiaohua',
age:21
}
}
}
}
</script>
动态组件
比如渲染多个动态楼层:
<template>
<div>
<div v-for="value in myComponent" :key="value.id">
<component :is="value.id"></component>
</div>
</div>
</template>
<script>
import Mytext from './mytext.vue'
import Box from './box.vue'
export default {
data(){
return{
myComponent:[
{
id:'Mytext'
},
{
id:'Box'
}
]
}
},
components: {
Mytext,
Box
}
}
</script>
异步组件
<template>
<div>
<button @click="show = true">Load Tooltip</button>
<div v-if="show">
<Tooltip />
</div>
</div>
</template>
<script>
export default {
data: () => ({
show: false
}),
components: {
Tooltip: () => import('./components/Tooltip')
}
}
</script>
keep-live
例如tab组件,如果不加上 keep-alive 包含的组件就会每次重新挂载渲染【优化性能】
<template>
<div>
<button @click="clickMe('A')">A</button>
<button @click="clickMe('B')">B</button>
<keep-alive>
<Mytext v-if="status === 'A'"/>
<Box v-if="status === 'B'"/>
</keep-alive>
</div>
</template>
<script>
import Mytext from './mytext.vue'
import Box from './box.vue'
export default {
data(){
return{
status:'A'
}
},
components: {
Mytext,
Box
},
methods:{
clickMe(parmas){
this.status = parmas;
}
}
}
</script>
mixin
多个组件有相同的逻辑,抽离出来
<template>
<div>
<button @click="showCity">显示</button>
我的城市是{{city}}
</div>
</template>
<script>
import myMixin from './myMixin.js'
export default {
mixins:[myMixin],
data() {
return {
name:'xiaohua'
}
}
}
</script>
对应的mixin.js文件
export default{
data(){
return {
city:'beijing'
}
},
methods:{
showCity(){
console.log(this.name);
}
}
}
mixin的缺点
1.变量来源不明确,不利于阅读;
2.多个mixin可能会造成命名冲突
3.mixin和组件可能出现多对多的关系,复杂度高
VueX
1 vuex的基本概念:state、getters、action、mutation
2 用于Vue组件API: dispation、commit、mapState、mapGetters、mapActions、mapMutations
Vue-router 路由模式
- hash 模式:如 http://aaa.com/#/user/10
- H5的history模式:如 http://aaa.com/user/20;[该方式需要server支持,因此无特殊要求可以选择前者]
server要配置所有访问页面返回 index.html 主页面;
但是这样的话服务器将不再返回404页面,所以前端要设置好:
const router = new VueRouter({
mode:'history',
routes:[
{path:'*',component:NotFoundComponent}
]
})
3、动态路由
const User = {
//获取参数如 10 20
template:'<div>user is {{$route.parmas.id}}</div>'
}
const router = new VueRouter({
routes:[
//动态路径参数 以冒号开头 能命中 ‘/user/10’ '/user/20' 等格式的路由
{path:'/user/:id',component:User}
]
})
4、路由配置懒加载
export default new VueRouter({
routes:[
{
path:'/',
component:()=>import(/*webpackChunkName:"navigator"*/'./../components/Navgator')
}
]
})
===
Vue MVVM
View--ViewModel--Model
DOM-----Vue------JS Object
V-------VM--------M
例如,
V---表示template中的html
M---表示data中的数据
VM--表示template中用到的 方法、以及js中定义的方法,是一个连接层
<template>
<p @click="changeName">{{name}}</p>
</template>
export default{
data(){
return {
name:'vue'
}
},
methods:{
changeName(){
this.name = 'react'
}
}
}
Vue的响应式原理
核心API:Object.defineProperty
1.用defineProperty如何监听复杂数据
function updateView(){
console.log('更新视图')
}
function defineReactive(target,key,value){
/*
监听复杂数据
例如data中的数据:
data(){
return {
info:{address:'北京'}
}
}
*/
observer(value);//深度监听数据变化
Object.defineProperty(target,key,{
get(){
return value
},
set(newValue){
observer(newValue);//深度监听数据变化
/*
比如设置这样的数据
data.age = {num:21}
然后改变数据
data.age.num = 22;
*/
if(newValue !== value){
//注意,value一直在闭包中,此处设置完之后,再get时也是
value = newValue;
updateView();
}
}
})
}
function observer(target){
if(typeof target !== 'object' || targe === null){
//不是数组或者对象
return target
}
//重新遍历定义各个属性
for(let key in target){
defineReactive(target,key,target[key])
}
}
const data = {
name:'zhangsan',
info:{
address:'bejing'
},
nums:[10,20,30]
}
//监听数据
observer(data)
data.name = 'lisa'
data.x = '100' //新增属性,监听不到--所以有 Vue.set
delete dta.name //删除属性,监听不到--所以有 Vue.delete
data.nums.push(40);//defineProperty无法监听到数据变化
Object.defineProperty的一些缺点(Vue3.0 启动 Proxy)
1.由上面的例子可以看出:需要深度监听,需要递归到底,一次性递归计算量大
2.无法监听到新增属性/删除属性(Vue.set;Vue.delete)
3.无法监听到数组发生变化,需要特殊处理【其实defineProperty本身可以通过数组索引值,监听到数组发生变化,但是考虑到性能问题,vue没有做这个处理】
Vue 将被侦听的数组的变异方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
pushpopshiftunshift等等,原理如下:
先看一段代码示例:
const oldArrayProperty = Array.prototype;
//创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型,这样避免污染全局的原型
const arrProto = Object.create(oldArrayProperty)
arrProto.push = function(){ //相当于Object.create(Array.prototype).push = function(){}
console.log(100);
}
arrProto.push()
let aa = [];
aa.push(10);
console.log(aa);
const arrProto = Array.prototype;
arrProto.push = function(){//相当于Array.protptype.push = function(){}
console.log(100);
}
arrProto.push()
let aa = [];
aa.push(10);
console.log(aa);//直接挂载到prototype,影响了array方法
所以使用类似的方法,重新定义push等方法:
//重新定义数组原型
const oldArrayProperty = Array.prototype;
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshifr','splice'].forEach((methodName)=>{
arrProto[methodName] = function(){
updateView();//触发更新视图
oldArrayProperty[methodName].call(this,...arguments)//相当于call了数组Array原型链上的方法
}
})
//修改observer方法:
function observer(target){
if(typeof target !== 'object' || targe === null){
//不是数组或者对象
return target
}
if(Array.isArray(target)){
target.__proto__ = arrProto;//将data中的数组的隐式原型赋值给新改造的 arrProto
}
//重新遍历定义各个属性
for(let key in target){
defineReactive(target,key,target[key])
}
}
由于 JavaScript 的限制,Vue 不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
,
当你修改数组的长度时,例如:vm.items.length = newLength
以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将在响应式系统内触发状态更新:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
Proxy 有兼容性问题
Proxy兼容性不好,且无法polyfill
VDom
因为直接操作 DOM 元素非常耗费性能,所以使用js来模拟dom,待diff后,只改变变化的元素。所以是用js来模拟的虚拟dom。
原html:
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size:12px">
<li>a</li>
</ul>
</div>
转成js的DOM
{
tag:'div',
props:{
className:'container',
id:'div1'
},
children:[
{
tag:'p',
children:'vdom'
},
{
tag:'ul',
props:{style:'font-size:12px'},
children:{
tag:'li',
chidren:'a'
}
}
]
}
diff 算法
如果按照两个tree比较,时间复杂度是 O(n^3)
优化时间复杂度到O(n):
1.只比较同一层级,不做跨级比较;
2.tag不相同,则直接删掉重建,不再深度比较;
3.tag和key,两者都相同,则认为是相同节点,不再深度比较;
编译模板
- 模板不是html,有指令、插值、JS表达式,能实现判断、循环
- html是标签语言,只有JS才能实现判断、循环等逻辑
- 因此,模板一定是转成某种JS代码,即编译模版
const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return createELement('p',[createTextVNode(toString(message))])}
// createELement返回的是vnode
const template2 = `<p>{{flag?message:'no message found'}}</p>`;
// 所以转成了js代码
// with(this){return _c('p',[_v(_s(flag?message:'no message found'))])}
上面的转换,在使用 webpack vue-loader,会在开发环境下编译模板;如果是引用的 vue.js 的cdn方式;则是在浏览器内部编译的(所以cdn方式引入的vue.js 不支持html中 驼峰式命名)
Vue-router 的原理
1.hash变化会触发网页跳转,即浏览器的前进后退;
2.hash变化不会刷新页面,SPA必须的特点【SPA单页面应用】
3.hash永远不会提交到server端
hash 方式
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
history 方式
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
总结
hash---window.onhashchange函数
H5 history---history.pushState 和 window.onpopstate
Vue 面试真题演练
1. 为何在 v-for 中用 key
-
- 必须用 key,且不能是 index 和 random
-
- diff算法中通过 tag 和 key 来判断,是否是 sameNode;
-
- 减少渲染次数,提升渲染性能
2.为何组件的data必须是一个函数
最后生成的代码,vue是一个类,实例化一个vue的类,如果是函数,则实例化的时候就会针对该对象返回一个函数,也就是闭包,这样对应的data在每个对象中都不同。
3. Vuex中action和mutation有何区别
-
- action 中处理异步,mutation不可以
-
- mutaion 做原子操作,也就是最小最简单的操作;
-
- action 可以整合多个 mutation
4. Vue常见性能优化方式
-
- 合理使用 v-show,v-if
-
- 合理使用 computed 【可以缓存数据,data不变,对应的计算属性不变】
-
- v-for 时加 key,以避免和 v-if 同时使用;
-
- 自定义事件、DOM事件及时销毁,导致内存泄漏,页面会越来约卡【vue定义的事件不用管,因为vue可以自动销毁】
-
- 合理使用异步组件
-
- 合理使用 keep-alive
-
- data层级不要太深,避免响应式监听数据时,递归太深。
Vue3--Proxy实现响应式
-
- Proxy和Reflect是相对应的,Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法;
const proxyData = new Proxy(data,{
get(targe,key,receiver){//receiver 是 proxyData
const result = Reflect.get(targe,key,receiver)
console.log('get',key);
return result;//返回结果
},
set(target,key,val,receiver){
const result = Reflect.set(target,key,val,receiver)
return result;//返回是否设置成功
},
deleteProperty(target,key){
const result = Refleect.deleteProperty(target,key);
return result;//是否删除成功
}
})
-
- 规范化、标准化、函数式;
例如,判断对象中是否有某个key
- 规范化、标准化、函数式;
const obj = {a:100,b:200};
'a' in obj;//true
Reflect.has(obj,'a');//true
//再如删除某个元素
delete obj.a;
Reflect.deleteProperty(obj,'b')
const data = {
name:'zhangsan',
age:20
}
-
- 替换掉 Object 上的工具函数;
//获取key值
const obj = {a:10,b:20};
//原来的方法。因为Object是个对象,不应该集成很多方法;
Object.getOwnPropertyNames(obj);//['a','b']
//现在改到 Reflect,逐渐替换Object上的方法
Reflect.ownKeys(obj);//['a','b']
proxy响应式:
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听,get到那一层级,才reactive到那一层级,不像defineReactive,在函数顶部递归循环
// 性能如何提升的?
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
- Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
- Object.defineProperty对新增属性需要手动进行Observe。
由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
如果采用 proxy 实现, Proxy 通过 set(target, propKey, value, receiver) 拦截对象属性的设置,是可以拦截到对象的新增属性的。
不止如此, Proxy 对数组的方法也可以监测到,不需要像上面vue2.x源码中那样进行 hack
总结:
Object.defineProperty 对数组和对象的表现一直,并非不能监控数组下标的变化,vue2.x中无法通过数组索引来实现响应式数据的自动更新是vue本身的设计导致的,不是 defineProperty 的锅。
Object.defineProperty 和 Proxy 本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动 Observe 的问题。
Proxy 作为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,并且目前还没有完整的polifill方案。