# Vue
# 响应数据原理
Vue底层响应数据的核心是Object.defineProperty。Vue通过数据劫持配合发布者-订阅者的设计模式,内部通过调用object.defineProperty()来劫持各个属性的getter和setter,在数据变化的时候通知订阅者,并触发相应的回调。
Vue主要通过4个步骤实现数据双向绑定:
- 实现一个监听器「Observer」。对数据对象进行遍历,利用Object.defineProperty()在属性上都加上getter和setter来给对象赋值触发setter,这样就能监听到数据变化。
- 实现一个解析器「Compile」。解析Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者。一旦数据变化,就调用更新函数进行数据更新。
- 实现一个订阅者「Watcher」。Watcher订阅者是Observer和Compile之间通信的桥梁,主要任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数。
- 实现一个订阅器「Dep」。订阅器采用发布-订阅设计模式,用来收集订阅者Watcher,对监听器Observer和订阅者Watcher进行统一管理。
# 如何监听对象/数组
Object.defineProerty不能监听数组变化,Vue通过重写push、pop等方法实现数组监听。
# 复杂对象深度监听
# 缺点
深度监听,需要递归到底,一次性计算量大
无法监听新增/删除的data属性(需用Vue.set/Vue.delete)
无法原生监听数组
# 虚拟DOM
虚拟DOM是一个存储有DOM信息的JS对象。当我们修改我们的数据,重新生成新的虚拟DOM,新老虚拟DOM进行DIFF操作之后,框架底层内部会对我们的真实DOM进行操作。
# 优缺点
优点:
- 保证性能下限。框架的虚拟Dom需要适配任何上层API可能产生的操作,它的一些Dom操作的实现必须是普遍适用的,所以它的性能并不是最优的,但比起粗暴的Dom操作要好很躲,因此保证了性能的下限。
- 跨平台。虚拟Dom本质上是JavaScript对象,而真实Dom与平台相关,相比下虚拟Dom可以更好地跨平台操作。
- 无需手动操作DOM。框架会根据虚拟Dom和数据的双向绑定,帮我们更新视图。
缺点:
无法做到极致的优化。虚拟DOM的使用可以保证性能的下限,但也正是因为如此,它也无法做到极致的优化。
# 实现原理
- 用JS对象模拟真实Dom树,对真实Dom进行抽象。
- 通过diff算法对比两棵虚拟DOM树。
- 通过patch算法将两个虚拟DOM对象的差异应用到真实的DOM树上。
# 借鉴的来源
- Vue2中虚拟DOM借鉴了snabbdom.js
- Vue3中借鉴inferno.js
# Diff算法
# 核心概念
h、vnode、patch、diff、key
# 模板编译
通过vue-template-compiler编译为render函数
执行render函数返回vnode
基于vnode再执行patch和diff
# 初次渲染过程
解析模板为render函数
触发响应式,监听data属性的getter和setter
执行render函数,生成vnode,patch
# 更新过程
data中的数据被修改后,触发setter
重新执行render函数,生成newVnode
patch(vnode,newVnode)
# nextTick
# 实现原理
EventLoop事件循环机制
# 作用
dom重新渲染挂载完毕执行的回调,当我们修改数据之后立即使用nextTick()来获取最新更新的dom。(页面渲染时会将data的修改整合,多次修改会合并一次渲染)
# Vue初始化干了什么
合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等
# 对MVVM的理解
# View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建 。
# Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
# ViewModel 层
ViewModel是指视图数据层,数据驱动视图。数据更新相应的视图也发生改变,无需手动操作DOM。
# Vue和React
# 相似之处
# 虚拟DOM
vue 和 react 中都使用了虚拟DOM(virtual DOM)技术。
# 组件化
Vue和React都提倡组件化。
# 不同之处
# 设计理念
- React更偏向于构建稳定大型的项目
- Vue偏向于小型灵活的项目
# 模板语法
**Vue:**接近HTML的语法。
React:JS语法拓展——JSX,它是JavaScript混合着XML语法。
# 组合功能方式
- React一开始使用mixin,后来转HoC(高阶组件)。高阶组件的本质是高阶函数,而React的组件是一个纯粹的函数。所以,React使用HoC很简单。
- Vue一直都是使用mixin。Vue中的组件是一个被包装的函数,Vue经过一系列处理才返回高阶组件的。所以,处理起来没有那么方便。
# 数据流
**Vue:**Vue2.0之后,父子组件变成了单向绑定。
**React:**从一开始就不支持双向绑定,使用的是单向数据流。
# 状态管理
- Redux 使用的是不可变数据,而Vuex的数据是可变的。Redux每次都是用新的state替换旧的state,而Vuex是直接修改。
- Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的。
# Vue常用指令
- v-bind,缩写为:
- v-on,缩写为@
- v-show
- v-if
- v-for
- v-model
- v-text,等价{{}}
- v-html,用来识别HTML标签并进行渲染
- v-once
# 常用修饰符
# 表单修饰符
# lazy——光标离开更新
# trim——过滤首尾空格
# number
如果你先输入数字,那它就会限制你输入的只能是数字。 如果你先输入字符串,那它就相当于没有加.number 而不是单一的限制输入数字或者输入的东西转换成数字
# 事件修饰符
# stop——阻止事件冒泡
阻止事件向父级传递,相当于调用了event.stopPropagation()
# self——点击元素本身触发
<div class="blue" @click.self="shout(2)">
<button @click="shout(1)">ok</button>
</div>
# 按键修饰符
.ctrl Alt或者Shift被同时按下时触发
.ctrl.exact 有且只有ctrl按下时触发
.exact 没有任何系统修饰符时触发
# prevent——阻止事件默认行为
用于阻止事件的默认行为,例如,当点击提交按钮时阻止对表单的提交。相当于调用了event.preventDefault()方法。
# once——只触发一次
# capture——向下捕获
默认事件触发是从目标开始往上冒泡的,但 当加了这个.capture以后就反过来了
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
# 滚动事件延迟
当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
# native——转化为原生事件
# v-bind修饰符
- prevent :拦截默认事件
- passive :不拦截默认事件
- stop :阻止事件冒泡
- self :当事件发生在该元素而不是子元素的时候会触发
- capture :事件侦听,事件发生的时候会调用
# 动态绑定Class/Style
# 对象
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<div :class="[isActive ? activeClass : '', errorClass]"></div>
<p :style="styleData">使用style</p>
data: {
activeClass: 'active',
errorClass: 'text-danger',
isActive: true,
hasError: false,
styleData: {
fontSize: '24px',
color: 'red'
}
}
# v-if和v-show区别
# v-if
渲染:只有条件为true时,才会渲染页面。
切换:在切换的过程中,子组件会被销毁和重建。
开销:有更高的切换开销。
# v-show
渲染:不管初始条件是什么,v-show里面的元素总是会被渲染。
切换:只是简单地基于 CSS 进行切换。(display: none)
开销:有更高的初始渲染开销。
# computed和watch区别
# computed
- computed主要是计算值得,并且具有缓存功能。只要在它依赖的值变化时才会重新计算。
- 不支持异步,当computed中有异步操作时,无法监听数据的变化。
# watch
- watch主要是监听属性变化的。
- 支持异步监听。
computed缓存值原理: computed拥有自己的watcher,它内部有个dirty属性,用来决定是否要重新计算当前的值。第一次求值的时候,会将dirty设置为false,当与它相关的data属性发生了变化之后,会通知相对应的watcher把自身的dirty设置为true。下次再调用的时候,就会重新计算了。
# Watch中的deep:true实现原理
因为 Vue 内部对需要 deep watch 的属性会进行递归的访问,而在此过程中也会不断发生依赖收集。
注意:
watch监听引用类型是拿不到oldVal的,引用类型复制是指针赋值的关系
监听引用类型需要深度监听
watch: {
list(oldValue, value) {
console.log(oldValue, value);
},
person: {
handler(oldValue, value) {
console.log(oldValue, value);
},
deep: true
},
}
# 生命周期
Vue的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
beforeCreate
Created
- 数据观测
- 属性和方法的运算
- 事件回调
- 发送请求
- 还没有$el。
beforeMount
mounted
- DOM操作
beforeUpdate
updated
beforeDestroy
destroyed
- 清空定时器
- 清理缓存
# 父子组件生命钩子执行顺序
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
# 绑定事件
<h3 @click="handleWatch">Ecosystem</h3>
<h3 @click="handleWatch1(1, $event)">Ecosystem</h3>
handleWatch(event) {
console.log(event.__proto__.constructor);
},
handleWatch1(1,event) {
console.log(event.__proto__.constructor);
},
# 插槽
# 原理
当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当子组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
# 分类
# 默认插槽
# 具名插槽
多个插槽
# 作用域插槽
让插槽内容能够访问子组件中才有的数据
# 组件间通信
# 父子组件通信
# props/$emit
# 父传子(props)
父组件通过props向下传递数据给子组件。注:组件中的数据共有三种形式:data、props、computed
父组件
<compB :moreBtn="moreBtn" @more="onMore('form~')"/>
<script>
data() {
return {
title: 'title~',
moreBtn: true
}
},
</script>
// 子组件数据写在props里面,而不是data
props:{
users:{ //这个就是父组件中子标签自定义名字
type:Array,
required:true
}
}
# 子传父(emit和回调函数)
// 子组件
this.$emit("titleChanged","子向父组件传值");//自定义事件 传递值“子向父组件传值
子组件
// 父组件
<app-header v-on:titleChanged="updateTitle" ></app-header>//与子组件titleChanged自定义事件保持一致
// updateTitle($event)接受传递过来的文字
<h2>{{title}}</h2>
<script>
...
data(){
title:"传递的是一个值"
}
methods:{
updateTitle(e){ //声明这个函数
this.title = e;
}
},
...
</script>
# $parent/$children
$children 的值是数组,而$parent是个对象
// 获取到子组件A
this.$children[0].messageA = 'this is new value'
// 获取父组件msg的值
computed:{
parentVal(){
return this.$parent.msg;
}
}
# ref/refs
ref在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据。
# 跨级通信
# provide/inject
父组件中通过
provide来提供变量, 然后再子组件中通过inject来注入变量。provide 和 inject 绑定并不是可响应的。但是,如果你传入了一个可监听的对象,那么这个对象的属性还是可响应的。
export default {
name: "A",
// 提供变量
provide: {
for: "demo"
},
components:{
comB
}
}
// 注入变量
export default {
name: "C",
inject: ['for'],
data() {
return {
demo: this.for
}
}
}
# 事件总线(自定义事件)
可用在跨组件或者兄弟组件之间
发送事件用event.$emit
event.$emit("get-footer", "Fendy's Footer");
接收事件用event.$on
event.$on("get-footer", function (content) {
// ...
});
注意:
- 记得在destroyed生命周期用$off解绑on绑定的事件
- event为Vue实例
原理:可以使用一个空的 Vue 实例作为事件总线来实现自定义事件
# $attrs/$listeners
- 父组件跟props一样传值
- 中间组件在使用子组件时,需要v-bind="$attrs"将值传下去
- 子组件通过this.$attrs获取值
// 中间组件
<template class="border">
<div>
<p>name: {{ name}}</p>
<p>childCom1的$attrs: {{ $attrs }}</p>
<child-com2 v-bind="$attrs"></child-com2>
</div>
</template>
<script>
const childCom2 = () => import("./childCom2.vue");
export default {
components: {
childCom2
},
inheritAttrs: false, // 关闭当前组件根元素上没有用props声明的属性
props: {
name: String // name作为props属性绑定
},
created() {
console.log(this.$attrs);
// { "age": "18", "gender": "女", "height": "158", "title": "程序员成长指北" }
}
};
</script
# Vuex
# 兄弟通信
# 事件总线
# Vuex
# Vuex
Vuex 是一个专为Vue.js 应用程序开发的状态管理模式。
# 优缺点
优点:
- 解决了非父子组件的消息传递(将数据存放在state中)
- 减少了Ajax请求次数,有些情景可以直接从内存中的State获取
- 存储在vuex里面的数据都是响应式的,能够实时保持数据与页面同步
缺点:
- 刷新浏览器后,Vuex中state恢复初始状态。
# 常见属性
- State:vuex的基本数据,用来存储变量。
- Getter :从基本数据state派生的数据,相当于state的计算属性。
- Mutation :提交更新数据的方法,必须是同步的。
- Action :和mutation的功能大致相同,不同在于它不是直接更改状态的,可以包含任何异步操作。
- Module:模块化vuex,可以让每一个模块拥有自己的state,mutation,action,getter,使得结构清晰,方便管理。
# Vue Router
- route表示路由信息对象,包括:path,params,hash,query,fullPath,matched,name等路由信息参数。
- router表示路由实例对象,包括了路由的跳转方法,钩子函数等。
# 导航守卫
# 全局前置钩子
- beforeEach,
- afterEach,
- beforeResolve,
# 路由独享守卫
- beforeEnter
# 组件内部守卫
- beforeRouteEnter,
- beforeRouteUpdate,
- beforeRouteLeave
# 路由模式
hash/history
# 区别
- hash模式会在url上显示'#',而history模式没有。
- 刷新页面的时候,hash可以根据#号后面的内容加载到相应的页面,但是history模式不行,会返回404。
- 实现原理不一样。hash模式主要依靠onhashchange事件监听location.hash的;而history主要是依靠H5中history新增的pushState()和replaceState()这两个方法。
- 兼容性上,hash模式可以支持低版本浏览器和IE。
# 实现
# hash
后面的hash值不会刷新页面的,同时通过监听hash的变化而执行相应的实践,从而实现局部刷新。
# history
history模式最要用到HTML5里面的pushState和replaceState这两个API,这两个api可以更改url但是不会重新刷新页面,只会进行局部更新。
# 传参
# 通过params
- 只能用name,不能用path
- 参数不会显示在url上
- 浏览器强制刷新会清空参数
# 通过query
- 只能用path,不能用name
- 参数会显示在url上
- 浏览器刷新不清空参数
{
path:'/details/:id',
name:'Details',
components:Details
}
this.$route.params.id
# 路由配置
动态路由、懒加载(异步加载组件)
# Vue实现对象和数组监听
由于Object.defineProperty()只能对属性进行数据劫持,而不能对整个对象(数组)进行数据劫持,因此Vue框架通过遍历数组和对象,对每一个属性进行劫持,从而达到利用Object.defineProperty()也能对对象和数组(部分方法的操作)进行监听。
# Vue检测数组变化原理
# 核心思想
使用了函数劫持的方式,重写了数组方法。(push、unshift、pop、shift)
Vue将data中的数组,进行了原型链的重写,指向了自己所定义的数组原型方法,当调用数组的API时,可以通知依赖更新,如果数组中包含着引用类型,会对数组中的引用类型再次进行监控。
# 常见问题
data中新增了数组或者对象属性,视图没有更新
解决方案:
- 使用$set进行更新。
- 直接使用Vue重写的那起个数组方法进行修改。
$set原理:
对新增的对象进行了依赖收集,调用触发更新视图的方法notify。
# 过滤器
过滤器是用来过滤数据的。
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
# Object.defineProperty缺点
- Object.defineProperty只能劫持对象的属性,因此需要遍历对象的每个属性,而Proxy可以直接代理对象。
- Object.defineProperty对新增属性需要手动进行观察。($set)
- Proxy性能高,支持13种拦截方式。
# data写成函数形式原因
维护每个组件间数据的独立性,不会相互影响。由于组件都是可复用的,所以组件内的data必须相互隔离。由于JS对象是引用类型的,如果以对象的形式返回的话,作用域没有隔离。
# Vue循环
# Vue中key的作用
key的作用是为了高效精准地更新DOM,减少渲染次数,提升渲染性能。因为diff算法是通过tag和key来判断是否为相同节点的。
不推荐使用数组索引作为key的原因:
index 在同一个页面会有重复的情况,违背了高效渲染的初衷。
例如数组删除了一个元素,那么这个元素后方元素的下标全都前移了一位,之前key对应的数据和dom就会乱了。
# v-if和v-for不推荐同时使用
不推荐同时使用 v-if 和 v-for。 当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级。
解决方案:使用computed过滤再for循环。
# vue的单向数据流
- 父组件可以通过prop将数据传递给子组件,但这个prop只能由父组件来修改,子组件修改的话会抛出错误。
- 如果是子组件想要修改数据,只能通过$emit由子组件派发事件,并由父组件接收事件进行修改。
# keep-alive
keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
# 作用
用来缓存组件,一般结合动态路由和动态组件一起使用。
# 场景
用户在某个列表页面选择筛选条件过滤出一份数据列表,由列表页面进入数据详情页面,再返回该列表页面,我们希望:列表页面可以保留用户的筛选(或选中)状态。keep-alive就是用来解决这种场景。当然keep-alive不仅仅是能够保存页面/组件的状态这么简单,它还可以避免组件反复创建和渲染,有效提升系统性能。 总的来说,keep-alive用于保存组件的渲染状态。
# 独立生命周期
# activated
切换回组件时,渲染后执行activated钩子。
# deactivated
切换到别的组件时,将该组件缓存到内存中并执行deactivated钩子。
# 属性
# include
表示只有名称匹配的组件才会被缓存,
# exclude
表示只有名称匹配的组件都不会被缓存,优先级高于include。
# 实现原理
- created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。
- destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。
# Vue首屏加载优化
# UI组件按需加载
# 路由异步加载
# SPA单页面
SPA是指当页面加载完成,不会因为用户的操作而重新加载或跳转。它只是进行Html内容的变换,避免重新加载。
# 优点
- 用户体验好,页面切换速度快。
- SPA相对于服务器压力较小,因为不需要加载整个页面。
- 前后端分离,架构更清晰。
# 缺点
- 不利于SEO
- 首次加载耗时长。
- 不能使用前进后退功能。
# SSR
服务端渲染,是指Vue将浏览器渲染HTML的工作交给服务端,服务端渲染完成直接发送HTML给浏览器。
# 优点
- 更好的SEO。由于服务端返回已经渲染完成的页面,数据已经包含在页面中了。所以,搜索爬虫可以更好的抓取页面的里面的信息。
- 首屏加载更快。由于服务端返回已经渲染完成的页面,无需在等待下载JS文件后在渲染。所以,SSR有更快的内容到达时间。
# 缺点
- 增加服务器压力。
- 更多的开发条件限制。需要在NodeJs的环境中运行。
# Vue小技巧
# vue v-for 中更改item 属性值后,v-show不生效的问题
添加this.$forceUpdate();进行强制渲染,效果实现。 因为数据层次太多,render函数没有自动更新,需手动强制刷新。
# 组件中 data 为什么是一个函数,而 new Vue 实例里,data 可以直接是一个对象?
因为组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响,如果组件中 data 选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会互相影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
# axios中断请求
# 如何取消请求
通过Axios提供的cancelToken来取消请求
config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => {
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel)
}
})
# 如何取消重复请求
新建一个map对象来维护已经发送但是还没有响应的请求,一般以请求的方法、url、参数这几个联合为key。在响应拦截器那里判断map对象是否存在当前的请求,如果不存在就添加进去。如果存在的话,就调用cancel方法取消请求,在响应请求拦截器中删掉map对象中当前的请求。
# CancelToken工作原理
# 修改ElementUI样式
# 使用全局样式统一覆盖
# 修改组件的style样式
# ElementUI官方修改样式的api
# 使用v-deep穿透
# !important
# v-model
# 原理
v-model 本质上是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
# 自定义v-model
<template>
<div>
<input
type="text"
:value="text1"
@input="$emit('change1', $event.target.value)"
/>
</div>
</template>
<script>
export default {
model: {
prop: "text1",
event: "change1",
},
props: {
text1: String,
default() {
return "";
},
},
};
</script>
# vue-router 路由模式有几种?
其中,3 种路由模式的说明如下:
- hash: 使用 URL hash 值来作路由。原理很简单,location.hash 的值就是 URL 中 # 后面的内容。
- history : 依赖 HTML5 History API 和服务器配置。其中做最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。
- abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
# 事件总线原理
它的工作原理是发布/订阅方法,通常称为Pub/Sub 。 由于是全局的,必然所有事件都订阅它, 所有组件也发布到它,订阅组件获得更新。 也就是说所有组件都能够将事件发布到总线,然后总线由另一个组件订阅,然后订阅它的组件将得到更新。
# Mixin
# 缺点:
变量来源不明确,不利于阅读
多个mixin可能造成命名冲突
# Vue常见的性能优化
v-if v-show
合理使用computed
自定义事件、DOM事件及时销毁
合理使用keep-alive
data数据不要太深