一、前言
- 1.重点学习和我们使用vue相关联的原理。比如vdom、模板渲染等。
二、Vue基本使用
1.指令、插槽
考察方向
- 1.插值、表达式
- 2.指令、动态属性
- 3.v-hmlt 指令:存在xss风险,会覆盖子组件
xss攻击 :> Cross-Site Scripting(跨站脚本攻击)简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
vue 使用的是双花括号的插值表达式
// 插值表达式
<div>{{name}}</div>
指令
常用的指令有 v-bind v-on 和 v-html等
条件语句
在 v-show v-if指令和表达式中,延伸出下面两个比较常见的面试问题
1. v-show 和 v-if的区别?
v-show 使用的是css中的display来控制显示隐藏
v-if是通过条件判断是否删除或者渲染对应的模块,这里会有dom的操作,相对消耗性能。
2. v-if 和v-show的使用场景?
当需要控制的模块需要经常切换显示隐藏,就使用v-show或者keepalive组件
当不那么频繁使用,或者需要用到组件的生命周期的时候,就使要用v-if
v-for 循环列表渲染
- 如何遍历对象?也可以使用v-for
- 在遍历的时候,key是diff算法提高效率的方法。不能随便给,比如给index,或者随机数。
- v-if和v-for不能一起使用,影响性能。
v-for 的运算层级比v=if高; v-for比v-if优先级高,所以使用的话,每次v-for都会执行v-if,造成不必要的计算,影响性能,尤其是当之需要渲染很小一部分的时候。
总结:v-if比v-for优先级高,一起使用在性能上会造成极大的浪费,并且官网也并不推荐我们这样做,所以我们可以选择使用computed过滤掉列表中不需要显示的项目。
computed 和 watch
- 使用了computed 计算的值,是有缓存的,也就是data中的数据不变,就不会重新计算。
- watch 如何进行深度监听
watch 有个特点,组件一开始挂载是不会执行。而且不会对对象的时候,不会进行深度监听。 这两个问题,vue都提供了解决方式如下:
watch: {
obj: {
handler(newVal, oldVal) {
console.log('obj.a changed');
},
immediate: true, // 为true时,挂载就会执行
deep: true // 开启深度监听,监听对象的时候,属性变化也会被监听到
}
}
注意:vue监听对象的时候,是拿不到oldvalue的
事件
事件注意的几点
- vue中的event参数,是原生的参数
- vue中的事件是绑定到当前元素的
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
},
methods: {
increment1(event) {
// eslint-disable-next-line
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
// eslint-disable-next-line
console.log(event.target)
// eslint-disable-next-line
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
this.num++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val, event) {
// eslint-disable-next-line
console.log(event.target)
this.num = this.num + val
},
loadHandler() {
// do some thing
}
},
mounted() {
window.addEventListener('load', this.loadHandler)
},
beforeDestroy() {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', this.loadHandler)
}
}
</script>
事件修饰符及按键修饰符
<!--阻止单击事件继续传播-->
<a @click.stop="doThis"></a>
<!--阻止默认行为,提交事件不重载页面-->
<form @click.prevent="doThis"></from>
<!--串联修饰符-->
<a @click.prevent.stop="doThis"></a>
<!--也可以只有修饰符-->
<a @click.prevent.stop></a>
<!--添加事件监听时,使用事件捕获模式-->
<a @click.capture="doThis"></a>
<!--只有当event.target是当前元素时,才触发处理函数-->
<a @click.self="doThis"></a>
按键修饰符
<!--即使alt和ctrl同时按下也会除服-->
<button @click.ctrl="doThis"></button>
<!--只有ctrl按下才触发-->
<button @click.ctrl.exact="doThis"></button>
<!--没有任何系统修饰符被按下的时候触发-->
<button @click.ctrl.exact="doThis"></button>
表单
v-model
常见的表单项:textarea,checkbox,radio,select等
修饰符:lazy,number,trim
组件的使用
组件通讯
1. 父子组件通讯
props父组件向子组件 no)
2. 组建通信,自定义事件
1.vue本身就实现自定义事件系统,有emit,及$off方法。所以如果不使用Vuex,可以使用另一个vue实例作为自定义事件,用于兄弟组件之间的传值。也就是平时所说的eventbus。 vue的observe函数也能实现自定义事件。
3.组件生命周期
生命周期分为三个阶段
- 挂载阶段
graph TD
实例化(new Vue)-->step1(初始化事件和生命周期及渲染)
step1--触发beforeCreate函数-->step2(初始化inject provide state data methods等属性)
step2--触发created函数 '此时data和methods等属性和方法初始化完成 可以访问调用' -->step3(是否有el对象)
step3--没有-->step3-1(就需要调用vm.$mount函数进行挂载)
step3--有--> step4(判断是否有template选项)
step3-1-->step4
step4--有-->step5-1(有就将模板转化为render函数)
step4--没有-->step5-2(则将el父级作为模板)
step5-1-->step6(在挂载前 首次调用render函数 生成虚拟dom)
step5-2-->step6
step6--触发beforeMount函数--> step7(创建vue实例下的虚拟$el 并替换成真正的dom)
step7--触发mounted函数--> step8(挂载完成 dom已渲染到页面 可以进行dom操作)
- 更新阶段
graph TD
setp1(挂载)--监听数据变化-->step2(触发beforeUpdate方法)
step2-->step3(虚拟dom调用patch方法 即用diff算法进行比对 得到最新的dom tree 然后重新渲染)
step3--触发update方法-->setp1
- 销毁阶段
graph TD
step1(挂载完成)--触发vm.$beforeDestroy-->step2(开始销毁前 触发vm.$destroy 此时还能访问data和methods等)
step2--> step3(清除子组件 - watcher - 事件监听器等)
step3-->step4(销毁组件 此时无法访问data和methods等)
step4-->step5(触发destroy钩子函数)
4. 父子组件生命周期调用顺序
vue组件是从外到内实例化的,但渲染是从内到外的 假设有index和list两个组件,index组件作为父组件,list作为子组件,生命周期调用顺序如下:
graph TD
step1(index created)-->step2(list组件created)
step2--> step3(list组件mounted)
step3-->step4(index组件mounted)
父子组件更新过程
graph TD
step1(index beforeUpdate)-->step2(list组件 beforeUpdate)
step2--> step3(list组件updated)
step3-->step4(index组件updated)
Vue高级特性
- 自定义v-model
- 动态、异步组件 动态组件
动态组件使用的是component标签,需要使用:is="component-name"的写法
使用场景 需要根据数据,动态渲染的场景。即组件类型不定。
比如新闻详情页,在渲染内容过程中,有text组件、image组件、video组件、等等。这时候就可以用到动态组件。
异步组件
异步组件 即使用import()函数引入的组件
当按需加载,或异步加载大组件使用
- $nextTick
vue 是异步渲染的
data改变之后,dom不会立刻渲染
nextTick的回调中获取新的DOM节点。
- keep-alive
缓存组件,当组件需要频繁切换,但不需要重复渲染的时候使用
它是Vue常见的性能优化
- slot
默认插槽 具名插槽 作用域插槽
- mixin
mixin可以将多个组件公用的方法和逻辑抽离出来。
但mixin并不是完美的解决方案,存在以下问题:
1. 变量来源不明确,不利于阅读
2. 多mixin会造成命名冲突
3. mixin和组件可能出现多对多的关系,复杂度较高
5.vuex
vuex的api
- dispatch, 派发action
- commit 提交一个mutation
- 辅助函数,mapState、mapGetters、mapActions、mapMutations
6.路由(vue-router)
vue-router有两种模式,hash模式和H5 history模式
hash模式
hash —— 即地址栏 URL 中的 # 符号。 比如这个 URL:www.abc.com/#/hello,has… 的值为 #/hello。它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
hash模式是通过hashchange改变事件来实现路由页面的切换
history模式
hash模式带#号,history模式的url更好看,相对安全一些。需要后端配合。 HTML5引入了history.pushState()和history.replaceState()方法,他们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate配合使用。
<!--
stateObject:当浏览器跳转到新的状态时,将触发popState事件,该事件将携带这个stateObject参数的副本
title:所添加记录的标题
url:所添加记录的url(可选的)
-->
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
<!--1. 路由配置:动态路由-->
const router = new VueRouter({
routes: [
{
// 动态路径参数 以冒号开头。对应user/10的路由格式
path: user/:id,
component: User
}
]
})
<!--路由懒加载,即:用import函数引入的组件-->
三)vue原理
一)Vue原理考察的范围
原理主要考察以下六个方面:
- 1.组件化
- 2.响应式原理
- 3.Vdom 和diff算法
- 4.模板编译
- 5.渲染过程
- 6.前端路由
二)组件化
1. 组件化基础
A、很很久以前的组件化用的是后端渲染,当代的组件化用的是数据驱动视图。
2. 数据驱动视图
-
传统的组件,只是渲染,更新还依赖于操作DOM
-
现代的组件,使用的是数据驱动视图。例如,vue使用的是MVVM (model,view,view model)
-
React的数据驱动视图---React setState
3. vue的mvvm模型
M:model 数据模块层 提供数据
V:view 视图层 渲染数据
VM: ViewModel 视图模型层 调用数据,渲染视图
由数据来驱动视图(不需要多考虑DOM操作,把重心放在VM上)
mvvm分为三块,分别是model(数据模型),view(视图)和viewModel(视图模型)
数据模型负责数据,视图模型负责渲染展示界面,viewmodel负责连接数据模型和视图。
mvvm优点
- 低耦合。视图(View)可以独立于Model变化和修改,
一个ViewModel可以绑定到不同的"View"上,
当View变化的时候Model可以不变,
当Model变化的时候View也可以不变。
- 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel)
- 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。
sequenceDiagram
participant view
participant viewModel
participant model
view->>model: dom listener(视图通过事件监听修改model的数据)
model->>view: directive(object对象通过指令,修改view的视图展示)
4. MVC
MVC是一种架构模式,它将应用抽象为3个部分:模型(数据)、视图、控制器(分发器)。
-
M: model 数据模型层 提供数据
-
V: view 视图层 显示页面
-
C: controller 控制层 调用数据渲染视图
即模型-视图-控制器。M和V指的意思和MVVM中的M和V意思⼀样。C即Controller指的是页⾯业务逻辑。使⽤用MVC的目的就是将M和V的代码分离。‘MVC是单向通信。也就是View跟Model,必须通过Controller来承上启下。 controller,让它来定义用户界面对用户输入的响应方式,它连接模型和视图,用于控制应用程序的流程,处理用户的行为和数据上的改变。
一个事件发生的过程(通信单向流动)
-
1、用户在视图 V 上与应用程序交互
-
2、控制器 C 触发相应的事件,要求模型 M 改变状态(读写数据)
-
3、模型 M 将数据发送到视图 V ,更新数据,展现给用户
MVC模式的业务逻辑主要集中在Controller,而前端的View其实已经具备了独立处理用户事件的能力,当每个事件都流经Controller时,这层会变得十分臃肿。而且MVC中View和Controller一般是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的连接让Controller的复用性成了问题,如果想多个View共用一个Controller该怎么办呢?这里有一个解决方案MVP
5. MVP
MVP(Model-View-Presenter)是MVC模式的改良,由IBM的子公司Taligent提出。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。
虽然在MVC里,View是可以直接访问Model的,但MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。 与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。
Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。而且由于没有数据绑定,如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。
三、vue响应式
1. 响应式概述
- vue响应式的实现
核心API: Object.defineProperty
如何实现,如下代码所示。
object.defineProperty有一些缺点: 1.监听劫持数据,需要不断的遍历,如果对象层级较深,需耗费较多的资源和性能。
2.不能监听数组,需要特殊处理。
3.无法监听新增的属性和删除的属性,需要使用vue.set和vue,delete
4.Vue本身无法监听数组的变化,用的是重写数组方法来做到监听变化
vue3.0中,使用了proxy代替defineProperty来进行数据拦截,不需要遍历即可对对象的属性进行监听,并且可以监听数据的变化。但是有兼容性问题,不能使用polyfill。
/* 重写数组方法 */
// 复制原型
const arrayOldProperty = Array.prototype
// 根据复制的原型新建一个对象
const arrProto = Object.create(arrayOldProperty)
// 重写这个新对象的数组方法
const methodNameList = ['push','slice','splice','shift','unshift', 'pop']
console.log('methodNameList', methodNameList.forEach)
methodNameList.forEach(methodName =>{
// 重写方法,不能给原型,否则会污染原生对象
arrProto[methodName] = function () {
// 调用原生的方法
arrayOldProperty[methodName].call(this,...arguments)
// 然后调用更新视图的方法
updateView(...arguments)
}
});
// 更新视图
function updateView(value) {
console.log('update view', value)
}
/* 监听数据 */
function observe(obj) {
// 不是对象,不作监听
if(typeof obj !== 'object') {
return obj
}
// 如果是数组,则修改一下它指向的原型
if(Array.isArray(obj)) {
obj.__proto__ = arrProto
}
// 如果是对象则进行监听, 遍历
Object.keys(obj).forEach(key =>{
defineReactive(obj, key, obj[key])
})
}
/* 拦截数据 */
function defineReactive(target,key, value) {
// 如果value是对象的话,需要对象要做深度监听
observe(value)
// 数据拦截监听
Object.defineProperty(target, key, {
get() {
// 获取直接返回
return value
},
set(newValue) {
// 设置新值之后,更新视图
observe(newValue)
value = newValue
// 设置的新值有可能是对象,所以这里也要监听
// 更新视图,这就是所谓的数据拦截操作
updateView(value)
}
})
}
const data = {
arr: [1],
a: 123,
b: 456,
c: {
d: 852
}
}
observe(data)
data.a = 789
data.c.d = 258
data.arr.push(88)
四、虚拟dom
一)为什么使用虚拟dom
以往的dom操作中
- dom的操作非常耗费性能
- 以前jquery可以自行控制DOM的操作实际,但非常繁琐
- Vue和React是数据驱动视图,简化了DOM操作,那么是如何有效控制dom的?
- vdom用js模拟DOM结构,利用js执行速度比操作dom更快的特性,从而达到性能优化的目的。
五、diff算法
(一)、diff算法概述
- diff即对比,是一个广泛的概念,如linux的diff命令,git的diff等等
- 两个js对象也可以做diff,如github.com/cujojs/jiff
- 两棵树做diff,如这里vdom diff
graph TD
step1(A)-->step2(B)
step1-->step3(C)
step2-->step4(D)
step2-->step5(E)
step3-->step6(F)
step3-->step7(G)
S1(A)-->S2(B)
S1-->S3(C)
S2-->S4(D)
S2-->S5(H)
S3-->S6(F)
S3-->S7(I)
假如对上面的这两棵树今夕diff,算法复杂度为O(n^3)
第一:遍历tree1,第二遍历tree2
第三:排序
1000个节点,要计算一亿次,算法不可用
因此需要将算法优化到o(n)
将diff算法优化到o(n)
- 只比较同一层级,不跨级比较
- tag不相同,则直接删掉重建,不再深度比较
- tag和key相同,则认为是相同节点,不再深度比较。
使用key和不使用key
在新节点和老节点进行对比时,有key的话不用一个个对比;没有key的话,需要遍历进行对比。
diff算法总结
- patchVnode方法对节点进行对比
- addVnodes添加虚拟dom,removeVnodes,删除虚拟dom
- updateChildren,更新字节,这里能体现key的重要性。
vdom 和 diff算法总结
- vue源码中的细节过程不重要,比如updateChildren的过程也不重要,不需要深究。
- vdom的核心是h(渲染函数),vnode、patch、diff、key等
- vdom存在价值更重要:数据驱动视图、控制DOM操作
六、模板编译
1. 前置知识:js的with语法
const obj = {a:1,b:2}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined
// 使用with语法,能改变{}内部变量的查找方式,
// 会将{}内的自由变量,当做 obj 的属性来查找
with (obj) {
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // 会报错
}
总结:
- 能改变{}内部自由变量的查找规则,当做obj的属性来查找
- 如果找到obj的属性会报错
- with要慎用,因为它打破了作用域的规则,易读性变差。
2. 模板编译
- 模板编译不是html代码,它还居右插值、指令、js表达式,能实现判断、循环。
- html是标签语言,只有js才能实现判断、循环(图灵完备的语言)
- 因此,模板一定是转换为某种js代码,及模板编译。
// 使用vue-template-compiler编译后会获得如下代码
const template = <div>{{message}}<div>
with(this){
return _c('div',[
_v((_s(message)))
])
}
模板编译流程
- 模板编译为render函数,执行render函数,返回vnode
- 基于vnode在执行patch和diff 进行节点的比对。最后渲染成真实dom
- 使用webpack vue-loader,会在开发环境下编译模板。