一、vue-router的实现原理
路由的概念来源于服务端,服务端中的路由描述的是URL和处理函数之间的映射关系;web前端单页应用SPA(single page application)中,路由描述的是URL和UI之前的映射关系,这种映射是单向的,即URL的变化引起UI更新;
如何实现前端路由?
解决两个核心问题:
- 如何监测URL发生变化?
- 如何改变URL却不引起页面刷新?
使用hash和history两种实现方法可以解决上面两个问题;
- hash实现
- hash是URL中hash(#)后面的内容,改变URL的hash部分不引起页面刷新;
- hash通过hashchange事件监听URL的变化;改变URL的方式有:window.location、a标签、浏览器的前进后退 ;这几种方式都可以被hashchange监听到;
- history实现
- history提供了pushState和replaceState两种方法,这两种方法改变URL的pash部分不会引起页面的刷新;
- history提供类似hashchange事件的popstate事件,用来监听URL的变化,但是只能监听到浏览器前进后退改变的URL,不能监听a标签、以及pushState和replaceState改变的URL;
- 解决办法:拦截pushState和replaceState的调用以及a标签的点击事件来检测URL的变化;
<a href="https://jingyan.baidu.com/">百度经验</a>
// 默认在当前页打开链接网页
<a href="https://jingyan.baidu.com/" target="_blank">百度经验</a>
// 加上target,在新的标签页打开链接网页
二、Vue的双向绑定原理和生命周期函数
Vue的双向绑定首先就是MVVM(model-view-viewmodel)模式;当视图发生改变的时候传递给VM,再让数据得到更新,当数据发生改变时传给VM,使得视图发生改变。
MVVM模式通过以下三个核心组件组成,每个都有它独特的角色:
- Model:包含了业务和验证逻辑的数据模型;
- view:定义了屏幕中view的结构,布局和外观;
- viewmodel:扮演model和view之间的使者,帮忙处理view的全部业务逻辑;
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210505182436753.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY3ODQwMg==,size_16,color_FFFFFF,t_70)
vue的数据双向绑定主要通过Object.defineProperty(obj对象,prop属性,descriptor具体的改变方法)方法,进行数据劫持以及发布者——订阅者模式;
任务拆分:
1)组件初始化时,利用Object.defineProperty(在一个对象上定义一个新的属性)方法,给每一个data属性注册getter和setter方法,也就是reaction化(数据劫持);
2)在mount阶段会创建watcher类(每个组件对应一个watcher),然后再new一个watcher对象,watcher对象会立即调用组件的render函数生成虚拟DOM,在调用render函数时会访问data的属性值,此时会触发属性的getter函数,getter函数将当前的watcher注册到属性的dep中;watcher在组件渲染的过程中把“接触”过的data属性记录为依赖;
3)之后当data属性发生变化时,触发setter函数,之后调用dep的notify函数,通知watcher调用updata函数进行更新,从而使它关联的组件重新渲染。
- 实现一个解析器Compile,可以扫描和解析每个节点的相关指令,把它们初始化成一个订阅者Watcher,并且绑定相应的函数。
- 实现一个监听器Observer,用来劫持并监听所有属性,如有变动,就通过订阅者管理器(Dep,每个data属性都有一个dep)来通知订阅者Watcher;(每一个data的属性都会有一个dep对象)
- 实现一个订阅者Watcher,(一个组件对应一个watcher)可以收到属性的变化通知,并执行相应的patch函数,从而更新视图;(watcher在mounted阶段生成)
三、Vue的生命周期
创建阶段:
- beforeCreate:data和methods中的数据还没有被初始化;
- created:data和methods已经被初始化好;调用methods中的方法,最早在created中操作;
- beforeMount:模板在内存中编译完成,但尚未把模板渲染到页面中,此时页面还是旧的;
beforeMount() {
console.log(document.getElementById('h3').innerText)
//{{msg}}
},
- mounted:内存中编译好的模板替换到浏览器的页面中;(如果想要通过某些插件操作页面上的DOM节点,最早要在mounted中进行);执行完mounted,就表示整个Vue实例已经初始化完了;此时组件脱离了创建阶段,进入到了运行阶段。
mounted() {
console.log(document.getElementById('h3').innerText)
//ok
},
运行阶段:
- beforeUpdate:页面中的显示的数据,还是旧的;此时data数据是最新的;页面尚未和最新的数据保持同步。
- updated:页面和data数据已经保持同步了,都是最新的。
销毁阶段:
- beforeDestroy:实例身上所有的data和所有的methods,以及过滤器,指令都处于可用阶段,此时,还没有真正执行销毁的过程。
- destroyed:组件完全被销毁。
四、页面之间的通信
子组件Scroll.vue
<template>
<div class="wrapper" ref='wrapper'>
<div class='content'>
<slot></slot>
</div>
</div>
</template>
<script>
import BScroll from 'better-scroll'
export default {
name: 'Scroll',
props: {
probeType: {
type: Number,
default: 0
},
},
data() {
return {
scroll: null,
}
}
</script>
父组件:Detail.vue
<template>
<div id="detail">
<scroll class="scroll-content" ref="scroll"
:probe-type="2" @scroll="contentScroll" >
<detail-swiper :top-images="topImages"/>
<goods-list :goods="recommends" ref="recommend"/>
</scroll>
</div>
</template>
<script>
import Scroll from '@/components/common/scroll/Scroll'
export default {
name: "Detail",
components: {
Scroll
},
</script>
五、Vue2
data() {
return {xx: xx, yy:yy}
},
methods: {
login() { },
layout() { }
},
mounted() {
this.login()
},
computed:{
lowerCaseUsername () {
return this.username.toLowerCase()
}
}
六、Vue中的data为什么要使用函数?
七、动态绑定
<div v-bind:class="{ active: isActive }"></div>
八、项目的创建
1、创建项目时可以选择runtime-only和runtime-compiler
- runtime-only:代码中不可以任何的template
- runtime-compiler:代码中,可以有template,因为compiler可以来编译template
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210506182926829.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY3ODQwMg==,size_16,color_FFFFFF,t_70)
vue-template-compiler可以将template解析成ast,再将ast编译为render函数,render函数可以将template翻译成虚拟DOM树,之后将虚拟DOM树渲染成真实DOM树;
使用runtime-compiler:template => ast => render函数 => 虚拟DOM树 => 真实DOM树;
使用runtime-only:render函数 => 虚拟DOM树 => 真实DOM树;
九、vue中的keep-alive:
kee-alive 是 Vue 内置的一个抽象组件,它自身不会渲染一个DOM元素;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存;
使用keep-alive组件保留了组件状态且不会重新渲染,这样会导致created(){ } 钩子函数不会再次触发;
keep-alive不仅仅能保存页面 / 组件的状态这么简单,还可以避免组件反复创建和渲染,有效提升系统性能。
include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;
keep-alive实现原理 原文
export default {
name: 'keep-alive',
abstract: true,
// 判断当前组件虚拟dom是否渲染成真实dom的关键,true不渲染
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件
},
created() {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的键集合
},
destroyed() {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matched(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render() {
// 先省略...
}
}
keep-alive在它生命周期内定义了三个钩子函数:
- created:初始化两个对象cache和keys,分别缓存VNode(虚拟DOM)和Vnode对应的键集合
- destroyed:删除this.cache中缓存的VNode实例;这不是简单地将this.cache置为null,而是遍历调用pruneCacheEntry函数删除。删除缓存的VNode还要对应组件实例的destory钩子函数
- mounted:在mounted这个钩子中对include和exclude参数进行监听,然后实时地更新this.cahce对象数据;
- 取出keep-alive包裹着的子组件对象,按照黑白名单进行匹配,决定是否缓存;在缓存对象中查找是否缓存过该组件实例;最后讲该组件实例的keepAlive属性值设为true
vue渲染:render函数将组件对象转化为一个VNode实例,之后调用update函数把Vnode渲染成真实DOM;这个工程调用patch函数完成;
activated // 用于keep-alive激活
activated() {
//刚进入该组件时,执行
// console.log('组件刚进来')
this.$refs.scroll.scrollTo(0, this.saveY, 0)
this.$refs.scroll.refresh()
},
destroyed // 用于keep-alive停用
deactivated() {
//离开该组件时,执行
//1.保存Y值
this.saveY = -this.$refs.scroll.getScrollY
//2.取消全局事件的监听
this.$bus.$off('itemImageLoad', this.itemImgListener)
},
新的问题:keep-alive不会生成真正的DOM节点,因为Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件;keep-alive中设置了abstract:true,那么Vue就会跳过该组件实例。
被缓存的组件实例会为其设置keepAlive = true,而在初始化组件钩子函数中,不会再进入$mount过程,那么mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。
十、v-if和v-show的区别:
1、相同点:v-if和v-show都可以动态控制DOM元素的显示与隐藏
2、不同点:
-
v-if动态地向DOM树中添加或删除DOM元素节点;
-
v-show通过向DOM元素设置样式display属性值控制显示隐藏。
当两个demo用一个button按钮执行函数控制v-if和v-show的值,在控制台可以看出,两值都为false时,v-if控制的元素直接从DOM树销毁,而v-show控制的元素还是在DOM树中,只是以display:none的样式隐藏元素内容;
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210512164320626.png)
-
编译方面:v-if惰性,若最初指令值为false,不会编译;直至指令值为true时,才会编译存入缓存;
v-show不管最初指令值为真还是为假,都会马上编译存入缓存;
-
消耗方面:v-if切换性能消耗较大;v-show最初渲染消耗较大;
-
适用场景:v-if适用切换条件、项目需求稳定;v-show使用与频繁需要切换;
-
语法方面:v-if可以v-else、v-else-if配合使用,进行判断执行;但一定需要相邻,不可中断。
拓展
display:none; 不在文档流,无法触发绑定的事件,不会被子元素继承;但是父元素都不在了,子元素肯定也不在了~
visibilty:hidden; 会被子元素继承;在文档流中,会占据位置,但是不能触发绑定的事件;由于父元素隐藏,所以子元素也会隐藏,为了使子元素不被隐藏,可以使用visibility:visible;
opacity:0; 在文档流中,会占据位置;绑定的事件也可以触发。
十一、监听input中value属性值发生变化的事件
1、onchange事件:
此事件会在元素内容发生改变,且失去焦点的时候触发。
浏览器支持度较好。
2、onpropertychange事件:
此事件会在元素内容发生改变时立即触发,即便是通过js改变的内容也会触发此事件。
元素的任何属性改变都会触发该事件,不止是value。只有IE11以下浏览器支持此事件。
3、oninput事件:
此事件会在value属性值发生改变时触发,通过js改变value属性值不会触发此事件。只有IE8以上或者谷歌火狐等标准浏览器支持。
十二、keep-alive组件的使用
参考 https://www.cnblogs.com/answershuto/p/7825022.html
是Vue.js一个内置组件;它能够将不活动的组件实例保存在内存中,而不是直接将其销毁,它是一个抽象组件,不会被渲染到真实DOM中也不会出现在父组件链中;它提供了include
与exclude
两个属性,允许组件有条件地进行缓存;
<keep-alive>
<component></component>
</keep-alive>
这里面的component组件会被缓存起来。
举例说明:
<keep-alive>
<coma v-if="test"></coma>
<comb v-else="test"></comb>
</keep-alive>
<button @click="test=handleClick">请点击</button>
export default {
data () {
return {
test: true
}
},
methods: {
handleClick () {
this.test = !this.test;
}
}
}
在点击button时候,coma与comb两个组件会发生切换,但是这时候这两个组件的状态会被缓存起来,比如说coma与comb组件中都有一个input标签,那么input标签中的内容不会因为组件的切换而消失。
1、include与exclude属性
keep-alive组件提供了include与exclude两个属性来允许组件有条件地进行缓存,二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。
<keep-alive include="a">
<component></component>
</keep-alive>
将缓存name为a的组件
<keep-alive exclude="a">
<component></component>
</keep-alive>
name为a的组件将不会被缓存
2、keep-alive提供的生命钩子
activated与deactivated
keep-alive会将组件保存在内存中,并不会销毁以及重新创建,所以不会重新调用组件的created等方法;需要使用activated与deactivated两个生命钩子来得知当前组件是否处于活动状态;
3、深入keep-alive组件实现
created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点;
created () {
/* 缓存对象 */
this.cache = Object.create(null)
},
destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
/* destroyed钩子中销毁所有cache中的组件实例 */
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache[key])
}
},
4、render函数
首先通过getFirstComponentChild获取第一个子组件,获取该组件的name(存在组件名则直接使用组件名,否则会使用tag)。接下来会将这个name通过include与exclude属性进行匹配,匹配不成功(name不在inlcude中或者在exlude中,说明不需要进行缓存)则不进行任何操作直接返回vnode,vnode是一个VNode类型的对象,不了解VNode的同学可以参考笔者的另一篇文章《VNode节点》。
5、watch
用watch来监听pruneCache与pruneCache这两个属性的改变,在改变的时候修改cache缓存中的缓存数据。
6、总结
Vue.js内部将DOM节点抽象成了一个个的VNode节点,keep-alive组件的缓存也是基于VNode节点的而不是直接存储DOM结构。它将满足条件(pruneCache与pruneCache)的组件在cache对象中缓存起来,在需要重新渲染的时候再将vnode节点从cache对象中取出并渲染。
十三、element.js表单验证
在失去焦点时,进行规则的验证,并且发送ajax请求,在数据库中搜索,用户名有没有被注册;
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{ type: 'string', min: 6, message: '密码不允许小于6位', trigger: 'blur' }
],
十四、async / await
来源:https://www.jianshu.com/p/72e36168944f
async/await 执行原理详解
async函数就是generator函数的语法糖。
async函数,就是将generator函数的 *
换成async
,将yield
替换成await
。
1、async函数对generator的改进
2、async函数作用
异步编程的终极解决方案
3、通俗理解
async/await,就是异步编程回调函数写法的替代方法。(使代码以同步方式的写法完成异步操作)
4、执行顺序
函数执行时,一旦遇到await就会返回。等到触发的异步操作完成(并且调用栈清空),再接着执行函数体内后面的语句。
await语句后面的代码,相当于回调函数。(即:await的下一行开始,都视作回调函数的内容)
async showSetRightDialog(role) {
const { data: res } = await this.$http.get('rights/tree')
if (res.meta.status !== 200) {
return this.$message.error('获取权限列表失败')
}
this.rightsList = res.data
// console.log(this.rightsList)
this.getLeafKeys(role, this.defKeys)
this.dialogVisible = true
this.roleId = role.id
}
回调函数会被压入microtask队列
,当主线程调用栈被清空时,去microtask队列
里取出各个回调函数,逐个执行。
await只是让当前async函数内部的代码等待,并不是所有代码都卡在这里。遇到await就先返回,执行async函数之后的代码。
(2019.8.20测试发现:浏览器实际执行结果发生变化。await后面的函数,无论返回promise、还是非promise,执行结果都与曾经返回非promise相同)
主线程执行过程中,遇到await后面的函数调用,会直接进入函数,并执行。
(1)当这个函数返回非promise:
await后面的代码被压入microtask队列。当主线程执行完毕,取出这个回调,执行。
(2)当这个函数返回promise:—— 这种情况,看浏览器实际表现,已经不是这样处理了。
await后面的代码被压入microtask队列。当主线程执行完毕,取出这个回调,发现await语句等待的函数返回了promise,把后续代码赋给这个promise对象的then,并把这个promise的回调再压入microtask队列,重新排队。当它前面的回调函数都被取出执行后,再取出它,执行。
- 举例说明
【1】await返回非promise
async function func1(){
console.log('func1');
var a = await func2(); //当await返回非promise
console.log('func1 return');
}
function func2(){
console.log('func2');
}
func1();
new Promise(function(resolve){
console.log('promise1')
resolve('resolved');
}).then(function(data){
console.log(data);
})
// 结果:
// func1
// func2
// promise1
// func1 return
// resolved
【No2】await返回promise(来自头条笔试题)
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
//结果:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// settimeout
【注】
async1 end、promise2的console顺序,原来是反过来的。
十五、Vuex
来源
这里有一个非常重要的问题: 为什么要用Action管理异步操作?
action和mutation
乍一想,使用mutation也可以实现异步操作,为什么要多此一举,使用actions呢?
mutation必须同步执行这个限制,action不受约束;
可以在actions内部执行异步操作;
如果遇到异步操作涉及互相依赖的情况的时候,我们希望被依赖的操作执行完成之后,再执行依赖项,这样能保证程序执行得到正确的结果。但是异步操作,比如接口访问,往往不能确定执行完成的时间;
比如:在接口A中得到一个值,a;接口B需要使用这个值,结合B接口返回的值进行一些判断操作,if(a&&b){ … }; 这个时候如果B接口执行完毕了,A接口的值还没过来的话,就可能得到错误的结果。 a => undefined
addCountAction (context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('add') // 先提交
resolve()
}, 1000)
})
}
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021051416545614.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY3ODQwMg==,size_16,color_FFFFFF,t_70)
具体实例:
- 点击“加入购物车”,将商品添加到购物车
在mutations中写入方法:this.$store.commit("addCart", product)
首先在状态的carList列表中使用find函数去查找之前数组中是否有该商品,有的话,购物车数量 + 1;
没有的话,将该数据添加到carList中,将其count属性设为1;
- 购物车页面需要显示加入商品的数量,所以需要拿到state,可以使用getters;
- actions返回一个promise
addCart(context, payload) {
return new Promise((resolve, reject) => {
})
}
1、state状态的更改
当在其他地方更改了state状态时,其他组件应用到这个状态的时候,也会得到更新;
2、mutations:
- 更改Vuex的store中的状态的唯一方法是提交mutation;
- 通过this.$store.state来直接修改store中的状态也是有效的;但是这种方式的修改无法被Vue的调试工具记录(所以不推荐)
- 通常情况下,Vuex要求mutation中的方法必须是同步,这是由于mutation对异步方法的提交无法被Vue调试工具记录。
3、store的状态是响应式的
Vuex中的store的状态是响应式的,mutations作为更改store中状态的唯一方法,需要遵循Vue的响应规则;
提前在store中初始化好所需的属性;
当给state中的对象添加新属性时,使用以下方法;
方式一:使用Vue.set()方法
addProp(state, payload) {
// 该方法新增的属性不会添加至响应系统
// state.obj.id = payload.id
// Vue.set()添加的属性可以加入响应式系统
// 三个参数分别是改变的对象,新增的属性名,新增的属性值
Vue.set(state.obj, 'id', payload.id)
// 删除对象使用Vue.delete()方法
}
方式二:用新对象给旧对象重新赋值(新对象替换旧对象)
addPorp(state, payload) {
// (...) 扩展运算符(对象展开符)
state.obj = {...state.obj, 'id': payload.id}
}
4、为什么要加action功能?
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,无法知道什么时候回调和哪个先回调。