vue指令的三要素是响应式、模板引擎和渲染。
1、v-show 与 v-if 区别
v-show
(通过display属性控制是否显示) 只是 CSS 级别的 display: none;
和 display: block;
之间的切换,而 v-if
(先判断在渲染,是通过操纵dom元素来进行切换显示)决定是否会选择代码块的内容(或组件)。
v-if需要操作dom元素,有更高的切换消耗,v-show只是修改元素的的CSS属性有更高的初始渲染消耗,如果需要非常频繁的切换,建议使用v-show较好,如果在运行时条件很少改变,则使用v-if较好
v-if
在性能优化上有什么经验?
因为当 v-if="false"
时,内部组件是不会渲染的,所以在特定条件才渲染部分组件(或内容)时,可以先将条件设置为 false
,需要时(或异步,比如 $nextTick)再设置为 true
,这样可以优先渲染重要的其它内容,合理利用,可以进行性能优化。
2、vue-SPA
一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转。取而代之的是利用JavaScript动态的变换HTML的内容
优点
用户体验好,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染
减少了不必要的跳转和重复渲染,这样相对减轻了服务器的压力
前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点
初次加载耗时多
不能使用浏览器的前进后退功能,由于单页应用在一个页面中显示所有的内容,所以,无法前进后退
不利于搜索引擎检索:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
MPA(MultiPage-page application),翻译过来就是多页应用
在MPA
中,每个页面都是一个主页面,都是独立的,当我们在访问另一个页面的时候,都需要重新加载html
、css
、js
文件,公共文件则根据需求按需加载
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EaHZE41p-1663509494278)(C:\Users\34818\AppData\Roaming\Typora\typora-user-images\image-20220611172714473.png)]
服务端渲染简称 SSR,全称是 Server Side Render
,是指一种传统的渲染方式,就是在浏览器请求页面URL的时候,服务端将我们需要的HTML文本组装好,并返回给浏览器,这个HTML文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。
那么,SSR 技术到底有哪些优点呢?我们来列举一下:
- 更快的响应时间,相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。
- 有利于 SEO ,可以将 SEO 的关键信息直接在后台就渲染成 HTML,而保证搜索引擎的爬虫都能爬取到关键数据,然后在别人使用搜索引擎搜索相关的内容时,你的网页排行能靠得更前,这样你的流量就有越高。
以上是 SSR 技术最主要的两大优点,虽有优势,但缺点也不容忽视:
- 相对于仅仅需要提供静态文件的服务器,SSR中使用的渲染程序自然会占用更多的CPU和内存资源。
- 一些常用的浏览器API可能无法正常使用,比如
window
、docment
和alert
等,如果使用的话需要对运行的环境加以判断。 - 开发调试会有一些麻烦,因为涉及了浏览器及服务器,对于SPA的一些组件的生命周期的管理会变得复杂。
- 可能会由于某些因素导致服务器端渲染的结果与浏览器端的结果不一致。
3、vue生命周期
vue的生命周期是指,从创建vue对象到销毁vue对象的过程。
经历了从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程。
1. **beforeCreate (创建前)**
2. **created (创建后)**
3. **beforeMount (挂载前)**
4. **mounted (挂载后)**
5. beforeUpdate (数据更新前)
6. updated (数据更新后)
7. beforeDestroy (销毁前)
8. destroyed (销毁后)
进入到页面回执行哪些?(前四个)
beforeCreate
created
beforeMount
mounted
在哪个阶段有$el,$date?
beforeCreate//啥也没有
created//有date没有el
beforeMount//有date没有el
mounted//都有
4、keep-alive理解?(未完成)
1.keep-alive是系统自带的组件;是用来缓存组件的==》提升性能 (在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性)
2.
5、v-if和v-for优先级
在vue2中,v-for的优先级是高于v-if的,
如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能;
另外需要注意的是在vue3则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常。
6、ref
vue给我们提供一个操作dom的属性ref。绑定在dom元素上时,用起来与id差不多,通过this.$refs来调用:
7、nextTick是什么?
获取更新后的dom内容
8、scoped原理
作用:让样式在本组件中生效,不影响其他组件
原理:给节点新增自定义属性,然后css根据属性选择器添加样式
(给HTML的dom节点添加一个不重复的data属性(例如: data-v-5558831a)来唯一标识这个dom 元素)
9、组件传值
父传子:
父组件:自定义属性
子组件:props:[‘msg’] 或
props:{
msg:数据类型
}
子传父:
子组件:
父组件:
兄弟传值:
1、通过事件总线bus传值
再main.js里首先声明
Vue.prototype.$bus=new Vue();
创建全局空Vue实例:eventBus
import Vue from 'vue';
const eventBus= new Vue() //创建事件总线
export default eventBus;
具体页面使用$emit发布事件 - 传递值
import eventBus from '@u/eventBus'
eventBus.$emit('send',‘hello’)
具体页面使用$on订阅事件 - 接收组件值
import eventBus from '@u/eventBus'
eventBus.$on('send', msg => {
console.log(msg) //控制台输出 hello
}
2、通过父组件在中间做中间件,相互传值
3.vuex
10、vuex有哪些属性?
state 类似于组件中的date
getters 类似于组件中的computed
mutations 类似于组件中的methods
actons 提交mutations的
modules 把上面四个组件细分,让仓库更好管理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6THIt2oE-1663509494288)(C:\Users\34818\AppData\Roaming\Typora\typora-user-images\image-20220713154904072.png)]
11、dom(reflow重排,回流和repaint重绘)
(1) dom 树的实现模块和 js 模块是分开的这些跨模块的通讯增加了成本
(2) dom 操作引起的浏览器的回流和重绘,使得性能开销巨大。
- reflow:浏览器要花时间去渲染,当它发现了某个部分发生了变化并且影响了布局,就需要倒回去重新渲染
- repaint:如果只是改变了某个元素的背景颜色或文字颜色等,不影响元素周围或内部布局,就只会引起浏览器的repaint,重画其中一部分。
- reflow比repaint更花费时间,也就更影响性能,所以在写代码时应尽量避免过多的reflow。
11.1.虚拟DOM
通过js创建一个Object对象来模拟真实DOM结构,这个对象包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,通过vue中的render()函数把虚拟dom编译成真实dom,在通过appendChild()添加到页面中。
11.2diff算法
-
diff 比较两个虚拟dom只会在同层级之间进行比较,不会跨层级进行比较。而用来判断是否是同层级的标准就是
-
是否在同一层
是否有相同的父级
下面,我们来一张图,就很好理解了(盗用网上一张很经典的图)diff是采用先序深度优先遍历得方式进行节点比较的,即,当比较某个节点时,如果该节点存在子节点,那么会优先比较他的子节点,直到所有子节点全部比较完成,才会开始去比较改节点的下一个同层级节点。不好理解吗?没关系,我们画个图看一下,就很清晰了
当比较新旧两个dom时,会按照图中1-9的顺序去进行比较。
patch函数:
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)//如果新老节点是同一节点,那么进一步通过patchVnode来比较子节点
} else {
/* -----否则新节点直接替换老节点----- */
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
//判断两节点是否为同一节点
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
patch方法的作用是判断传进来的两节点是否相同,相同比较下一对节点,不同则不用比较下一对节点,直接将新元素替换旧元素
patchVnode函数:
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el //找到对应的真实dom,称为el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return // 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text) //如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch) // 如果两者都有子节点,则执行updateChildren函数比较子节点!!!!!!!!!
}else if (ch){ // 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
createEle(vnode) //create el's children dom
}else if (oldCh){ // 如果oldVnode有子节点而Vnode没有,则删除el的子节点
api.removeChildren(el)
}
}
}
updateChildren:
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 空对比
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
// 首首对比
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
// 尾尾对比
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
// 首尾对比
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
// 尾首对比
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 上述四种对比方法都没找到 只好老老实实的依次遍历对比数据(key)
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
//比较在旧的key值列表中是否有新的节点的key值
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
// 旧的key值列表中不存在新的key值 直接新增节点
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
// 旧的key值列表中存在新的key值
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
// 对应key值相同的旧节点的标签名不一样 新增节点
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
// 更新以及判断是否递归updateChildren
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
// 遍历完毕 将新增的节点批量新增 将多余的节点批量删除
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
函数作用:
- 将Vnode的子节点Vch和oldVnode的子节点oldCh提取出来
- oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。
11.3.浏览器收到一个html页面是如何解析成页面呈现给用户的呢?
-
解析html:会按顺序解析。浏览器有专门的html解析器来解析HTML,并在解析的过程中构建DOM树
-
构建dom树:它和步骤(1) 是同步进行,可以理解为边解析边构建。
-
构建呈现树renderTree:将dom树与css结合,也就是将样式应用到dom节点上
-
布局:计算呈现树节点的大小和位置,这一位置是递归进行的。
-
绘制:布局完成后,便是将呈现树绘制出来显示在屏幕上。
12、mvvm 、mvc、mvp框架
mvvm:
MVVM是Model-View-ViewModel的缩写,Model代表数据模型负责业务逻辑和数据封装,View代表UI组件负责界面和显示,ViewModel监听模型数据的改变和控制视图行为,处理用户交互,简单来说就是通过双向数据绑定把View层和Model层连接起来。在MVVM架构下,View和Model没有直接联系,而是通过ViewModel进行交互,我们只关注业务逻辑,不需要手动操作DOM,不需要关注View和Model的同步工作
MVVM模式的优点:
- 低耦合:视图(View)可以独立于 Model 变化和修改,一个 ViewModel 可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
- 可重用性:你可以把一些视图逻辑放在一个ViewModel里面,让很多 view 重用这段视图逻辑。
- 独立开发:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
- 可测试:界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。
mvc:****
MVC是应用最广泛的软件架构之一,一般MVC分为:
Model( 模型 )、Controller( 控制器 )、View( 视图 )。
这主要是基于分层的目的,让彼此的职责分开。View一般通过Controller来和Model进行联系。Controller是Model和View的协调者,View和Model不直接联系。基本联系都是单向的。
-
View 传送指令到 Controller
-
Controller 完成业务逻辑后,要求 Model 改变状态
-
Model 将新的数据发送到 View,用户得到反馈
mvp:
MVP 模式将 Controller 改名为Presenter
,同时改变了通信方向。
- 各部分之间的通信,都是双向的。
- View 与 Model 不发生联系,都通过 Presenter 传递。
- View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
MVP优点:
模型与视图完全分离,我们可以修改视图而不影响模型;
可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部;
我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁;
如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)。
13、vue-router传参
-
query方式传参和接收参数
传参: this.$router.push({ path:'/xxx', query:{ id:id } }) 接收参数: this.$route.query.id 注意:传参是this.$router,接收参数是this.$route,这里千万要看清了!!!
-
params方式传参和接收参数
传参: this.$router.push({ name:'xxx', params:{ id:id } }) 接收参数: this.$route.params.id
直白的来说query相当于get请求,页面跳转的时候,可以在地址栏看到请求参数,而params相当于post请求,参数不会再地址栏中显示
14、前端框架(framework ) 与 库(library) 怎么区别
库,更多是一个封装好的特定集合,提供给开发者使用,而且是特定与某一方面的集合(方法和函数),库没有控制权,控制权在使用者手中
前端框架,顾名思义就是一套架构,控制权在框架本身,使用者需按照框架的某种规范进行开发
Vue初始化过程中(new Vue(options))都做了什么?
1、处理组件配置项;初始化根组件时进行了选项合并操作,将全局配置合并到根组件的局部配置上;初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率;
2、初始化组件实例的关系属性,比如 p a r e n t 、 parent、parent、children、r o o t 、 root、root、refs 等
3、处理自定义事件
4、调用 beforeCreate 钩子函数
5、初始化组件的 inject 配置项,得到 ret[key] = val 形式的配置对象,然后对该配置对象进行响应式处理,并代理每个 key 到 vm 实例上
6、数据响应式,处理 props、methods、data、computed、watch 等选项
7、解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
8、调用 created 钩子函数
如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount
9、接下来则进入挂载阶段
Vue数据双向绑定原理
实现mvvm的数据双向绑定,是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来给各个属性添加setter,getter并劫持监听,在数据变动时发布消息给订阅者,触发相应的监听回调。就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
ods、data、computed、watch 等选项
7、解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
8、调用 created 钩子函数
如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount 方法,反之,没提供 el 选项则必须调用 $mount
9、接下来则进入挂载阶段